diff --git a/frontend/build.ts b/frontend/build.ts
index f4a3494..cc3f867 100644
--- a/frontend/build.ts
+++ b/frontend/build.ts
@@ -5,6 +5,7 @@ import { renderLogin } from "./src/login";
import { renderKostenrechner } from "./src/kostenrechner";
import { renderFristenrechner } from "./src/fristenrechner";
import { renderDownloads } from "./src/downloads";
+import { renderGlossar } from "./src/glossar";
const DIST = join(import.meta.dir, "dist");
@@ -21,6 +22,7 @@ async function build() {
join(import.meta.dir, "src/client/kostenrechner.ts"),
join(import.meta.dir, "src/client/fristenrechner.ts"),
join(import.meta.dir, "src/client/downloads.ts"),
+ join(import.meta.dir, "src/client/glossar.ts"),
],
outdir: join(DIST, "assets"),
naming: "[name].js",
@@ -47,6 +49,7 @@ async function build() {
await Bun.write(join(DIST, "kostenrechner.html"), renderKostenrechner());
await Bun.write(join(DIST, "fristenrechner.html"), renderFristenrechner());
await Bun.write(join(DIST, "downloads.html"), renderDownloads());
+ await Bun.write(join(DIST, "glossar.html"), renderGlossar());
console.log("Build complete \u2192 dist/");
}
diff --git a/frontend/src/client/glossar.ts b/frontend/src/client/glossar.ts
new file mode 100644
index 0000000..72f067c
--- /dev/null
+++ b/frontend/src/client/glossar.ts
@@ -0,0 +1,207 @@
+import { initI18n, onLangChange, t } from "./i18n";
+import { initSidebar } from "./sidebar";
+
+interface Term {
+ de: string;
+ en: string;
+ definition?: string;
+ category: string;
+}
+
+let allTerms: Term[] = [];
+let activeCategory = "all";
+let searchQuery = "";
+
+const ICON_FEEDBACK = '';
+
+async function loadTerms() {
+ const resp = await fetch("/api/glossar");
+ if (!resp.ok) return;
+ allTerms = await resp.json();
+ render();
+}
+
+function getFilteredTerms(): Term[] {
+ let terms = allTerms;
+ if (activeCategory !== "all") {
+ terms = terms.filter((t) => t.category === activeCategory);
+ }
+ if (searchQuery) {
+ const q = searchQuery.toLowerCase();
+ terms = terms.filter(
+ (t) =>
+ t.de.toLowerCase().includes(q) ||
+ t.en.toLowerCase().includes(q) ||
+ (t.definition && t.definition.toLowerCase().includes(q))
+ );
+ }
+ return terms;
+}
+
+function render() {
+ const tbody = document.getElementById("glossar-body")!;
+ const empty = document.getElementById("glossar-empty")!;
+ const count = document.getElementById("glossar-count")!;
+ const filtered = getFilteredTerms();
+
+ count.textContent = `${filtered.length} / ${allTerms.length}`;
+
+ if (filtered.length === 0) {
+ tbody.innerHTML = "";
+ empty.style.display = "block";
+ return;
+ }
+ empty.style.display = "none";
+
+ tbody.innerHTML = filtered
+ .map(
+ (term) => `
+ | ${esc(term.de)} |
+ ${esc(term.en)} |
+ ${term.definition ? esc(term.definition) : '\u2014'} |
+ |
+
`
+ )
+ .join("");
+
+ // Attach feedback button listeners
+ tbody.querySelectorAll(".glossar-feedback-btn").forEach((btn) => {
+ btn.addEventListener("click", () => {
+ openSuggestModal("correction", btn.dataset.termDe ?? "", btn.dataset.termEn ?? "", btn.dataset.termCat ?? "General");
+ });
+ });
+}
+
+function esc(s: string): string {
+ const d = document.createElement("div");
+ d.textContent = s;
+ return d.innerHTML;
+}
+
+// --- Search ---
+function initSearch() {
+ const input = document.getElementById("glossar-search") as HTMLInputElement;
+ input.addEventListener("input", () => {
+ searchQuery = input.value;
+ render();
+ });
+}
+
+// --- Category filters ---
+function initFilters() {
+ const container = document.getElementById("glossar-filters")!;
+ container.addEventListener("click", (e) => {
+ const btn = (e.target as HTMLElement).closest(".filter-pill");
+ if (!btn) return;
+ container.querySelectorAll(".filter-pill").forEach((p) => p.classList.remove("active"));
+ btn.classList.add("active");
+ activeCategory = btn.dataset.category ?? "all";
+ render();
+ });
+}
+
+// --- Suggestion modal ---
+function openSuggestModal(type: "new" | "correction", termDE = "", termEN = "", category = "General") {
+ const modal = document.getElementById("suggest-modal")!;
+ const titleEl = document.getElementById("modal-title")!;
+ const typeInput = document.getElementById("suggest-type") as HTMLInputElement;
+ const existingInput = document.getElementById("suggest-existing") as HTMLInputElement;
+ const deInput = document.getElementById("suggest-de") as HTMLInputElement;
+ const enInput = document.getElementById("suggest-en") as HTMLInputElement;
+ const defInput = document.getElementById("suggest-def") as HTMLTextAreaElement;
+ const catInput = document.getElementById("suggest-cat") as HTMLSelectElement;
+ const msg = document.getElementById("suggest-msg")!;
+
+ typeInput.value = type;
+ msg.textContent = "";
+ msg.className = "form-msg";
+
+ if (type === "correction") {
+ titleEl.textContent = t("glossar.feedback.title");
+ existingInput.value = termDE;
+ deInput.value = termDE;
+ enInput.value = termEN;
+ defInput.value = "";
+ catInput.value = category;
+ } else {
+ titleEl.textContent = t("glossar.suggest.title");
+ existingInput.value = "";
+ deInput.value = "";
+ enInput.value = "";
+ defInput.value = "";
+ catInput.value = "General";
+ }
+
+ modal.style.display = "flex";
+ deInput.focus();
+}
+
+function closeModal() {
+ document.getElementById("suggest-modal")!.style.display = "none";
+}
+
+async function submitSuggestion(e: Event) {
+ e.preventDefault();
+ const msg = document.getElementById("suggest-msg")!;
+ const submitBtn = document.querySelector("#suggest-form .btn-submit") as HTMLButtonElement;
+
+ const payload = {
+ term_de: (document.getElementById("suggest-de") as HTMLInputElement).value.trim(),
+ term_en: (document.getElementById("suggest-en") as HTMLInputElement).value.trim(),
+ definition: (document.getElementById("suggest-def") as HTMLTextAreaElement).value.trim(),
+ category: (document.getElementById("suggest-cat") as HTMLSelectElement).value,
+ suggestion_type: (document.getElementById("suggest-type") as HTMLInputElement).value,
+ existing_term_de: (document.getElementById("suggest-existing") as HTMLInputElement).value,
+ };
+
+ if (!payload.term_de || !payload.term_en) {
+ msg.textContent = t("glossar.suggest.error.required");
+ msg.className = "form-msg form-msg-error";
+ return;
+ }
+
+ submitBtn.disabled = true;
+ try {
+ const resp = await fetch("/api/glossar/suggest", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+ if (!resp.ok) {
+ const data = await resp.json();
+ msg.textContent = data.error || t("glossar.suggest.error.generic");
+ msg.className = "form-msg form-msg-error";
+ return;
+ }
+ msg.textContent = t("glossar.suggest.success");
+ msg.className = "form-msg form-msg-success";
+ setTimeout(closeModal, 1500);
+ } catch {
+ msg.textContent = t("glossar.suggest.error.generic");
+ msg.className = "form-msg form-msg-error";
+ } finally {
+ submitBtn.disabled = false;
+ }
+}
+
+function initModal() {
+ document.getElementById("btn-suggest-new")!.addEventListener("click", () => {
+ openSuggestModal("new");
+ });
+ document.getElementById("modal-close")!.addEventListener("click", closeModal);
+ document.getElementById("modal-cancel")!.addEventListener("click", closeModal);
+ document.getElementById("suggest-modal")!.addEventListener("click", (e) => {
+ if (e.target === e.currentTarget) closeModal();
+ });
+ document.getElementById("suggest-form")!.addEventListener("submit", submitSuggestion);
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ initI18n();
+ initSidebar();
+ initSearch();
+ initFilters();
+ initModal();
+ onLangChange(render);
+ loadTerms();
+});
diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts
index 884821e..e423d45 100644
--- a/frontend/src/client/i18n.ts
+++ b/frontend/src/client/i18n.ts
@@ -15,6 +15,7 @@ const translations: Record> = {
"nav.kostenrechner": "Kostenrechner",
"nav.fristenrechner": "Fristenrechner",
"nav.downloads": "Downloads",
+ "nav.glossar": "Glossar",
"nav.logout": "Abmelden",
// Footer
@@ -35,6 +36,8 @@ const translations: Record> = {
"index.cost.desc": "Sch\u00e4tzung der Verfahrenskosten f\u00fcr DE-Gerichte, UPC und EPA-Verfahren. Gerichts- und Anwaltskosten auf einen Blick.",
"index.deadline.title": "Fristenrechner",
"index.deadline.desc": "Berechnung von Verfahrensfristen f\u00fcr UPC-, deutsche und EPA-Verfahren mit Feiertags-Anpassung.",
+ "index.glossar.title": "Patentglossar",
+ "index.glossar.desc": "Zweisprachiges DE/EN-Glossar der wichtigsten Begriffe im Patentrecht. Durchsuchbar nach Kategorien.",
"index.downloads": "Downloads",
"index.style.title": "HL Patents Style",
"index.style.desc": "Word-Vorlage im HL Patents Style. Formatierung, Schriftarten und Makros f\u00fcr standardisierte Schrifts\u00e4tze.",
@@ -153,6 +156,33 @@ const translations: Record> = {
"downloads.style.title": "HL Patents Style",
"downloads.style.desc": "Word-Vorlage im HL Patents Style. Formatierung, Schriftarten und Makros f\u00fcr standardisierte Schrifts\u00e4tze.",
"downloads.btn": "Herunterladen",
+
+ // Glossar
+ "glossar.title": "Patentglossar \u2014 patHoLo",
+ "glossar.heading": "Patentglossar",
+ "glossar.subtitle": "Zweisprachiges Glossar der wichtigsten Begriffe im Patentrecht.",
+ "glossar.search.placeholder": "Suchen...",
+ "glossar.filter.all": "Alle",
+ "glossar.filter.litigation": "Litigation",
+ "glossar.filter.prosecution": "Prosecution",
+ "glossar.filter.general": "Allgemein",
+ "glossar.col.de": "Deutsch",
+ "glossar.col.en": "English",
+ "glossar.col.definition": "Definition",
+ "glossar.empty": "Keine Treffer.",
+ "glossar.suggest": "Begriff vorschlagen",
+ "glossar.suggest.title": "Neuen Begriff vorschlagen",
+ "glossar.suggest.de": "Deutscher Begriff",
+ "glossar.suggest.en": "Englischer Begriff",
+ "glossar.suggest.definition": "Definition (optional)",
+ "glossar.suggest.category": "Kategorie",
+ "glossar.suggest.cancel": "Abbrechen",
+ "glossar.suggest.submit": "Absenden",
+ "glossar.suggest.success": "Vorschlag eingereicht. Vielen Dank!",
+ "glossar.suggest.error.required": "Bitte DE und EN Begriff eingeben.",
+ "glossar.suggest.error.generic": "Fehler beim Senden. Bitte versuchen Sie es erneut.",
+ "glossar.feedback.title": "Korrektur vorschlagen",
+ "glossar.feedback.tooltip": "Korrektur vorschlagen",
},
en: {
@@ -161,6 +191,7 @@ const translations: Record> = {
"nav.kostenrechner": "Cost Calculator",
"nav.fristenrechner": "Deadline Calculator",
"nav.downloads": "Downloads",
+ "nav.glossar": "Glossary",
"nav.logout": "Sign Out",
// Footer
@@ -181,6 +212,8 @@ const translations: Record> = {
"index.cost.desc": "Estimate litigation costs for DE courts, UPC, and EPA proceedings. Court and attorney fees at a glance.",
"index.deadline.title": "Deadline Calculator",
"index.deadline.desc": "Calculate procedural deadlines for UPC, German, and EPA proceedings with holiday adjustment.",
+ "index.glossar.title": "Patent Glossary",
+ "index.glossar.desc": "Bilingual DE/EN glossary of key patent law terminology. Searchable by category.",
"index.downloads": "Downloads",
"index.style.title": "HL Patents Style",
"index.style.desc": "Word template in HL Patents style. Formatting, fonts, and macros for standardised briefs.",
@@ -299,6 +332,33 @@ const translations: Record> = {
"downloads.style.title": "HL Patents Style",
"downloads.style.desc": "Word template in HL Patents style. Formatting, fonts, and macros for standardised briefs.",
"downloads.btn": "Download",
+
+ // Glossar
+ "glossar.title": "Patent Glossary \u2014 patHoLo",
+ "glossar.heading": "Patent Glossary",
+ "glossar.subtitle": "Bilingual glossary of key patent law terminology.",
+ "glossar.search.placeholder": "Search...",
+ "glossar.filter.all": "All",
+ "glossar.filter.litigation": "Litigation",
+ "glossar.filter.prosecution": "Prosecution",
+ "glossar.filter.general": "General",
+ "glossar.col.de": "German",
+ "glossar.col.en": "English",
+ "glossar.col.definition": "Definition",
+ "glossar.empty": "No matches found.",
+ "glossar.suggest": "Suggest a term",
+ "glossar.suggest.title": "Suggest a new term",
+ "glossar.suggest.de": "German term",
+ "glossar.suggest.en": "English term",
+ "glossar.suggest.definition": "Definition (optional)",
+ "glossar.suggest.category": "Category",
+ "glossar.suggest.cancel": "Cancel",
+ "glossar.suggest.submit": "Submit",
+ "glossar.suggest.success": "Suggestion submitted. Thank you!",
+ "glossar.suggest.error.required": "Please enter both DE and EN terms.",
+ "glossar.suggest.error.generic": "Error submitting. Please try again.",
+ "glossar.feedback.title": "Suggest a correction",
+ "glossar.feedback.tooltip": "Suggest a correction",
},
};
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index d3c74d6..47c5cb1 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -4,6 +4,7 @@ const ICON_HOME = '