You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

269 lines
11 KiB
JavaScript

import { useSignal, signal, useComputed, batch, effect } from '@preact/signals'
import { TreeView, EXAMPLE_TREE } from '@/client/components/TreeView.jsx'
import { Editor } from '@/client/components/Editor.jsx'
import clsx from 'clsx'
import { useEffect, useState } from 'preact/hooks'
import { fetchJson } from '../utils.js'
import { Terminal } from './Terminal.jsx'
function ensurePrefix(s, prefix) {
return s.slice(0, prefix.length) === prefix ? s : prefix + s
}
function stripPrefix(s, prefix) {
return s.slice(0, prefix.length) === prefix ? s.slice(prefix.length) : s
}
function clamp(min, value, max) {
return Math.max(min, Math.min(value, max))
}
export const Ide = ({}) => {
const ws = useSignal(null)
const uuid = useSignal(null)
useEffect(() => {
ws.value = new WebSocket(`ws://${location.host}/api/docker/node:current-alpine/pty`)
ws.value.addEventListener('message', e => {
const event = JSON.parse(e.data)
if (event.type === 'created') {
uuid.value = event.id
history.pushState(event.id, '', '/@container/' + event.id)
}
})
}, [])
if (!uuid.value) return <>Creating container...</>
const activeTab = useSignal(null)
const tabs = useSignal([
// {
// id: '/project/main.c',
// content: signal('...main.c...'),
// },
// {
// id: '/project/data.csv',
// content: signal('...data.csv...'),
// },
])
const activePath = useComputed(() => {
if (activeTab.value === null) return null
return tabs.value[activeTab.value].id
})
const activeContent = useComputed(() => {
if (activeTab.value === null) return null
return tabs.value[activeTab.value].content
})
effect(() => {
const path = activePath.value
const contentSig = activeContent.value
if (contentSig === null || path === null) return
console.log(path, contentSig.value)
})
return (
<div class="ide">
<div class="sidebar">
<div class="logo">PHC / Run</div>
<div class="flex-row">
<button class="icon">
<div class="material-symbols-outlined">home</div>
</button>
<div class="project-details flex-grow">
<div class="title">{uuid.value}</div>
<div class="open">
<div class="material-symbols-outlined">unfold_more</div>
</div>
</div>
</div>
<div class="section">
<div class="spoiler">
<div class="material-symbols-outlined">account_tree</div>
<div class="title">Files</div>
<div class="material-symbols-outlined">keyboard_arrow_down</div>
</div>
<div class="content">
<TreeView
listDir={async path => {
const entries = await fetchJson(
`/api/container/${uuid.value}/exec/ls/${stripPrefix(path, '/')}`
)
return entries
}}
actionOpenFile={async path => {
const content = await fetchJson(
`/api/container/${uuid.value}/fs/${stripPrefix(path, '/')}`
)
console.log('content', content)
batch(() => {
tabs.value = [
...tabs.value,
{
id: path,
content: signal(content),
},
]
activeTab.value = tabs.value.length - 1
})
}}
rootPath={'/'}
/>
</div>
</div>
<div class="section">
<div class="spoiler">
<div class="material-symbols-outlined">chat</div>
<div class="title">Conversation</div>
<div class="material-symbols-outlined">keyboard_arrow_down</div>
</div>
<div class="content">...</div>
</div>
<div class="section">
<div class="spoiler">
<div class="material-symbols-outlined">settings</div>
<div class="title">Settings</div>
<div class="material-symbols-outlined">keyboard_arrow_down</div>
</div>
</div>
<div class="spacer"></div>
<div class="user-details">
<div class="profile">
<img
src={`https://api.dicebear.com/9.x/identicon/svg?seed=${uuid.value}`}
alt="profile picture"
/>
</div>
<div class="username">{'aziis98'}</div>
<div class="email">{'aziis98'}@exmaple.com</div>
</div>
</div>
<div class="header">
<div class="search">
<div class="material-symbols-outlined">search</div>
<input type="text" placeholder="Search a snippet..." />
</div>
<div class="actions">
<button class="icon">
<div class="material-symbols-outlined">terminal</div>
</button>
<button
class="run"
onClick={() => {
console.log('Running file:', activePath.value)
if (!activePath.value) return
const fullPath = '/project' + activePath.value
if (fullPath.endsWith('.sh')) {
ws.value.send(`sh ${fullPath}\n`)
return
}
if (fullPath.endsWith('.js')) {
ws.value.send(`node ${fullPath}\n`)
return
}
}}
>
<div class="material-symbols-outlined">play_arrow</div>
Run File
</button>
</div>
</div>
<div class="tabbed-editor">
<div class="tabs">
{tabs.value.map((tab, index) => (
<div
class={clsx('tab', activeTab.value === index && 'active')}
onClick={() => (activeTab.value = index)}
>
<div class="title">{tab.id.split('/').at(-1)}</div>
<div class="buttons">
<button
class="flat icon"
onClick={e => {
e.stopPropagation()
batch(() => {
const newValues = [...tabs.value]
newValues.splice(index, 1)
console.log(
newValues.length,
activeTab.value,
clamp(0, activeTab.value, newValues.length - 1)
)
tabs.value = newValues
activeTab.value =
newValues.length === 0
? null
: clamp(
0,
activeTab.value > index
? activeTab.value - 1
: activeTab.value,
newValues.length - 1
)
})
}}
>
<div class="material-symbols-outlined">close</div>
</button>
</div>
</div>
))}
</div>
<div class="editor">
{activeContent.value === null ? (
<>No file opened</>
) : (
<Editor
code={activeContent.value}
setCode={async source => {
activeContent.value.value = source
const path = activePath.value
await fetchJson(`/api/container/${uuid.value}/fs/${stripPrefix(path, '/')}`, {
method: 'PUT',
body: source,
})
}}
/>
)}
</div>
</div>
<div class="terminal">
<div class="title">Terminal</div>
<div class="actions">
<button class="flat icon">
<div class="material-symbols-outlined">delete</div>
</button>
<button class="flat icon">
<div class="material-symbols-outlined">close</div>
</button>
</div>
<div class="content" xterm-dimensions>
<Terminal ws={ws.value} />
</div>
</div>
<div class="status">Websocket connection active</div>
</div>
)
}