Add global search functionality for tools

- Introduced a global search input in the top bar for easy access to tools.
- Implemented JavaScript logic to filter and display search results dynamically.
- Enhanced CSS for the search input and dropdown results, including styling for empty states.
- Updated HTML to include necessary elements for the global search feature.
This commit is contained in:
dzakdzaks
2026-06-07 01:27:58 +07:00
parent b643f527d0
commit 3e54ed40b0
3 changed files with 194 additions and 1 deletions
+81
View File
@@ -163,6 +163,87 @@ a { color: var(--primary); text-decoration: none; }
z-index: 50;
}
.top-title { font-weight: 600; font-size: 1.05rem; }
.global-search {
flex: 1;
position: relative;
max-width: 360px;
}
.global-search input {
width: 100%;
padding: .4rem .8rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font-size: .9rem;
font-family: var(--font);
outline: none;
transition: border-color .15s, box-shadow .15s;
}
.global-search input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(67,97,238,.12);
}
.global-search-dropdown {
display: none;
position: absolute;
top: calc(100% + .4rem);
left: 0;
right: 0;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
max-height: 320px;
overflow-y: auto;
z-index: 200;
}
.global-search-dropdown.open { display: block; }
.global-search-result {
display: flex;
align-items: center;
gap: .75rem;
padding: .55rem .85rem;
color: var(--text);
font-size: .875rem;
text-decoration: none;
transition: background .1s;
}
.global-search-result:hover,
.global-search-result.active { background: var(--bg); color: var(--text); }
.global-search-result .result-info {
display: flex;
flex-direction: column;
gap: .1rem;
min-width: 0;
}
.global-search-result .result-name { font-weight: 500; }
.global-search-result .result-desc {
font-size: .75rem;
color: var(--text-light);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.global-search-result .result-cat {
margin-left: auto;
flex-shrink: 0;
font-size: .72rem;
color: var(--text-light);
white-space: nowrap;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
padding: .1rem .4rem;
}
.global-search-empty {
padding: .8rem;
color: var(--text-light);
font-size: .85rem;
text-align: center;
}
.menu-btn {
display: none;
border: none;
+108
View File
@@ -90,12 +90,120 @@ document.addEventListener("DOMContentLoaded", () => {
initTheme();
initToolSearch();
initGlobalSearch();
initUploadZone();
initToolForm();
initDependentOptions();
initCapabilityStatus();
});
/* ── Global Toolbar Search ────────────────────── */
function initGlobalSearch() {
const wrapper = document.getElementById("global-search");
const input = document.getElementById("global-search-input");
const dropdown = document.getElementById("global-search-dropdown");
if (!wrapper || !input || !dropdown) return;
// Hide on home page; show everywhere else
if (window.location.pathname === "/") {
wrapper.style.display = "none";
return;
}
wrapper.style.display = "";
// Build tool list from sidebar nav items (already in DOM)
const tools = [];
document.querySelectorAll(".nav-category").forEach(cat => {
const catBtn = cat.querySelector(".nav-category-btn");
const catName = catBtn ? catBtn.textContent.trim() : "";
cat.querySelectorAll(".nav-item").forEach(a => {
tools.push({
name: a.textContent.trim(),
href: a.getAttribute("href"),
cat: catName,
desc: a.dataset.desc || "",
});
});
});
let activeIdx = -1;
function openDropdown() {
dropdown.classList.add("open");
input.setAttribute("aria-expanded", "true");
}
function closeDropdown() {
dropdown.classList.remove("open");
input.setAttribute("aria-expanded", "false");
activeIdx = -1;
}
function renderResults(query) {
if (!query) { closeDropdown(); return; }
const matches = tools.filter(t =>
t.name.toLowerCase().includes(query) ||
t.cat.toLowerCase().includes(query) ||
t.desc.includes(query)
).slice(0, 8);
if (matches.length === 0) {
dropdown.innerHTML = `<div class="global-search-empty">No tools found.</div>`;
openDropdown();
return;
}
dropdown.innerHTML = matches.map((t, i) => `
<a href="${t.href}" class="global-search-result" role="option" data-idx="${i}">
<span class="result-info">
<span class="result-name">${escapeHtml(t.name)}</span>
<span class="result-desc">${escapeHtml(t.desc)}</span>
</span>
<span class="result-cat">${escapeHtml(t.cat)}</span>
</a>
`).join("");
activeIdx = -1;
openDropdown();
}
function setActive(idx) {
const items = dropdown.querySelectorAll(".global-search-result");
items.forEach(el => el.classList.remove("active"));
if (idx >= 0 && idx < items.length) {
items[idx].classList.add("active");
items[idx].scrollIntoView({ block: "nearest" });
}
activeIdx = idx;
}
input.addEventListener("input", () => {
renderResults(input.value.trim().toLowerCase());
});
input.addEventListener("keydown", e => {
const items = dropdown.querySelectorAll(".global-search-result");
if (e.key === "ArrowDown") {
e.preventDefault();
setActive(Math.min(activeIdx + 1, items.length - 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setActive(Math.max(activeIdx - 1, 0));
} else if (e.key === "Enter") {
if (activeIdx >= 0 && items[activeIdx]) {
window.location.href = items[activeIdx].getAttribute("href");
}
} else if (e.key === "Escape") {
input.blur();
closeDropdown();
}
});
document.addEventListener("click", e => {
if (!wrapper.contains(e.target)) closeDropdown();
});
}
/* ── Home Page Tool Search ────────────────────── */
function initToolSearch() {
const input = document.getElementById("tool-search");
+5 -1
View File
@@ -30,7 +30,7 @@
</button>
<div class="nav-items">
{% for tool in cat.tools %}
<a href="/{{ cat.id }}/{{ tool.id }}" class="nav-item">
<a href="/{{ cat.id }}/{{ tool.id }}" class="nav-item" data-desc="{{ tool.desc | lower }}">
<i class="bi {{ tool.icon }}"></i> {{ tool.name }}
</a>
{% endfor %}
@@ -46,6 +46,10 @@
<header class="top-bar">
<button class="menu-btn" onclick="openSidebar()"><i class="bi bi-list"></i></button>
<span class="top-title">{% block top_title %}Your Everyday Tools{% endblock %}</span>
<div class="global-search" id="global-search">
<input type="text" id="global-search-input" placeholder="Search tools..." autocomplete="off" aria-label="Search tools" aria-expanded="false" aria-controls="global-search-dropdown">
<div class="global-search-dropdown" id="global-search-dropdown" role="listbox"></div>
</div>
<div class="theme-switcher" id="theme-switcher" role="group" aria-label="Color theme">
<button class="theme-btn" data-theme-mode="system" title="System theme"><i class="bi bi-circle-half"></i></button>
<button class="theme-btn" data-theme-mode="light" title="Light theme"><i class="bi bi-sun-fill"></i></button>