feat: Грибы Крыма — полная энциклопедия и справочник грибника
- Энциклопедия 20 видов грибов Крыма с детальными описаниями - Интерактивный календарь грибника по месяцам - Справочник: правила сбора, первая помощь, кулинария - Поиск и фильтрация по съедобности и сезону - Адаптивный дизайн, природная цветовая палитра - Docker-конфигурация для деплоя Tech: Next.js 15, TypeScript, Tailwind CSS 4, React 19 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
98
src/components/Header.tsx
Normal file
98
src/components/Header.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Menu, X, TreePine } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Главная', href: '/' },
|
||||
{ name: 'Энциклопедия', href: '/encyclopedia' },
|
||||
{ name: 'Календарь', href: '/calendar' },
|
||||
{ name: 'Справочник', href: '/guide' },
|
||||
];
|
||||
|
||||
export function Header() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b border-border bg-white/80 backdrop-blur-md">
|
||||
<nav className="mx-auto flex max-w-7xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
|
||||
<Link href="/" className="flex items-center gap-2.5 group">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-forest-600 text-white shadow-sm transition-transform group-hover:scale-105">
|
||||
<TreePine className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-lg font-bold leading-tight text-forest-900">
|
||||
Грибы Крыма
|
||||
</span>
|
||||
<span className="block text-[11px] font-medium leading-tight text-muted-foreground tracking-wide">
|
||||
Энциклопедия грибника
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Desktop nav */}
|
||||
<div className="hidden md:flex md:items-center md:gap-1">
|
||||
{navigation.map((item) => {
|
||||
const isActive = pathname === item.href ||
|
||||
(item.href !== '/' && pathname.startsWith(item.href));
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'rounded-lg px-4 py-2 text-sm font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-forest-50 text-forest-700'
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
type="button"
|
||||
className="md:hidden rounded-lg p-2 text-muted-foreground hover:bg-muted"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
{mobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden border-t border-border bg-white animate-fade-in">
|
||||
<div className="space-y-1 px-4 py-3">
|
||||
{navigation.map((item) => {
|
||||
const isActive = pathname === item.href ||
|
||||
(item.href !== '/' && pathname.startsWith(item.href));
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className={cn(
|
||||
'block rounded-lg px-4 py-2.5 text-sm font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-forest-50 text-forest-700'
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user