feat: searchable DE/EN patent glossary with term suggestions

- New /glossar page with 73 bilingual patent law terms across 5 categories
  (Litigation, Prosecution, UPC, EPA, General)
- Client-side instant search filtering both DE and EN columns
- Category filter pills for quick narrowing
- Suggest-a-term button opens modal form for new term submissions
- Per-term feedback icon for correction suggestions
- Suggestions stored in Supabase (glossar_suggestions table with RLS)
- Go API: GET /api/glossar (terms JSON), POST /api/glossar/suggest
- Full DE/EN i18n support, responsive layout, print-friendly
- Added to sidebar nav, landing page tools section, and build pipeline
This commit is contained in:
m
2026-04-14 20:06:51 +02:00
parent a5fdaff909
commit 0960235bb8
9 changed files with 1019 additions and 1 deletions

View File

@@ -5,6 +5,7 @@ import { renderLogin } from "./src/login";
import { renderKostenrechner } from "./src/kostenrechner"; import { renderKostenrechner } from "./src/kostenrechner";
import { renderFristenrechner } from "./src/fristenrechner"; import { renderFristenrechner } from "./src/fristenrechner";
import { renderDownloads } from "./src/downloads"; import { renderDownloads } from "./src/downloads";
import { renderGlossar } from "./src/glossar";
const DIST = join(import.meta.dir, "dist"); 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/kostenrechner.ts"),
join(import.meta.dir, "src/client/fristenrechner.ts"), join(import.meta.dir, "src/client/fristenrechner.ts"),
join(import.meta.dir, "src/client/downloads.ts"), join(import.meta.dir, "src/client/downloads.ts"),
join(import.meta.dir, "src/client/glossar.ts"),
], ],
outdir: join(DIST, "assets"), outdir: join(DIST, "assets"),
naming: "[name].js", naming: "[name].js",
@@ -47,6 +49,7 @@ async function build() {
await Bun.write(join(DIST, "kostenrechner.html"), renderKostenrechner()); await Bun.write(join(DIST, "kostenrechner.html"), renderKostenrechner());
await Bun.write(join(DIST, "fristenrechner.html"), renderFristenrechner()); await Bun.write(join(DIST, "fristenrechner.html"), renderFristenrechner());
await Bun.write(join(DIST, "downloads.html"), renderDownloads()); await Bun.write(join(DIST, "downloads.html"), renderDownloads());
await Bun.write(join(DIST, "glossar.html"), renderGlossar());
console.log("Build complete \u2192 dist/"); console.log("Build complete \u2192 dist/");
} }

View File

@@ -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 = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
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) => `<tr data-category="${term.category}">
<td class="glossar-de">${esc(term.de)}</td>
<td class="glossar-en">${esc(term.en)}</td>
<td class="glossar-def">${term.definition ? esc(term.definition) : '<span class="glossar-no-def">\u2014</span>'}</td>
<td class="glossar-actions"><button class="glossar-feedback-btn" type="button" title="${t("glossar.feedback.tooltip")}" data-term-de="${esc(term.de)}" data-term-en="${esc(term.en)}" data-term-cat="${term.category}">${ICON_FEEDBACK}</button></td>
</tr>`
)
.join("");
// Attach feedback button listeners
tbody.querySelectorAll<HTMLButtonElement>(".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<HTMLButtonElement>(".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();
});

View File

