edit code #3
|
|
@ -88,6 +88,9 @@ const CodeHeader = define('CodeHeader', {
|
|||
borderBottom: `1px solid ${theme('colors-border')}`,
|
||||
fontWeight: 'bold',
|
||||
fontSize: '14px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
})
|
||||
|
||||
const ErrorBox = define('ErrorBox', {
|
||||
|
|
@ -190,6 +193,46 @@ const DownloadButton = define('DownloadButton', {
|
|||
},
|
||||
})
|
||||
|
||||
const EditButton = define('EditButton', {
|
||||
base: 'button',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
padding: '6px 12px',
|
||||
backgroundColor: theme('colors-bgElement'),
|
||||
color: theme('colors-text'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
borderRadius: theme('radius-md'),
|
||||
fontSize: '13px',
|
||||
cursor: 'pointer',
|
||||
states: {
|
||||
':hover': {
|
||||
backgroundColor: theme('colors-bgHover'),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const EditLink = define('EditLink', {
|
||||
base: 'a',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
padding: '6px 12px',
|
||||
backgroundColor: theme('colors-bgElement'),
|
||||
color: theme('colors-text'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
borderRadius: theme('radius-md'),
|
||||
fontSize: '13px',
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'none',
|
||||
states: {
|
||||
':hover': {
|
||||
backgroundColor: theme('colors-bgHover'),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const FolderIcon = () => (
|
||||
<FileIcon viewBox="0 0 24 24">
|
||||
<path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" />
|
||||
|
|
@ -206,6 +249,7 @@ interface LayoutProps {
|
|||
title: string
|
||||
children: Child
|
||||
highlight?: boolean
|
||||
editable?: boolean
|
||||
}
|
||||
|
||||
const fileMemoryScript = `
|
||||
|
|
@ -233,7 +277,7 @@ const fileMemoryScript = `
|
|||
})();
|
||||
`
|
||||
|
||||
function Layout({ title, children, highlight }: LayoutProps) {
|
||||
function Layout({ title, children, highlight, editable }: LayoutProps) {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
|
|
@ -241,13 +285,27 @@ function Layout({ title, children, highlight }: LayoutProps) {
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{title}</title>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
{highlight && (
|
||||
{highlight && !editable && (
|
||||
<>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css" media="(prefers-color-scheme: light)" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" media="(prefers-color-scheme: dark)" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
</>
|
||||
)}
|
||||
{editable && (
|
||||
<>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" media="(prefers-color-scheme: light)" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" media="(prefers-color-scheme: dark)" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-jsx.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-tsx.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-yaml.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markdown.min.js"></script>
|
||||
</>
|
||||
)}
|
||||
</head>
|
||||
<body>
|
||||
<script dangerouslySetInnerHTML={{ __html: fileMemoryScript }} />
|
||||
|
|
@ -255,7 +313,7 @@ function Layout({ title, children, highlight }: LayoutProps) {
|
|||
<Container>
|
||||
{children}
|
||||
</Container>
|
||||
{highlight && <script dangerouslySetInnerHTML={{ __html: 'hljs.highlightAll();' }} />}
|
||||
{highlight && !editable && <script dangerouslySetInnerHTML={{ __html: 'hljs.highlightAll();' }} />}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
|
@ -284,6 +342,26 @@ app.get('/raw', async c => {
|
|||
return new Response(file)
|
||||
})
|
||||
|
||||
app.post('/save', async c => {
|
||||
const appName = c.req.query('app')
|
||||
const version = c.req.query('version') || 'current'
|
||||
const filePath = c.req.query('file')
|
||||
|
||||
if (!appName || !filePath) {
|
||||
return c.text('Missing app or file parameter', 400)
|
||||
}
|
||||
|
||||
const fullPath = join(APPS_DIR, appName, version, filePath)
|
||||
const content = await c.req.text()
|
||||
|
||||
try {
|
||||
await Bun.write(fullPath, content)
|
||||
return c.text('OK')
|
||||
} catch (err) {
|
||||
return c.text(`Failed to save: ${err}`, 500)
|
||||
}
|
||||
})
|
||||
|
||||
async function listFiles(appPath: string, subPath: string = '') {
|
||||
const fullPath = join(appPath, subPath)
|
||||
const entries = await readdir(fullPath, { withFileTypes: true })
|
||||
|
|
@ -373,6 +451,29 @@ function getLanguage(filename: string): string {
|
|||
return langMap[ext] || 'plaintext'
|
||||
}
|
||||
|
||||
function getPrismLanguage(filename: string): string {
|
||||
const ext = extname(filename).toLowerCase()
|
||||
const langMap: Record<string, string> = {
|
||||
'.js': 'javascript',
|
||||
'.jsx': 'javascript',
|
||||
'.ts': 'typescript',
|
||||
'.tsx': 'typescript',
|
||||
'.json': 'json',
|
||||
'.css': 'css',
|
||||
'.html': 'html',
|
||||
'.md': 'markdown',
|
||||
'.sh': 'bash',
|
||||
'.yml': 'yaml',
|
||||
'.yaml': 'yaml',
|
||||
'.py': 'python',
|
||||
'.rb': 'ruby',
|
||||
'.go': 'go',
|
||||
'.rs': 'rust',
|
||||
'.sql': 'sql',
|
||||
}
|
||||
return langMap[ext] || 'plaintext'
|
||||
}
|
||||
|
||||
|
||||
app.get('/', async c => {
|
||||
const appName = c.req.query('app')
|
||||
|
|
@ -478,12 +579,91 @@ app.get('/', async c => {
|
|||
// Text file - show with syntax highlighting
|
||||
const content = readFileSync(fullPath, 'utf-8')
|
||||
const language = getLanguage(filename)
|
||||
const prismLang = getPrismLanguage(filename)
|
||||
const edit = c.req.query('edit') === '1'
|
||||
|
||||
if (edit) {
|
||||
const editorScript = `
|
||||
import { CodeJar } from 'https://cdn.jsdelivr.net/npm/codejar@4.2.0/dist/codejar.js';
|
||||
|
||||
const editor = document.getElementById('editor');
|
||||
const saveBtn = document.getElementById('save-btn');
|
||||
const status = document.getElementById('save-status');
|
||||
let dirty = false;
|
||||
|
||||
const highlight = (el) => {
|
||||
Prism.highlightElement(el);
|
||||
};
|
||||
|
||||
const jar = CodeJar(editor, highlight, { tab: ' ', addClosing: false });
|
||||
|
||||
// Initial highlight
|
||||
highlight(editor);
|
||||
|
||||
jar.onUpdate(() => {
|
||||
if (!dirty) {
|
||||
dirty = true;
|
||||
saveBtn.textContent = 'Save *';
|
||||
}
|
||||
});
|
||||
|
||||
saveBtn.onclick = async () => {
|
||||
if (!dirty) return;
|
||||
saveBtn.disabled = true;
|
||||
status.textContent = 'Saving...';
|
||||
|
||||
try {
|
||||
const res = await fetch('/save?app=${appName}${versionParam}&file=${filePath}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
body: jar.toString()
|
||||
});
|
||||
if (!res.ok) throw new Error('Save failed');
|
||||
dirty = false;
|
||||
saveBtn.textContent = 'Save';
|
||||
saveBtn.disabled = false;
|
||||
status.textContent = 'Saved!';
|
||||
setTimeout(() => { status.textContent = ''; }, 2000);
|
||||
} catch (err) {
|
||||
saveBtn.disabled = false;
|
||||
status.textContent = 'Error: ' + err.message;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||
e.preventDefault();
|
||||
saveBtn.click();
|
||||
}
|
||||
});
|
||||
`
|
||||
return c.html(
|
||||
<Layout title={`${appName}/${filePath}`} editable>
|
||||
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
|
||||
<CodeBlock>
|
||||
<CodeHeader>
|
||||
<span>{filename}</span>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<span id="save-status" style="font-size:12px;font-weight:normal;color:var(--colors-textMuted)"></span>
|
||||
<EditButton id="save-btn">Save</EditButton>
|
||||
<EditLink href={`/?app=${appName}${versionParam}&file=${filePath}`}>Done</EditLink>
|
||||
</div>
|
||||
</CodeHeader>
|
||||
<pre id="editor" class={`language-${prismLang}`} contenteditable style="margin:0;padding:15px;min-height:300px;outline:none">{content}</pre>
|
||||
</CodeBlock>
|
||||
<script type="module" dangerouslySetInnerHTML={{ __html: editorScript }} />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
return c.html(
|
||||
<Layout title={`${appName}/${filePath}`} highlight>
|
||||
<PathBreadcrumb appName={appName} filePath={filePath} versionParam={versionParam} />
|
||||
<CodeBlock>
|
||||
<CodeHeader>{filename}</CodeHeader>
|
||||
<CodeHeader>
|
||||
<span>{filename}</span>
|
||||
<EditLink href={`/?app=${appName}${versionParam}&file=${filePath}&edit=1`}>Edit</EditLink>
|
||||
</CodeHeader>
|
||||
<pre><code class={`language-${language}`}>{content}</code></pre>
|
||||
</CodeBlock>
|
||||
</Layout>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user