Skip to content

Commit 66c01b0

Browse files
authored
Merge pull request #3 from ayushedith/impt/web-if
feat(web): set up initial Next.js application structure with WebSocke…
2 parents 551887f + 1a87a43 commit 66c01b0

File tree

11 files changed

+260
-0
lines changed

11 files changed

+260
-0
lines changed

web/Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM node:18-alpine AS web-build
2+
WORKDIR /app
3+
COPY package*.json ./
4+
RUN npm ci --silent
5+
COPY . .
6+
RUN npm run build
7+
8+
FROM node:18-alpine AS web-runtime
9+
WORKDIR /app
10+
ENV NODE_ENV=production
11+
COPY --from=web-build /app/ .
12+
EXPOSE 3000
13+
CMD ["npm", "run", "start"]

web/components/Editor.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import dynamic from 'next/dynamic'
2+
import React from 'react'
3+
4+
const Monaco = dynamic(() => import('@monaco-editor/react'), { ssr: false })
5+
6+
type Props = {
7+
value: string
8+
language?: string
9+
onChange?: (v: string | undefined) => void
10+
height?: string | number
11+
}
12+
13+
export default function Editor({ value, language = 'yaml', onChange, height = '60vh' }: Props) {
14+
return (
15+
// @ts-ignore - dynamic import types
16+
<Monaco
17+
value={value}
18+
language={language}
19+
height={height}
20+
onChange={onChange}
21+
options={{ automaticLayout: true, tabSize: 2 }}
22+
/>
23+
)
24+
}

web/lib/ws.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export function connectWS(room: string, onMessage: (m: string) => void) {
2+
const backend = process.env.NEXT_PUBLIC_BACKEND_URL || process.env.BACKEND_URL || 'http://localhost:8080'
3+
const url = backend.replace(/^http/, 'ws') + `/ws?room=${encodeURIComponent(room)}`
4+
try {
5+
const ws = new WebSocket(url)
6+
ws.addEventListener('message', (ev) => onMessage(ev.data))
7+
ws.addEventListener('open', () => onMessage('[ws] connected'))
8+
ws.addEventListener('close', () => onMessage('[ws] disconnected'))
9+
ws.addEventListener('error', () => onMessage('[ws] error'))
10+
return ws
11+
} catch (err) {
12+
onMessage('[ws] connection failed')
13+
return null
14+
}
15+
}

web/next.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/** @type {import('next').NextConfig} */
2+
const nextConfig = {
3+
reactStrictMode: true,
4+
}
5+
6+
module.exports = nextConfig

web/package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "nexus-web",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "next dev",
7+
"build": "next build",
8+
"start": "next start -p $PORT"
9+
},
10+
"dependencies": {
11+
"next": "14.0.0",
12+
"react": "18.2.0",
13+
"react-dom": "18.2.0",
14+
"@monaco-editor/react": "^5.0.1"
15+
},
16+
"devDependencies": {
17+
"typescript": "5.5.0"
18+
}
19+
}

web/pages/_app.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import '../styles/globals.css'
2+
import type { AppProps } from 'next/app'
3+
4+
export default function App({ Component, pageProps }: AppProps) {
5+
return <Component {...pageProps} />
6+
}

