astro_redi/frontend/src/components/layout/header/Search.astro
2026-03-26 08:56:25 +05:00

180 lines
No EOL
8.3 KiB
Text
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.

---
// 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>