169 lines
6.5 KiB
HTML
169 lines
6.5 KiB
HTML
{{define "base"}}
|
||
<!DOCTYPE html>
|
||
<html lang="en" data-theme="light">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="referrer" content="no-referrer">
|
||
<meta name="robots" content="noarchive">
|
||
<meta name="description" content="kafka — a privacy-respecting, open metasearch engine">
|
||
<title>{{template "title" .}}kafka</title>
|
||
<link rel="stylesheet" href="/static/css/kafka.css">
|
||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
|
||
<link title="kafka" type="application/opensearchdescription+xml" rel="search" href="/opensearch.xml">
|
||
</head>
|
||
<body>
|
||
<header class="site-header">
|
||
<a href="/" class="site-logo">
|
||
<svg class="site-logo-mark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<circle cx="11" cy="11" r="8"/>
|
||
<path d="m21 21-4.35-4.35"/>
|
||
</svg>
|
||
<span class="site-name">kafka</span>
|
||
</a>
|
||
<button id="settings-trigger" class="settings-trigger" aria-label="Preferences" aria-expanded="false" aria-controls="settings-popover">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
|
||
<circle cx="12" cy="12" r="3"/>
|
||
</svg>
|
||
</button>
|
||
</header>
|
||
|
||
<main class="{{if .Query}}page-results{{else}}page-home{{end}}">
|
||
{{template "content" .}}
|
||
</main>
|
||
|
||
<footer>
|
||
<p>Powered by <a href="https://git.ashisgreat.xyz/penal-colony/kafka">kafka</a> — a privacy-respecting, open metasearch engine · <a href="https://git.ashisgreat.xyz/penal-colony/kafka">Source</a> · <a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPLv3</a></p>
|
||
</footer>
|
||
|
||
<script src="/static/js/settings.js"></script>
|
||
|
||
<div id="settings-popover" data-open="false" role="dialog" aria-label="Preferences" aria-modal="true">
|
||
<div class="settings-popover-header">
|
||
Preferences
|
||
<button class="settings-popover-close" aria-label="Close">×</button>
|
||
</div>
|
||
<div class="settings-popover-body"></div>
|
||
</div>
|
||
|
||
<div id="autocomplete-dropdown"></div>
|
||
|
||
<script>
|
||
(function () {
|
||
'use strict';
|
||
|
||
var input = document.getElementById('q');
|
||
var dropdown = document.getElementById('autocomplete-dropdown');
|
||
var form = document.getElementById('search-form');
|
||
var debounceTimer = null;
|
||
var suggestions = [];
|
||
var activeIndex = -1;
|
||
var fetchController = null;
|
||
|
||
function escapeRegex(str) {
|
||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
}
|
||
|
||
function highlight(text, query) {
|
||
if (!query) return text;
|
||
var re = new RegExp('^(' + escapeRegex(query) + ')', 'i');
|
||
return text.replace(re, '<mark>$1</mark>');
|
||
}
|
||
|
||
function renderDropdown() {
|
||
if (suggestions.length === 0) {
|
||
dropdown.classList.remove('open');
|
||
return;
|
||
}
|
||
var html = '';
|
||
for (var i = 0; i < suggestions.length; i++) {
|
||
var escaped = highlight(suggestions[i], input.value);
|
||
html += '<div class="autocomplete-suggestion" data-index="' + i + '">' + escaped + '</div>';
|
||
}
|
||
html += '<div class="autocomplete-footer">↑↓ navigate · Enter select · Esc close</div>';
|
||
dropdown.innerHTML = html;
|
||
dropdown.classList.add('open');
|
||
activeIndex = -1;
|
||
}
|
||
|
||
function closeDropdown() {
|
||
dropdown.classList.remove('open');
|
||
suggestions = [];
|
||
activeIndex = -1;
|
||
}
|
||
|
||
function selectSuggestion(index) {
|
||
if (index < 0 || index >= suggestions.length) return;
|
||
input.value = suggestions[index];
|
||
closeDropdown();
|
||
form.submit();
|
||
}
|
||
|
||
function updateActive(newIndex) {
|
||
var items = dropdown.querySelectorAll('.autocomplete-suggestion');
|
||
items.forEach(function (el) { el.classList.remove('active'); });
|
||
if (newIndex >= 0 && newIndex < items.length) {
|
||
items[newIndex].classList.add('active');
|
||
items[newIndex].scrollIntoView({ block: 'nearest' });
|
||
}
|
||
activeIndex = newIndex;
|
||
}
|
||
|
||
function fetchSuggestions(query) {
|
||
if (fetchController) fetchController.abort();
|
||
fetchController = new AbortController();
|
||
fetch('/autocompleter?q=' + encodeURIComponent(query), { signal: fetchController.signal })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
suggestions = data || [];
|
||
renderDropdown();
|
||
})
|
||
.catch(function (e) {
|
||
if (e.name !== 'AbortError') suggestions = [];
|
||
dropdown.classList.remove('open');
|
||
});
|
||
}
|
||
|
||
input.addEventListener('input', function () {
|
||
clearTimeout(debounceTimer);
|
||
var q = input.value.trim();
|
||
if (q.length < 2) { closeDropdown(); return; }
|
||
debounceTimer = setTimeout(function () { fetchSuggestions(q); }, 250);
|
||
});
|
||
|
||
input.addEventListener('keydown', function (e) {
|
||
if (!dropdown.classList.contains('open')) return;
|
||
var items = dropdown.querySelectorAll('.autocomplete-suggestion');
|
||
if (e.key === 'ArrowDown') {
|
||
e.preventDefault();
|
||
updateActive(Math.min(activeIndex + 1, items.length - 1));
|
||
} else if (e.key === 'ArrowUp') {
|
||
e.preventDefault();
|
||
updateActive(Math.max(activeIndex - 1, -1));
|
||
} else if (e.key === 'Enter' && activeIndex >= 0) {
|
||
e.preventDefault();
|
||
selectSuggestion(activeIndex);
|
||
} else if (e.key === 'Escape') {
|
||
closeDropdown();
|
||
}
|
||
});
|
||
|
||
input.addEventListener('blur', function () {
|
||
setTimeout(closeDropdown, 150);
|
||
});
|
||
|
||
dropdown.addEventListener('mousedown', function (e) {
|
||
var item = e.target.closest('.autocomplete-suggestion');
|
||
if (item) {
|
||
e.preventDefault();
|
||
var idx = parseInt(item.getAttribute('data-index'), 10);
|
||
selectSuggestion(idx);
|
||
}
|
||
});
|
||
}());
|
||
</script>
|
||
</body>
|
||
</html>
|
||
{{end}}
|