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 = ' diff --git a/frontend/src/glossar.tsx b/frontend/src/glossar.tsx new file mode 100644 index 0000000..9b1144e --- /dev/null +++ b/frontend/src/glossar.tsx @@ -0,0 +1,128 @@ +import { h } from "./jsx"; +import { Sidebar } from "./components/Sidebar"; +import { Footer } from "./components/Footer"; + +export function renderGlossar(): string { + return "" + ( + + + + + Patentglossar — patHoLo + + + + + +
+
+
+
+
+
+

Patentglossar

+

+ Zweisprachiges Glossar der wichtigsten Begriffe im Patentrecht. +

+
+ +
+
+ +
+
+ + + + + + +
+
+ + + + + + +
+
+ +
+ + + + + + + + + +
DeutschEnglishDefinition +
+
+ + + + {/* Suggestion modal */} +