astro_redi/frontend/src/components/layout/header/Search.astro

180 lines
8.3 KiB
Text
Raw Normal View History

2026-03-26 03:56:25 +00:00
---
// Search.astro
---
<!-- Кнопка вызова -->
<button
type="button"
id="search-trigger"
aria-label="Открыть поиск"
class="group relative hidden w-full transform items-center justify-center p-2 text-center font-medium tracking-wide text-neutral-400 transition-transform duration-200 ease-out hover:scale-110 hover:text-white sm:mb-0 md:w-auto lg:flex"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
<!-- Модальное окно -->
<dialog id="search-modal" class="bg-transparent p-0 m-0 w-full h-full max-w-none max-h-none fixed inset-0 z-50 backdrop:bg-black/70 backdrop:backdrop-blur-sm open:animate-fade-in outline-none">
<div class="fixed inset-0 flex items-start justify-center p-4 pt-20" id="search-container">
<div class="relative w-full max-w-2xl rounded-2xl border border-neutral-800 bg-neutral-900 shadow-2xl overflow-hidden flex flex-col max-h-[80vh]" id="search-panel">
<!-- Кнопка закрытия -->
<button
type="button"
id="close-search"
class="absolute right-5 top-5 z-20 flex h-10 w-10 items-center justify-center rounded-full bg-neutral-800 text-neutral-400 transition-all duration-200 hover:bg-neutral-700 hover:text-white hover:rotate-90 border border-neutral-700"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<!-- Контейнер инпута с ОГРОМНЫМ отступом сверху (pt-24 = 96px) -->
<div class="px-8 pb-6 pt-24 border-b border-neutral-800 bg-neutral-900/50">
<div class="relative">
<div class="absolute left-5 top-1/2 -translate-y-1/2 text-indigo-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</div>
<input
id="search-input"
type="text"
placeholder="Введите запрос..."
class="w-full rounded-xl border border-neutral-700 bg-black/20 py-4 pl-14 pr-4 text-xl text-white placeholder:text-neutral-500 transition-all focus:border-indigo-500 focus:bg-neutral-950 focus:outline-none focus:ring-1 focus:ring-indigo-500"
/>
</div>
</div>
<!-- Результаты -->
<div id="search-results" class="flex-1 overflow-y-auto p-4 space-y-2 min-h-[100px]">
<p class="py-10 text-center text-neutral-500 text-lg">Начните вводить текст для поиска</p>
</div>
</div>
</div>
</dialog>
<!-- TEMPLATE и SCRIPT без изменений (как в прошлом ответе) -->
<template id="search-result-template">
<a href="" class="block rounded-lg p-4 transition-colors hover:bg-neutral-800 group border border-transparent hover:border-neutral-700">
<h3 class="text-lg font-semibold text-white group-hover:text-indigo-400 transition-colors result-title"></h3>
<p class="mt-1 text-sm text-neutral-400 line-clamp-2 result-desc"></p>
</a>
</template>
<style>
dialog[open] { animation: fadeIn 0.2s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: scale(0.98); } to { opacity: 1; transform: scale(1); } }
</style>
<!-- Оставьте HTML как есть, замените только секцию <script> -->
<script>
// Интерфейс для типизации (если используете TypeScript)
interface SearchItem {
id: string;
title: string;
description: string;
slug: string;
}
const trigger = document.getElementById('search-trigger');
const modal = document.getElementById('search-modal') as HTMLDialogElement | null;
const closeBtn = document.getElementById('close-search');
const container = document.getElementById('search-container');
const input = document.getElementById('search-input') as HTMLInputElement | null;
const resultsContainer = document.getElementById('search-results');
const template = document.getElementById('search-result-template') as HTMLTemplateElement | null;
// Открытие модалки
const openModal = () => {
if (!modal) return;
modal.showModal();
document.body.style.overflow = 'hidden'; // Блокируем скролл страницы
input?.focus();
if (resultsContainer) resultsContainer.innerHTML = '<p class="py-10 text-center text-neutral-500">Начните вводить текст для поиска</p>';
};
// Закрытие модалки
const closeModal = () => {
if (!modal) return;
modal.close();
document.body.style.overflow = '';
if (input) input.value = ''; // Очищаем поле
};
// Слушатели событий
trigger?.addEventListener('click', openModal);
closeBtn?.addEventListener('click', closeModal);
// Закрытие по клику вне области (на backdrop)
modal?.addEventListener('click', (e) => {
const rect = modal.getBoundingClientRect();
// Проверяем, был ли клик внутри диалога или снаружи
// (HTMLDialogElement работает специфично с backdrop)
if (e.target === modal) {
closeModal();
}
});
// Логика поиска
let debounceTimer: ReturnType<typeof setTimeout>;
input?.addEventListener('input', (e: Event) => {
const target = e.target as HTMLInputElement;
const query = target.value.trim();
// Сброс таймера
clearTimeout(debounceTimer);
// Очистка если пусто
if (!query) {
if (resultsContainer) resultsContainer.innerHTML = '<p class="py-10 text-center text-neutral-500">Начните вводить текст</p>';
return;
}
// Задержка перед запросом (Debounce)
debounceTimer = setTimeout(async () => {
try {
if (resultsContainer) resultsContainer.innerHTML = '<div class="py-10 text-center text-neutral-500">Поиск...</div>';
// ! ВАЖНО: Запрос идет на наш созданный API
const res = await fetch(`/api/search.json?q=${encodeURIComponent(query)}`);
if (!res.ok) throw new Error('Network error');
const data: SearchItem[] = await res.json();
// Очистка контейнера перед вставкой
if (resultsContainer) resultsContainer.innerHTML = '';
if (data.length === 0) {
if (resultsContainer) resultsContainer.innerHTML = '<p class="py-10 text-center text-neutral-400">Ничего не найдено</p>';
return;
}
// Рендер результатов
data.forEach(item => {
if (template) {
const clone = template.content.cloneNode(true) as DocumentFragment;
const link = clone.querySelector('a');
const titleEl = clone.querySelector('.result-title');
const descEl = clone.querySelector('.result-desc');
// Подставляем данные. Убедитесь, что пути URL совпадают с вашей структурой (/blog/ или /posts/)
if (link) link.href = `/blog/${item.slug}`;
if (titleEl) titleEl.textContent = item.title;
// Если описания нет, можно не выводить
if (descEl) descEl.textContent = item.description || '';
if (resultsContainer) resultsContainer.appendChild(clone);
}
});
} catch (error) {
console.error('Ошибка:', error);
if (resultsContainer) resultsContainer.innerHTML = '<p class="py-10 text-center text-red-400">Ошибка при поиске</p>';
}
}, 300); // Ждем 300мс после ввода последней буквы
});
</script>