web/pages/collections/[name].tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React, { useEffect, useState } from 'react'
2+
import { useRouter } from 'next/router'
3+
import { connectWS } from '../../lib/ws'
4+
import Editor from '../../components/Editor'
5+
6+
export default function CollectionPage() {
7+
const router = useRouter()
8+
const name = Array.isArray(router.query.name) ? router.query.name[0] : router.query.name || ''
9+
10+
const [content, setContent] = useState('')
11+
const [status, setStatus] = useState<string | null>(null)
12+
const [wsLog, setWsLog] = useState<string[]>([])
13+
const [ws, setWs] = useState<WebSocket | null>(null)
14+
15+
useEffect(() => {
16+
if (!name) return
17+
const backend = process.env.NEXT_PUBLIC_BACKEND_URL || process.env.BACKEND_URL || 'http://localhost:8080'
18+
fetch(`${backend}/api/collections/get?name=${encodeURIComponent(name)}`)
19+
.then((r) => r.json())
20+
.then((data) => {
21+
setContent(JSON.stringify(data, null, 2))
22+
})
23+
.catch((e) => setStatus(String(e)))
24+
25+
const socket = connectWS(name, (m) => setWsLog((s) => [...s, m]))
26+
setWs(socket)
27+
return () => {
28+
try {
29+
socket?.close()
30+
} catch {}
31+
}
32+
}, [name])
33+
34+
function save() {
35+
const backend = process.env.NEXT_PUBLIC_BACKEND_URL || process.env.BACKEND_URL || 'http://localhost:8080'
36+
fetch(`${backend}/api/collections/save`, {
37+
method: 'POST',
38+
headers: { 'Content-Type': 'application/json' },
39+
body: JSON.stringify({ name, content }),
40+
})
41+
.then((r) => r.json())
42+
.then(() => setStatus('saved'))
43+
.catch((e) => setStatus(String(e)))
44+
}
45+
46+
function run() {
47+
const backend = process.env.NEXT_PUBLIC_BACKEND_URL || process.env.BACKEND_URL || 'http://localhost:8080'
48+
setStatus('running')
49+
fetch(`${backend}/api/run`, {
50+
method: 'POST',
51+
headers: { 'Content-Type': 'application/json' },
52+
body: JSON.stringify({ name }),
53+
})
54+
.then((r) => r.json())
55+
.then((data) => setStatus(JSON.stringify(data)))
56+
.catch((e) => setStatus(String(e)))
57+
}
58+
59+
return (
60+
<main style={{ padding: 24 }}>
61+
<h1>Collection: {name}</h1>
62+
<div style={{ marginBottom: 8 }}>
63+
<button onClick={save} style={{ marginRight: 8 }}>Save</button>
64+
<button onClick={run}>Run</button>
65+
</div>
66+
67+
<Editor value={content} onChange={(v) => setContent(v ?? '')} height="60vh" />
68+
69+
<div style={{ marginTop: 12 }}>
70+
<strong>Status:</strong> {status}
71+
</div>
72+
73+
<div style={{ marginTop: 12 }}>
74+
<strong>WS log:</strong>
75+
<div style={{ maxHeight: 200, overflow: 'auto', border: '1px solid #eee', padding: 8 }}>
76+
{wsLog.map((l, i) => (
77+
<div key={i}>{l}</div>
78+
))}
79+
</div>
80+
</div>
81+
</main>
82+
)
83+
}

web/pages/collections/index.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from 'react'
2+
import Link from 'next/link'
3+
import type { GetServerSideProps } from 'next'
4+
5+
type Props = { collections: string[] }
6+
7+
export default function Collections({ collections }: Props) {
8+
return (
9+
<main style={{ padding: 24 }}>
10+
<h1>Collections</h1>
11+
<ul>
12+
{collections.map((c) => (
13+
<li key={c}>
14+
<Link href={`/collections/${encodeURIComponent(c)}`}>{c}</Link>
15+
</li>
16+
))}
17+
</ul>
18+
</main>
19+
)
20+
}
21+
22+
export const getServerSideProps: GetServerSideProps<Props> = async () => {
23+
const backend = process.env.BACKEND_URL || 'http://localhost:8080'
24+
try {
25+
const res = await fetch(`${backend}/api/collections`)
26+
const data = await res.json()
27+
return { props: { collections: data.collections || [] } }
28+
} catch (err) {
29+
return { props: { collections: [] } }
30+
}
31+
}

web/pages/index.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react'
2+
import type { GetServerSideProps } from 'next'
3+
4+
type Props = {
5+
collections: string[]
6+
error?: string
7+
}
8+
9+
export default function Home({ collections, error }: Props) {
10+
if (error) return <div>Error: {error}</div>
11+
12+
return (
13+
<main style={{ padding: 24, fontFamily: 'Inter, system-ui' }}>
14+
<h1>Nexus — Collections</h1>
15+
<p>Collections stored in repository</p>
16+
<ul>
17+
{collections.map((c) => (
18+
<li key={c}>{c}</li>
19+
))}
20+
</ul>
21+
</main>
22+
)
23+
}
24+
25+
export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
26+
const backend = process.env.BACKEND_URL || 'http://localhost:8080'
27+
try {
28+
const res = await fetch(`${backend}/api/collections`)
29+
if (!res.ok) {
30+
return { props: { collections: [], error: `backend responded ${res.status}` } }
31+
}
32+
const data = await res.json()
33+
return { props: { collections: data.collections || [] } }
34+
} catch (err: any) {
35+
return { props: { collections: [], error: err.message } }
36+
}
37+
}

web/styles/globals.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
html, body, #__next {
2+
height: 100%;
3+
}
4+
body {
5+
margin: 0;
6+
font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
7+
}

0 commit comments

Comments
 (0)