chore: checkpoint ai search comments and i18n foundation
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import { terminalConfig } from '../lib/config/terminal';
|
||||
import { getI18n, SUPPORTED_LOCALES } from '../lib/i18n';
|
||||
import type { SiteSettings } from '../lib/types';
|
||||
|
||||
interface Props {
|
||||
@@ -11,11 +12,28 @@ const {
|
||||
siteName = Astro.props.siteSettings?.siteShortName || terminalConfig.branding?.shortName || 'Termi'
|
||||
} = Astro.props;
|
||||
|
||||
const navItems = terminalConfig.navLinks;
|
||||
const { locale, t, buildLocaleUrl } = getI18n(Astro);
|
||||
const aiEnabled = Boolean(Astro.props.siteSettings?.ai?.enabled);
|
||||
const navItems = [
|
||||
{ icon: 'fa-file-code', text: t('nav.articles'), href: '/articles' },
|
||||
{ icon: 'fa-folder', text: t('nav.categories'), href: '/categories' },
|
||||
{ icon: 'fa-tags', text: t('nav.tags'), href: '/tags' },
|
||||
{ icon: 'fa-stream', text: t('nav.timeline'), href: '/timeline' },
|
||||
{ icon: 'fa-star', text: t('nav.reviews'), href: '/reviews' },
|
||||
{ icon: 'fa-link', text: t('nav.friends'), href: '/friends' },
|
||||
{ icon: 'fa-user-secret', text: t('nav.about'), href: '/about' },
|
||||
...(aiEnabled ? [{ icon: 'fa-robot', text: t('nav.ask'), href: '/ask' }] : []),
|
||||
];
|
||||
const localeLinks = SUPPORTED_LOCALES.map((item) => ({
|
||||
locale: item,
|
||||
href: buildLocaleUrl(item),
|
||||
label: t(`common.languages.${item}`),
|
||||
shortLabel: item === 'zh-CN' ? '中' : 'EN',
|
||||
}));
|
||||
const currentPath = Astro.url.pathname;
|
||||
---
|
||||
|
||||
<header class="sticky top-0 z-50 border-b border-[var(--border-color)] backdrop-blur-xl" style="background-color: color-mix(in oklab, var(--bg) 88%, transparent);">
|
||||
<header data-ai-search-enabled={aiEnabled ? 'true' : 'false'} class="sticky top-0 z-50 border-b border-[var(--border-color)] backdrop-blur-xl" style="background-color: color-mix(in oklab, var(--bg) 88%, transparent);">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
|
||||
<div class="terminal-toolbar-shell">
|
||||
<div class="flex flex-col gap-3">
|
||||
@@ -54,17 +72,39 @@ const currentPath = Astro.url.pathname;
|
||||
</div>
|
||||
|
||||
<div class="relative hidden md:block flex-1 min-w-0">
|
||||
<div class="terminal-toolbar-module">
|
||||
<div class="terminal-toolbar-label">grep -i</div>
|
||||
<div class="terminal-toolbar-module gap-3">
|
||||
<div class="terminal-toolbar-label" id="search-label">grep -i</div>
|
||||
{aiEnabled && (
|
||||
<div id="search-mode-panel" class="flex items-center gap-1 rounded-xl border border-[var(--border-color)] bg-[var(--header-bg)]/80 p-1">
|
||||
<button
|
||||
type="button"
|
||||
class="search-mode-btn rounded-lg px-3 py-2 text-xs font-medium text-[var(--text-secondary)] transition hover:bg-[var(--header-bg)]"
|
||||
data-search-mode="keyword"
|
||||
aria-pressed="true"
|
||||
>
|
||||
<i class="fas fa-search mr-1 text-[11px]"></i>
|
||||
<span>{t('header.searchModeKeyword')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="search-mode-btn rounded-lg px-3 py-2 text-xs font-medium text-[var(--text-secondary)] transition hover:bg-[var(--header-bg)]"
|
||||
data-search-mode="ai"
|
||||
aria-pressed="false"
|
||||
>
|
||||
<i class="fas fa-robot mr-1 text-[11px]"></i>
|
||||
<span>{t('header.searchModeAi')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
id="search-input"
|
||||
placeholder="'关键词'"
|
||||
placeholder={t('header.searchPlaceholderKeyword')}
|
||||
class="terminal-console-input"
|
||||
/>
|
||||
<span class="hidden xl:inline text-xs font-mono text-[var(--secondary)]">articles/*.md</span>
|
||||
<span id="search-hint" class="hidden xl:inline text-xs font-mono text-[var(--secondary)]">articles/*.md</span>
|
||||
<button id="search-btn" class="terminal-toolbar-iconbtn">
|
||||
<i class="fas fa-search text-sm"></i>
|
||||
<i id="search-btn-icon" class="fas fa-search text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
@@ -73,11 +113,30 @@ const currentPath = Astro.url.pathname;
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:flex items-center gap-1 rounded-xl border border-[var(--border-color)] bg-[var(--header-bg)]/80 p-1">
|
||||
{localeLinks.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
data-locale-switch={item.locale}
|
||||
class:list={[
|
||||
'rounded-lg px-3 py-2 text-xs font-semibold transition',
|
||||
item.locale === locale
|
||||
? 'bg-[var(--primary)] text-white shadow-sm'
|
||||
: 'text-[var(--text-secondary)] hover:bg-[var(--header-bg)]'
|
||||
]}
|
||||
aria-current={item.locale === locale ? 'true' : undefined}
|
||||
title={item.label}
|
||||
>
|
||||
{item.shortLabel}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="theme-toggle"
|
||||
class="theme-toggle terminal-toolbar-iconbtn h-11 w-11 shrink-0"
|
||||
aria-label="切换主题"
|
||||
title="切换主题"
|
||||
aria-label={t('header.themeToggle')}
|
||||
title={t('header.themeToggle')}
|
||||
>
|
||||
<i id="theme-icon" class="fas fa-moon text-[var(--primary)]"></i>
|
||||
</button>
|
||||
@@ -85,14 +144,14 @@ const currentPath = Astro.url.pathname;
|
||||
<button
|
||||
id="mobile-menu-btn"
|
||||
class="lg:hidden terminal-toolbar-iconbtn h-11 w-11 shrink-0"
|
||||
aria-label="Toggle menu"
|
||||
aria-label={t('header.toggleMenu')}
|
||||
>
|
||||
<i class="fas fa-bars text-[var(--text)]"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:flex items-center gap-3 border-t border-[var(--border-color)]/70 pt-3">
|
||||
<div class="terminal-toolbar-label">navigation</div>
|
||||
<div class="terminal-toolbar-label">{t('header.navigation')}</div>
|
||||
<nav class="min-w-0 flex-1 flex items-center gap-1.5 overflow-x-auto pb-1">
|
||||
{navItems.map((item) => {
|
||||
const isActive = currentPath === item.href || (item.href !== '/' && currentPath.startsWith(item.href));
|
||||
@@ -117,17 +176,62 @@ const currentPath = Astro.url.pathname;
|
||||
<!-- Mobile Menu -->
|
||||
<div id="mobile-menu" class="hidden lg:hidden border-t border-[var(--border-color)] bg-[var(--bg)]">
|
||||
<div class="px-4 py-3 space-y-3">
|
||||
<div class="terminal-toolbar-module md:hidden">
|
||||
<span class="terminal-toolbar-label">grep -i</span>
|
||||
<input
|
||||
type="text"
|
||||
id="mobile-search-input"
|
||||
placeholder="'关键词'"
|
||||
class="terminal-console-input"
|
||||
/>
|
||||
<button id="mobile-search-btn" class="terminal-toolbar-iconbtn">
|
||||
<i class="fas fa-search text-sm"></i>
|
||||
</button>
|
||||
<div class="space-y-3 md:hidden">
|
||||
{aiEnabled && (
|
||||
<div class="flex items-center gap-2 rounded-2xl border border-[var(--border-color)] bg-[var(--header-bg)]/80 p-1">
|
||||
<button
|
||||
type="button"
|
||||
class="search-mode-btn flex-1 rounded-xl px-3 py-2 text-sm font-medium text-[var(--text-secondary)] transition hover:bg-[var(--header-bg)]"
|
||||
data-search-mode="keyword"
|
||||
aria-pressed="true"
|
||||
>
|
||||
<i class="fas fa-search mr-2 text-xs"></i>
|
||||
<span>{t('header.searchModeKeywordMobile')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="search-mode-btn flex-1 rounded-xl px-3 py-2 text-sm font-medium text-[var(--text-secondary)] transition hover:bg-[var(--header-bg)]"
|
||||
data-search-mode="ai"
|
||||
aria-pressed="false"
|
||||
>
|
||||
<i class="fas fa-robot mr-2 text-xs"></i>
|
||||
<span>{t('header.searchModeAiMobile')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="terminal-toolbar-label">{t('common.language')}</span>
|
||||
<div class="flex flex-1 items-center gap-2">
|
||||
{localeLinks.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
data-locale-switch={item.locale}
|
||||
class:list={[
|
||||
'flex-1 rounded-xl border px-3 py-2 text-center text-sm font-medium transition',
|
||||
item.locale === locale
|
||||
? 'border-[var(--primary)] bg-[var(--primary)]/10 text-[var(--primary)]'
|
||||
: 'border-[var(--border-color)] bg-[var(--header-bg)] text-[var(--text-secondary)]'
|
||||
]}
|
||||
aria-current={item.locale === locale ? 'true' : undefined}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-toolbar-module">
|
||||
<span class="terminal-toolbar-label" id="mobile-search-label">grep -i</span>
|
||||
<input
|
||||
type="text"
|
||||
id="mobile-search-input"
|
||||
placeholder={t('header.searchPlaceholderKeyword')}
|
||||
class="terminal-console-input"
|
||||
/>
|
||||
<button id="mobile-search-btn" class="terminal-toolbar-iconbtn">
|
||||
<i id="mobile-search-btn-icon" class="fas fa-search text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p id="mobile-search-hint" class="px-1 text-xs font-mono text-[var(--text-tertiary)]">articles/*.md</p>
|
||||
</div>
|
||||
{navItems.map(item => (
|
||||
<a
|
||||
@@ -320,11 +424,39 @@ const currentPath = Astro.url.pathname;
|
||||
updateTitle();
|
||||
|
||||
// Search functionality
|
||||
const headerRoot = document.querySelector('header[data-ai-search-enabled]');
|
||||
const aiSearchEnabled = headerRoot?.getAttribute('data-ai-search-enabled') === 'true';
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchBtn = document.getElementById('search-btn');
|
||||
const searchBtnIcon = document.getElementById('search-btn-icon');
|
||||
const searchResults = document.getElementById('search-results');
|
||||
const searchLabel = document.getElementById('search-label');
|
||||
const searchHint = document.getElementById('search-hint');
|
||||
const mobileSearchLabel = document.getElementById('mobile-search-label');
|
||||
const mobileSearchHint = document.getElementById('mobile-search-hint');
|
||||
const mobileSearchBtnIcon = document.getElementById('mobile-search-btn-icon');
|
||||
const searchModePanel = document.getElementById('search-mode-panel');
|
||||
const searchModeButtons = Array.from(document.querySelectorAll('.search-mode-btn'));
|
||||
const localeSwitchLinks = Array.from(document.querySelectorAll('[data-locale-switch]'));
|
||||
const searchApiBase = 'http://localhost:5150/api';
|
||||
const searchInputs = [searchInput, mobileSearchInput].filter(Boolean);
|
||||
const t = window.__termiTranslate;
|
||||
const searchModeConfig = {
|
||||
keyword: {
|
||||
label: 'grep -i',
|
||||
hint: t('header.searchHintKeyword'),
|
||||
placeholder: t('header.searchPlaceholderKeyword'),
|
||||
buttonIcon: 'fa-search'
|
||||
},
|
||||
ai: {
|
||||
label: 'ask ai',
|
||||
hint: t('header.searchHintAi'),
|
||||
placeholder: t('header.searchPlaceholderAi'),
|
||||
buttonIcon: 'fa-robot'
|
||||
}
|
||||
};
|
||||
let searchTimer = null;
|
||||
let currentSearchMode = 'keyword';
|
||||
|
||||
function escapeHtml(value) {
|
||||
return value
|
||||
@@ -349,6 +481,102 @@ const currentPath = Astro.url.pathname;
|
||||
);
|
||||
}
|
||||
|
||||
function syncSearchInputs(sourceInput) {
|
||||
const nextValue = sourceInput && 'value' in sourceInput ? sourceInput.value : '';
|
||||
searchInputs.forEach((input) => {
|
||||
if (input !== sourceInput) {
|
||||
input.value = nextValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getQueryFromInput(input) {
|
||||
return input && 'value' in input ? input.value.trim() : '';
|
||||
}
|
||||
|
||||
function buildLocalizedUrl(path) {
|
||||
const nextUrl = new URL(path, window.location.origin);
|
||||
nextUrl.searchParams.set('lang', document.documentElement.lang || 'zh-CN');
|
||||
return `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`;
|
||||
}
|
||||
|
||||
function buildSearchTarget(query) {
|
||||
return buildLocalizedUrl(
|
||||
currentSearchMode === 'ai'
|
||||
? `/ask?q=${encodeURIComponent(query)}`
|
||||
: `/articles?search=${encodeURIComponent(query)}`
|
||||
);
|
||||
}
|
||||
|
||||
function syncSearchModeUI() {
|
||||
const config = searchModeConfig[currentSearchMode] || searchModeConfig.keyword;
|
||||
|
||||
if (searchLabel) {
|
||||
searchLabel.textContent = config.label;
|
||||
}
|
||||
if (mobileSearchLabel) {
|
||||
mobileSearchLabel.textContent = config.label;
|
||||
}
|
||||
if (searchHint) {
|
||||
searchHint.textContent = config.hint;
|
||||
}
|
||||
if (mobileSearchHint) {
|
||||
mobileSearchHint.textContent = config.hint;
|
||||
}
|
||||
|
||||
searchInputs.forEach((input) => {
|
||||
input.setAttribute('placeholder', config.placeholder);
|
||||
});
|
||||
|
||||
if (searchBtnIcon) {
|
||||
searchBtnIcon.className = `fas ${config.buttonIcon} text-sm`;
|
||||
}
|
||||
if (mobileSearchBtnIcon) {
|
||||
mobileSearchBtnIcon.className = `fas ${config.buttonIcon} text-sm`;
|
||||
}
|
||||
|
||||
searchModeButtons.forEach((button) => {
|
||||
const isActive = button.getAttribute('data-search-mode') === currentSearchMode;
|
||||
button.setAttribute('aria-pressed', String(isActive));
|
||||
button.classList.toggle('bg-[var(--primary)]', isActive);
|
||||
button.classList.toggle('text-white', isActive);
|
||||
button.classList.toggle('shadow-sm', isActive);
|
||||
button.classList.toggle('text-[var(--text-secondary)]', !isActive);
|
||||
});
|
||||
}
|
||||
|
||||
function renderAiSearchResults(query) {
|
||||
if (!searchResults) return;
|
||||
|
||||
searchResults.innerHTML = `
|
||||
<div class="overflow-hidden">
|
||||
<div class="border-b border-[var(--border-color)] px-4 py-2 text-xs uppercase tracking-[0.2em] text-[var(--text-tertiary)]">
|
||||
${escapeHtml(t('header.aiModeTitle'))}
|
||||
</div>
|
||||
<div class="space-y-4 px-4 py-4">
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm font-semibold text-[var(--title-color)]">${escapeHtml(t('header.aiModeHeading'))}</div>
|
||||
<p class="text-sm leading-6 text-[var(--text-secondary)]">
|
||||
${escapeHtml(t('header.aiModeDescription'))}
|
||||
</p>
|
||||
<p class="text-xs leading-5 text-[var(--text-tertiary)]">
|
||||
${escapeHtml(t('header.aiModeNotice'))}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--bg)]/70 p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-[var(--text-tertiary)]">${escapeHtml(t('common.search'))}</div>
|
||||
<div class="mt-2 font-mono text-sm text-[var(--title-color)]">${escapeHtml(query)}</div>
|
||||
</div>
|
||||
<a href="${buildSearchTarget(query)}" class="flex items-center justify-between rounded-2xl border border-[var(--primary)]/30 bg-[var(--primary)]/10 px-4 py-3 text-sm font-medium text-[var(--primary)] transition hover:bg-[var(--primary)]/16">
|
||||
<span><i class="fas fa-robot mr-2 text-xs"></i>${escapeHtml(t('header.aiModeCta'))}</span>
|
||||
<i class="fas fa-arrow-right text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
searchResults.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideSearchResults() {
|
||||
if (!searchResults) return;
|
||||
searchResults.classList.add('hidden');
|
||||
@@ -361,7 +589,7 @@ const currentPath = Astro.url.pathname;
|
||||
if (state === 'loading') {
|
||||
searchResults.innerHTML = `
|
||||
<div class="px-4 py-4 text-sm text-[var(--text-secondary)]">
|
||||
正在搜索 <span class="text-[var(--primary)] font-mono">${escapeHtml(query)}</span> ...
|
||||
${escapeHtml(t('header.searching', { query }))}
|
||||
</div>
|
||||
`;
|
||||
searchResults.classList.remove('hidden');
|
||||
@@ -371,7 +599,7 @@ const currentPath = Astro.url.pathname;
|
||||
if (state === 'error') {
|
||||
searchResults.innerHTML = `
|
||||
<div class="px-4 py-4 text-sm text-[var(--danger)]">
|
||||
搜索失败,请稍后再试。
|
||||
${escapeHtml(t('header.searchFailed'))}
|
||||
</div>
|
||||
`;
|
||||
searchResults.classList.remove('hidden');
|
||||
@@ -379,9 +607,18 @@ const currentPath = Astro.url.pathname;
|
||||
}
|
||||
|
||||
if (!results.length) {
|
||||
const aiRetry = aiSearchEnabled
|
||||
? `
|
||||
<a href="${buildLocalizedUrl(`/ask?q=${encodeURIComponent(query)}`)}" class="mt-3 inline-flex items-center gap-2 rounded-full border border-[var(--primary)]/30 bg-[var(--primary)]/10 px-3 py-1.5 text-xs font-medium text-[var(--primary)] transition hover:bg-[var(--primary)]/16">
|
||||
<i class="fas fa-robot text-[11px]"></i>
|
||||
<span>${escapeHtml(t('header.searchEmptyCta'))}</span>
|
||||
</a>
|
||||
`
|
||||
: '';
|
||||
searchResults.innerHTML = `
|
||||
<div class="px-4 py-4 text-sm text-[var(--text-secondary)]">
|
||||
没有找到和 <span class="text-[var(--primary)] font-mono">${escapeHtml(query)}</span> 相关的内容。
|
||||
${escapeHtml(t('header.searchEmpty', { query }))}
|
||||
${aiRetry}
|
||||
</div>
|
||||
`;
|
||||
searchResults.classList.remove('hidden');
|
||||
@@ -399,7 +636,7 @@ const currentPath = Astro.url.pathname;
|
||||
return `
|
||||
<a href="/articles/${encodeURIComponent(item.slug)}" class="block border-b border-[var(--border-color)] px-4 py-3 transition-colors hover:bg-[var(--header-bg)] last:border-b-0">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm font-semibold text-[var(--title-color)]">${highlightText(item.title || 'Untitled', query)}</div>
|
||||
<div class="text-sm font-semibold text-[var(--title-color)]">${highlightText(item.title || t('header.untitled'), query)}</div>
|
||||
<div class="text-[11px] text-[var(--text-tertiary)]">${escapeHtml(item.category || '')}</div>
|
||||
</div>
|
||||
<div class="mt-1 text-xs leading-5 text-[var(--text-secondary)]">${highlightText(item.description || item.content || '', query)}</div>
|
||||
@@ -408,15 +645,25 @@ const currentPath = Astro.url.pathname;
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const aiFooter = aiSearchEnabled
|
||||
? `
|
||||
<a href="${buildLocalizedUrl(`/ask?q=${encodeURIComponent(query)}`)}" class="block border-t border-[var(--border-color)] px-4 py-3 text-sm font-medium text-[var(--primary)] transition-colors hover:bg-[var(--header-bg)]">
|
||||
<i class="fas fa-robot mr-2 text-xs"></i>
|
||||
${escapeHtml(t('header.searchAiFooter'))}
|
||||
</a>
|
||||
`
|
||||
: '';
|
||||
|
||||
searchResults.innerHTML = `
|
||||
<div class="max-h-[26rem] overflow-auto">
|
||||
<div class="border-b border-[var(--border-color)] px-4 py-2 text-xs uppercase tracking-[0.2em] text-[var(--text-tertiary)]">
|
||||
实时搜索结果
|
||||
${escapeHtml(t('header.liveResults'))}
|
||||
</div>
|
||||
${itemsHtml}
|
||||
<a href="/articles?search=${encodeURIComponent(query)}" class="block px-4 py-3 text-sm font-medium text-[var(--primary)] transition-colors hover:bg-[var(--header-bg)]">
|
||||
查看全部结果
|
||||
<a href="${buildLocalizedUrl(`/articles?search=${encodeURIComponent(query)}`)}" class="block px-4 py-3 text-sm font-medium text-[var(--primary)] transition-colors hover:bg-[var(--header-bg)]">
|
||||
${escapeHtml(t('header.searchAllResults'))}
|
||||
</a>
|
||||
${aiFooter}
|
||||
</div>
|
||||
`;
|
||||
searchResults.classList.remove('hidden');
|
||||
@@ -428,6 +675,11 @@ const currentPath = Astro.url.pathname;
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentSearchMode === 'ai') {
|
||||
renderAiSearchResults(query);
|
||||
return;
|
||||
}
|
||||
|
||||
renderSearchResults(query, [], 'loading');
|
||||
|
||||
try {
|
||||
@@ -444,38 +696,59 @@ const currentPath = Astro.url.pathname;
|
||||
}
|
||||
}
|
||||
|
||||
function submitSearch() {
|
||||
const query = searchInput && 'value' in searchInput ? searchInput.value.trim() : '';
|
||||
if (query) {
|
||||
window.location.href = `/articles?search=${encodeURIComponent(query)}`;
|
||||
function setSearchMode(mode) {
|
||||
if (!aiSearchEnabled && mode === 'ai') {
|
||||
currentSearchMode = 'keyword';
|
||||
} else {
|
||||
currentSearchMode = mode;
|
||||
}
|
||||
|
||||
syncSearchModeUI();
|
||||
|
||||
const query = getQueryFromInput(searchInput);
|
||||
if (query && document.activeElement === searchInput) {
|
||||
void runLiveSearch(query);
|
||||
} else if (!query) {
|
||||
hideSearchResults();
|
||||
}
|
||||
}
|
||||
|
||||
function submitSearch(preferredInput) {
|
||||
const query = getQueryFromInput(preferredInput) || getQueryFromInput(searchInput) || getQueryFromInput(mobileSearchInput);
|
||||
if (query) {
|
||||
window.location.href = buildSearchTarget(query);
|
||||
}
|
||||
}
|
||||
|
||||
searchModeButtons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const nextMode = button.getAttribute('data-search-mode') || 'keyword';
|
||||
setSearchMode(nextMode);
|
||||
});
|
||||
});
|
||||
|
||||
searchBtn?.addEventListener('click', function() {
|
||||
submitSearch();
|
||||
submitSearch(searchInput);
|
||||
});
|
||||
mobileSearchBtn?.addEventListener('click', function() {
|
||||
const query = mobileSearchInput && 'value' in mobileSearchInput ? mobileSearchInput.value.trim() : '';
|
||||
if (query) {
|
||||
window.location.href = `/articles?search=${encodeURIComponent(query)}`;
|
||||
}
|
||||
submitSearch(mobileSearchInput);
|
||||
});
|
||||
|
||||
searchInput?.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
submitSearch();
|
||||
e.preventDefault();
|
||||
submitSearch(searchInput);
|
||||
}
|
||||
});
|
||||
mobileSearchInput?.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
const query = this.value.trim();
|
||||
if (query) {
|
||||
window.location.href = `/articles?search=${encodeURIComponent(query)}`;
|
||||
}
|
||||
e.preventDefault();
|
||||
submitSearch(mobileSearchInput);
|
||||
}
|
||||
});
|
||||
|
||||
searchInput?.addEventListener('input', function() {
|
||||
syncSearchInputs(searchInput);
|
||||
const query = this.value.trim();
|
||||
if (searchTimer) {
|
||||
clearTimeout(searchTimer);
|
||||
@@ -485,6 +758,10 @@ const currentPath = Astro.url.pathname;
|
||||
}, 180);
|
||||
});
|
||||
|
||||
mobileSearchInput?.addEventListener('input', function() {
|
||||
syncSearchInputs(mobileSearchInput);
|
||||
});
|
||||
|
||||
searchInput?.addEventListener('focus', function() {
|
||||
const query = this.value.trim();
|
||||
if (query) {
|
||||
@@ -492,11 +769,27 @@ const currentPath = Astro.url.pathname;
|
||||
}
|
||||
});
|
||||
|
||||
syncSearchModeUI();
|
||||
|
||||
localeSwitchLinks.forEach((link) => {
|
||||
link.addEventListener('click', () => {
|
||||
const nextLocale = link.getAttribute('data-locale-switch');
|
||||
if (!nextLocale) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem('locale', nextLocale);
|
||||
document.cookie = `${'termi_locale'}=${encodeURIComponent(nextLocale)};path=/;max-age=31536000;samesite=lax`;
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(event) {
|
||||
const target = event.target;
|
||||
if (
|
||||
searchResults &&
|
||||
!searchResults.contains(target) &&
|
||||
!searchModePanel?.contains(target) &&
|
||||
!target?.closest?.('.search-mode-btn') &&
|
||||
target !== searchInput &&
|
||||
target !== searchBtn &&
|
||||
!searchBtn?.contains(target)
|
||||
|
||||
Reference in New Issue
Block a user