diff --git a/.dockerignore b/.dockerignore index a13dacc..1004b80 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,3 +4,5 @@ .m *.md !README.md +frontend/node_modules +frontend/dist diff --git a/.gitignore b/.gitignore index 9ee729e..54dce20 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ /patholo *.exe +# Frontend +frontend/node_modules/ +frontend/dist/ + # IDE .vscode/ .idea/ diff --git a/Dockerfile b/Dockerfile index ab0e137..baffe2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,9 @@ -FROM golang:1.23-alpine AS build +FROM oven/bun:1 AS frontend +WORKDIR /app/frontend +COPY frontend/ . +RUN bun install && bun run build +FROM golang:1.23-alpine AS backend WORKDIR /src COPY go.mod ./ RUN go mod download @@ -9,9 +13,7 @@ RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /patholo ./cmd/server FROM alpine:3.21 RUN apk add --no-cache ca-certificates WORKDIR /app -COPY --from=build /patholo /app/patholo -COPY templates/ /app/templates/ -COPY static/ /app/static/ - +COPY --from=backend /patholo /app/patholo +COPY --from=frontend /app/frontend/dist /app/dist EXPOSE 8080 CMD ["/app/patholo"] diff --git a/frontend/build.ts b/frontend/build.ts new file mode 100644 index 0000000..eac4ac2 --- /dev/null +++ b/frontend/build.ts @@ -0,0 +1,42 @@ +import { mkdir, cp, rm } from "fs/promises"; +import { join } from "path"; +import { renderIndex } from "./src/index"; +import { renderLogin } from "./src/login"; + +const DIST = join(import.meta.dir, "dist"); + +async function build() { + // Clean dist/ + await rm(DIST, { recursive: true, force: true }); + await mkdir(join(DIST, "assets"), { recursive: true }); + + // Bundle client-side JS + const result = await Bun.build({ + entrypoints: [join(import.meta.dir, "src/client/login.ts")], + outdir: join(DIST, "assets"), + naming: "[name].js", + minify: true, + }); + + if (!result.success) { + console.error("JS build failed:"); + for (const log of result.logs) { + console.error(log); + } + process.exit(1); + } + + // Copy CSS + await cp( + join(import.meta.dir, "src/styles/global.css"), + join(DIST, "assets/global.css"), + ); + + // Render HTML pages + await Bun.write(join(DIST, "index.html"), renderIndex()); + await Bun.write(join(DIST, "login.html"), renderLogin("login.js")); + + console.log("Build complete \u2192 dist/"); +} + +build(); diff --git a/frontend/bun.lock b/frontend/bun.lock new file mode 100644 index 0000000..9416c8e --- /dev/null +++ b/frontend/bun.lock @@ -0,0 +1,21 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "patholo-frontend", + "devDependencies": { + "@types/bun": "latest", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], + + "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + + "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], + + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + } +} diff --git a/frontend/bunfig.toml b/frontend/bunfig.toml new file mode 100644 index 0000000..74d5f21 --- /dev/null +++ b/frontend/bunfig.toml @@ -0,0 +1,2 @@ +[install] +peer = false diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..142ffa8 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,10 @@ +{ + "name": "patholo-frontend", + "private": true, + "scripts": { + "build": "bun run build.ts" + }, + "devDependencies": { + "@types/bun": "latest" + } +} diff --git a/frontend/src/client/login.ts b/frontend/src/client/login.ts new file mode 100644 index 0000000..0f45402 --- /dev/null +++ b/frontend/src/client/login.ts @@ -0,0 +1,108 @@ +document.addEventListener("DOMContentLoaded", () => { + const tabs = document.querySelectorAll(".login-tab"); + const loginForm = document.getElementById("login-form") as HTMLFormElement; + const registerForm = document.getElementById("register-form") as HTMLFormElement; + + // Tab switching + tabs.forEach((btn) => { + btn.addEventListener("click", () => { + tabs.forEach((t) => t.classList.remove("active")); + btn.classList.add("active"); + const isLogin = btn.dataset.tab === "login"; + loginForm.style.display = isLogin ? "" : "none"; + registerForm.style.display = isLogin ? "none" : ""; + clearMessages(); + }); + }); + + // Login form + loginForm.addEventListener("submit", async (e) => { + e.preventDefault(); + clearMessages(); + const data = new FormData(loginForm); + + try { + const res = await fetch("/api/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: data.get("email"), + password: data.get("password"), + }), + }); + const json = await res.json(); + if (res.ok) { + window.location.href = "/"; + } else { + showError(json.error); + } + } catch { + showError("Verbindungsfehler. Bitte versuchen Sie es erneut."); + } + }); + + // Register form + registerForm.addEventListener("submit", async (e) => { + e.preventDefault(); + clearMessages(); + const data = new FormData(registerForm); + const password = data.get("password") as string; + const confirm = data.get("confirm") as string; + + if (password !== confirm) { + showError("Passw\u00F6rter stimmen nicht \u00FCberein."); + return; + } + if (password.length < 8) { + showError("Passwort muss mindestens 8 Zeichen lang sein."); + return; + } + + try { + const res = await fetch("/api/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: data.get("email"), + password: password, + }), + }); + const json = await res.json(); + if (res.ok) { + if (json.redirect) { + window.location.href = json.redirect; + } else { + showSuccess(json.message || "Account erstellt. Bitte melden Sie sich an."); + tabs[0].click(); + } + } else { + showError(json.error); + } + } catch { + showError("Verbindungsfehler. Bitte versuchen Sie es erneut."); + } + }); +}); + +function clearMessages() { + document.querySelectorAll(".login-error, .login-success").forEach((el) => el.remove()); +} + +function showError(msg: string) { + showMessage(msg, "login-error"); +} + +function showSuccess(msg: string) { + showMessage(msg, "login-success"); +} + +function showMessage(msg: string, cls: string) { + clearMessages(); + const div = document.createElement("div"); + div.className = cls; + div.textContent = msg; + const tabs = document.querySelector(".login-tabs"); + if (tabs) { + tabs.after(div); + } +} diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx new file mode 100644 index 0000000..a50e74e --- /dev/null +++ b/frontend/src/components/Footer.tsx @@ -0,0 +1,11 @@ +import { h } from "../jsx"; + +export function Footer(): string { + return ( + + ); +} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx new file mode 100644 index 0000000..ae91800 --- /dev/null +++ b/frontend/src/components/Header.tsx @@ -0,0 +1,30 @@ +import { h } from "../jsx"; + +interface HeaderProps { + showLogout?: boolean; +} + +export function Header({ showLogout }: HeaderProps): string { + return ( +
+
+ +
+
+ ); +} diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx new file mode 100644 index 0000000..75bff9a --- /dev/null +++ b/frontend/src/index.tsx @@ -0,0 +1,74 @@ +import { h } from "./jsx"; +import { Header } from "./components/Header"; +import { Footer } from "./components/Footer"; + +const ICON_BOOK = ''; +const ICON_FILE = ''; +const ICON_FOLDER = ''; + +export function renderIndex(): string { + return "" + ( + + + + + patholo — Patent Knowledge for Hogan Lovells + + + +
+ +
+
+
+

Patent Knowledge
für Hogan Lovells

+

+ Leitfäden, Vorlagen und Dokumente für das HL Patent-Team. +
+ Guides, templates, and documents for the HL patent team. +

+
+
+ +
+
+
+
+
+

Leitfäden Guides

+

Praxisleitfäden zu Verfahren vor dem EPA, BPatG und UPC. Schritt-für-Schritt-Anleitungen für typische Workflows.

+
+ +
+
+

Vorlagen Templates

+

Standardisierte Vorlagen für Schriftsätze, Korrespondenz und interne Dokumente. HL Patents Style Guide.

+
+ +
+
+

Dokumente Documents

+

Referenzmaterialien, Checklisten und Arbeitshilfen für den Praxisalltag im Patentrecht.

+
+
+
+
+ +
+
+

Standorte Offices

+
+ München + Düsseldorf + Amsterdam + London +
+
+
+
+ +