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:
@@ -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/");
|
||||||
}
|
}
|
||||||
|
|||||||
207
frontend/src/client/glossar.ts
Normal file
207
frontend/src/client/glossar.ts
Normal 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();
|
||||||
|
});
|
||||||
@@ -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",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
128
frontend/src/glossar.tsx
Normal 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 — 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">×</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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ür UPC-, deutsche und EPA-Verfahren mit Feiertags-Anpassung.</p>
|
<p data-i18n="index.deadline.desc">Berechnung von Verfahrensfristen fü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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
231
internal/handlers/glossar.go
Normal file
231
internal/handlers/glossar.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user