Initial commit

This commit is contained in:
MrFastwind
2026-07-06 14:14:58 +02:00
commit b853aead8c
48 changed files with 5822 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
import Editor from '@monaco-editor/react'
interface Props {
value: string
onChange: (value: string) => void
language?: string
}
export default function ConfigEditor({ value, onChange, language = 'plaintext' }: Props) {
return (
<div className="border border-neutral-800 rounded-lg overflow-hidden">
<Editor
height="500px"
language={language}
value={value}
onChange={v => onChange(v ?? '')}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 13,
fontFamily: '"JetBrains Mono", "Fira Code", monospace',
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 2,
}}
/>
</div>
)
}
+37
View File
@@ -0,0 +1,37 @@
import { Link, useLocation } from 'react-router-dom'
import type { ReactNode } from 'react'
const nav = [
{ to: '/', label: 'Dashboard', icon: '⊞' },
{ to: '/settings', label: 'Settings', icon: '🖥' },
{ to: '/configs', label: 'Configs', icon: '⚙' },
{ to: '/modlists', label: 'Modlists', icon: '📦' },
{ to: '/logs', label: 'Logs', icon: '📋' },
]
export default function Layout({ children }: { children: ReactNode }) {
const loc = useLocation()
return (
<div className="flex h-screen bg-neutral-950 text-neutral-100">
<aside className="w-56 border-r border-neutral-800 p-4 flex flex-col gap-2">
<h1 className="text-lg font-bold mb-4 tracking-tight">Arma3 Web</h1>
{nav.map(item => (
<Link
key={item.to}
to={item.to}
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors ${
loc.pathname === item.to || (item.to !== '/' && loc.pathname.startsWith(item.to))
? 'bg-neutral-800 text-white'
: 'text-neutral-400 hover:text-white hover:bg-neutral-800/50'
}`}
>
<span>{item.icon}</span>
{item.label}
</Link>
))}
</aside>
<main className="flex-1 overflow-auto p-6">{children}</main>
</div>
)
}
+87
View File
@@ -0,0 +1,87 @@
import { useEffect, useRef } from 'react'
import { Terminal } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import '@xterm/xterm/css/xterm.css'
interface Props {
wsUrl: string
}
export default function LiveTerminal({ wsUrl }: Props) {
const containerRef = useRef<HTMLDivElement>(null)
const terminalRef = useRef<Terminal | null>(null)
const fitRef = useRef<FitAddon | null>(null)
useEffect(() => {
if (!containerRef.current) return
const term = new Terminal({
theme: { background: '#0a0a0a', foreground: '#e0e0e0', cursor: '#e0e0e0' },
fontSize: 13,
fontFamily: '"JetBrains Mono", "Fira Code", monospace',
cursorBlink: true,
convertEol: true,
rows: 30,
})
const fit = new FitAddon()
term.loadAddon(fit)
term.open(containerRef.current)
fit.fit()
term.write('Connecting...\r\n')
terminalRef.current = term
fitRef.current = fit
let ws: WebSocket | null = null
let reconnectTimer: ReturnType<typeof setTimeout>
let disposed = false
function connect() {
if (disposed) return
ws = new WebSocket(wsUrl)
ws.onopen = () => {
if (disposed) { ws?.close(); return }
term.clear()
term.write('--- Connected ---\r\n')
}
ws.onmessage = (event) => {
const line = event.data as string
if (line === '[SERVER_PROCESS_EXITED]') {
term.write('\r\n\x1b[31m--- Process exited ---\x1b[0m\r\n')
} else if (line.startsWith('[STEAMCMD] SUCCESS:')) {
term.write('\r\n\x1b[32m' + line + '\x1b[0m\r\n')
} else if (line.startsWith('[STEAMCMD] ERROR:')) {
term.write('\r\n\x1b[31m' + line + '\x1b[0m\r\n')
} else if (line.startsWith('[STEAMCMD]')) {
term.write('\r\n\x1b[33m' + line + '\x1b[0m\r\n')
} else {
term.writeln(line)
}
}
ws.onclose = () => {
if (disposed) return
term.write('\r\n\x1b[33m--- Disconnected, reconnecting in 3s ---\x1b[0m\r\n')
reconnectTimer = setTimeout(connect, 3000)
}
ws.onerror = () => ws?.close()
}
connect()
const resizeObserver = new ResizeObserver(() => {
try { fit.fit() } catch {}
})
resizeObserver.observe(containerRef.current)
return () => {
disposed = true
clearTimeout(reconnectTimer)
resizeObserver.disconnect()
ws?.close()
term.dispose()
}
}, [wsUrl])
return <div ref={containerRef} className="h-full min-h-[400px] bg-[#0a0a0a] rounded-lg" />
}