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

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">
@ -14,245 +27,327 @@ const title = 'Поиск статей';
<div class="modal-content">
<h2 id="search-modal-title" class="modal-title">{title}</h2>
<p class="modal-description">
Введите ключевые слова для поиска статей
Найдите ответ на ваш вопрос среди наших публикаций
</p>
<form class="search-form" id="search-form">
<div class="search-form">
<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>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input
type="text"
id="search-input"
name="search"
class="search-input"
placeholder="Например: ДТП, ОСАГО, лишение прав..."
required
autocomplete="off"
type="text"
id="search-input"
class="search-input"
placeholder="Например: ДТП, ОСАГО, лишение прав..."
autocomplete="off"
/>
</div>
</div>
<button type="submit" class="search-submit">
Найти статьи
</button>
</form>
<!-- Контейнер для динамических результатов -->
<div class="search-results" id="search-results">
</div>
</div>
</div>
</div>
<script>
<script define:vars={{ postsJson }}>
(function() {
const modal = document.getElementById('search-modal');
const closeBtn = document.getElementById('search-modal-close-btn');
const form = document.getElementById('search-form');
const searchInput = document.getElementById('search-input');
const resultsContainer = document.getElementById('search-results');
const posts = JSON.parse(postsJson);
if (!modal || !searchInput || !resultsContainer) return;
function openModal() {
if (!modal) return;
modal.classList.add('active');
modal.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
// Фокус на поле ввода после открытия
setTimeout(() => (searchInput as HTMLInputElement)?.focus(), 100);
setTimeout(() => searchInput.focus(), 200);
}
function closeModal() {
if (!modal) return;
modal.classList.remove('active');
modal.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
(searchInput as HTMLInputElement).value = '';
searchInput.value = '';
resultsContainer.innerHTML = '';
}
// Открытие по кастомному событию
window.addEventListener('open-modal', (e: Event) => {
const customEvent = e as CustomEvent<string>;
if (customEvent.detail === 'search-modal') {
openModal();
function handleSearch(query) {
const trimmed = query.trim().toLowerCase();
if (trimmed.length < 2) {
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);
});
// Закрытие по крестику
closeBtn?.addEventListener('click', closeModal);
// Закрытие по клику вне модального окна
modal?.addEventListener('click', (e) => {
if (e.target === modal) {
closeModal();
}
window.addEventListener('open-modal', (e) => {
if (e.detail === 'search-modal') openModal();
});
// Закрытие по Escape
closeBtn.addEventListener('click', closeModal);
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModal();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal?.classList.contains('active')) {
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();
}
if (e.key === 'Escape' && modal.classList.contains('active')) closeModal();
});
})();
</script>
<style>
<style is:global>
/* --- БАЗОВЫЕ СТИЛИ МОДАЛКИ (БЕЗ ИЗМЕНЕНИЙ) --- */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
inset: 0;
background: rgba(10, 25, 41, 0.85);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
align-items: flex-start;
justify-content: center;
z-index: 1000;
z-index: 9999;
opacity: 0;
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 {
opacity: 1;
visibility: visible;
}
.modal-overlay.active { opacity: 1; visibility: visible; }
.modal-container {
background: #ffffff;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
border-radius: 24px;
width: 100%;
max-width: 720px;
max-height: 85vh;
box-shadow: 0 30px 60px -12px rgba(0, 0, 0, 0.4);
position: relative;
transform: translateY(-20px) scale(0.95);
transition: transform 0.3s ease;
transform: translateY(-20px);
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 {
transform: translateY(0) scale(1);
}
.modal-overlay.active .modal-container { transform: translateY(0); }
.modal-close {
position: absolute;
top: 16px;
right: 16px;
background: transparent;
top: 1.25rem;
right: 1.25rem;
background: #f1f5f9;
border: none;
cursor: pointer;
padding: 8px;
border-radius: 8px;
color: #535e6c;
transition: background 0.2s ease, color 0.2s ease;
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #64748b;
z-index: 10;
transition: all 0.2s ease;
}
.modal-close:hover {
background: #f0f2f5;
color: #1e3050;
}
.modal-close:hover { background: #e2e8f0; color: #0f172a; transform: rotate(90deg); }
.modal-content {
padding: 40px 32px 32px;
padding: 3rem 2.5rem 2rem;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.modal-title {
font-size: 1.5rem;
font-weight: 700;
color: #1e3050;
margin: 0 0 12px;
text-align: center;
font-size: 1.85rem;
font-weight: 800;
color: #0f172a;
margin: 0 0 0.5rem;
letter-spacing: -0.02em;
}
.modal-description {
color: #535e6c;
font-size: 0.95rem;
text-align: center;
margin: 0 0 28px;
line-height: 1.5;
}
.modal-description { color: #64748b; margin-bottom: 2rem; font-size: 1.05rem; }
.search-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.search-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.search-input-wrapper { position: relative; margin-bottom: 2rem; }
.search-icon {
position: absolute;
left: 16px;
color: #9ca3af;
pointer-events: none;
left: 1.25rem;
top: 50%;
transform: translateY(-50%);
color: #d4af37; /* Акцентный золотой для иконки */
}
.search-input {
width: 100%;
padding: 14px 16px 14px 48px;
border: 2px solid #e0e4e8;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
padding: 1.1rem 1.1rem 1.1rem 3.75rem;
border: 2px solid #e2e8f0;
border-radius: 16px;
font-size: 1.15rem;
transition: all 0.3s ease;
outline: none;
background: #f8fafc;
}
.search-input:focus {
border-color: #1e3050;
box-shadow: 0 0 0 3px rgba(30, 48, 80, 0.1);
border-color: #d4af37;
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 {
background: linear-gradient(180deg, #eac26e 0%, #ce9f40 100%);
color: #ffffff;
border: none;
padding: 14px 24px;
border-radius: 8px;
font-size: 1rem;
.search-result-item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 20px;
border-radius: 12px;
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;
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;
box-shadow: 0 4px 12px rgba(234, 194, 110, 0.3);
display: flex;
align-items: center;
justify-content: center;
}
.search-submit:hover {
box-shadow: 0 6px 20px rgba(234, 194, 110, 0.5);
transform: translateY(-2px);
.search-result-item:hover .result-arrow {
color: #d4af37;
transform: translateX(4px);
}
.search-submit:active {
transform: translateY(0);
.no-results {
padding: 4rem 1rem;
text-align: center;
color: #64748b;
background: #f8fafc;
border-radius: 20px;
border: 2px dashed #e2e8f0;
}
@media (max-width: 480px) {
.modal-content {
padding: 32px 24px 24px;
}
/* Скроллбар */
.search-results::-webkit-scrollbar { width: 6px; }
.search-results::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 10px; }
.modal-title {
font-size: 1.25rem;
}
@media (max-width: 640px) {
.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,16 +7,27 @@ export interface Props {
const { categories, activeCategory = 'Все' } = Astro.props;
---
<div class="blog-categories animate-on-scroll" data-animation="fade-up">
<div class="categories-wrapper">
{categories.map((cat) => (
<button
class={`category-btn ${cat === activeCategory ? 'active' : ''}`}
data-category={cat}
>
{cat}
<div class="blog-categories">
<div class="site-container">
<div class="categories-inner animate-on-scroll" data-animation="fade-up">
<div class="categories-wrapper">
{categories.map((cat) => (
<button
class={`category-btn ${cat === activeCategory ? 'active' : ''}`}
data-category={cat}
>
{cat}
</button>
))}
</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>
@ -26,13 +37,38 @@ const { categories, activeCategory = 'Все' } = Astro.props;
background: #f8fafc;
}
.categories-wrapper {
.categories-inner {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
justify-content: center;
max-width: 900px;
margin: 0 auto;
gap: 0.75rem;
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 {
@ -87,6 +123,10 @@ const { categories, activeCategory = 'Все' } = Astro.props;
}
@media (max-width: 640px) {
.categories-inner {
flex-wrap: wrap;
}
.categories-wrapper {
gap: 0.5rem;
}
@ -99,29 +139,54 @@ const { categories, activeCategory = 'Все' } = Astro.props;
</style>
<script>
// Клиентская логика фильтрации (пока заглушка)
document.addEventListener('DOMContentLoaded', () => {
// Клиентская логика фильтрации
document.addEventListener('DOMContentLoaded', initFilter);
document.addEventListener('astro:page-load', initFilter);
function initFilter() {
const buttons = document.querySelectorAll('.category-btn');
const cards = document.querySelectorAll('.blog-card-wrapper');
buttons.forEach(btn => {
btn.addEventListener('click', () => {
const category = btn.getAttribute('data-category');
// Обновляем активную кнопку
buttons.forEach(b => b.classList.remove('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');
buttons.forEach(btn => {
btn.addEventListener('click', () => {
buttons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
// Открытие поиска
const searchBtn = document.getElementById('search-icon-btn');
searchBtn?.addEventListener('click', () => {
window.dispatchEvent(new CustomEvent('open-modal', {
detail: 'search-modal'
}));
});
});
}
</script>

View file

@ -5,7 +5,6 @@ import BlogHero from '@components/blog/BlogHero.astro';
import BlogCategories from '@components/blog/BlogCategories.astro';
import BlogCard from '@components/blog/BlogCard.astro';
import BlogPagination from '@components/blog/BlogPagination.astro';
import SearchIcon from '@components/blog/SearchIcon.astro';
import SearchModal from '@components/base/SearchModal.astro';
import { blogPosts, categories } from '@data/blogData';
---
@ -16,34 +15,26 @@ import { blogPosts, categories } from '@data/blogData';
canonicalLink={`${SITE_URL}/blog`}
>
<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} />
<!-- Сетка статей -->
<section class="blog-grid-section">
<div class="site-container">
<div class="blog-grid">
<div class="blog-grid" id="blog-grid">
{blogPosts.map((post) => (
<BlogCard
title={post.title}
description={post.description}
category={post.category}
categoryColor={post.categoryColor}
date={post.date}
readTime={post.readTime}
imageUrl={post.imageUrl}
slug={post.slug}
/>
<article class="blog-card-wrapper" data-category={post.category}>
<BlogCard
title={post.title}
description={post.description}
category={post.category}
categoryColor={post.categoryColor}
date={post.date}
readTime={post.readTime}
imageUrl={post.imageUrl}
slug={post.slug}
/>
</article>
))}
</div>
@ -66,28 +57,11 @@ import { blogPosts, categories } from '@data/blogData';
</div>
</div>
</section>
<SearchModal />
</Layout>
<SearchModal />
<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 {
padding: 3rem 0 0;
background: #f8fafc;
@ -99,6 +73,10 @@ import { blogPosts, categories } from '@data/blogData';
gap: 2rem;
}
.blog-card-wrapper {
transition: opacity 0.3s ease, transform 0.3s ease;
}
/* ===== CTA ===== */
.blog-cta {
padding: 5rem 0;