feat(links): add text search input + honor ?q= from search palette (t-paliad-046 follow-up)

The global search palette emits /links?q=<title> when a user clicks a link
result, but /links had no search input — only category filter pills — so
the deep link silently landed on the unfiltered catalog.

Added a search input matching the glossary/courts pattern: live keystroke
filtering across title + DE/EN description + URL, combined with the
existing category filter, and ?q= URL prefill on init. Result count chip
("2 / 47") added next to the input for parity with the other catalogs.

i18n: links.search.placeholder added in DE + EN.
This commit is contained in:
m
2026-04-26 14:54:16 +02:00
parent 58692a4411
commit 044166ffed
3 changed files with 52 additions and 5 deletions

View File

@@ -221,6 +221,7 @@ const translations: Record<Lang, Record<string, string>> = {
"links.heading": "Links",
"links.subtitle": "N\u00fctzliche externe Links f\u00fcr die t\u00e4gliche Patentpraxis.",
"links.filter.all": "Alle",
"links.search.placeholder": "Suchen nach Titel, Beschreibung...",
"links.empty": "Keine Links in dieser Kategorie.",
"links.suggest.btn": "Link vorschlagen",
"links.suggest.title": "Link vorschlagen",
@@ -1353,6 +1354,7 @@ const translations: Record<Lang, Record<string, string>> = {
"links.heading": "Links",
"links.subtitle": "Useful external links for daily patent practice.",
"links.filter.all": "All",
"links.search.placeholder": "Search by title, description...",
"links.empty": "No links in this category.",
"links.suggest.btn": "Suggest a Link",
"links.suggest.title": "Suggest a Link",

View File

@@ -26,10 +26,12 @@ const ICON_FEEDBACK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor
let data: LinksData | null = null;
let activeCategory = "all";
let searchQuery = "";
document.addEventListener("DOMContentLoaded", () => {
initI18n();
initSidebar();
initSearch();
loadLinks();
loadSuggestionCount();
setupSuggestModal();
@@ -39,6 +41,22 @@ document.addEventListener("DOMContentLoaded", () => {
});
});
function initSearch() {
const input = document.getElementById("links-search") as HTMLInputElement | null;
if (!input) return;
input.addEventListener("input", () => {
searchQuery = input.value;
renderLinks();
});
// Honor `?q=` from the global search-palette deep links. renderLinks()
// runs after loadLinks() resolves and reads the module-level searchQuery.
const q = new URLSearchParams(location.search).get("q");
if (q) {
input.value = q;
searchQuery = q;
}
}
async function loadLinks() {
try {
const resp = await fetch("/api/links");
@@ -100,9 +118,18 @@ function renderLinks() {
if (!grid) return;
const lang = getLang();
const filtered = activeCategory === "all"
? data.links
: data.links.filter((l) => l.category === activeCategory);
const q = searchQuery.trim().toLowerCase();
const filtered = data.links.filter((l) => {
if (activeCategory !== "all" && l.category !== activeCategory) return false;
if (q) {
const hay = `${l.title} ${l.descDE} ${l.descEN} ${l.url}`.toLowerCase();
if (!hay.includes(q)) return false;
}
return true;
});
const count = document.getElementById("links-count");
if (count) count.textContent = `${filtered.length} / ${data.links.length}`;
if (filtered.length === 0) {
grid.innerHTML = `<p class="links-empty">${t("links.empty")}</p>`;

View File

@@ -41,8 +41,26 @@ export function renderLinks(): string {
</div>
</div>
<div className="links-filters" id="category-filters">
<button className="links-filter-btn active" data-category="all" type="button" data-i18n="links.filter.all">Alle</button>
<div className="gerichte-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="links-search"
className="glossar-search"
placeholder="Suchen / Search..."
data-i18n-placeholder="links.search.placeholder"
autocomplete="off"
/>
<span className="glossar-count" id="links-count" />
</div>
<div className="links-filters" id="category-filters">
<button className="links-filter-btn active" data-category="all" type="button" data-i18n="links.filter.all">Alle</button>
</div>
</div>
<div className="links-grid" id="links-grid">