334b2f51df
- Журнальная главная: hero, CategoryRow, PopularBlock, RecentBlock (Сегодня/Вчера/Неделя) - ArticleCard: 3 размера (hero/regular/compact), цветной badge без дублей тегов - ArticleCoverSVG: 6 брендовых палитр, аватар Зеро в углу вместо #ZEROPOST - /about/zero: страница персонажа с галереей 8 поз - Footer: TG-баннер с аватаром Зеро на каждой странице - Конец статьи: блок «Понравилась? → Подписаться на канал» - ChannelEditor: 4 вкладки (Настройки/Расписание/Авто-публикация/Ручная) - AutoPublishTab: toggle, категории, delay, template, live preview - ArticlePicker: typeahead с was_sent_to_channel / next_scheduled_at флагами - /admin/channels/[id]/stats: график роста подписчиков (recharts) - Dashboard: блок TG-статистики (подписчики, delta 24h/7d, постов) - Header: упрощён до 2 пунктов desktop + расширенное мобильное меню - AutogenPanel: корректные time-picker'ы, calcNextRun с учётом last_run_at
121 lines
5.4 KiB
JavaScript
121 lines
5.4 KiB
JavaScript
'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';
|
||
|
||
// Минимум: главная + о проекте. Серии и заметки доступны через карточки на главной и футер.
|
||
const NAV_LINKS = [
|
||
{ href: '/', label: 'Главная' },
|
||
{ href: '/about', label: 'О проекте' },
|
||
];
|
||
|
||
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">
|
||
{NAV_LINKS.map(link => (
|
||
<Link key={link.href} href={link.href} className="btn btn-ghost text-sm py-1.5">
|
||
{link.label}
|
||
</Link>
|
||
))}
|
||
<div className="ml-2 flex items-center gap-1 pl-2 border-l border-soft">
|
||
<SearchBox />
|
||
<ThemeToggle />
|
||
</div>
|
||
</nav>
|
||
|
||
{/* Mobile controls */}
|
||
<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>
|
||
<div className="py-3 border-b-soft">
|
||
<div className="text-xs uppercase tracking-widest mute mb-2">Темы</div>
|
||
<div className="grid grid-cols-2 gap-2">
|
||
<Link onClick={() => setOpen(false)} href="/category/ai-tools" className="text-base font-medium py-2">🤖 AI Tools</Link>
|
||
<Link onClick={() => setOpen(false)} href="/category/ai-dev" className="text-base font-medium py-2">💻 AI Dev</Link>
|
||
<Link onClick={() => setOpen(false)} href="/category/automation" className="text-base font-medium py-2">⚡ Automation</Link>
|
||
<Link onClick={() => setOpen(false)} href="/category/cybersec" className="text-base font-medium py-2">🔒 Cybersec</Link>
|
||
</div>
|
||
</div>
|
||
<Link href="/series" onClick={() => setOpen(false)} className="text-base font-medium ink py-3 border-b-soft">Серии</Link>
|
||
<Link href="/notes" onClick={() => setOpen(false)} className="text-base font-medium ink py-3 border-b-soft">Заметки</Link>
|
||
<Link href="/archive" onClick={() => setOpen(false)} className="text-base font-medium ink py-3 border-b-soft">Архив</Link>
|
||
<Link href="/about" onClick={() => setOpen(false)} className="text-base font-medium ink py-3 border-b-soft">О проекте</Link>
|
||
<div className="mt-8 mute text-sm leading-relaxed">
|
||
Блог, который ведёт ИИ — а человек только следит за курсом.
|
||
</div>
|
||
</nav>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|