Создана форма поиска

This commit is contained in:
Web-serfer 2026-04-07 20:53:26 +05:00
parent ffd99c404a
commit 7d4289bd9e
3 changed files with 353 additions and 215 deletions

View file

@ -1,5 +1,18 @@
--- ---
const title = 'Поиск статей'; import { blogPosts } from '@data/blogData';
const title = 'Поиск по статьям';
const searchData = blogPosts.map(post => ({
title: post.title,
description: post.description,
slug: post.slug,
category: post.category,
categoryColor: post.categoryColor,
date: post.date
}));
const postsJson = JSON.stringify(searchData);
--- ---
<div id="search-modal" class="modal-overlay" aria-hidden="true"> <div id="search-modal" class="modal-overlay" aria-hidden="true">
@ -14,245 +27,327 @@ const title = 'Поиск статей';
<div class="modal-content"> <div class="modal-content">
<h2 id="search-modal-title" class="modal-title">{title}</h2> <h2 id="search-modal-title" class="modal-title">{title}</h2>
<p class="modal-description"> <p class="modal-description">
Введите ключевые слова для поиска статей Найдите ответ на ваш вопрос среди наших публикаций
</p> </p>
<form class="search-form" id="search-form"> <div class="search-form">
<div class="search-input-wrapper"> <div class="search-input-wrapper">
<svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle> <circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line> <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg> </svg>
<input <input
type="text" type="text"
id="search-input" id="search-input"
name="search"
class="search-input" class="search-input"
placeholder="Например: ДТП, ОСАГО, лишение прав..." placeholder="Например: ДТП, ОСАГО, лишение прав..."
required
autocomplete="off" autocomplete="off"
/> />
</div> </div>
</div>
<button type="submit" class="search-submit"> <!-- Контейнер для динамических результатов -->
Найти статьи <div class="search-results" id="search-results">
</button> </div>
</form>
</div> </div>
</div> </div>
</div> </div>
<script> <script define:vars={{ postsJson }}>
(function() { (function() {
const modal = document.getElementById('search-modal'); const modal = document.getElementById('search-modal');
const closeBtn = document.getElementById('search-modal-close-btn'); const closeBtn = document.getElementById('search-modal-close-btn');
const form = document.getElementById('search-form');
const searchInput = document.getElementById('search-input'); const searchInput = document.getElementById('search-input');
const resultsContainer = document.getElementById('search-results');
const posts = JSON.parse(postsJson);
if (!modal || !searchInput || !resultsContainer) return;
function openModal() { function openModal() {
if (!modal) return;
modal.classList.add('active'); modal.classList.add('active');
modal.setAttribute('aria-hidden', 'false'); modal.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
// Фокус на поле ввода после открытия setTimeout(() => searchInput.focus(), 200);
setTimeout(() => (searchInput as HTMLInputElement)?.focus(), 100);
} }
function closeModal() { function closeModal() {
if (!modal) return;
modal.classList.remove('active'); modal.classList.remove('active');
modal.setAttribute('aria-hidden', 'true'); modal.setAttribute('aria-hidden', 'true');
document.body.style.overflow = ''; document.body.style.overflow = '';
(searchInput as HTMLInputElement).value = ''; searchInput.value = '';
resultsContainer.innerHTML = '';
} }
// Открытие по кастомному событию function handleSearch(query) {
window.addEventListener('open-modal', (e: Event) => { const trimmed = query.trim().toLowerCase();
const customEvent = e as CustomEvent<string>;
if (customEvent.detail === 'search-modal') { if (trimmed.length < 2) {
openModal(); resultsContainer.innerHTML = '';
return;
} }
const filtered = posts.filter(post =>
post.title.toLowerCase().includes(trimmed) ||
post.description.toLowerCase().includes(trimmed) ||
post.category.toLowerCase().includes(trimmed)
);
if (filtered.length === 0) {
resultsContainer.innerHTML = `
<div class="no-results">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" style="margin-bottom: 1rem; opacity: 0.5;">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<p>По запросу <strong>"${query}"</strong> ничего не найдено</p>
</div>`;
return;
}
// вывод динамического списка статей
resultsContainer.innerHTML = filtered.map(post => `
<a href="/blog/${post.slug}" class="search-result-item">
<span class="result-date">${post.date}</span>
<h4 class="result-title">${post.title}</h4>
<p class="result-description">${post.description.substring(0, 120)}...</p>
<span class="result-arrow">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</span>
</a>
`).join('');
}
let timeout;
searchInput.addEventListener('input', (e) => {
clearTimeout(timeout);
timeout = setTimeout(() => handleSearch(e.target.value), 200);
}); });
// Закрытие по крестику window.addEventListener('open-modal', (e) => {
closeBtn?.addEventListener('click', closeModal); if (e.detail === 'search-modal') openModal();
// Закрытие по клику вне модального окна
modal?.addEventListener('click', (e) => {
if (e.target === modal) {
closeModal();
}
}); });
// Закрытие по Escape closeBtn.addEventListener('click', closeModal);
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModal();
});
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal?.classList.contains('active')) { if (e.key === 'Escape' && modal.classList.contains('active')) closeModal();
closeModal();
}
});
// Обработка отправки формы
form?.addEventListener('submit', (e) => {
e.preventDefault();
const query = (searchInput as HTMLInputElement).value.trim();
if (query) {
// Перенаправление на страницу поиска с параметром
window.location.href = `/blog/search?q=${encodeURIComponent(query)}`;
closeModal();
}
}); });
})(); })();
</script> </script>
<style> <style is:global>
/* --- БАЗОВЫЕ СТИЛИ МОДАЛКИ (БЕЗ ИЗМЕНЕНИЙ) --- */
.modal-overlay { .modal-overlay {
position: fixed; position: fixed;
top: 0; inset: 0;
left: 0; background: rgba(10, 25, 41, 0.85);
width: 100%; backdrop-filter: blur(8px);
height: 100%;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
display: flex; display: flex;
align-items: center; align-items: flex-start;
justify-content: center; justify-content: center;
z-index: 1000; z-index: 9999;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
padding: 2rem 1rem;
} }
.modal-overlay.active { .modal-overlay.active { opacity: 1; visibility: visible; }
opacity: 1;
visibility: visible;
}
.modal-container { .modal-container {
background: #ffffff; background: #ffffff;
border-radius: 16px; border-radius: 24px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); width: 100%;
max-width: 600px; max-width: 720px;
width: 90%; max-height: 85vh;
max-height: 90vh; box-shadow: 0 30px 60px -12px rgba(0, 0, 0, 0.4);
overflow-y: auto;
position: relative; position: relative;
transform: translateY(-20px) scale(0.95); transform: translateY(-20px);
transition: transform 0.3s ease; transition: transform 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
display: flex;
flex-direction: column;
overflow: hidden;
} }
.modal-overlay.active .modal-container { .modal-overlay.active .modal-container { transform: translateY(0); }
transform: translateY(0) scale(1);
}
.modal-close { .modal-close {
position: absolute; position: absolute;
top: 16px; top: 1.25rem;
right: 16px; right: 1.25rem;
background: transparent; background: #f1f5f9;
border: none; border: none;
cursor: pointer; cursor: pointer;
padding: 8px; width: 36px;
border-radius: 8px; height: 36px;
color: #535e6c; border-radius: 50%;
transition: background 0.2s ease, color 0.2s ease;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #64748b;
z-index: 10;
transition: all 0.2s ease;
} }
.modal-close:hover { .modal-close:hover { background: #e2e8f0; color: #0f172a; transform: rotate(90deg); }
background: #f0f2f5;
color: #1e3050;
}
.modal-content { .modal-content {
padding: 40px 32px 32px; padding: 3rem 2.5rem 2rem;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
} }
.modal-title { .modal-title {
font-size: 1.5rem; font-size: 1.85rem;
font-weight: 700; font-weight: 800;
color: #1e3050; color: #0f172a;
margin: 0 0 12px; margin: 0 0 0.5rem;
text-align: center; letter-spacing: -0.02em;
} }
.modal-description { .modal-description { color: #64748b; margin-bottom: 2rem; font-size: 1.05rem; }
color: #535e6c;
font-size: 0.95rem;
text-align: center;
margin: 0 0 28px;
line-height: 1.5;
}
.search-form { .search-input-wrapper { position: relative; margin-bottom: 2rem; }
display: flex;
flex-direction: column;
gap: 20px;
}
.search-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.search-icon { .search-icon {
position: absolute; position: absolute;
left: 16px; left: 1.25rem;
color: #9ca3af; top: 50%;
pointer-events: none; transform: translateY(-50%);
color: #d4af37; /* Акцентный золотой для иконки */
} }
.search-input { .search-input {
width: 100%; width: 100%;
padding: 14px 16px 14px 48px; padding: 1.1rem 1.1rem 1.1rem 3.75rem;
border: 2px solid #e0e4e8; border: 2px solid #e2e8f0;
border-radius: 8px; border-radius: 16px;
font-size: 1rem; font-size: 1.15rem;
font-family: inherit; transition: all 0.3s ease;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
outline: none; outline: none;
background: #f8fafc;
} }
.search-input:focus { .search-input:focus {
border-color: #1e3050; border-color: #d4af37;
box-shadow: 0 0 0 3px rgba(30, 48, 80, 0.1); background: #fff;
box-shadow: 0 0 0 4px rgba(212, 175, 55, 0.1);
} }
.search-input::placeholder { /* --- СТИЛИЗАЦИЯ СПИСКА РЕЗУЛЬТАТОВ --- */
color: #9ca3af; .search-results {
overflow-y: auto;
flex: 1;
padding-right: 0.5rem;
display: flex;
flex-direction: column;
gap: 16px;
} }
.search-submit { .search-result-item {
background: linear-gradient(180deg, #eac26e 0%, #ce9f40 100%); display: flex;
color: #ffffff; flex-direction: column;
border: none; gap: 8px;
padding: 14px 24px; padding: 20px;
border-radius: 8px; border-radius: 12px;
font-size: 1rem; text-decoration: none;
background: #ffffff;
border: 1px solid #eef2f6;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.search-result-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: linear-gradient(180deg, #d4af37 0%, #f4d03f 100%);
opacity: 0;
transition: opacity 0.3s ease;
border-radius: 4px 0 0 4px;
}
.search-result-item:hover {
border-color: #cbd5e1;
transform: translateX(4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.search-result-item:hover::before {
opacity: 1;
}
.result-date {
font-size: 0.8rem;
color: #94a3b8;
font-weight: 600; font-weight: 600;
cursor: pointer; letter-spacing: 0.02em;
}
.result-title {
font-size: 1.15rem;
font-weight: 700;
color: #0f172a;
margin: 0;
line-height: 1.4;
}
.result-description {
font-size: 0.9rem;
color: #64748b;
margin: 0;
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
padding-right: 28px;
}
.result-arrow {
position: absolute;
right: 20px;
bottom: 20px;
color: #cbd5e1;
transition: all 0.3s ease; transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(234, 194, 110, 0.3); display: flex;
align-items: center;
justify-content: center;
} }
.search-submit:hover { .search-result-item:hover .result-arrow {
box-shadow: 0 6px 20px rgba(234, 194, 110, 0.5); color: #d4af37;
transform: translateY(-2px); transform: translateX(4px);
} }
.search-submit:active { .no-results {
transform: translateY(0); padding: 4rem 1rem;
text-align: center;
color: #64748b;
background: #f8fafc;
border-radius: 20px;
border: 2px dashed #e2e8f0;
} }
@media (max-width: 480px) { /* Скроллбар */
.modal-content { .search-results::-webkit-scrollbar { width: 6px; }
padding: 32px 24px 24px; .search-results::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 10px; }
}
.modal-title { @media (max-width: 640px) {
font-size: 1.25rem; .modal-content { padding: 2.5rem 1.5rem 1.5rem; }
} .modal-title { font-size: 1.5rem; }
.search-result-item { padding: 16px; }
.result-title { font-size: 1.05rem; }
.result-description { padding-right: 0; -webkit-line-clamp: 1; }
.result-arrow { right: 16px; bottom: 16px; }
} }
</style> </style>

View file

@ -7,7 +7,9 @@ export interface Props {
const { categories, activeCategory = 'Все' } = Astro.props; const { categories, activeCategory = 'Все' } = Astro.props;
--- ---
<div class="blog-categories animate-on-scroll" data-animation="fade-up"> <div class="blog-categories">
<div class="site-container">
<div class="categories-inner animate-on-scroll" data-animation="fade-up">
<div class="categories-wrapper"> <div class="categories-wrapper">
{categories.map((cat) => ( {categories.map((cat) => (
<button <button
@ -18,6 +20,15 @@ const { categories, activeCategory = 'Все' } = Astro.props;
</button> </button>
))} ))}
</div> </div>
<button class="search-icon-btn" id="search-icon-btn" data-modal-target="search-modal" aria-label="Поиск статей">
<svg width="22" height="22" 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>
</div>
</div>
</div> </div>
<style> <style>
@ -26,13 +37,38 @@ const { categories, activeCategory = 'Все' } = Astro.props;
background: #f8fafc; background: #f8fafc;
} }
.categories-wrapper { .categories-inner {
display: flex; display: flex;
flex-wrap: wrap; align-items: center;
gap: 0.75rem;
justify-content: center; justify-content: center;
max-width: 900px; gap: 0.75rem;
margin: 0 auto; flex-wrap: wrap;
}
.categories-wrapper {
display: contents;
}
.search-icon-btn {
background: #ffffff;
border: 2px solid #e2e8f0;
color: #64748b;
width: 42px;
height: 42px;
border-radius: 2rem;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.search-icon-btn:hover {
border-color: #d4af37;
color: #d4af37;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(212, 175, 55, 0.3);
} }
.category-btn { .category-btn {
@ -87,6 +123,10 @@ const { categories, activeCategory = 'Все' } = Astro.props;
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.categories-inner {
flex-wrap: wrap;
}
.categories-wrapper { .categories-wrapper {
gap: 0.5rem; gap: 0.5rem;
} }
@ -99,29 +139,54 @@ const { categories, activeCategory = 'Все' } = Astro.props;
</style> </style>
<script> <script>
// Клиентская логика фильтрации (пока заглушка) // Клиентская логика фильтрации
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', initFilter);
document.addEventListener('astro:page-load', initFilter);
function initFilter() {
const buttons = document.querySelectorAll('.category-btn'); const buttons = document.querySelectorAll('.category-btn');
const cards = document.querySelectorAll('.blog-card-wrapper');
buttons.forEach(btn => { buttons.forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const category = btn.getAttribute('data-category');
// Обновляем активную кнопку
buttons.forEach(b => b.classList.remove('active')); buttons.forEach(b => b.classList.remove('active'));
btn.classList.add('active'); btn.classList.add('active');
// TODO: Добавить логику фильтрации при интеграции с коллекцией // Фильтрация карточек
console.log('Выбрана категория:', btn.getAttribute('data-category')); cards.forEach(card => {
const cardCategory = card.getAttribute('data-category');
if (category === 'Все' || cardCategory === category) {
card.style.display = '';
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
requestAnimationFrame(() => {
card.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
card.style.opacity = '1';
card.style.transform = 'translateY(0)';
});
} else {
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
setTimeout(() => {
card.style.display = 'none';
}, 300);
}
}); });
}); });
}); });
document.addEventListener('astro:page-load', () => { // Открытие поиска
const buttons = document.querySelectorAll('.category-btn'); const searchBtn = document.getElementById('search-icon-btn');
searchBtn?.addEventListener('click', () => {
buttons.forEach(btn => { window.dispatchEvent(new CustomEvent('open-modal', {
btn.addEventListener('click', () => { detail: 'search-modal'
buttons.forEach(b => b.classList.remove('active')); }));
btn.classList.add('active');
});
});
}); });
}
</script> </script>

