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 { renderFristenrechner } from "./src/fristenrechner";
|
||||
import { renderDownloads } from "./src/downloads";
|
||||
import { renderGlossar } from "./src/glossar";
|
||||
|
||||
const DIST = join(import.meta.dir, "dist");
|
||||
|
||||
@@ -21,6 +22,7 @@ async function build() {
|
||||
join(import.meta.dir, "src/client/kostenrechner.ts"),
|
||||
join(import.meta.dir, "src/client/fristenrechner.ts"),
|
||||
join(import.meta.dir, "src/client/downloads.ts"),
|
||||
join(import.meta.dir, "src/client/glossar.ts"),
|
||||
],
|
||||
outdir: join(DIST, "assets"),
|
||||
naming: "[name].js",
|
||||
@@ -47,6 +49,7 @@ async function build() {
|
||||
await Bun.write(join(DIST, "kostenrechner.html"), renderKostenrechner());
|
||||
await Bun.write(join(DIST, "fristenrechner.html"), renderFristenrechner());
|
||||
await Bun.write(join(DIST, "downloads.html"), renderDownloads());
|
||||
await Bun.write(join(DIST, "glossar.html"), renderGlossar());
|
||||
|
||||
console.log("Build complete \u2192 dist/");
|
||||
}
|
||||
|
||||
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.fristenrechner": "Fristenrechner",
|
||||
"nav.downloads": "Downloads",
|
||||
"nav.glossar": "Glossar",
|
||||
"nav.logout": "Abmelden",
|
||||
|
||||
// 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.deadline.title": "Fristenrechner",
|
||||
"index.deadline.desc": "Berechnung von Verfahrensfristen f\u00fcr UPC-, deutsche und EPA-Verfahren mit Feiertags-Anpassung.",
|
||||
"index.glossar.title": "Patentglossar",
|
||||
"index.glossar.desc": "Zweisprachiges DE/EN-Glossar der wichtigsten Begriffe im Patentrecht. Durchsuchbar nach Kategorien.",
|
||||
"index.downloads": "Downloads",
|
||||
"index.style.title": "HL Patents Style",
|
||||
"index.style.desc": "Word-Vorlage im HL Patents Style. Formatierung, Schriftarten und Makros f\u00fcr standardisierte Schrifts\u00e4tze.",
|
||||
@@ -153,6 +156,33 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"downloads.style.title": "HL Patents Style",
|
||||
"downloads.style.desc": "Word-Vorlage im HL Patents Style. Formatierung, Schriftarten und Makros f\u00fcr standardisierte Schrifts\u00e4tze.",
|
||||
"downloads.btn": "Herunterladen",
|
||||
|
||||
// Glossar
|
||||
"glossar.title": "Patentglossar \u2014 patHoLo",
|
||||
"glossar.heading": "Patentglossar",
|
||||
"glossar.subtitle": "Zweisprachiges Glossar der wichtigsten Begriffe im Patentrecht.",
|
||||
"glossar.search.placeholder": "Suchen...",
|
||||
"glossar.filter.all": "Alle",
|
||||
"glossar.filter.litigation": "Litigation",
|
||||
"glossar.filter.prosecution": "Prosecution",
|
||||
"glossar.filter.general": "Allgemein",
|
||||
"glossar.col.de": "Deutsch",
|
||||
"glossar.col.en": "English",
|
||||
"glossar.col.definition": "Definition",
|
||||
"glossar.empty": "Keine Treffer.",
|
||||
"glossar.suggest": "Begriff vorschlagen",
|
||||
"glossar.suggest.title": "Neuen Begriff vorschlagen",
|
||||
"glossar.suggest.de": "Deutscher Begriff",
|
||||
"glossar.suggest.en": "Englischer Begriff",
|
||||
"glossar.suggest.definition": "Definition (optional)",
|
||||
"glossar.suggest.category": "Kategorie",
|
||||
"glossar.suggest.cancel": "Abbrechen",
|
||||
"glossar.suggest.submit": "Absenden",
|
||||
"glossar.suggest.success": "Vorschlag eingereicht. Vielen Dank!",
|
||||
"glossar.suggest.error.required": "Bitte DE und EN Begriff eingeben.",
|
||||
"glossar.suggest.error.generic": "Fehler beim Senden. Bitte versuchen Sie es erneut.",
|
||||
"glossar.feedback.title": "Korrektur vorschlagen",
|
||||
"glossar.feedback.tooltip": "Korrektur vorschlagen",
|
||||
},
|
||||
|
||||
en: {
|
||||
@@ -161,6 +191,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"nav.kostenrechner": "Cost Calculator",
|
||||
"nav.fristenrechner": "Deadline Calculator",
|
||||
"nav.downloads": "Downloads",
|
||||
"nav.glossar": "Glossary",
|
||||
"nav.logout": "Sign Out",
|
||||
|
||||
// Footer
|
||||
@@ -181,6 +212,8 @@ const translations: Record<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.deadline.title": "Deadline Calculator",
|
||||
"index.deadline.desc": "Calculate procedural deadlines for UPC, German, and EPA proceedings with holiday adjustment.",
|
||||
"index.glossar.title": "Patent Glossary",
|
||||
"index.glossar.desc": "Bilingual DE/EN glossary of key patent law terminology. Searchable by category.",
|
||||
"index.downloads": "Downloads",
|
||||
"index.style.title": "HL Patents Style",
|
||||
"index.style.desc": "Word template in HL Patents style. Formatting, fonts, and macros for standardised briefs.",
|
||||
@@ -299,6 +332,33 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"downloads.style.title": "HL Patents Style",
|
||||
"downloads.style.desc": "Word template in HL Patents style. Formatting, fonts, and macros for standardised briefs.",
|
||||
"downloads.btn": "Download",
|
||||
|
||||
// Glossar
|
||||
"glossar.title": "Patent Glossary \u2014 patHoLo",
|
||||
"glossar.heading": "Patent Glossary",
|
||||
"glossar.subtitle": "Bilingual glossary of key patent law terminology.",
|
||||
"glossar.search.placeholder": "Search...",
|
||||
"glossar.filter.all": "All",
|
||||
"glossar.filter.litigation": "Litigation",
|
||||
"glossar.filter.prosecution": "Prosecution",
|
||||
"glossar.filter.general": "General",
|
||||
"glossar.col.de": "German",
|
||||
"glossar.col.en": "English",
|
||||
"glossar.col.definition": "Definition",
|
||||
"glossar.empty": "No matches found.",
|
||||
"glossar.suggest": "Suggest a term",
|
||||
"glossar.suggest.title": "Suggest a new term",
|
||||
"glossar.suggest.de": "German term",
|
||||
"glossar.suggest.en": "English term",
|
||||
"glossar.suggest.definition": "Definition (optional)",
|
||||
"glossar.suggest.category": "Category",
|
||||
"glossar.suggest.cancel": "Cancel",
|
||||
"glossar.suggest.submit": "Submit",
|
||||
"glossar.suggest.success": "Suggestion submitted. Thank you!",
|
||||
"glossar.suggest.error.required": "Please enter both DE and EN terms.",
|
||||
"glossar.suggest.error.generic": "Error submitting. Please try again.",
|
||||
"glossar.feedback.title": "Suggest a correction",
|
||||
"glossar.feedback.tooltip": "Suggest a correction",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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_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_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_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>';
|
||||
@@ -41,6 +42,7 @@ export function Sidebar({ currentPath }: SidebarProps): string {
|
||||
{navItem("/", ICON_HOME, "nav.home", "Home", currentPath)}
|
||||
{navItem("/tools/kostenrechner", ICON_CALC, "nav.kostenrechner", "Kostenrechner", 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)}
|
||||
</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_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_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 {
|
||||
return "<!DOCTYPE html>" + (
|
||||
@@ -70,6 +71,12 @@ export function renderIndex(): string {
|
||||
<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>
|
||||
</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>
|
||||
</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 --- */
|
||||
|
||||
/* --- Downloads --- */
|
||||
@@ -1556,6 +1912,27 @@ input[type="range"]::-moz-range-thumb {
|
||||
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 {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
@@ -1570,7 +1947,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
/* --- 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user