197 lines
4.7 KiB
TypeScript
197 lines
4.7 KiB
TypeScript
import { Hype } from '@because/hype'
|
|
import { theme, define, stylesToCSS, initScript, baseStyles } from '@because/toes/tools'
|
|
import { readdir, readlink, stat } from 'fs/promises'
|
|
import { join } from 'path'
|
|
import type { Child } from 'hono/jsx'
|
|
|
|
const APPS_DIR = process.env.APPS_DIR!
|
|
|
|
const app = new Hype({ prettyHTML: false })
|
|
|
|
const Container = define('Container', {
|
|
fontFamily: theme('fonts-sans'),
|
|
padding: '20px',
|
|
maxWidth: '800px',
|
|
margin: '0 auto',
|
|
color: theme('colors-text'),
|
|
})
|
|
|
|
const Header = define('Header', {
|
|
marginBottom: '20px',
|
|
paddingBottom: '10px',
|
|
borderBottom: `2px solid ${theme('colors-border')}`,
|
|
})
|
|
|
|
const Title = define('Title', {
|
|
margin: 0,
|
|
fontSize: '24px',
|
|
fontWeight: 'bold',
|
|
})
|
|
|
|
const Subtitle = define('Subtitle', {
|
|
color: theme('colors-textMuted'),
|
|
fontSize: '18px',
|
|
marginTop: '5px',
|
|
})
|
|
|
|
const VersionList = define('VersionList', {
|
|
listStyle: 'none',
|
|
padding: 0,
|
|
margin: '20px 0',
|
|
border: `1px solid ${theme('colors-border')}`,
|
|
borderRadius: theme('radius-md'),
|
|
overflow: 'hidden',
|
|
})
|
|
|
|
const VersionItem = define('VersionItem', {
|
|
padding: '12px 15px',
|
|
borderBottom: `1px solid ${theme('colors-border')}`,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
states: {
|
|
':last-child': {
|
|
borderBottom: 'none',
|
|
},
|
|
':hover': {
|
|
backgroundColor: theme('colors-bgHover'),
|
|
},
|
|
},
|
|
})
|
|
|
|
const VersionLink = define('VersionLink', {
|
|
base: 'a',
|
|
textDecoration: 'none',
|
|
color: theme('colors-link'),
|
|
fontFamily: theme('fonts-mono'),
|
|
fontSize: '15px',
|
|
cursor: 'pointer',
|
|
states: {
|
|
':hover': {
|
|
textDecoration: 'underline',
|
|
},
|
|
},
|
|
})
|
|
|
|
const Badge = define('Badge', {
|
|
fontSize: '12px',
|
|
padding: '2px 8px',
|
|
borderRadius: theme('radius-md'),
|
|
backgroundColor: theme('colors-bgElement'),
|
|
color: theme('colors-statusRunning'),
|
|
fontWeight: 'bold',
|
|
})
|
|
|
|
const ErrorBox = define('ErrorBox', {
|
|
color: theme('colors-error'),
|
|
padding: '20px',
|
|
backgroundColor: theme('colors-bgElement'),
|
|
borderRadius: theme('radius-md'),
|
|
margin: '20px 0',
|
|
})
|
|
|
|
interface LayoutProps {
|
|
title: string
|
|
subtitle?: string
|
|
children: Child
|
|
}
|
|
|
|
function Layout({ title, subtitle, children }: LayoutProps) {
|
|
return (
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>{title}</title>
|
|
<link rel="stylesheet" href="/styles.css" />
|
|
</head>
|
|
<body>
|
|
<script dangerouslySetInnerHTML={{ __html: initScript }} />
|
|
<Container>
|
|
<Header>
|
|
<Title>Versions</Title>
|
|
{subtitle && <Subtitle>{subtitle}</Subtitle>}
|
|
</Header>
|
|
{children}
|
|
</Container>
|
|
</body>
|
|
</html>
|
|
)
|
|
}
|
|
|
|
app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, {
|
|
'Content-Type': 'text/css; charset=utf-8',
|
|
}))
|
|
|
|
async function getVersions(appPath: string): Promise<{ name: string; isCurrent: boolean }[]> {
|
|
const entries = await readdir(appPath, { withFileTypes: true })
|
|
|
|
let currentTarget = ''
|
|
try {
|
|
currentTarget = await readlink(join(appPath, 'current'))
|
|
} catch {}
|
|
|
|
return entries
|
|
.filter(e => e.isDirectory() && /^\d{8}-\d{6}$/.test(e.name))
|
|
.map(e => ({ name: e.name, isCurrent: e.name === currentTarget }))
|
|
.sort((a, b) => b.name.localeCompare(a.name))
|
|
}
|
|
|
|
function formatTimestamp(ts: string): string {
|
|
return `${ts.slice(0, 4)}-${ts.slice(4, 6)}-${ts.slice(6, 8)} ${ts.slice(9, 11)}:${ts.slice(11, 13)}:${ts.slice(13, 15)}`
|
|
}
|
|
|
|
app.get('/', async c => {
|
|
const appName = c.req.query('app')
|
|
|
|
if (!appName) {
|
|
return c.html(
|
|
<Layout title="Versions">
|
|
<ErrorBox>Please specify an app name with ?app=<name></ErrorBox>
|
|
</Layout>
|
|
)
|
|
}
|
|
|
|
const appPath = join(APPS_DIR, appName)
|
|
|
|
try {
|
|
await stat(appPath)
|
|
} catch {
|
|
return c.html(
|
|
<Layout title="Versions">
|
|
<ErrorBox>App "{appName}" not found</ErrorBox>
|
|
</Layout>
|
|
)
|
|
}
|
|
|
|
const versions = await getVersions(appPath)
|
|
|
|
if (versions.length === 0) {
|
|
return c.html(
|
|
<Layout title="Versions" subtitle={appName}>
|
|
<ErrorBox>No versions found</ErrorBox>
|
|
</Layout>
|
|
)
|
|
}
|
|
|
|
return c.html(
|
|
<Layout title="Versions" subtitle={appName}>
|
|
<VersionList>
|
|
{versions.map(v => (
|
|
<VersionItem>
|
|
<VersionLink
|
|
href="#"
|
|
onclick={`window.parent.postMessage({type:'navigate-tool',tool:'code',params:{app:'${appName}',version:'${v.name}'}}, '*'); return false;`}
|
|
>
|
|
{formatTimestamp(v.name)}
|
|
</VersionLink>
|
|
{v.isCurrent && <Badge>current</Badge>}
|
|
</VersionItem>
|
|
))}
|
|
</VersionList>
|
|
</Layout>
|
|
)
|
|
})
|
|
|
|
export default app.defaults
|