kafka/internal/views/templates/base.html

169 lines
6.5 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{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}}