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:
Денис Шкабатур
2026-02-11 13:05:24 +03:00
parent 08263135dd
commit 72e07dad3d
28 changed files with 2598 additions and 104 deletions

98
src/components/Header.tsx Normal file
View 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>
);
}