search added

This commit is contained in:
nahakubuilde
2025-08-25 18:02:41 +01:00
parent 240b71e321
commit da71deb7e2
3 changed files with 238 additions and 1 deletions

View File

@@ -265,7 +265,10 @@
<div class="flex items-center justify-between">
<h1 class="sidebar-title text-xl font-bold text-white">{{.app_name}}</h1>
<div class="flex items-center space-x-2 items-center">
<div class="sidebar-actions flex items-center space-x-2">
<div class="sidebar-actions flex items-center space-x-3">
<button id="open-search" class="text-gray-400 hover:text-white transition-colors" title="Search" aria-label="Search">
<i class="fas fa-magnifying-glass"></i>
</button>
<a href="/settings" class="text-gray-400 hover:text-white transition-colors" title="Settings">
<i class="fas fa-cog"></i>
</a>
@@ -334,6 +337,26 @@
</div>
</div>
<!-- Search Modal -->
<div id="search-modal" class="modal-overlay hidden">
<div class="modal-content w-full max-w-3xl">
<div class="flex items-center mb-3">
<div class="relative flex-1">
<i class="fas fa-magnifying-glass absolute left-3 top-3 text-gray-400"></i>
<input id="search-input" type="text" placeholder="Search notes..." class="form-input pl-9 w-full" />
</div>
<button id="clear-search" class="ml-2 px-3 py-2 rounded bg-red-600 hover:bg-red-700 text-white" title="Clear">
Clear
</button>
<button id="close-search" class="btn-secondary ml-2" title="Close">
<i class="fas fa-xmark"></i>
</button>
</div>
<div id="search-status" class="text-sm text-gray-400 mb-2"></div>
<div id="search-results" class="space-y-3"></div>
</div>
</div>
<!-- Scripts -->
<script>
// Initialize syntax highlighting
@@ -441,6 +464,118 @@
// Expand active path in tree
expandActivePath();
// Search modal wiring
const searchModal = document.getElementById('search-modal');
const openSearchBtn = document.getElementById('open-search');
const closeSearchBtn = document.getElementById('close-search');
const clearSearchBtn = document.getElementById('clear-search');
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
const searchStatus = document.getElementById('search-status');
const LS_KEY_QUERY = 'globalSearch:query';
function openSearch() {
if (!searchModal) return;
searchModal.classList.remove('hidden');
setTimeout(() => searchInput && searchInput.focus(), 0);
const lastQuery = localStorage.getItem(LS_KEY_QUERY) || '';
if (searchInput) {
searchInput.value = lastQuery;
if (lastQuery) doSearch(lastQuery);
}
}
function closeSearch() {
if (!searchModal) return;
searchModal.classList.add('hidden');
}
function clearSearch() {
if (!searchInput) return;
searchInput.value = '';
localStorage.removeItem(LS_KEY_QUERY);
renderResults({ query: '', results: [] });
}
let debounceTimer = null;
function debounce(fn, delay) {
return function(...args) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => fn.apply(this, args), delay);
}
}
function escapeHTML(str) {
return str.replace(/[&<>"']/g, s => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;','\'':'&#39;'}[s]));
}
function renderResults(data) {
if (!searchResults || !searchStatus) return;
searchResults.innerHTML = '';
if (!data || !data.results) {
searchStatus.textContent = '';
return;
}
searchStatus.textContent = `${data.results.length} result(s)`;
data.results.forEach(r => {
const item = document.createElement('div');
item.className = 'p-3 bg-slate-700 rounded border border-slate-600';
const title = document.createElement('div');
title.className = 'flex items-center justify-between text-sm text-blue-300 hover:text-blue-200 cursor-pointer';
title.innerHTML = `<span><i class="fas ${r.type === 'md' ? 'fa-file-lines' : 'fa-file'} mr-2"></i>${escapeHTML(r.path)}</span>`;
title.addEventListener('click', () => {
const url = r.path.endsWith('.md') ? `/note/${r.path}` : `/view_text/${r.path}`;
// remember query
if (searchInput) localStorage.setItem(LS_KEY_QUERY, searchInput.value.trim());
window.location.href = url;
});
item.appendChild(title);
(r.snippets || []).forEach(snip => {
const pre = document.createElement('pre');
pre.className = 'mt-2 bg-slate-800 p-2 rounded text-xs whitespace-pre-wrap border border-slate-600';
pre.textContent = `Line ${snip.line}:\n` + snip.preview;
item.appendChild(pre);
});
searchResults.appendChild(item);
});
}
const doSearch = debounce(async function(q) {
const query = (q || '').trim();
if (!query) {
renderResults({ query: '', results: [] });
return;
}
try {
searchStatus.textContent = 'Searching...';
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await res.json();
if (res.ok) {
localStorage.setItem(LS_KEY_QUERY, query);
renderResults(data);
} else {
searchStatus.textContent = data.error || 'Search failed';
}
} catch (e) {
searchStatus.textContent = 'Search failed';
}
}, 250);
if (openSearchBtn) openSearchBtn.addEventListener('click', openSearch);
if (closeSearchBtn) closeSearchBtn.addEventListener('click', closeSearch);
if (clearSearchBtn) clearSearchBtn.addEventListener('click', clearSearch);
if (searchInput) searchInput.addEventListener('input', (e) => doSearch(e.target.value));
// Close modal on overlay click (but not when clicking inside content)
if (searchModal) {
searchModal.addEventListener('click', (e) => {
if (e.target === searchModal) closeSearch();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !searchModal.classList.contains('hidden')) closeSearch();
});
}
});
</script>