Files
paliad/frontend/src/jsx.ts
m 40a9c927fb feat: rewrite frontend from Go templates to Bun + TSX
Replace Go HTML template rendering with a Bun + TSX build-time static
site generator. Go backend becomes API-only for auth.

Frontend:
- Custom JSX-to-HTML-string factory (zero dependencies)
- TSX components for Header, Footer, index page, login page
- Client-side login.ts handles tab switching and fetch()-based auth
- Bun bundler compiles client JS, build.ts renders pages to dist/

Backend:
- Auth handlers return JSON (POST /api/login, POST /api/register)
- Login page served as static HTML from dist/
- Static assets served from /assets/ (public)
- Auth middleware unchanged (cookie check, redirect to /login)
- Removed template parsing and renderPage

Dockerfile:
- 3-stage build: Bun frontend -> Go backend -> alpine runtime
- Frontend dist copied to /app/dist in final image

Removed: templates/, static/css/ (replaced by frontend/)
2026-04-14 16:50:27 +02:00

63 lines
1.6 KiB
TypeScript

const VOID_ELEMENTS = new Set([
"area", "base", "br", "col", "embed", "hr", "img", "input",
"link", "meta", "param", "source", "track", "wbr",
]);
const ATTR_MAP: Record<string, string> = {
className: "class",
htmlFor: "for",
};
function escapeAttr(s: string): string {
return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
}
function flatten(children: any[]): string {
return children
.flat(Infinity)
.filter((c) => c != null && c !== false && c !== true)
.map((c) => String(c))
.join("");
}
export function h(
tag: string | ((props: any) => string),
props: Record<string, any> | null,
...children: any[]
): string {
if (typeof tag === "function") {
return tag({ ...props, children: children.length === 1 ? children[0] : children });
}
let attrs = "";
let innerHTML = "";
if (props) {
for (const [key, value] of Object.entries(props)) {
if (key === "children") continue;
if (key === "dangerouslySetInnerHTML") {
innerHTML = value.__html;
continue;
}
if (value == null || value === false) continue;
const name = ATTR_MAP[key] || key;
if (value === true) {
attrs += ` ${name}`;
} else {
attrs += ` ${name}="${escapeAttr(String(value))}"`;
}
}
}
if (VOID_ELEMENTS.has(tag)) {
return `<${tag}${attrs}>`;
}
const content = innerHTML || flatten(children);
return `<${tag}${attrs}>${content}</${tag}>`;
}
export function Fragment({ children }: { children: any }): string {
return Array.isArray(children) ? flatten(children) : String(children ?? "");
}