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:
@@ -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",
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user