Skip to content

Commit

Permalink
feat: Add Web/UI
Browse files Browse the repository at this point in the history
  • Loading branch information
PartMan7 committed Oct 29, 2024
1 parent f38a73a commit 515923c
Show file tree
Hide file tree
Showing 11 changed files with 102 additions and 7 deletions.
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import '@/globals';

log('PartBot is starting up...');

import '@/ps';
import '@/discord';
import '@/web';
Expand Down
11 changes: 9 additions & 2 deletions src/types/web.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import type { Request, Response } from 'express';
import type { ReactElement } from 'react';

export type RouteHandler = (req: Request, res: Response) => void;
export type RouteHandler = (req: Request, res: Response & { render: Render }) => void;

export type Route = {
export type APIRoute = {
handler: RouteHandler;
verb?: 'get' | 'post';
};

export type UIRoute = {
handler: RouteHandler;
};

export type Render = (jsx: ReactElement, title: string, hydrate: boolean) => Promise<Response>;
5 changes: 3 additions & 2 deletions src/web/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import express from 'express';
import { port } from '@/config/web';

import loadAPI from '@/web/loaders/api';
import loadUI from '@/web/loaders/ui';

const app = express();

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

loadAPI(app);
loadAPI(app).then(() => loadUI(app));

app.listen(port, () => log(`Web API is running!`));
app.listen(port, () => log(`Web is running!`));

export default app;
4 changes: 2 additions & 2 deletions src/web/loaders/api.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Router, Application } from 'express';
import { readFileStructure } from '@/web/loaders/util';
import type { Route } from '@/types/web';
import type { APIRoute } from '@/types/web';

export default async function init(app: Application): Promise<void> {
const router = Router();

const routes = await readFileStructure(fsPath('web', 'api'));
await Promise.all(
Object.entries(routes).map(async ([urlPath, filepath]) => {
const route: Route = await import(filepath);
const route: APIRoute = await import(filepath);
router[route.verb ?? 'get'](urlPath, route.handler);
})
);
Expand Down
21 changes: 21 additions & 0 deletions src/web/loaders/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { renderToString } from 'react-dom/server';
import { jsxToHTML } from '@/utils/jsx-to-html';
import { renderTemplate } from '@/web/loaders/util';

import type { Request, Response, NextFunction } from 'express';
import type { Render } from '@/types/web';

export function renderReact(req: Request, res: Response, next: NextFunction): void {
const render: Render = async (jsx, title, hydrate) => {
if (!hydrate) {
const content = jsxToHTML(jsx);
const page = await renderTemplate('static-react.html', { title, content });
return res.send(page);
}
const preHydrated = renderToString(jsx);
const page = await renderTemplate('react.html', { title, preHydrated });
return res.send(page);
};
Object.assign(res, { render });
next();
}
22 changes: 22 additions & 0 deletions src/web/loaders/ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Router, Application } from 'express';
import { readFileStructure } from '@/web/loaders/util';

import { renderReact } from '@/web/loaders/middleware';

import type { UIRoute } from '@/types/web';

export default async function init(app: Application): Promise<void> {
const router = Router();

router.use(renderReact);

const routes = await readFileStructure(fsPath('web', 'ui'));
await Promise.all(
Object.entries(routes).map(async ([urlPath, filepath]) => {
const route: UIRoute = await import(filepath);
router.get(urlPath, route.handler);
})
);

app.use(router);
}
8 changes: 8 additions & 0 deletions src/web/loaders/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,11 @@ export async function readFileStructure(root: string): Promise<Record<string, st
return acc;
}, {});
}

export async function renderTemplate(path: string, variables: Record<string, string> = {}): Promise<string> {
const baseTemplate = await fs.readFile(fsPath('web', 'templates', path), 'utf8');
return Object.entries(variables).reduce(
(template, [variable, value]) => template.replaceAll(`{{${variable}}}`, value),
baseTemplate
);
}
17 changes: 17 additions & 0 deletions src/web/templates/react.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>{{title}}</title>
</head>

<body id="react-root">
{{preHydrated}}
</body>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script>
ReactDOM.hydrateRoot({{content}}); // Okay crap this won't work; TODO
</script>
</html>
12 changes: 12 additions & 0 deletions src/web/templates/static-react.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>{{title}}</title>
</head>

<body id="react-root">
{{content}}
</body>
</html>
5 changes: 5 additions & 0 deletions src/web/ui/test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const Div = () => <div>Test content here; fully static</div>;

export const handler: RouteHandler = (req, res) => {
return res.render(<Div />, 'Test Title', false);
};
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"baseUrl": "src",
"target": "es2020",
"target": "es2021",
"noEmit": true, // We run PartBot with ts-node instead of a two-step process
"jsx": "react",
"module": "commonjs", // 'require' syntax makes HMR possible; 'import' doesn't
Expand Down

0 comments on commit 515923c

Please sign in to comment.