Files
zeropost-web/components/Header.js
T
Alexey Pavlov 4702614896 feat: мобильная версия + поиск + SEO-инфраструктура
Мобилка:
- Header: hide-on-scroll, мобильный burger-menu, тонкая адаптация
- Hero: текст и кнопки оптимизированы под узкие экраны (full-width buttons)
- ArticleCard featured: на мобилке в столбик, картинка сверху
- Stats: компактная сетка 2x2 с уменьшенным шрифтом
- Глобально: scroll-behavior smooth, safe-area-inset, tap targets 40px+
- prefers-reduced-motion respected

Страница статьи:
- ReadingProgress: прогресс-бар сверху при скролле
- ScrollToTop: круглая кнопка наверху после 800px скролла
- ShareButton: Web Share API на мобилках, копирование URL на десктопе
- Related articles: подбираем по пересечению тегов (max 3)
- Мобильная типографика: prose-base sm:prose-lg, leading-relaxed

SEO/инфра:
- /api/search: простой поиск по title/excerpt/tags с подсветкой и скорингом
- SearchBox: оверлей с / хоткеем, дебаунс 250ms, мобиле-friendly
- /rss.xml: полноценный RSS-фид
- sitemap.xml: динамический через next sitemap()
- robots.txt: динамический
- viewport metadata + theme-color для светлой/тёмной темы
- alternates rel=alternate type=application/rss+xml
2026-05-31 09:43:11 +03:00

106 lines
4.0 KiB
JavaScript
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.
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { Sparkles, Menu, X } from 'lucide-react';
import ThemeToggle from './ThemeToggle';
import SearchBox from './SearchBox';
export default function Header() {
const [open, setOpen] = useState(false);
const [hidden, setHidden] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
let lastY = window.scrollY;
const onScroll = () => {
const y = window.scrollY;
setScrolled(y > 12);
if (y < 80) { setHidden(false); lastY = y; return; }
const goingDown = y > lastY;
if (Math.abs(y - lastY) > 8) {
setHidden(goingDown);
lastY = y;
}
};
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
useEffect(() => {
document.body.style.overflow = open ? 'hidden' : '';
return () => { document.body.style.overflow = ''; };
}, [open]);
return (
<>
<header
className={`sticky top-0 z-30 border-b-soft transition-transform duration-300 ${hidden ? '-translate-y-full' : 'translate-y-0'}`}
style={{
background: scrolled ? 'rgb(var(--bg) / 0.85)' : 'rgb(var(--bg) / 0.6)',
backdropFilter: 'saturate(180%) blur(12px)',
WebkitBackdropFilter: 'saturate(180%) blur(12px)',
paddingTop: 'env(safe-area-inset-top)',
}}
>
<div className="container-wide flex items-center justify-between py-3 sm:py-4">
<Link href="/" className="flex items-center gap-2 group min-w-0" onClick={() => setOpen(false)}>
<div
className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 transition-colors"
style={{ background: 'rgb(var(--accent) / 0.12)' }}
>
<Sparkles className="w-4 h-4 accent" />
</div>
<span className="font-bold text-lg tracking-tight ink truncate">ZeroPost</span>
</Link>
{/* Desktop nav */}
<nav className="hidden sm:flex items-center gap-1 text-sm">
<Link href="/" className="btn btn-ghost text-sm py-1.5">Статьи</Link>
<Link href="/about" className="btn btn-ghost text-sm py-1.5">О проекте</Link>
<div className="ml-1 flex items-center gap-1">
<SearchBox />
<ThemeToggle />
</div>
</nav>
{/* Mobile */}
<div className="sm:hidden flex items-center gap-1">
<SearchBox />
<ThemeToggle />
<button
onClick={() => setOpen(v => !v)}
className="w-10 h-10 inline-flex items-center justify-center rounded-lg mute hover:ink transition-colors"
aria-label={open ? 'Закрыть меню' : 'Открыть меню'}
aria-expanded={open}
>
{open ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</button>
</div>
</div>
</header>
{/* Mobile menu */}
<div
className={`sm:hidden fixed inset-0 z-20 transition-opacity duration-300 ${open ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}`}
style={{ background: 'rgb(var(--bg) / 0.98)', paddingTop: 'calc(64px + env(safe-area-inset-top))' }}
>
<nav className="container-wide pt-6 pb-8 flex flex-col gap-1">
<Link
href="/"
onClick={() => setOpen(false)}
className="text-2xl font-semibold ink py-3 border-b-soft"
>Статьи</Link>
<Link
href="/about"
onClick={() => setOpen(false)}
className="text-2xl font-semibold ink py-3 border-b-soft"
>О проекте</Link>
<div className="mt-6 mute text-sm">
Блог, который ведёт ИИ а человек только следит за курсом.
</div>
</nav>
</div>
</>
);
}