Files
grib/src/app/calendar/page.tsx
Денис Шкабатур 72e07dad3d feat: Грибы Крыма — полная энциклопедия и справочник грибника
- Энциклопедия 20 видов грибов Крыма с детальными описаниями
- Интерактивный календарь грибника по месяцам
- Справочник: правила сбора, первая помощь, кулинария
- Поиск и фильтрация по съедобности и сезону
- Адаптивный дизайн, природная цветовая палитра
- Docker-конфигурация для деплоя

Tech: Next.js 15, TypeScript, Tailwind CSS 4, React 19
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 13:05:24 +03:00

234 lines
11 KiB
TypeScript
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 } from 'react';
import Link from 'next/link';
import { Calendar, ChevronLeft, ChevronRight } from 'lucide-react';
import { mushrooms } from '@/data/mushrooms';
import { EdibilityBadge } from '@/components/EdibilityBadge';
import { cn, getCurrentMonth, getMushroomsByMonth, getMushroomAbundance } from '@/lib/utils';
import { MONTH_NAMES, type Month } from '@/lib/types';
import type { Metadata } from 'next';
const abundanceLabel: Record<string, string> = {
rare: 'Редко',
moderate: 'Умеренно',
abundant: 'Обильно',
};
const abundanceDot: Record<string, string> = {
rare: 'bg-amber-300',
moderate: 'bg-forest-400',
abundant: 'bg-forest-600',
};
const monthDescriptions: Record<number, string> = {
1: 'Январь — самый тихий месяц для грибника. В тёплые зимы можно найти вёшенки на стволах деревьев.',
2: 'Февраль похож на январь. Вёшенки продолжают плодоносить в буковых лесах горного Крыма.',
3: 'Весна начинается. Появляются первые вёшенки. В лесу пока ещё прохладно для массового роста грибов.',
4: 'Апрель — месяц первых весенних грибов. Вёшенки обильны, могут появиться первые сморчки.',
5: 'Май — начало грибного сезона. Появляются первые маслята и шампиньоны после весенних дождей.',
6: 'Июнь — разнообразие нарастает. Лисички, маслята, белые грибы начинают появляться в горных лесах.',
7: 'Июль — жаркий месяц. Грибы появляются после дождей. В горных лесах можно найти белые, лисички, рыжики.',
8: 'Август — активный месяц. Дождевики, маслята, моховики обильны. Начинают появляться осенние виды.',
9: 'Сентябрь — разгар сезона! Максимальное разнообразие видов. Белые грибы, лисички, опята, рыжики.',
10: 'Октябрь — пик грибного сезона в Крыму! Мышата, опята, маслята, белые — всё в изобилии.',
11: 'Ноябрь — продолжение осеннего сезона. Мышата, опята, вёшенки в большом количестве.',
12: 'Декабрь — завершение сезона. Мышата, опята, вёшенки — последний шанс набрать корзинку.',
};
export default function CalendarPage() {
const currentMonth = getCurrentMonth();
const [selectedMonth, setSelectedMonth] = useState<Month>(currentMonth);
const monthMushrooms = getMushroomsByMonth(mushrooms, selectedMonth);
const prevMonth = () => {
setSelectedMonth((m) => (m === 1 ? 12 : m - 1) as Month);
};
const nextMonth = () => {
setSelectedMonth((m) => (m === 12 ? 1 : m + 1) as Month);
};
return (
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-2">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-amber-100">
<Calendar className="h-5 w-5 text-amber-600" />
</div>
<h1 className="text-3xl font-bold text-foreground">Календарь грибника</h1>
</div>
<p className="text-muted-foreground ml-[52px]">
Узнайте, какие грибы растут в каждом месяце в Крыму
</p>
</div>
{/* Month Selector - Grid */}
<div className="grid grid-cols-4 sm:grid-cols-6 lg:grid-cols-12 gap-2 mb-8">
{Array.from({ length: 12 }, (_, i) => {
const month = (i + 1) as Month;
const count = getMushroomsByMonth(mushrooms, month).length;
const isCurrent = month === currentMonth;
const isSelected = month === selectedMonth;
return (
<button
key={month}
onClick={() => setSelectedMonth(month)}
className={cn(
'relative rounded-xl border p-3 text-center transition-all',
isSelected
? 'border-forest-400 bg-forest-50 shadow-sm ring-2 ring-forest-200'
: isCurrent
? 'border-amber-300 bg-amber-50 hover:shadow-sm'
: 'border-border bg-card hover:bg-muted hover:shadow-sm'
)}
>
<div className={cn(
'text-xs font-semibold',
isSelected ? 'text-forest-700' : isCurrent ? 'text-amber-700' : 'text-muted-foreground'
)}>
{MONTH_NAMES[month].slice(0, 3)}
</div>
<div className={cn(
'mt-1 text-lg font-bold',
isSelected ? 'text-forest-800' : 'text-foreground'
)}>
{count}
</div>
<div className="text-[10px] text-muted-foreground">
{count === 0 ? 'нет' : count === 1 ? 'вид' : count < 5 ? 'вида' : 'видов'}
</div>
{isCurrent && (
<div className="absolute -top-1 -right-1 h-3 w-3 rounded-full bg-amber-400 border-2 border-white" />
)}
</button>
);
})}
</div>
{/* Selected Month Content */}
<div className="rounded-2xl border border-border bg-card overflow-hidden">
{/* Month Header */}
<div className="flex items-center justify-between border-b border-border bg-muted/50 px-6 py-4">
<button
onClick={prevMonth}
className="rounded-lg p-1.5 hover:bg-white transition-colors"
aria-label="Предыдущий месяц"
>
<ChevronLeft className="h-5 w-5 text-muted-foreground" />
</button>
<div className="text-center">
<h2 className="text-xl font-bold text-foreground">
{MONTH_NAMES[selectedMonth]}
</h2>
<p className="text-xs text-muted-foreground mt-0.5">
{monthMushrooms.length} {monthMushrooms.length === 1 ? 'вид' : monthMushrooms.length < 5 ? 'вида' : 'видов'} грибов
</p>
</div>
<button
onClick={nextMonth}
className="rounded-lg p-1.5 hover:bg-white transition-colors"
aria-label="Следующий месяц"
>
<ChevronRight className="h-5 w-5 text-muted-foreground" />
</button>
</div>
{/* Month Description */}
<div className="px-6 py-4 border-b border-border bg-amber-50/30">
<p className="text-sm text-muted-foreground leading-relaxed">
{monthDescriptions[selectedMonth]}
</p>
</div>
{/* Mushroom List */}
<div className="divide-y divide-border">
{monthMushrooms.length > 0 ? (
monthMushrooms
.sort((a, b) => {
const aAbundance = getMushroomAbundance(a, selectedMonth);
const bAbundance = getMushroomAbundance(b, selectedMonth);
const order = { abundant: 0, moderate: 1, rare: 2, none: 3 };
return order[aAbundance] - order[bAbundance];
})
.map((mushroom) => {
const abundance = getMushroomAbundance(mushroom, selectedMonth);
return (
<Link
key={mushroom.id}
href={`/encyclopedia/${mushroom.slug}`}
className="flex items-center gap-4 px-6 py-4 hover:bg-muted/50 transition-colors group"
>
{/* Image */}
<div className="h-14 w-14 flex-shrink-0 overflow-hidden rounded-xl bg-muted">
<img
src={mushroom.imageUrl}
alt={mushroom.name}
className="h-full w-full object-cover"
loading="lazy"
/>
</div>
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold text-foreground group-hover:text-forest-700 transition-colors truncate">
{mushroom.name}
</h3>
<EdibilityBadge edibility={mushroom.edibility} size="sm" />
</div>
<p className="text-xs italic text-muted-foreground mt-0.5">
{mushroom.scientificName}
</p>
</div>
{/* Abundance */}
<div className="flex items-center gap-2 flex-shrink-0">
<span className={cn('h-2.5 w-2.5 rounded-full', abundanceDot[abundance])} />
<span className="text-xs font-medium text-muted-foreground hidden sm:inline">
{abundanceLabel[abundance]}
</span>
</div>
</Link>
);
})
) : (
<div className="px-6 py-12 text-center">
<Calendar className="mx-auto h-10 w-10 text-muted-foreground/30" />
<p className="mt-3 text-sm text-muted-foreground">
В этом месяце грибной сезон не активен
</p>
<p className="mt-1 text-xs text-muted-foreground">
Основной сезон в Крыму с мая по декабрь
</p>
</div>
)}
</div>
</div>
{/* Legend */}
<div className="mt-6 flex flex-wrap items-center gap-6 text-sm text-muted-foreground">
<span className="font-medium">Обилие:</span>
<span className="flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-amber-300" />
Редко
</span>
<span className="flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-forest-400" />
Умеренно
</span>
<span className="flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-forest-600" />
Обильно
</span>
<span className="flex items-center gap-1.5">
<span className="h-3 w-3 rounded-full bg-amber-400 border-2 border-white shadow-sm" />
Текущий месяц
</span>
</div>
</div>
);
}