View file

@ -5,7 +5,6 @@ import BlogHero from '@components/blog/BlogHero.astro';
import BlogCategories from '@components/blog/BlogCategories.astro'; import BlogCategories from '@components/blog/BlogCategories.astro';
import BlogCard from '@components/blog/BlogCard.astro'; import BlogCard from '@components/blog/BlogCard.astro';
import BlogPagination from '@components/blog/BlogPagination.astro'; import BlogPagination from '@components/blog/BlogPagination.astro';
import SearchIcon from '@components/blog/SearchIcon.astro';
import SearchModal from '@components/base/SearchModal.astro'; import SearchModal from '@components/base/SearchModal.astro';
import { blogPosts, categories } from '@data/blogData'; import { blogPosts, categories } from '@data/blogData';
--- ---
@ -17,23 +16,14 @@ import { blogPosts, categories } from '@data/blogData';
> >
<BlogHero /> <BlogHero />
<!-- Иконка поиска -->
<section class="search-section">
<div class="site-container">
<div class="search-container">
<SearchIcon />
<p class="search-hint">Нажмите для поиска статей</p>
</div>
</div>
</section>
<BlogCategories categories={categories} /> <BlogCategories categories={categories} />
<!-- Сетка статей --> <!-- Сетка статей -->
<section class="blog-grid-section"> <section class="blog-grid-section">
<div class="site-container"> <div class="site-container">
<div class="blog-grid"> <div class="blog-grid" id="blog-grid">
{blogPosts.map((post) => ( {blogPosts.map((post) => (
<article class="blog-card-wrapper" data-category={post.category}>
<BlogCard <BlogCard
title={post.title} title={post.title}
description={post.description} description={post.description}
@ -44,6 +34,7 @@ import { blogPosts, categories } from '@data/blogData';
imageUrl={post.imageUrl} imageUrl={post.imageUrl}
slug={post.slug} slug={post.slug}
/> />
</article>
))} ))}
</div> </div>
@ -66,28 +57,11 @@ import { blogPosts, categories } from '@data/blogData';
</div> </div>
</div> </div>
</section> </section>
</Layout>
<SearchModal /> <SearchModal />
</Layout>
<style> <style>
.search-section {
padding: 2rem 0;
background: linear-gradient(135deg, #0a2540 0%, #1e3a5f 100%);
}
.search-container {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.search-hint {
color: rgba(255, 255, 255, 0.7);
font-size: 0.9rem;
margin: 0;
}
.blog-grid-section { .blog-grid-section {
padding: 3rem 0 0; padding: 3rem 0 0;
background: #f8fafc; background: #f8fafc;
@ -99,6 +73,10 @@ import { blogPosts, categories } from '@data/blogData';
gap: 2rem; gap: 2rem;
} }
.blog-card-wrapper {
transition: opacity 0.3s ease, transform 0.3s ease;
}
/* ===== CTA ===== */ /* ===== CTA ===== */
.blog-cta { .blog-cta {
padding: 5rem 0; padding: 5rem 0;