@@ -15,6 +15,7 @@ const translations: Record<Lang, Record<string, string>> = {
"nav.kostenrechner": "Kostenrechner", "nav.kostenrechner": "Kostenrechner",
"nav.fristenrechner": "Fristenrechner", "nav.fristenrechner": "Fristenrechner",
"nav.downloads": "Downloads", "nav.downloads": "Downloads",
"nav.glossar": "Glossar",
"nav.logout": "Abmelden", "nav.logout": "Abmelden",
// Footer // Footer
@@ -35,6 +36,8 @@ const translations: Record<Lang, Record<string, string>> = {
"index.cost.desc": "Sch\u00e4tzung der Verfahrenskosten f\u00fcr DE-Gerichte, UPC und EPA-Verfahren. Gerichts- und Anwaltskosten auf einen Blick.", "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.title": "Fristenrechner",
"index.deadline.desc": "Berechnung von Verfahrensfristen f\u00fcr UPC-, deutsche und EPA-Verfahren mit Feiertags-Anpassung.", "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.downloads": "Downloads",
"index.style.title": "HL Patents Style", "index.style.title": "HL Patents Style",
"index.style.desc": "Word-Vorlage im HL Patents Style. Formatierung, Schriftarten und Makros f\u00fcr standardisierte Schrifts\u00e4tze.", "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<Lang, Record<string, string>> = {
"downloads.style.title": "HL Patents Style", "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.style.desc": "Word-Vorlage im HL Patents Style. Formatierung, Schriftarten und Makros f\u00fcr standardisierte Schrifts\u00e4tze.",
"downloads.btn": "Herunterladen", "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: { en: {
@@ -161,6 +191,7 @@ const translations: Record<Lang, Record<string, string>> = {
"nav.kostenrechner": "Cost Calculator", "nav.kostenrechner": "Cost Calculator",
"nav.fristenrechner": "Deadline Calculator", "nav.fristenrechner": "Deadline Calculator",
"nav.downloads": "Downloads", "nav.downloads": "Downloads",
"nav.glossar": "Glossary",
"nav.logout": "Sign Out", "nav.logout": "Sign Out",
// Footer // Footer
@@ -181,6 +212,8 @@ const translations: Record<Lang, Record<string, string>> = {
"index.cost.desc": "Estimate litigation costs for DE courts, UPC, and EPA proceedings. Court and attorney fees at a glance.", "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.title": "Deadline Calculator",
"index.deadline.desc": "Calculate procedural deadlines for UPC, German, and EPA proceedings with holiday adjustment.", "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.downloads": "Downloads",
"index.style.title": "HL Patents Style", "index.style.title": "HL Patents Style",
"index.style.desc": "Word template in HL Patents style. Formatting, fonts, and macros for standardised briefs.", "index.style.desc": "Word template in HL Patents style. Formatting, fonts, and macros for standardised briefs.",
@@ -299,6 +332,33 @@ const translations: Record<Lang, Record<string, string>> = {
"downloads.style.title": "HL Patents Style", "downloads.style.title": "HL Patents Style",
"downloads.style.desc": "Word template in HL Patents style. Formatting, fonts, and macros for standardised briefs.", "downloads.style.desc": "Word template in HL Patents style. Formatting, fonts, and macros for standardised briefs.",
"downloads.btn": "Download", "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",
}, },
}; };

View File

@@ -4,6 +4,7 @@ const ICON_HOME = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" st
const ICON_CALC = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="14" x2="8" y2="14.01"/><line x1="12" y1="14" x2="12" y2="14.01"/><line x1="16" y1="14" x2="16" y2="14.01"/><line x1="8" y1="18" x2="16" y2="18"/></svg>'; const ICON_CALC = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="14" x2="8" y2="14.01"/><line x1="12" y1="14" x2="12" y2="14.01"/><line x1="16" y1="14" x2="16" y2="14.01"/><line x1="8" y1="18" x2="16" y2="18"/></svg>';
const ICON_CLOCK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>'; const ICON_CLOCK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
const ICON_DOWNLOAD = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>'; const ICON_DOWNLOAD = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>';
const ICON_BOOK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>';
const ICON_GLOBE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z"/></svg>'; const ICON_GLOBE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z"/></svg>';
const ICON_LOGOUT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>'; const ICON_LOGOUT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>';
const ICON_PIN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v6l-2 4h10l-2-4V4"/><line x1="12" y1="16" x2="12" y2="21"/><line x1="8" y1="4" x2="16" y2="4"/></svg>'; const ICON_PIN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v6l-2 4h10l-2-4V4"/><line x1="12" y1="16" x2="12" y2="21"/><line x1="8" y1="4" x2="16" y2="4"/></svg>';
@@ -41,6 +42,7 @@ export function Sidebar({ currentPath }: SidebarProps): string {
{navItem("/", ICON_HOME, "nav.home", "Home", currentPath)} {navItem("/", ICON_HOME, "nav.home", "Home", currentPath)}
{navItem("/tools/kostenrechner", ICON_CALC, "nav.kostenrechner", "Kostenrechner", currentPath)} {navItem("/tools/kostenrechner", ICON_CALC, "nav.kostenrechner", "Kostenrechner", currentPath)}
{navItem("/tools/fristenrechner", ICON_CLOCK, "nav.fristenrechner", "Fristenrechner", currentPath)} {navItem("/tools/fristenrechner", ICON_CLOCK, "nav.fristenrechner", "Fristenrechner", currentPath)}
{navItem("/glossar", ICON_BOOK, "nav.glossar", "Glossar", currentPath)}
{navItem("/downloads", ICON_DOWNLOAD, "nav.downloads", "Downloads", currentPath)} {navItem("/downloads", ICON_DOWNLOAD, "nav.downloads", "Downloads", currentPath)}
</nav> </nav>

128
frontend/src/glossar.tsx Normal file
View File

@@ -0,0 +1,128 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { Footer } from "./components/Footer";
export function renderGlossar(): string {
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title data-i18n="glossar.title">Patentglossar &mdash; patHoLo</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/glossar" />
<main>
<section className="tool-page">
<div className="container">
<div className="tool-header">
<div className="glossar-header-row">
<div>
<h1 data-i18n="glossar.heading">Patentglossar</h1>
<p className="tool-subtitle" data-i18n="glossar.subtitle">
Zweisprachiges Glossar der wichtigsten Begriffe im Patentrecht.
</p>
</div>
<button className="btn-suggest" id="btn-suggest-new" type="button">
<span className="btn-suggest-icon">+</span>
<span data-i18n="glossar.suggest">Begriff vorschlagen</span>
</button>
</div>
</div>
<div className="glossar-controls">
<div className="glossar-search-wrap">
<svg className="glossar-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
<input
type="text"
id="glossar-search"
className="glossar-search"
placeholder="Suchen / Search..."
data-i18n-placeholder="glossar.search.placeholder"
autocomplete="off"
/>
<span className="glossar-count" id="glossar-count" />
</div>
<div className="glossar-filters" id="glossar-filters">
<button className="filter-pill active" data-category="all" type="button" data-i18n="glossar.filter.all">Alle</button>
<button className="filter-pill" data-category="Litigation" type="button" data-i18n="glossar.filter.litigation">Litigation</button>
<button className="filter-pill" data-category="Prosecution" type="button" data-i18n="glossar.filter.prosecution">Prosecution</button>
<button className="filter-pill" data-category="UPC" type="button">UPC</button>
<button className="filter-pill" data-category="EPA" type="button">EPA</button>
<button className="filter-pill" data-category="General" type="button" data-i18n="glossar.filter.general">Allgemein</button>
</div>
</div>
<div className="glossar-table-wrap">
<table className="glossar-table" id="glossar-table">
<thead>
<tr>
<th data-i18n="glossar.col.de">Deutsch</th>
<th data-i18n="glossar.col.en">English</th>
<th data-i18n="glossar.col.definition">Definition</th>
<th className="glossar-col-actions" />
</tr>
</thead>
<tbody id="glossar-body" />
</table>
</div>
<div className="glossar-empty" id="glossar-empty" style="display:none">
<p data-i18n="glossar.empty">Keine Treffer.</p>
</div>
{/* Suggestion modal */}
<div className="modal-overlay" id="suggest-modal" style="display:none">
<div className="modal-card">
<div className="modal-header">
<h2 id="modal-title" data-i18n="glossar.suggest.title">Begriff vorschlagen</h2>
<button className="modal-close" id="modal-close" type="button">&times;</button>
</div>
<form id="suggest-form">
<input type="hidden" id="suggest-type" value="new" />
<input type="hidden" id="suggest-existing" value="" />
<div className="form-field">
<label htmlFor="suggest-de" data-i18n="glossar.suggest.de">Deutscher Begriff</label>
<input type="text" id="suggest-de" required />
</div>
<div className="form-field">
<label htmlFor="suggest-en" data-i18n="glossar.suggest.en">English term</label>
<input type="text" id="suggest-en" required />
</div>
<div className="form-field">
<label htmlFor="suggest-def" data-i18n="glossar.suggest.definition">Definition (optional)</label>
<textarea id="suggest-def" rows={3} />
</div>
<div className="form-field">
<label htmlFor="suggest-cat" data-i18n="glossar.suggest.category">Kategorie</label>
<select id="suggest-cat" required>
<option value="Litigation">Litigation</option>
<option value="Prosecution">Prosecution</option>
<option value="UPC">UPC</option>
<option value="EPA">EPA</option>
<option value="General" data-i18n="glossar.filter.general">Allgemein</option>
</select>
</div>
<div className="form-actions">
<button type="button" className="btn-cancel" id="modal-cancel" data-i18n="glossar.suggest.cancel">Abbrechen</button>
<button type="submit" className="btn-submit" data-i18n="glossar.suggest.submit">Absenden</button>
</div>
<p className="form-msg" id="suggest-msg" />
</form>
</div>
</div>
</div>
</section>
</main>
<Footer />
<script src="/assets/glossar.js"></script>
</body>
</html>
);
}

View File

@@ -8,6 +8,7 @@ const ICON_FOLDER = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
const ICON_CALC = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="14" x2="8" y2="14.01"/><line x1="12" y1="14" x2="12" y2="14.01"/><line x1="16" y1="14" x2="16" y2="14.01"/><line x1="8" y1="18" x2="16" y2="18"/></svg>'; const ICON_CALC = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="14" x2="8" y2="14.01"/><line x1="12" y1="14" x2="12" y2="14.01"/><line x1="16" y1="14" x2="16" y2="14.01"/><line x1="8" y1="18" x2="16" y2="18"/></svg>';
const ICON_CLOCK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>'; const ICON_CLOCK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
const ICON_DOWNLOAD = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>'; const ICON_DOWNLOAD = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>';
const ICON_GLOSSAR = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>';
export function renderIndex(): string { export function renderIndex(): string {
return "<!DOCTYPE html>" + ( return "<!DOCTYPE html>" + (
@@ -70,6 +71,12 @@ export function renderIndex(): string {
<h2 data-i18n="index.deadline.title">Fristenrechner</h2> <h2 data-i18n="index.deadline.title">Fristenrechner</h2>
<p data-i18n="index.deadline.desc">Berechnung von Verfahrensfristen f&uuml;r UPC-, deutsche und EPA-Verfahren mit Feiertags-Anpassung.</p> <p data-i18n="index.deadline.desc">Berechnung von Verfahrensfristen f&uuml;r UPC-, deutsche und EPA-Verfahren mit Feiertags-Anpassung.</p>
</a> </a>
<a href="/glossar" className="card card-link">
<div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_GLOSSAR }} />
<h2 data-i18n="index.glossar.title">Patentglossar</h2>
<p data-i18n="index.glossar.desc">Zweisprachiges DE/EN-Glossar der wichtigsten Begriffe im Patentrecht. Durchsuchbar nach Kategorien.</p>
</a>
</div> </div>
</div> </div>
</section> </section>

View File

@@ -1432,6 +1432,362 @@ input[type="range"]::-moz-range-thumb {
} }
} }
/* --- Glossar --- */
.glossar-header-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.btn-suggest {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.55rem 1.1rem;
font-size: 0.85rem;
font-weight: 600;
color: #fff;
background: var(--color-accent);
border: none;
border-radius: var(--radius);
cursor: pointer;
white-space: nowrap;
transition: background 0.15s ease;
flex-shrink: 0;
margin-top: 0.25rem;
}
.btn-suggest:hover {
background: var(--color-accent-light);
}
.btn-suggest-icon {
font-size: 1.1rem;
font-weight: 700;
line-height: 1;
}
.glossar-controls {
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.glossar-search-wrap {
position: relative;
display: flex;
align-items: center;
}
.glossar-search-icon {
position: absolute;
left: 0.75rem;
width: 1.1rem;
height: 1.1rem;
color: var(--color-text-muted);
pointer-events: none;
}
.glossar-search {
width: 100%;
padding: 0.65rem 0.75rem 0.65rem 2.5rem;
font-size: 0.92rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
color: var(--color-text);
font-family: var(--font-sans);
outline: none;
transition: border-color 0.15s ease;
}
.glossar-search:focus {
border-color: var(--color-accent);
}
.glossar-count {
position: absolute;
right: 0.75rem;
font-size: 0.78rem;
color: var(--color-text-muted);
}
.glossar-filters {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.filter-pill {
padding: 0.35rem 0.85rem;
font-size: 0.8rem;
font-weight: 500;
border: 1px solid var(--color-border);
border-radius: 99px;
background: var(--color-surface);
color: var(--color-text-muted);
cursor: pointer;
transition: all 0.15s ease;
font-family: var(--font-sans);
}
.filter-pill:hover {
border-color: var(--color-accent);
color: var(--color-accent);
}
.filter-pill.active {
background: var(--color-accent);
border-color: var(--color-accent);
color: #fff;
}
.glossar-table-wrap {
overflow-x: auto;
}
.glossar-table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
}
.glossar-table thead th {
text-align: left;
padding: 0.7rem 0.75rem;
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--color-text-muted);
border-bottom: 2px solid var(--color-border);
white-space: nowrap;
}
.glossar-table tbody tr {
border-bottom: 1px solid var(--color-border);
transition: background 0.1s ease;
}
.glossar-table tbody tr:hover {
background: rgba(101, 163, 13, 0.04);
}
.glossar-table td {
padding: 0.65rem 0.75rem;
vertical-align: top;
}
.glossar-de {
font-weight: 600;
color: var(--color-text);
white-space: nowrap;
}
.glossar-en {
color: var(--color-text);
}
.glossar-def {
color: var(--color-text-muted);
font-size: 0.84rem;
line-height: 1.5;
max-width: 380px;
}
.glossar-no-def {
color: var(--color-border);
}
.glossar-col-actions {
width: 2.5rem;
}
.glossar-actions {
text-align: center;
}
.glossar-feedback-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
border: none;
background: none;
color: var(--color-text-muted);
cursor: pointer;
border-radius: 4px;
transition: all 0.15s ease;
opacity: 0.4;
}
.glossar-table tbody tr:hover .glossar-feedback-btn {
opacity: 1;
}
.glossar-feedback-btn:hover {
background: rgba(101, 163, 13, 0.1);
color: var(--color-accent);
opacity: 1;
}
.glossar-empty {
text-align: center;
padding: 3rem 1rem;
color: var(--color-text-muted);
font-size: 0.95rem;
}
/* --- Modal --- */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
padding: 1rem;
}
.modal-card {
background: var(--color-surface);
border-radius: calc(var(--radius) * 1.5);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
width: 100%;
max-width: 480px;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.25rem 1.5rem 0;
}
.modal-header h2 {
font-size: 1.15rem;
font-weight: 700;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--color-text-muted);
line-height: 1;
padding: 0.25rem;
}
.modal-close:hover {
color: var(--color-text);
}
#suggest-form {
padding: 1.25rem 1.5rem 1.5rem;
}
.form-field {
margin-bottom: 1rem;
}
.form-field label {
display: block;
font-size: 0.82rem;
font-weight: 600;
margin-bottom: 0.3rem;
color: var(--color-text);
}
.form-field input,
.form-field textarea,
.form-field select {
width: 100%;
padding: 0.55rem 0.75rem;
font-size: 0.88rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-sans);
outline: none;
transition: border-color 0.15s ease;
}
.form-field input:focus,
.form-field textarea:focus,
.form-field select:focus {
border-color: var(--color-accent);
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1.25rem;
}
.btn-cancel {
padding: 0.5rem 1rem;
font-size: 0.85rem;
font-weight: 500;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
color: var(--color-text-muted);
cursor: pointer;
font-family: var(--font-sans);
transition: border-color 0.15s ease;
}
.btn-cancel:hover {
border-color: var(--color-text-muted);
}
.btn-submit {
padding: 0.5rem 1.25rem;
font-size: 0.85rem;
font-weight: 600;
border: none;
border-radius: var(--radius);
background: var(--color-accent);
color: #fff;
cursor: pointer;
font-family: var(--font-sans);
transition: background 0.15s ease;
}
.btn-submit:hover {
background: var(--color-accent-light);
}
.btn-submit:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.form-msg {
margin-top: 0.75rem;
font-size: 0.82rem;
min-height: 1.25rem;
}
.form-msg-error {
color: #dc2626;
}
.form-msg-success {
color: var(--color-accent);
}
/* --- Responsive: General --- */ /* --- Responsive: General --- */
/* --- Downloads --- */ /* --- Downloads --- */
@@ -1556,6 +1912,27 @@ input[type="range"]::-moz-range-thumb {
gap: 0.25rem; gap: 0.25rem;
} }
.glossar-header-row {
flex-direction: column;
}
.glossar-de {
white-space: normal;
}
.glossar-def {
max-width: none;
}
.glossar-table {
font-size: 0.82rem;
}
.glossar-table td,
.glossar-table thead th {
padding: 0.5rem;
}
.download-card { .download-card {
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
@@ -1570,7 +1947,7 @@ input[type="range"]::-moz-range-thumb {
/* --- Print --- */ /* --- Print --- */
@media print { @media print {
.header, .footer, .sidebar, .sidebar-hamburger, .sidebar-overlay, .tool-input, .print-btn, .reset-btn, #step-1, #step-2, .calculate-btn { .header, .footer, .sidebar, .sidebar-hamburger, .sidebar-overlay, .tool-input, .print-btn, .reset-btn, #step-1, #step-2, .calculate-btn, .modal-overlay, .glossar-feedback-btn, .btn-suggest {
display: none !important; display: none !important;
} }

View File

@@ -0,0 +1,231 @@
package handlers
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"mgit.msbls.de/m/patholo/internal/auth"
)
type GlossarTerm struct {
DE string `json:"de"`
EN string `json:"en"`
Definition string `json:"definition,omitempty"`
Category string `json:"category"`
}
type GlossarSuggestion struct {
TermDE string `json:"term_de"`
TermEN string `json:"term_en"`
Definition string `json:"definition,omitempty"`
Category string `json:"category"`
SuggestionType string `json:"suggestion_type"` // "new" or "correction"
ExistingTermDE string `json:"existing_term_de,omitempty"`
}
var glossarTerms = []GlossarTerm{
// --- Litigation ---
{DE: "Patentverletzung", EN: "Patent infringement", Definition: "Benutzung einer patentgeschützten Erfindung ohne Zustimmung des Patentinhabers.", Category: "Litigation"},
{DE: "Verletzungsklage", EN: "Infringement action", Definition: "Klage des Patentinhabers gegen den Verletzer auf Unterlassung, Schadensersatz und Auskunft.", Category: "Litigation"},
{DE: "Nichtigkeitsklage", EN: "Nullity action", Definition: "Klage auf Erklärung der Nichtigkeit eines Patents, z.\u202fB. wegen fehlender Neuheit oder erfinderischer Tätigkeit.", Category: "Litigation"},
{DE: "Unterlassungsanspruch", EN: "Injunctive relief", Definition: "Anspruch auf gerichtliches Verbot der weiteren Patentverletzung.", Category: "Litigation"},
{DE: "Schadensersatz", EN: "Damages", Definition: "Geldersatz für den durch die Patentverletzung entstandenen Schaden.", Category: "Litigation"},
{DE: "Streitwert", EN: "Amount in dispute", Definition: "Geschätzter Wert des Streitgegenstands, Grundlage für Gerichts- und Anwaltsgebühren.", Category: "Litigation"},
{DE: "Einstweilige Verfügung", EN: "Preliminary injunction", Definition: "Vorläufige gerichtliche Maßnahme zur sofortigen Unterbindung einer Patentverletzung.", Category: "Litigation"},
{DE: "Berufung", EN: "Appeal", Definition: "Rechtsmittel gegen ein Urteil der ersten Instanz.", Category: "Litigation"},
{DE: "Revision", EN: "Revision (appeal on points of law)", Definition: "Rechtsmittel zum BGH, beschränkt auf Rechtsfragen.", Category: "Litigation"},
{DE: "Prozesskostensicherheit", EN: "Security for costs", Definition: "Sicherheitsleistung für die Verfahrenskosten, die von ausländischen Klägern verlangt werden kann.", Category: "Litigation"},
{DE: "Aussetzung", EN: "Stay of proceedings", Definition: "Vorübergehende Unterbrechung des Verletzungsverfahrens, oft bis zur Entscheidung über die Nichtigkeit.", Category: "Litigation"},
{DE: "Widerklage", EN: "Counterclaim", Definition: "Klage des Beklagten gegen den Kläger im selben Verfahren.", Category: "Litigation"},
{DE: "Beweissicherung", EN: "Preservation of evidence", Definition: "Gerichtliche Anordnung zur Sicherung von Beweismitteln vor deren Vernichtung.", Category: "Litigation"},
{DE: "Beschwerde", EN: "Complaint / Appeal", Definition: "Rechtsbehelf gegen gerichtliche Entscheidungen, die kein Urteil sind.", Category: "Litigation"},
{DE: "Lizenzbereitschaftserklärung", EN: "Licence of right declaration", Definition: "Erklärung des Patentinhabers, jedem eine Lizenz zu angemessenen Bedingungen zu erteilen.", Category: "Litigation"},
{DE: "Merkmalsgliederung", EN: "Feature analysis / Claim breakdown", Definition: "Zerlegung des Patentanspruchs in einzelne Merkmale zur Prüfung der Verletzung.", Category: "Litigation"},
{DE: "Äquivalente Patentverletzung", EN: "Infringement by equivalents", Definition: "Verletzung durch eine Lösung, die dem Patentanspruch zwar nicht wortlautgemäß, aber funktional gleichwertig entspricht.", Category: "Litigation"},
{DE: "Auskunftsanspruch", EN: "Right to information / Disclosure order", Definition: "Anspruch auf Auskunft über Herkunft und Vertriebswege patentverletzender Erzeugnisse.", Category: "Litigation"},
{DE: "Rechnungslegung", EN: "Rendering of accounts", Definition: "Pflicht des Verletzers, über Umsätze und Gewinne aus der Verletzung Auskunft zu erteilen.", Category: "Litigation"},
{DE: "Vernichtungsanspruch", EN: "Destruction claim", Definition: "Anspruch auf Vernichtung patentverletzender Erzeugnisse.", Category: "Litigation"},
{DE: "Rückrufanspruch", EN: "Recall claim", Definition: "Anspruch auf Rückruf patentverletzender Produkte aus dem Vertriebsweg.", Category: "Litigation"},
{DE: "Abmahnung", EN: "Cease-and-desist letter", Definition: "Außergerichtliche Aufforderung an den Verletzer, die Verletzungshandlung einzustellen.", Category: "Litigation"},
{DE: "Unterlassungsvertragsstrafe", EN: "Contractual penalty for breach of injunction", Definition: "Vertragsstrafe bei Verstoß gegen eine strafbewehrte Unterlassungserklärung.", Category: "Litigation"},
// --- Prosecution ---
{DE: "Patentanmeldung", EN: "Patent application", Definition: "Antrag auf Erteilung eines Patents beim zuständigen Amt.", Category: "Prosecution"},
{DE: "Patentanspruch", EN: "Patent claim", Definition: "Definiert den Schutzbereich des Patents. Unterscheidung zwischen unabhängigen und abhängigen Ansprüchen.", Category: "Prosecution"},
{DE: "Priorität", EN: "Priority", Definition: "Recht, die Erstanmeldung als Zeitrang für Folgeanmeldungen in anderen Ländern zu nutzen (12 Monate).", Category: "Prosecution"},
{DE: "Offenlegung", EN: "Publication / Disclosure", Definition: "Veröffentlichung der Patentanmeldung, in der Regel 18 Monate nach Anmeldedatum.", Category: "Prosecution"},
{DE: "Erteilung", EN: "Grant", Definition: "Entscheidung des Patentamts, das Patent zu erteilen.", Category: "Prosecution"},
{DE: "Neuheit", EN: "Novelty", Definition: "Patentierbarkeitsvoraussetzung: Die Erfindung darf nicht zum Stand der Technik gehören.", Category: "Prosecution"},
{DE: "Erfinderische Tätigkeit", EN: "Inventive step", Definition: "Die Erfindung darf sich für den Fachmann nicht in naheliegender Weise aus dem Stand der Technik ergeben.", Category: "Prosecution"},
{DE: "Stand der Technik", EN: "Prior art", Definition: "Gesamtheit aller vor dem Anmeldetag öffentlich zugänglichen Informationen.", Category: "Prosecution"},
{DE: "Beschreibung", EN: "Description / Specification", Definition: "Teil der Patentanmeldung, der die Erfindung so offenbart, dass ein Fachmann sie nacharbeiten kann.", Category: "Prosecution"},
{DE: "Teilanmeldung", EN: "Divisional application", Definition: "Abspaltung eines Teils der ursprünglichen Anmeldung in eine eigene, neue Anmeldung.", Category: "Prosecution"},
{DE: "Gebrauchsmuster", EN: "Utility model", Definition: "Registriertes Schutzrecht für technische Erfindungen ohne Prüfung auf erfinderische Tätigkeit (nur DE).", Category: "Prosecution"},
{DE: "Schutzdauer", EN: "Term of protection", Definition: "Maximale Laufzeit eines Patents (20 Jahre ab Anmeldedatum bei Zahlung der Jahresgebühren).", Category: "Prosecution"},
{DE: "Jahresgebühr", EN: "Annual renewal fee", Definition: "Jährliche Gebühr zur Aufrechterhaltung des Patents.", Category: "Prosecution"},
// --- UPC ---
{DE: "Einheitliches Patentgericht", EN: "Unified Patent Court (UPC)", Definition: "Supranationales Gericht für Patentstreitigkeiten in teilnehmenden EU-Mitgliedstaaten.", Category: "UPC"},
{DE: "Einheitspatent", EN: "Unitary patent", Definition: "Europäisches Patent mit einheitlicher Wirkung in allen teilnehmenden EU-Staaten.", Category: "UPC"},
{DE: "Opt-out", EN: "Opt-out", Definition: "Erklärung, mit der ein europäisches Patent von der Zuständigkeit des UPC ausgenommen wird.", Category: "UPC"},
{DE: "Lokalkammer", EN: "Local division", Definition: "UPC-Kammer mit Sitz in einem teilnehmenden Mitgliedstaat für Verletzungsklagen.", Category: "UPC"},
{DE: "Zentralkammer", EN: "Central division", Definition: "UPC-Kammer in Paris und München für Nichtigkeitsklagen und bestimmte Verletzungsklagen.", Category: "UPC"},
{DE: "Berufungskammer", EN: "Court of Appeal", Definition: "UPC-Berufungsgericht mit Sitz in Luxemburg.", Category: "UPC"},
{DE: "Vertraulichkeitsklub", EN: "Confidentiality club", Definition: "Kreis von Personen, die Zugang zu vertraulichen Informationen im UPC-Verfahren erhalten.", Category: "UPC"},
{DE: "Verfahrenssprache", EN: "Language of proceedings", Definition: "Sprache, in der das UPC-Verfahren geführt wird (abhängig von Kammer und Parteivereinbarung).", Category: "UPC"},
{DE: "Bifurkation", EN: "Bifurcation", Definition: "Trennung von Verletzungs- und Nichtigkeitsverfahren vor verschiedenen Kammern.", Category: "UPC"},
{DE: "Vorläufige Maßnahmen", EN: "Provisional measures", Definition: "Einstweilige Anordnungen des UPC zur Sicherung von Ansprüchen vor dem Hauptverfahren.", Category: "UPC"},
{DE: "Schutzschrift", EN: "Protective letter", Definition: "Vorsorglich eingereichte Verteidigungsschrift gegen einen erwarteten Antrag auf einstweilige Maßnahmen.", Category: "UPC"},
{DE: "Übergangsphase", EN: "Transitional period", Definition: "Zeitraum, in dem bestehende europäische Patente noch von der UPC-Zuständigkeit ausgenommen werden können.", Category: "UPC"},
// --- EPA ---
{DE: "Einspruch", EN: "Opposition", Definition: "Anfechtung eines erteilten europäischen Patents vor dem EPA innerhalb von 9 Monaten nach Erteilung.", Category: "EPA"},
{DE: "Einspruchsabteilung", EN: "Opposition division", Definition: "Dreiköpfiges Gremium des EPA, das über Einsprüche entscheidet.", Category: "EPA"},
{DE: "Beschwerdekammer (EPA)", EN: "Board of Appeal", Definition: "Instanz des EPA für Beschwerden gegen Entscheidungen der Prüfungs- oder Einspruchsabteilung.", Category: "EPA"},
{DE: "Große Beschwerdekammer", EN: "Enlarged Board of Appeal", Definition: "Höchste Instanz des EPA für Rechtsfragen von grundlegender Bedeutung.", Category: "EPA"},
{DE: "Prüfungsabteilung", EN: "Examining division", Definition: "Abteilung des EPA, die Patentanmeldungen auf Patentierbarkeit prüft.", Category: "EPA"},
{DE: "Recherchenbericht", EN: "Search report", Definition: "Bericht des EPA über den für die Patentanmeldung relevanten Stand der Technik.", Category: "EPA"},
{DE: "Europäisches Patentübereinkommen", EN: "European Patent Convention (EPC)", Definition: "Völkerrechtlicher Vertrag, der das europäische Patentrecht und das EPA regelt.", Category: "EPA"},
{DE: "Benennungsstaaten", EN: "Designated states", Definition: "Staaten, für die Schutz aus einer europäischen Patentanmeldung beansprucht wird.", Category: "EPA"},
{DE: "Wiedereinsetzung", EN: "Re-establishment of rights", Definition: "Antrag auf Wiedereinsetzung in eine versäumte Frist beim EPA.", Category: "EPA"},
{DE: "Weiterbehandlung", EN: "Further processing", Definition: "Verfahren zur Heilung einer Fristversäumnis beim EPA gegen Zahlung einer Gebühr.", Category: "EPA"},
{DE: "Beschränkungsverfahren", EN: "Limitation proceedings", Definition: "Verfahren zur nachträglichen Einschränkung der Patentansprüche eines erteilten europäischen Patents.", Category: "EPA"},
// --- General ---
{DE: "Patentinhaber", EN: "Patent proprietor / Patentee", Definition: "Person oder Unternehmen, dem das Patent gehört.", Category: "General"},
{DE: "Erfinder", EN: "Inventor", Definition: "Natürliche Person, die die technische Lehre des Patents entwickelt hat.", Category: "General"},
{DE: "Lizenz", EN: "Licence", Definition: "Vertragliche Erlaubnis zur Nutzung eines Patents.", Category: "General"},
{DE: "Zwangslizenz", EN: "Compulsory licence", Definition: "Vom Gericht oder Behörde erteilte Lizenz ohne Zustimmung des Patentinhabers.", Category: "General"},
{DE: "Schutzbereich", EN: "Scope of protection", Definition: "Durch die Patentansprüche definierter Umfang des Patentschutzes.", Category: "General"},
{DE: "Fachmann", EN: "Person skilled in the art", Definition: "Hypothetische Bezugsperson mit durchschnittlichem Fachwissen im relevanten Technikgebiet.", Category: "General"},
{DE: "Patentübertragung", EN: "Patent assignment", Definition: "Übertragung des Eigentums an einem Patent auf eine andere Person oder ein Unternehmen.", Category: "General"},
{DE: "Patentfamilie", EN: "Patent family", Definition: "Gesamtheit aller Patentanmeldungen und Patente, die auf dieselbe Priorität zurückgehen.", Category: "General"},
{DE: "Patentregister", EN: "Patent register", Definition: "Amtliches Verzeichnis aller erteilten Patente mit Angaben zu Inhaber, Status und Rechtsänderungen.", Category: "General"},
{DE: "Patentverletzungsgutachten", EN: "Freedom-to-operate opinion", Definition: "Rechtsgutachten zur Frage, ob ein Produkt oder Verfahren Patente Dritter verletzt.", Category: "General"},
{DE: "Ergänzendes Schutzzertifikat", EN: "Supplementary protection certificate (SPC)", Definition: "Zusätzlicher Schutz für Arzneimittel und Pflanzenschutzmittel nach Patentablauf (max. 5 Jahre).", Category: "General"},
{DE: "PCT-Anmeldung", EN: "PCT application", Definition: "Internationale Patentanmeldung nach dem Vertrag über die Internationale Zusammenarbeit auf dem Gebiet des Patentwesens.", Category: "General"},
{DE: "Schriftsatz", EN: "Written submission / Brief", Definition: "Formelles Schreiben an das Gericht oder Amt mit rechtlichem Vorbringen.", Category: "General"},
{DE: "Mündliche Verhandlung", EN: "Oral hearing / Oral proceedings", Definition: "Mündliche Anhörung vor Gericht oder dem EPA.", Category: "General"},
{DE: "Neuheitsschädlich", EN: "Novelty-destroying", Definition: "Eigenschaft einer Entgegenhaltung, die die Neuheit der beanspruchten Erfindung zerstört.", Category: "General"},
{DE: "Aufgabe-Lösungs-Ansatz", EN: "Problem-solution approach", Definition: "Vom EPA angewandte Methode zur Prüfung der erfinderischen Tätigkeit.", Category: "General"},
{DE: "Formstücke", EN: "Formal documents / Forms", Definition: "Standardformulare der Patentämter für Anträge und Mitteilungen.", Category: "General"},
}
func handleGlossarPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/glossar.html")
}
func handleGlossarAPI(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, glossarTerms)
}
func handleGlossarSuggest(w http.ResponseWriter, r *http.Request) {
var suggestion GlossarSuggestion
if err := json.NewDecoder(r.Body).Decode(&suggestion); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Ungültige Anfrage."})
return
}
suggestion.TermDE = strings.TrimSpace(suggestion.TermDE)
suggestion.TermEN = strings.TrimSpace(suggestion.TermEN)
suggestion.Definition = strings.TrimSpace(suggestion.Definition)
suggestion.Category = strings.TrimSpace(suggestion.Category)
suggestion.SuggestionType = strings.TrimSpace(suggestion.SuggestionType)
suggestion.ExistingTermDE = strings.TrimSpace(suggestion.ExistingTermDE)
if suggestion.TermDE == "" || suggestion.TermEN == "" || suggestion.Category == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Felder term_de, term_en und category sind erforderlich."})
return
}
if suggestion.SuggestionType == "" {
suggestion.SuggestionType = "new"
}
if suggestion.SuggestionType != "new" && suggestion.SuggestionType != "correction" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "suggestion_type muss 'new' oder 'correction' sein."})
return
}
// Get user's access token for authenticated Supabase request
accessToken := ""
email := ""
if cookie, err := r.Cookie(auth.SessionCookieName); err == nil {
accessToken = cookie.Value
email = extractEmailFromJWT(cookie.Value)
}
payload := map[string]string{
"term_de": suggestion.TermDE,
"term_en": suggestion.TermEN,
"definition": suggestion.Definition,
"category": suggestion.Category,
"suggestion_type": suggestion.SuggestionType,
"existing_term_de": suggestion.ExistingTermDE,
"submitted_by": email,
}
jsonBody, err := json.Marshal(payload)
if err != nil {
log.Printf("glossar suggest marshal error: %v", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "Interner Fehler."})
return
}
endpoint := fmt.Sprintf("%s/rest/v1/glossar_suggestions", authClient.URL)
req2, err := http.NewRequest("POST", endpoint, bytes.NewReader(jsonBody))
if err != nil {
log.Printf("glossar suggest request error: %v", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "Interner Fehler."})
return
}
req2.Header.Set("Content-Type", "application/json")
req2.Header.Set("apikey", authClient.AnonKey)
if accessToken != "" {
req2.Header.Set("Authorization", "Bearer "+accessToken)
} else {
req2.Header.Set("Authorization", "Bearer "+authClient.AnonKey)
}
req2.Header.Set("Prefer", "return=minimal")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req2)
if err != nil {
log.Printf("glossar suggest supabase error: %v", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "Fehler beim Speichern."})
return
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
log.Printf("glossar suggest supabase status %d: %s", resp.StatusCode, string(body))
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "Fehler beim Speichern."})
return
}
writeJSON(w, http.StatusCreated, map[string]string{"ok": "true"})
}
func extractEmailFromJWT(token string) string {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return ""
}
decoded, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return ""
}
var claims struct {
Email string `json:"email"`
}
if err := json.Unmarshal(decoded, &claims); err != nil {
return ""
}
return claims.Email
}

View File

@@ -33,6 +33,9 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string) {
protected.HandleFunc("POST /api/tools/fristenrechner", handleFristenrechnerAPI) protected.HandleFunc("POST /api/tools/fristenrechner", handleFristenrechnerAPI)
protected.HandleFunc("GET /api/tools/proceeding-types", handleProceedingTypes) protected.HandleFunc("GET /api/tools/proceeding-types", handleProceedingTypes)
protected.HandleFunc("GET /downloads", handleDownloadsPage) protected.HandleFunc("GET /downloads", handleDownloadsPage)
protected.HandleFunc("GET /glossar", handleGlossarPage)
protected.HandleFunc("GET /api/glossar", handleGlossarAPI)
protected.HandleFunc("POST /api/glossar/suggest", handleGlossarSuggest)
protected.HandleFunc("GET /files/{filename}", handleFileDownload) protected.HandleFunc("GET /files/{filename}", handleFileDownload)
protected.HandleFunc("POST /api/files/refresh", handleFileRefresh) protected.HandleFunc("POST /api/files/refresh", handleFileRefresh)
mux.Handle("/", client.Middleware(protected)) mux.Handle("/", client.Middleware(protected))