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:
29
.cursor/rules/component-patterns.mdc
Normal file
29
.cursor/rules/component-patterns.mdc
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
description: React component patterns for the project
|
||||||
|
globs: "**/*.tsx"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Component Patterns
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
```tsx
|
||||||
|
// 1. Imports
|
||||||
|
// 2. Types/Interfaces
|
||||||
|
// 3. Component
|
||||||
|
// 4. Sub-components (if small)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
- Extract reusable logic into custom hooks (`src/hooks/`)
|
||||||
|
- Colocate component-specific types with the component
|
||||||
|
- Use `cn()` utility for conditional classNames
|
||||||
|
- Prefer composition over prop drilling
|
||||||
|
- Server Components by default, add `'use client'` only when needed
|
||||||
|
- Keep components under 150 lines — extract if larger
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
- All images must have descriptive `alt` text
|
||||||
|
- Interactive elements must be keyboard-accessible
|
||||||
|
- Use proper heading hierarchy (h1 → h2 → h3)
|
||||||
|
- Color contrast ratio ≥ 4.5:1
|
||||||
22
.cursor/rules/data-patterns.mdc
Normal file
22
.cursor/rules/data-patterns.mdc
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
description: Data patterns for mushroom encyclopedia
|
||||||
|
globs: "src/data/**"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Data Patterns
|
||||||
|
|
||||||
|
## Mushroom Data Structure
|
||||||
|
Each mushroom entry must include:
|
||||||
|
- Scientific name (Latin), Russian common name(s)
|
||||||
|
- Edibility classification: edible / conditionally-edible / inedible / poisonous
|
||||||
|
- Detailed description: cap, stem, flesh, spore print
|
||||||
|
- Habitat, season months, locations in Crimea
|
||||||
|
- Look-alikes with distinguishing features
|
||||||
|
- Photo URL(s)
|
||||||
|
|
||||||
|
## Data Accuracy
|
||||||
|
- Cross-reference with mycological sources
|
||||||
|
- Always include toxicity warnings for poisonous species
|
||||||
|
- Mark conditionally-edible species with preparation requirements
|
||||||
|
- Include confidence level for rare/disputed species
|
||||||
26
.cursor/rules/project-conventions.mdc
Normal file
26
.cursor/rules/project-conventions.mdc
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
description: Core project conventions for Crimean Mushroom Encyclopedia
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project: Грибы Крыма — Encyclopedia & Calendar
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- Next.js 15 (App Router), TypeScript strict, Tailwind CSS 4, React 19
|
||||||
|
- Static data (JSON) — no backend/database needed
|
||||||
|
- Deploy target: static export or Node.js server
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
- `src/app/` — pages (App Router)
|
||||||
|
- `src/components/` — reusable UI components
|
||||||
|
- `src/data/` — mushroom database (JSON/TS)
|
||||||
|
- `src/lib/` — utilities, helpers, types
|
||||||
|
- `public/images/` — mushroom photos
|
||||||
|
|
||||||
|
## Coding Standards
|
||||||
|
- Functional components only, prefer named exports
|
||||||
|
- Use `interface` for props, `type` for unions/intersections
|
||||||
|
- All components must be typed — no `any`
|
||||||
|
- Russian UI text, English code identifiers
|
||||||
|
- Use semantic HTML elements
|
||||||
|
- Mobile-first responsive design
|
||||||
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
.git
|
||||||
|
*.md
|
||||||
|
.cursor
|
||||||
|
.env*
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
24
.gitignore
vendored
24
.gitignore
vendored
@@ -1,6 +1,4 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# Dependencies
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.*
|
.pnp.*
|
||||||
@@ -10,32 +8,30 @@
|
|||||||
!.yarn/releases
|
!.yarn/releases
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
|
|
||||||
# testing
|
# Next.js
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
|
|
||||||
# production
|
# Production
|
||||||
/build
|
/build
|
||||||
|
|
||||||
# misc
|
# Misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
# debug
|
# Debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# Env files
|
||||||
.env*
|
.env*.local
|
||||||
|
.env
|
||||||
|
|
||||||
# vercel
|
# Vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
# typescript
|
# TypeScript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|||||||
44
Dockerfile
Normal file
44
Dockerfile
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
FROM node:20-alpine AS base
|
||||||
|
|
||||||
|
# Install dependencies only when needed
|
||||||
|
FROM base AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Rebuild the source code only when needed
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production image, copy all the files and run next
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
|
# Set the correct permission for prerender cache
|
||||||
|
RUN mkdir .next
|
||||||
|
RUN chown nextjs:nodejs .next
|
||||||
|
|
||||||
|
# Automatically leverage output traces to reduce image size
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
92
PROGRESS.md
Normal file
92
PROGRESS.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Грибы Крыма — Прогресс разработки
|
||||||
|
|
||||||
|
## Статус: В разработке
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
### Tech Stack
|
||||||
|
- **Framework**: Next.js 15 (App Router) + React 19
|
||||||
|
- **Language**: TypeScript (strict mode)
|
||||||
|
- **Styling**: Tailwind CSS 4
|
||||||
|
- **Data**: Статические JSON/TS файлы (без БД)
|
||||||
|
- **Deploy**: Static export → самохост на git.dshkabatur.ru
|
||||||
|
|
||||||
|
### Структура страниц
|
||||||
|
| Страница | Route | Описание |
|
||||||
|
|----------|-------|----------|
|
||||||
|
| Главная | `/` | Hero, избранные грибы, сезон сейчас |
|
||||||
|
| Энциклопедия | `/encyclopedia` | Каталог всех грибов с фильтрами |
|
||||||
|
| Карточка гриба | `/encyclopedia/[slug]` | Детальная страница гриба |
|
||||||
|
| Календарь | `/calendar` | Интерактивный календарь грибника |
|
||||||
|
| Справочник | `/guide` | Правила сбора, советы, первая помощь |
|
||||||
|
|
||||||
|
### Компоненты
|
||||||
|
- `Header` — навигация, логотип
|
||||||
|
- `Footer` — контакты, копирайт
|
||||||
|
- `MushroomCard` — карточка гриба в каталоге
|
||||||
|
- `MushroomDetail` — детальная информация
|
||||||
|
- `SeasonCalendar` — календарь по месяцам
|
||||||
|
- `SearchBar` — поиск с автодополнением
|
||||||
|
- `FilterPanel` — фильтры (съедобность, сезон, место)
|
||||||
|
- `EdibilityBadge` — бейдж съедобности
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Чеклист
|
||||||
|
|
||||||
|
### Фаза 1: Настройка
|
||||||
|
- [x] Cursor rules
|
||||||
|
- [x] Файл прогресса
|
||||||
|
- [ ] Инициализация Next.js проекта
|
||||||
|
- [ ] Настройка Tailwind CSS
|
||||||
|
- [ ] Базовый layout (Header/Footer)
|
||||||
|
|
||||||
|
### Фаза 2: Данные
|
||||||
|
- [ ] Типы TypeScript для грибов
|
||||||
|
- [ ] База данных грибов (20+ видов Крыма)
|
||||||
|
- [ ] Фотографии грибов
|
||||||
|
|
||||||
|
### Фаза 3: Страницы
|
||||||
|
- [ ] Главная страница
|
||||||
|
- [ ] Энциклопедия (каталог + фильтры)
|
||||||
|
- [ ] Детальная страница гриба
|
||||||
|
- [ ] Календарь грибника
|
||||||
|
- [ ] Справочник грибника
|
||||||
|
|
||||||
|
### Фаза 4: Полировка
|
||||||
|
- [ ] Адаптивность (mobile/tablet/desktop)
|
||||||
|
- [ ] SEO мета-теги
|
||||||
|
- [ ] Анимации и переходы
|
||||||
|
- [ ] Accessibility проверка
|
||||||
|
|
||||||
|
### Фаза 5: Деплой
|
||||||
|
- [ ] Git репозиторий
|
||||||
|
- [ ] Деплой на сервер
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Детали реализации
|
||||||
|
|
||||||
|
### Данные о грибах
|
||||||
|
Каждый гриб содержит:
|
||||||
|
- Научное название (латынь)
|
||||||
|
- Русское название (одно или несколько)
|
||||||
|
- Классификация съедобности: съедобный / условно-съедобный / несъедобный / ядовитый
|
||||||
|
- Описание: шляпка, ножка, мякоть, споровый порошок
|
||||||
|
- Места произрастания в Крыму
|
||||||
|
- Сезон сбора (по месяцам)
|
||||||
|
- Похожие виды (двойники)
|
||||||
|
- Фотографии
|
||||||
|
|
||||||
|
### Календарь
|
||||||
|
- 12 месяцев, интерактивный
|
||||||
|
- При клике на месяц — список грибов сезона
|
||||||
|
- Визуальная шкала обилия (мало/средне/много)
|
||||||
|
- Текущий месяц выделен
|
||||||
|
|
||||||
|
### Справочник
|
||||||
|
- Правила безопасного сбора
|
||||||
|
- Как отличить ядовитые от съедобных
|
||||||
|
- Первая помощь при отравлении
|
||||||
|
- Способы приготовления
|
||||||
|
- Снаряжение грибника
|
||||||
8
docker-compose.yml
Normal file
8
docker-compose.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
services:
|
||||||
|
grib:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
output: 'standalone',
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'images.unsplash.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -8,6 +8,8 @@
|
|||||||
"name": "grib",
|
"name": "grib",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.563.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
@@ -2576,6 +2578,15 @@
|
|||||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/clsx": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -4833,6 +4844,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.563.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz",
|
||||||
|
"integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.563.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
|
|||||||
233
src/app/calendar/page.tsx
Normal file
233
src/app/calendar/page.tsx
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
259
src/app/encyclopedia/[slug]/page.tsx
Normal file
259
src/app/encyclopedia/[slug]/page.tsx
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { ArrowLeft, MapPin, AlertTriangle, Lightbulb, ChefHat, Info } from 'lucide-react';
|
||||||
|
import { mushrooms, getMushroomBySlug } from '@/data/mushrooms';
|
||||||
|
import { EdibilityBadge } from '@/components/EdibilityBadge';
|
||||||
|
import { SeasonBar } from '@/components/SeasonBar';
|
||||||
|
import { getActiveSeasonMonths, getSeasonLabel } from '@/lib/utils';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
params: Promise<{ slug: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
return mushrooms.map((m) => ({ slug: m.slug }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||||
|
const { slug } = await params;
|
||||||
|
const mushroom = getMushroomBySlug(slug);
|
||||||
|
if (!mushroom) return {};
|
||||||
|
return {
|
||||||
|
title: `${mushroom.name} (${mushroom.scientificName})`,
|
||||||
|
description: mushroom.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function MushroomDetailPage({ params }: PageProps) {
|
||||||
|
const { slug } = await params;
|
||||||
|
const mushroom = getMushroomBySlug(slug);
|
||||||
|
|
||||||
|
if (!mushroom) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeMonths = getActiveSeasonMonths(mushroom);
|
||||||
|
const seasonLabel = getSeasonLabel(activeMonths);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-5xl px-4 py-8 sm:px-6 lg:px-8">
|
||||||
|
{/* Back */}
|
||||||
|
<Link
|
||||||
|
href="/encyclopedia"
|
||||||
|
className="inline-flex items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors mb-6"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
Назад к энциклопедии
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Hero */}
|
||||||
|
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
|
||||||
|
{/* Image */}
|
||||||
|
<div className="relative aspect-[4/3] overflow-hidden rounded-2xl bg-muted shadow-lg">
|
||||||
|
<img
|
||||||
|
src={mushroom.imageUrl}
|
||||||
|
alt={mushroom.name}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div>
|
||||||
|
<EdibilityBadge edibility={mushroom.edibility} size="lg" />
|
||||||
|
<h1 className="mt-4 text-3xl font-bold text-foreground sm:text-4xl">
|
||||||
|
{mushroom.name}
|
||||||
|
</h1>
|
||||||
|
<p className="mt-1 text-lg italic text-muted-foreground">
|
||||||
|
{mushroom.scientificName}
|
||||||
|
</p>
|
||||||
|
{mushroom.nameAlt && mushroom.nameAlt.length > 0 && (
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
Также: {mushroom.nameAlt.join(', ')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
Семейство: {mushroom.family}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-4 text-base text-foreground leading-relaxed">
|
||||||
|
{mushroom.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Season */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<h3 className="text-sm font-semibold text-foreground mb-2">
|
||||||
|
Сезон сбора: {seasonLabel}
|
||||||
|
</h3>
|
||||||
|
<SeasonBar season={mushroom.season} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Locations */}
|
||||||
|
<div className="mt-5">
|
||||||
|
<h3 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-1.5">
|
||||||
|
<MapPin className="h-4 w-4 text-forest-600" />
|
||||||
|
Где искать в Крыму
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{mushroom.locations.map((loc) => (
|
||||||
|
<span
|
||||||
|
key={loc}
|
||||||
|
className="rounded-lg bg-forest-50 border border-forest-100 px-3 py-1 text-xs font-medium text-forest-700"
|
||||||
|
>
|
||||||
|
{loc}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Details */}
|
||||||
|
<div className="mt-12 grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
|
{/* Description cards */}
|
||||||
|
{[
|
||||||
|
{ title: 'Шляпка', content: mushroom.cap, icon: '🍄' },
|
||||||
|
{ title: 'Ножка', content: mushroom.stem, icon: '🌿' },
|
||||||
|
{ title: 'Мякоть', content: mushroom.flesh, icon: '🔬' },
|
||||||
|
{ title: 'Споровый порошок', content: mushroom.sporePrint, icon: '🌫️' },
|
||||||
|
].map((detail) => (
|
||||||
|
<div
|
||||||
|
key={detail.title}
|
||||||
|
className="rounded-xl border border-border bg-card p-5"
|
||||||
|
>
|
||||||
|
<h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
|
||||||
|
<span>{detail.icon}</span>
|
||||||
|
{detail.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground leading-relaxed">
|
||||||
|
{detail.content}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Habitat */}
|
||||||
|
<div className="mt-6 rounded-xl border border-border bg-card p-5">
|
||||||
|
<h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
|
||||||
|
<span>🌲</span> Места обитания
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground leading-relaxed">
|
||||||
|
{mushroom.habitat}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Warnings */}
|
||||||
|
{mushroom.warnings && (
|
||||||
|
<div className="mt-6 rounded-xl border border-red-200 bg-red-50 p-5">
|
||||||
|
<h3 className="text-sm font-semibold text-red-700 flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
Предупреждение
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-sm text-red-600 leading-relaxed">
|
||||||
|
{mushroom.warnings}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Lookalikes */}
|
||||||
|
{mushroom.lookalikes && mushroom.lookalikes.length > 0 && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<h3 className="text-lg font-bold text-foreground mb-4 flex items-center gap-2">
|
||||||
|
<Info className="h-5 w-5 text-amber-500" />
|
||||||
|
Похожие виды (двойники)
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{mushroom.lookalikes.map((lookalike) => (
|
||||||
|
<div
|
||||||
|
key={lookalike.name}
|
||||||
|
className={`rounded-xl border p-4 ${
|
||||||
|
lookalike.dangerous
|
||||||
|
? 'border-red-200 bg-red-50/50'
|
||||||
|
: 'border-border bg-card'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h4 className="text-sm font-semibold text-foreground">
|
||||||
|
{lookalike.slug ? (
|
||||||
|
<Link href={`/encyclopedia/${lookalike.slug}`} className="hover:text-forest-600 underline underline-offset-2">
|
||||||
|
{lookalike.name}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
lookalike.name
|
||||||
|
)}
|
||||||
|
</h4>
|
||||||
|
{lookalike.dangerous && (
|
||||||
|
<span className="rounded-full bg-red-100 px-2 py-0.5 text-[10px] font-semibold text-red-700 border border-red-200">
|
||||||
|
ОПАСНЫЙ
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
<span className="font-medium">Отличия:</span> {lookalike.difference}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cooking */}
|
||||||
|
{mushroom.cookingTips && (
|
||||||
|
<div className="mt-6 rounded-xl border border-forest-200 bg-forest-50/50 p-5">
|
||||||
|
<h3 className="text-sm font-semibold text-forest-700 flex items-center gap-2">
|
||||||
|
<ChefHat className="h-4 w-4" />
|
||||||
|
Кулинарные советы
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-sm text-forest-600 leading-relaxed">
|
||||||
|
{mushroom.cookingTips}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Fun fact */}
|
||||||
|
{mushroom.funFact && (
|
||||||
|
<div className="mt-6 rounded-xl border border-amber-200 bg-amber-50/50 p-5">
|
||||||
|
<h3 className="text-sm font-semibold text-amber-700 flex items-center gap-2">
|
||||||
|
<Lightbulb className="h-4 w-4" />
|
||||||
|
Интересный факт
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-sm text-amber-600 leading-relaxed">
|
||||||
|
{mushroom.funFact}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<div className="mt-12 border-t border-border pt-8">
|
||||||
|
<h3 className="text-lg font-bold text-foreground mb-4">Другие грибы</h3>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||||
|
{mushrooms
|
||||||
|
.filter((m) => m.id !== mushroom.id)
|
||||||
|
.slice(0, 4)
|
||||||
|
.map((m) => (
|
||||||
|
<Link
|
||||||
|
key={m.id}
|
||||||
|
href={`/encyclopedia/${m.slug}`}
|
||||||
|
className="group rounded-xl border border-border p-3 hover:border-forest-200 hover:shadow-sm transition-all"
|
||||||
|
>
|
||||||
|
<div className="aspect-square rounded-lg overflow-hidden mb-2">
|
||||||
|
<img
|
||||||
|
src={m.imageUrl}
|
||||||
|
alt={m.name}
|
||||||
|
className="h-full w-full object-cover transition-transform group-hover:scale-105"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium text-foreground group-hover:text-forest-700 truncate">
|
||||||
|
{m.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground italic truncate">
|
||||||
|
{m.scientificName}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
186
src/app/encyclopedia/page.tsx
Normal file
186
src/app/encyclopedia/page.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import { Search, SlidersHorizontal, X, BookOpen } from 'lucide-react';
|
||||||
|
import { mushrooms } from '@/data/mushrooms';
|
||||||
|
import { MushroomCard } from '@/components/MushroomCard';
|
||||||
|
import { filterMushrooms } from '@/lib/utils';
|
||||||
|
import { EDIBILITY_LABELS, MONTH_NAMES, type Edibility, type Month } from '@/lib/types';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
|
function EncyclopediaContent() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const initialEdibility = searchParams.get('edibility') || '';
|
||||||
|
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const [edibility, setEdibility] = useState(initialEdibility);
|
||||||
|
const [month, setMonth] = useState<string>('');
|
||||||
|
const [showFilters, setShowFilters] = useState(!!initialEdibility);
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
return filterMushrooms(
|
||||||
|
mushrooms,
|
||||||
|
query,
|
||||||
|
edibility || undefined,
|
||||||
|
month ? (parseInt(month) as Month) : undefined
|
||||||
|
);
|
||||||
|
}, [query, edibility, month]);
|
||||||
|
|
||||||
|
const hasFilters = query || edibility || 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-forest-100">
|
||||||
|
<BookOpen className="h-5 w-5 text-forest-600" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl font-bold text-foreground">Энциклопедия грибов</h1>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground ml-[52px]">
|
||||||
|
{mushrooms.length} видов грибов Крымского полуострова
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search & Filters */}
|
||||||
|
<div className="mb-8 space-y-4">
|
||||||
|
{/* Search */}
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Поиск по названию, латинскому имени..."
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
className="w-full rounded-xl border border-border bg-white py-2.5 pl-10 pr-4 text-sm outline-none transition-colors focus:border-forest-400 focus:ring-2 focus:ring-forest-100"
|
||||||
|
/>
|
||||||
|
{query && (
|
||||||
|
<button
|
||||||
|
onClick={() => setQuery('')}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowFilters(!showFilters)}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-xl border px-4 py-2.5 text-sm font-medium transition-colors',
|
||||||
|
showFilters || hasFilters
|
||||||
|
? 'border-forest-300 bg-forest-50 text-forest-700'
|
||||||
|
: 'border-border bg-white text-muted-foreground hover:bg-muted'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SlidersHorizontal className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">Фильтры</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Panel */}
|
||||||
|
{showFilters && (
|
||||||
|
<div className="rounded-xl border border-border bg-white p-4 animate-fade-in">
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
{/* Edibility */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||||
|
Съедобность
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{(['', 'edible', 'conditionally-edible', 'poisonous'] as const).map((val) => (
|
||||||
|
<button
|
||||||
|
key={val}
|
||||||
|
onClick={() => setEdibility(val)}
|
||||||
|
className={cn(
|
||||||
|
'rounded-lg px-3 py-1.5 text-xs font-medium transition-colors border',
|
||||||
|
edibility === val
|
||||||
|
? 'border-forest-300 bg-forest-50 text-forest-700'
|
||||||
|
: 'border-border bg-white text-muted-foreground hover:bg-muted'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{val ? EDIBILITY_LABELS[val as Edibility] : 'Все'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Month */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||||
|
Месяц сбора
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={month}
|
||||||
|
onChange={(e) => setMonth(e.target.value)}
|
||||||
|
className="w-full rounded-lg border border-border bg-white px-3 py-1.5 text-sm outline-none focus:border-forest-400"
|
||||||
|
>
|
||||||
|
<option value="">Любой месяц</option>
|
||||||
|
{Array.from({ length: 12 }, (_, i) => i + 1).map((m) => (
|
||||||
|
<option key={m} value={m}>
|
||||||
|
{MONTH_NAMES[m as Month]}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasFilters && (
|
||||||
|
<button
|
||||||
|
onClick={() => { setQuery(''); setEdibility(''); setMonth(''); }}
|
||||||
|
className="mt-3 text-xs font-medium text-forest-600 hover:text-forest-700"
|
||||||
|
>
|
||||||
|
Сбросить все фильтры
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results */}
|
||||||
|
{filtered.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<p className="mb-4 text-sm text-muted-foreground">
|
||||||
|
Найдено: {filtered.length} {filtered.length === 1 ? 'вид' : filtered.length < 5 ? 'вида' : 'видов'}
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
|
{filtered.map((m) => (
|
||||||
|
<MushroomCard key={m.id} mushroom={m} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="py-16 text-center">
|
||||||
|
<BookOpen className="mx-auto h-12 w-12 text-muted-foreground/30" />
|
||||||
|
<h3 className="mt-4 text-lg font-semibold text-foreground">Ничего не найдено</h3>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
Попробуйте изменить параметры поиска или фильтры
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EncyclopediaPage() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={
|
||||||
|
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||||
|
<div className="animate-pulse space-y-4">
|
||||||
|
<div className="h-10 w-64 bg-muted rounded-lg" />
|
||||||
|
<div className="h-12 bg-muted rounded-xl" />
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||||
|
{Array.from({ length: 8 }).map((_, i) => (
|
||||||
|
<div key={i} className="h-80 bg-muted rounded-2xl" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<EncyclopediaContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,26 +1,142 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--color-forest-50: #f0f7ec;
|
||||||
--foreground: #171717;
|
--color-forest-100: #dcefd3;
|
||||||
|
--color-forest-200: #b8dfaa;
|
||||||
|
--color-forest-300: #8cc978;
|
||||||
|
--color-forest-400: #65b34e;
|
||||||
|
--color-forest-500: #4a9435;
|
||||||
|
--color-forest-600: #3a7629;
|
||||||
|
--color-forest-700: #2f5c23;
|
||||||
|
--color-forest-800: #294a21;
|
||||||
|
--color-forest-900: #243f1e;
|
||||||
|
--color-forest-950: #0f220c;
|
||||||
|
|
||||||
|
--color-amber-50: #fffbeb;
|
||||||
|
--color-amber-100: #fef3c7;
|
||||||
|
--color-amber-200: #fde68a;
|
||||||
|
--color-amber-400: #fbbf24;
|
||||||
|
--color-amber-500: #f59e0b;
|
||||||
|
--color-amber-600: #d97706;
|
||||||
|
--color-amber-700: #b45309;
|
||||||
|
|
||||||
|
--color-earth-50: #faf6f1;
|
||||||
|
--color-earth-100: #f0e6d6;
|
||||||
|
--color-earth-200: #e0ccad;
|
||||||
|
--color-earth-400: #c19a5e;
|
||||||
|
--color-earth-600: #8c6422;
|
||||||
|
--color-earth-700: #6e4e1a;
|
||||||
|
--color-earth-800: #5a3f17;
|
||||||
|
|
||||||
|
--background: #faf9f7;
|
||||||
|
--foreground: #1a1a1a;
|
||||||
|
--card: #ffffff;
|
||||||
|
--card-foreground: #1a1a1a;
|
||||||
|
--muted: #f4f2ee;
|
||||||
|
--muted-foreground: #6b6560;
|
||||||
|
--border: #e5e0d8;
|
||||||
|
--ring: #4a9435;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-geist-sans);
|
--color-card: var(--card);
|
||||||
--font-mono: var(--font-geist-mono);
|
--color-card-foreground: var(--card-foreground);
|
||||||
}
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
@media (prefers-color-scheme: dark) {
|
--color-border: var(--border);
|
||||||
:root {
|
--color-ring: var(--ring);
|
||||||
--background: #0a0a0a;
|
--color-forest-50: var(--color-forest-50);
|
||||||
--foreground: #ededed;
|
--color-forest-100: var(--color-forest-100);
|
||||||
}
|
--color-forest-200: var(--color-forest-200);
|
||||||
|
--color-forest-300: var(--color-forest-300);
|
||||||
|
--color-forest-400: var(--color-forest-400);
|
||||||
|
--color-forest-500: var(--color-forest-500);
|
||||||
|
--color-forest-600: var(--color-forest-600);
|
||||||
|
--color-forest-700: var(--color-forest-700);
|
||||||
|
--color-forest-800: var(--color-forest-800);
|
||||||
|
--color-forest-900: var(--color-forest-900);
|
||||||
|
--color-forest-950: var(--color-forest-950);
|
||||||
|
--color-earth-50: var(--color-earth-50);
|
||||||
|
--color-earth-100: var(--color-earth-100);
|
||||||
|
--color-earth-200: var(--color-earth-200);
|
||||||
|
--color-earth-400: var(--color-earth-400);
|
||||||
|
--color-earth-600: var(--color-earth-600);
|
||||||
|
--color-earth-700: var(--color-earth-700);
|
||||||
|
--color-earth-800: var(--color-earth-800);
|
||||||
|
--color-amber-50: var(--color-amber-50);
|
||||||
|
--color-amber-100: var(--color-amber-100);
|
||||||
|
--color-amber-200: var(--color-amber-200);
|
||||||
|
--color-amber-400: var(--color-amber-400);
|
||||||
|
--color-amber-500: var(--color-amber-500);
|
||||||
|
--color-amber-600: var(--color-amber-600);
|
||||||
|
--color-amber-700: var(--color-amber-700);
|
||||||
|
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: var(--font-sans);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth scrolling */
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { opacity: 0; transform: translateX(-20px); }
|
||||||
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.5s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in {
|
||||||
|
animation: slideIn 0.4s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Staggered animation delays */
|
||||||
|
.delay-100 { animation-delay: 0.1s; }
|
||||||
|
.delay-200 { animation-delay: 0.2s; }
|
||||||
|
.delay-300 { animation-delay: 0.3s; }
|
||||||
|
.delay-400 { animation-delay: 0.4s; }
|
||||||
|
|
||||||
|
/* Image hover zoom */
|
||||||
|
.img-zoom {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.img-zoom img {
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
.img-zoom:hover img {
|
||||||
|
transform: scale(1.07);
|
||||||
}
|
}
|
||||||
|
|||||||
149
src/app/guide/page.tsx
Normal file
149
src/app/guide/page.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { Shield, AlertTriangle, Heart, ChefHat, Backpack, MapPin } from 'lucide-react';
|
||||||
|
import { guideSections } from '@/data/guide';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Справочник грибника',
|
||||||
|
description: 'Правила безопасного сбора грибов, первая помощь при отравлении, советы по приготовлению и лучшие грибные места Крыма.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconMap: Record<string, typeof Shield> = {
|
||||||
|
Shield,
|
||||||
|
AlertTriangle,
|
||||||
|
Heart,
|
||||||
|
ChefHat,
|
||||||
|
Backpack,
|
||||||
|
MapPin,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sectionColors: Record<string, { bg: string; icon: string; border: string }> = {
|
||||||
|
safety: { bg: 'bg-forest-50', icon: 'text-forest-600', border: 'border-forest-200' },
|
||||||
|
identification: { bg: 'bg-amber-50', icon: 'text-amber-600', border: 'border-amber-200' },
|
||||||
|
firstaid: { bg: 'bg-red-50', icon: 'text-red-600', border: 'border-red-200' },
|
||||||
|
cooking: { bg: 'bg-emerald-50', icon: 'text-emerald-600', border: 'border-emerald-200' },
|
||||||
|
equipment: { bg: 'bg-blue-50', icon: 'text-blue-600', border: 'border-blue-200' },
|
||||||
|
'crimea-spots': { bg: 'bg-purple-50', icon: 'text-purple-600', border: 'border-purple-200' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function GuidePage() {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-4xl px-4 py-8 sm:px-6 lg:px-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-10">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-blue-100">
|
||||||
|
<Shield className="h-5 w-5 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl font-bold text-foreground">Справочник грибника</h1>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground ml-[52px]">
|
||||||
|
Всё, что нужно знать для безопасного и успешного сбора грибов в Крыму
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick nav */}
|
||||||
|
<nav className="mb-10 rounded-xl border border-border bg-card p-4">
|
||||||
|
<h2 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-3">
|
||||||
|
Содержание
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
|
{guideSections.map((section) => {
|
||||||
|
const colors = sectionColors[section.id] || sectionColors.safety;
|
||||||
|
const Icon = iconMap[section.icon] || Shield;
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={section.id}
|
||||||
|
href={`#${section.id}`}
|
||||||
|
className="flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium text-foreground hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
<Icon className={`h-4 w-4 ${colors.icon}`} />
|
||||||
|
{section.title}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Sections */}
|
||||||
|
<div className="space-y-8">
|
||||||
|
{guideSections.map((section) => {
|
||||||
|
const colors = sectionColors[section.id] || sectionColors.safety;
|
||||||
|
const Icon = iconMap[section.icon] || Shield;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
key={section.id}
|
||||||
|
id={section.id}
|
||||||
|
className={`rounded-2xl border ${colors.border} overflow-hidden scroll-mt-24`}
|
||||||
|
>
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className={`${colors.bg} px-6 py-5 border-b ${colors.border}`}>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Icon className={`h-6 w-6 ${colors.icon}`} />
|
||||||
|
<h2 className="text-xl font-bold text-foreground">{section.title}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="bg-white px-6 py-6">
|
||||||
|
<ul className="space-y-4">
|
||||||
|
{section.content.map((item, i) => (
|
||||||
|
<li key={i} className="flex gap-3">
|
||||||
|
<span className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-muted text-xs font-bold text-muted-foreground">
|
||||||
|
{i + 1}
|
||||||
|
</span>
|
||||||
|
<p className="text-sm text-foreground leading-relaxed pt-0.5">{item}</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Warning */}
|
||||||
|
{section.warning && (
|
||||||
|
<div className="mt-6 rounded-xl border border-red-200 bg-red-50 p-4">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<AlertTriangle className="h-4 w-4 text-red-600 mt-0.5 flex-shrink-0" />
|
||||||
|
<p className="text-sm text-red-700 font-medium leading-relaxed">
|
||||||
|
{section.warning}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tips */}
|
||||||
|
{section.tips && section.tips.length > 0 && (
|
||||||
|
<div className="mt-6 rounded-xl border border-forest-200 bg-forest-50/50 p-4">
|
||||||
|
<h3 className="text-xs font-semibold uppercase tracking-wider text-forest-700 mb-3">
|
||||||
|
Полезные советы
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{section.tips.map((tip, i) => (
|
||||||
|
<li key={i} className="flex items-start gap-2 text-sm text-forest-700">
|
||||||
|
<span className="mt-1.5 h-1.5 w-1.5 rounded-full bg-forest-400 flex-shrink-0" />
|
||||||
|
{tip}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Emergency */}
|
||||||
|
<div className="mt-10 rounded-2xl border-2 border-red-300 bg-red-50 p-6 text-center">
|
||||||
|
<h2 className="text-lg font-bold text-red-700">
|
||||||
|
Экстренная помощь при отравлении грибами
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-3xl font-extrabold text-red-800">
|
||||||
|
103 или 112
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-sm text-red-600">
|
||||||
|
При первых признаках отравления немедленно звоните в скорую помощь.
|
||||||
|
Сохраните остатки грибов для анализа.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,20 +1,31 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from 'next';
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Inter } from 'next/font/google';
|
||||||
import "./globals.css";
|
import { Header } from '@/components/Header';
|
||||||
|
import { Footer } from '@/components/Footer';
|
||||||
|
import './globals.css';
|
||||||
|
|
||||||
const geistSans = Geist({
|
const inter = Inter({
|
||||||
variable: "--font-geist-sans",
|
subsets: ['latin', 'cyrillic'],
|
||||||
subsets: ["latin"],
|
variable: '--font-inter',
|
||||||
});
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
|
||||||
variable: "--font-geist-mono",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: {
|
||||||
description: "Generated by create next app",
|
default: 'Грибы Крыма — Энциклопедия грибника',
|
||||||
|
template: '%s | Грибы Крыма',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'Полная энциклопедия грибов Крымского полуострова. Описания, фотографии, календарь сбора, справочник грибника. Съедобные и ядовитые грибы Крыма.',
|
||||||
|
keywords: [
|
||||||
|
'грибы Крыма',
|
||||||
|
'энциклопедия грибов',
|
||||||
|
'съедобные грибы',
|
||||||
|
'ядовитые грибы',
|
||||||
|
'грибной календарь',
|
||||||
|
'тихая охота',
|
||||||
|
'мышата',
|
||||||
|
'белый гриб',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -23,11 +34,11 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="ru" className={inter.variable}>
|
||||||
<body
|
<body className="min-h-screen flex flex-col antialiased">
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
<Header />
|
||||||
>
|
<main className="flex-1">{children}</main>
|
||||||
{children}
|
<Footer />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
24
src/app/not-found.tsx
Normal file
24
src/app/not-found.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import { TreePine, ArrowLeft } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center py-24 px-4 text-center">
|
||||||
|
<TreePine className="h-16 w-16 text-forest-300 mb-6" />
|
||||||
|
<h1 className="text-4xl font-bold text-foreground">404</h1>
|
||||||
|
<p className="mt-2 text-lg text-muted-foreground">
|
||||||
|
Этот гриб не найден в нашей энциклопедии
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
Возможно, он спрятался под листвой
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="mt-8 inline-flex items-center gap-2 rounded-xl bg-forest-600 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-forest-700"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
Вернуться на главную
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
269
src/app/page.tsx
269
src/app/page.tsx
@@ -1,65 +1,218 @@
|
|||||||
import Image from "next/image";
|
import Link from 'next/link';
|
||||||
|
import { ArrowRight, BookOpen, Calendar, Shield, TreePine, Leaf, Mountain } from 'lucide-react';
|
||||||
|
import { mushrooms } from '@/data/mushrooms';
|
||||||
|
import { MushroomCard } from '@/components/MushroomCard';
|
||||||
|
import { getCurrentMonth, getMushroomsByMonth } from '@/lib/utils';
|
||||||
|
import { MONTH_NAMES, type Month } from '@/lib/types';
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
const currentMonth = getCurrentMonth();
|
||||||
|
const seasonMushrooms = getMushroomsByMonth(mushrooms, currentMonth);
|
||||||
|
const featuredMushrooms = mushrooms.filter((m) => m.edibility === 'edible').slice(0, 4);
|
||||||
|
const poisonousMushrooms = mushrooms.filter((m) => m.edibility === 'poisonous').slice(0, 3);
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<div>
|
||||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
{/* Hero Section */}
|
||||||
<Image
|
<section className="relative overflow-hidden bg-gradient-to-br from-forest-900 via-forest-800 to-forest-950 text-white">
|
||||||
className="dark:invert"
|
<div className="absolute inset-0 opacity-10">
|
||||||
src="/next.svg"
|
<div className="absolute inset-0" style={{
|
||||||
alt="Next.js logo"
|
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.3'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
|
||||||
width={100}
|
}} />
|
||||||
height={20}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
||||||
To get started, edit the page.tsx file.
|
|
||||||
</h1>
|
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Templates
|
|
||||||
</a>{" "}
|
|
||||||
or the{" "}
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Learning
|
|
||||||
</a>{" "}
|
|
||||||
center.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
<div className="relative mx-auto max-w-7xl px-4 py-20 sm:px-6 sm:py-28 lg:px-8 lg:py-36">
|
||||||
<a
|
<div className="max-w-3xl">
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
<div className="flex items-center gap-2 text-forest-300 mb-4">
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<Mountain className="h-5 w-5" />
|
||||||
target="_blank"
|
<span className="text-sm font-medium tracking-wide uppercase">Крымский полуостров</span>
|
||||||
rel="noopener noreferrer"
|
</div>
|
||||||
>
|
<h1 className="text-4xl font-extrabold tracking-tight sm:text-5xl lg:text-6xl">
|
||||||
<Image
|
Энциклопедия{' '}
|
||||||
className="dark:invert"
|
<span className="text-forest-300">грибов Крыма</span>
|
||||||
src="/vercel.svg"
|
</h1>
|
||||||
alt="Vercel logomark"
|
<p className="mt-5 text-lg text-forest-200 leading-relaxed sm:text-xl max-w-2xl">
|
||||||
width={16}
|
Полный справочник грибника с описаниями, фотографиями и интерактивным
|
||||||
height={16}
|
календарём сбора. Узнайте всё о съедобных и ядовитых грибах полуострова.
|
||||||
/>
|
</p>
|
||||||
Deploy Now
|
<div className="mt-8 flex flex-wrap gap-3">
|
||||||
</a>
|
<Link
|
||||||
<a
|
href="/encyclopedia"
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
className="inline-flex items-center gap-2 rounded-xl bg-white px-6 py-3 text-sm font-semibold text-forest-900 shadow-lg transition-all hover:bg-forest-50 hover:shadow-xl"
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
>
|
||||||
target="_blank"
|
<BookOpen className="h-4 w-4" />
|
||||||
rel="noopener noreferrer"
|
Открыть энциклопедию
|
||||||
>
|
<ArrowRight className="h-4 w-4" />
|
||||||
Documentation
|
</Link>
|
||||||
</a>
|
<Link
|
||||||
|
href="/calendar"
|
||||||
|
className="inline-flex items-center gap-2 rounded-xl border border-forest-500 px-6 py-3 text-sm font-semibold text-white transition-all hover:bg-forest-700"
|
||||||
|
>
|
||||||
|
<Calendar className="h-4 w-4" />
|
||||||
|
Календарь грибника
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="mt-14 grid grid-cols-2 gap-4 sm:grid-cols-4 max-w-2xl">
|
||||||
|
{[
|
||||||
|
{ value: mushrooms.length, label: 'видов грибов' },
|
||||||
|
{ value: mushrooms.filter(m => m.edibility === 'edible').length, label: 'съедобных' },
|
||||||
|
{ value: mushrooms.filter(m => m.edibility === 'poisonous').length, label: 'ядовитых' },
|
||||||
|
{ value: '12', label: 'месяцев в календаре' },
|
||||||
|
].map((stat) => (
|
||||||
|
<div key={stat.label} className="rounded-xl bg-white/10 backdrop-blur-sm px-4 py-3">
|
||||||
|
<div className="text-2xl font-bold text-white">{stat.value}</div>
|
||||||
|
<div className="text-xs text-forest-300">{stat.label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</section>
|
||||||
|
|
||||||
|
{/* Current Season */}
|
||||||
|
<section className="bg-amber-50/50 border-b border-amber-100">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-amber-100">
|
||||||
|
<Leaf className="h-5 w-5 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-foreground">
|
||||||
|
Сейчас в сезоне — {MONTH_NAMES[currentMonth]}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{seasonMushrooms.length > 0
|
||||||
|
? `${seasonMushrooms.length} видов можно найти в этом месяце`
|
||||||
|
: 'В этом месяце грибной сезон не активен'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{seasonMushrooms.length > 0 ? (
|
||||||
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{seasonMushrooms.slice(0, 4).map((m) => (
|
||||||
|
<MushroomCard key={m.id} mushroom={m} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-muted-foreground text-center py-8">
|
||||||
|
Зимой грибной сезон в Крыму минимален. Обратите внимание на вёшенки, которые можно найти в тёплые зимы.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{seasonMushrooms.length > 4 && (
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<Link
|
||||||
|
href="/calendar"
|
||||||
|
className="inline-flex items-center gap-1.5 text-sm font-medium text-amber-700 hover:text-amber-800"
|
||||||
|
>
|
||||||
|
Все грибы сезона <ArrowRight className="h-3.5 w-3.5" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Featured Edible */}
|
||||||
|
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex items-end justify-between mb-8">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-foreground sm:text-3xl">
|
||||||
|
Съедобные грибы Крыма
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-muted-foreground">
|
||||||
|
Самые ценные и популярные виды для сбора
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/encyclopedia?edibility=edible"
|
||||||
|
className="hidden sm:inline-flex items-center gap-1.5 text-sm font-medium text-forest-600 hover:text-forest-700"
|
||||||
|
>
|
||||||
|
Все съедобные <ArrowRight className="h-3.5 w-3.5" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{featuredMushrooms.map((m) => (
|
||||||
|
<MushroomCard key={m.id} mushroom={m} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Danger Warning */}
|
||||||
|
<section className="bg-red-50/50 border-y border-red-100">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex items-end justify-between mb-8">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 text-red-600 mb-2">
|
||||||
|
<Shield className="h-5 w-5" />
|
||||||
|
<span className="text-sm font-semibold uppercase tracking-wide">Осторожно</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-foreground sm:text-3xl">
|
||||||
|
Ядовитые грибы Крыма
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-muted-foreground">
|
||||||
|
Знай врага в лицо — изучите опасные виды, чтобы избежать отравления
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/encyclopedia?edibility=poisonous"
|
||||||
|
className="hidden sm:inline-flex items-center gap-1.5 text-sm font-medium text-red-600 hover:text-red-700"
|
||||||
|
>
|
||||||
|
Все ядовитые <ArrowRight className="h-3.5 w-3.5" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{poisonousMushrooms.map((m) => (
|
||||||
|
<MushroomCard key={m.id} mushroom={m} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
icon: BookOpen,
|
||||||
|
title: 'Энциклопедия',
|
||||||
|
desc: 'Полные описания с фото, характеристиками и местами произрастания',
|
||||||
|
href: '/encyclopedia',
|
||||||
|
color: 'bg-forest-50 text-forest-600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Calendar,
|
||||||
|
title: 'Календарь сбора',
|
||||||
|
desc: 'Узнайте, какие грибы растут в каждом месяце года',
|
||||||
|
href: '/calendar',
|
||||||
|
color: 'bg-amber-50 text-amber-600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Shield,
|
||||||
|
title: 'Справочник грибника',
|
||||||
|
desc: 'Правила безопасности, первая помощь, советы по приготовлению',
|
||||||
|
href: '/guide',
|
||||||
|
color: 'bg-blue-50 text-blue-600',
|
||||||
|
},
|
||||||
|
].map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
className="group rounded-2xl border border-border p-6 transition-all hover:shadow-lg hover:border-forest-200 hover:-translate-y-0.5"
|
||||||
|
>
|
||||||
|
<div className={`inline-flex h-12 w-12 items-center justify-center rounded-xl ${item.color}`}>
|
||||||
|
<item.icon className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<h3 className="mt-4 text-lg font-bold text-foreground group-hover:text-forest-700 transition-colors">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground leading-relaxed">{item.desc}</p>
|
||||||
|
<span className="mt-3 inline-flex items-center gap-1 text-sm font-medium text-forest-600">
|
||||||
|
Перейти <ArrowRight className="h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" />
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/components/EdibilityBadge.tsx
Normal file
42
src/components/EdibilityBadge.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { EDIBILITY_LABELS, EDIBILITY_COLORS, type Edibility } from '@/lib/types';
|
||||||
|
import { ShieldCheck, ShieldAlert, ShieldX, Skull } from 'lucide-react';
|
||||||
|
|
||||||
|
interface EdibilityBadgeProps {
|
||||||
|
edibility: Edibility;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
}
|
||||||
|
|
||||||
|
const icons: Record<Edibility, typeof ShieldCheck> = {
|
||||||
|
'edible': ShieldCheck,
|
||||||
|
'conditionally-edible': ShieldAlert,
|
||||||
|
'inedible': ShieldX,
|
||||||
|
'poisonous': Skull,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EdibilityBadge({ edibility, size = 'md' }: EdibilityBadgeProps) {
|
||||||
|
const colors = EDIBILITY_COLORS[edibility];
|
||||||
|
const Icon = icons[edibility];
|
||||||
|
const label = EDIBILITY_LABELS[edibility];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center gap-1.5 rounded-full border font-medium',
|
||||||
|
colors.bg,
|
||||||
|
colors.text,
|
||||||
|
colors.border,
|
||||||
|
size === 'sm' && 'px-2 py-0.5 text-xs',
|
||||||
|
size === 'md' && 'px-3 py-1 text-sm',
|
||||||
|
size === 'lg' && 'px-4 py-1.5 text-base'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className={cn(
|
||||||
|
size === 'sm' && 'h-3 w-3',
|
||||||
|
size === 'md' && 'h-3.5 w-3.5',
|
||||||
|
size === 'lg' && 'h-4 w-4'
|
||||||
|
)} />
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
src/components/Footer.tsx
Normal file
70
src/components/Footer.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import { TreePine, Heart } from 'lucide-react';
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="border-t border-border bg-forest-950 text-white">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||||
|
{/* Brand */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-forest-600">
|
||||||
|
<TreePine className="h-4.5 w-4.5" />
|
||||||
|
</div>
|
||||||
|
<span className="text-lg font-bold">Грибы Крыма</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-3 text-sm text-forest-300 leading-relaxed">
|
||||||
|
Энциклопедия и справочник грибника. Достоверная информация о грибах
|
||||||
|
Крымского полуострова с интерактивным календарём сбора.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold uppercase tracking-wider text-forest-400">
|
||||||
|
Разделы
|
||||||
|
</h3>
|
||||||
|
<ul className="mt-3 space-y-2">
|
||||||
|
{[
|
||||||
|
{ name: 'Энциклопедия', href: '/encyclopedia' },
|
||||||
|
{ name: 'Календарь грибника', href: '/calendar' },
|
||||||
|
{ name: 'Справочник', href: '/guide' },
|
||||||
|
].map((link) => (
|
||||||
|
<li key={link.href}>
|
||||||
|
<Link
|
||||||
|
href={link.href}
|
||||||
|
className="text-sm text-forest-300 transition-colors hover:text-white"
|
||||||
|
>
|
||||||
|
{link.name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Disclaimer */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold uppercase tracking-wider text-forest-400">
|
||||||
|
Важно
|
||||||
|
</h3>
|
||||||
|
<p className="mt-3 text-sm text-forest-300 leading-relaxed">
|
||||||
|
Информация на сайте носит справочный характер. При сборе грибов
|
||||||
|
всегда руководствуйтесь правилом: не уверен — не бери. При отравлении
|
||||||
|
немедленно обратитесь к врачу.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-10 border-t border-forest-800 pt-6 flex flex-col sm:flex-row items-center justify-between gap-2">
|
||||||
|
<p className="text-xs text-forest-400">
|
||||||
|
© {new Date().getFullYear()} Грибы Крыма. Все права защищены.
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-forest-400 flex items-center gap-1">
|
||||||
|
Сделано с <Heart className="h-3 w-3 text-red-400 fill-red-400" /> для грибников Крыма
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
src/components/MushroomCard.tsx
Normal file
58
src/components/MushroomCard.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import { MapPin } from 'lucide-react';
|
||||||
|
import type { Mushroom } from '@/lib/types';
|
||||||
|
import { EdibilityBadge } from './EdibilityBadge';
|
||||||
|
import { SeasonBar } from './SeasonBar';
|
||||||
|
import { getActiveSeasonMonths, getSeasonLabel } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface MushroomCardProps {
|
||||||
|
mushroom: Mushroom;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MushroomCard({ mushroom }: MushroomCardProps) {
|
||||||
|
const activeMonths = getActiveSeasonMonths(mushroom);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/encyclopedia/${mushroom.slug}`}
|
||||||
|
className="group block overflow-hidden rounded-2xl border border-border bg-card shadow-sm transition-all hover:shadow-lg hover:border-forest-200 hover:-translate-y-0.5"
|
||||||
|
>
|
||||||
|
{/* Image */}
|
||||||
|
<div className="relative aspect-[4/3] overflow-hidden bg-muted">
|
||||||
|
<img
|
||||||
|
src={mushroom.imageUrl}
|
||||||
|
alt={mushroom.name}
|
||||||
|
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<div className="absolute top-3 left-3">
|
||||||
|
<EdibilityBadge edibility={mushroom.edibility} size="sm" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-4">
|
||||||
|
<h3 className="text-lg font-bold text-foreground group-hover:text-forest-700 transition-colors">
|
||||||
|
{mushroom.name}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-0.5 text-sm italic text-muted-foreground">
|
||||||
|
{mushroom.scientificName}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground line-clamp-2 leading-relaxed">
|
||||||
|
{mushroom.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Season */}
|
||||||
|
<div className="mt-3">
|
||||||
|
<SeasonBar season={mushroom.season} compact />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Meta */}
|
||||||
|
<div className="mt-3 flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
|
<MapPin className="h-3 w-3" />
|
||||||
|
<span className="truncate">{mushroom.locations.slice(0, 2).join(', ')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
67
src/components/SeasonBar.tsx
Normal file
67
src/components/SeasonBar.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { MONTH_NAMES_SHORT, type Month, type SeasonMonth } from '@/lib/types';
|
||||||
|
import { getCurrentMonth } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface SeasonBarProps {
|
||||||
|
season: SeasonMonth[];
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const abundanceColors: Record<string, string> = {
|
||||||
|
none: 'bg-gray-100',
|
||||||
|
rare: 'bg-amber-200',
|
||||||
|
moderate: 'bg-forest-300',
|
||||||
|
abundant: 'bg-forest-600',
|
||||||
|
};
|
||||||
|
|
||||||
|
const abundanceLabels: Record<string, string> = {
|
||||||
|
none: 'Не растёт',
|
||||||
|
rare: 'Редко',
|
||||||
|
moderate: 'Умеренно',
|
||||||
|
abundant: 'Обильно',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SeasonBar({ season, compact = false }: SeasonBarProps) {
|
||||||
|
const currentMonth = getCurrentMonth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className={cn('grid grid-cols-12 gap-0.5', compact ? 'gap-px' : 'gap-1')}>
|
||||||
|
{Array.from({ length: 12 }, (_, i) => {
|
||||||
|
const month = (i + 1) as Month;
|
||||||
|
const entry = season.find((s) => s.month === month);
|
||||||
|
const abundance = entry?.abundance ?? 'none';
|
||||||
|
const isCurrent = month === currentMonth;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={month} className="flex flex-col items-center gap-1" title={`${MONTH_NAMES_SHORT[month]}: ${abundanceLabels[abundance]}`}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-full rounded-sm transition-all',
|
||||||
|
compact ? 'h-2' : 'h-3',
|
||||||
|
abundanceColors[abundance],
|
||||||
|
isCurrent && 'ring-2 ring-forest-500 ring-offset-1'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{!compact && (
|
||||||
|
<span className={cn(
|
||||||
|
'text-[10px] leading-none',
|
||||||
|
isCurrent ? 'font-bold text-forest-700' : 'text-muted-foreground'
|
||||||
|
)}>
|
||||||
|
{MONTH_NAMES_SHORT[month]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{!compact && (
|
||||||
|
<div className="mt-2 flex items-center gap-3 text-[10px] text-muted-foreground">
|
||||||
|
<span className="flex items-center gap-1"><span className="h-2 w-2 rounded-sm bg-amber-200" /> Редко</span>
|
||||||
|
<span className="flex items-center gap-1"><span className="h-2 w-2 rounded-sm bg-forest-300" /> Умеренно</span>
|
||||||
|
<span className="flex items-center gap-1"><span className="h-2 w-2 rounded-sm bg-forest-600" /> Обильно</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
112
src/data/guide.ts
Normal file
112
src/data/guide.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import type { GuideSection } from '@/lib/types';
|
||||||
|
|
||||||
|
export const guideSections: GuideSection[] = [
|
||||||
|
{
|
||||||
|
id: 'safety',
|
||||||
|
title: 'Правила безопасного сбора',
|
||||||
|
icon: 'Shield',
|
||||||
|
content: [
|
||||||
|
'Собирайте только те грибы, которые вы знаете на 100%. Малейшее сомнение — оставьте гриб в лесу.',
|
||||||
|
'Берите с собой корзину или сетчатую сумку — в полиэтиленовом пакете грибы быстро портятся и могут стать ядовитыми.',
|
||||||
|
'Не собирайте грибы вдоль автомобильных дорог, вблизи промышленных предприятий и на свалках — они накапливают тяжёлые металлы.',
|
||||||
|
'Срезайте грибы ножом у основания ножки, не вырывайте с грибницей — это позволит грибнице продолжить плодоносить.',
|
||||||
|
'Не собирайте старые, перезрелые, червивые грибы — они могут содержать продукты разложения.',
|
||||||
|
'Перерабатывайте собранные грибы в день сбора — не храните их сырыми более 4–6 часов.',
|
||||||
|
],
|
||||||
|
tips: [
|
||||||
|
'Лучшее время для сбора — раннее утро, до жары',
|
||||||
|
'После дождя подождите 2–3 дня для максимального урожая',
|
||||||
|
'Используйте палку для раздвигания листвы',
|
||||||
|
'Запоминайте грибные места — грибница плодоносит годами',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'identification',
|
||||||
|
title: 'Как отличить ядовитые грибы',
|
||||||
|
icon: 'AlertTriangle',
|
||||||
|
content: [
|
||||||
|
'Не существует универсального народного способа определить ядовитость гриба. Все «бабушкины» методы (серебряная ложка, лук, чеснок) — МИФЫ и не работают!',
|
||||||
|
'Единственный надёжный способ — точное определение вида по совокупности признаков: форма, цвет, запах, место роста, споровый порошок.',
|
||||||
|
'Обращайте внимание на наличие вольвы (мешочка у основания ножки) — это признак мухоморовых, среди которых смертельно ядовитая бледная поганка.',
|
||||||
|
'Проверяйте наличие кольца на ножке — многие ядовитые виды имеют характерное кольцо.',
|
||||||
|
'Изучайте цвет пластинок: у молодых шампиньонов они розовые, у бледной поганки — белые.',
|
||||||
|
'При сборе трубчатых грибов обращайте внимание на цвет трубчатого слоя и изменение цвета мякоти на срезе.',
|
||||||
|
],
|
||||||
|
warning: 'Никогда не пробуйте незнакомые грибы на вкус! Некоторые смертельно ядовитые грибы (бледная поганка) имеют приятный вкус.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'firstaid',
|
||||||
|
title: 'Первая помощь при отравлении',
|
||||||
|
icon: 'Heart',
|
||||||
|
content: [
|
||||||
|
'При первых признаках отравления (тошнота, рвота, боль в животе, диарея) НЕМЕДЛЕННО вызовите скорую помощь (103 или 112).',
|
||||||
|
'До приезда скорой: дайте пострадавшему выпить 4–5 стаканов воды и вызовите рвоту (нажатие на корень языка).',
|
||||||
|
'Дайте активированный уголь — 1 таблетка на 10 кг массы тела.',
|
||||||
|
'Уложите пострадавшего, к ногам и животу приложите грелки.',
|
||||||
|
'СОХРАНИТЕ остатки грибов или рвотные массы для анализа — это поможет врачам определить вид гриба и назначить правильное лечение.',
|
||||||
|
'НЕ давайте алкоголь, молоко и не применяйте средства от диареи — это усугубит отравление.',
|
||||||
|
],
|
||||||
|
warning: 'Отравление бледной поганкой проявляется через 6–24 часа, когда яд уже поражает печень. При любом подозрении — немедленно в больницу!',
|
||||||
|
tips: [
|
||||||
|
'Запишите время появления симптомов',
|
||||||
|
'Сообщите врачу, какие грибы ели и когда',
|
||||||
|
'Помните: симптомы отравления могут появиться через 6–24 часа',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cooking',
|
||||||
|
title: 'Способы приготовления',
|
||||||
|
icon: 'ChefHat',
|
||||||
|
content: [
|
||||||
|
'Жарка — самый популярный способ. Нарежьте грибы, обжарьте с луком на сливочном или растительном масле 15–20 минут.',
|
||||||
|
'Маринование — классический способ заготовки. Маринад: вода, уксус, соль, сахар, перец, лавровый лист, гвоздика.',
|
||||||
|
'Засолка — холодная (для груздей, рыжиков) и горячая (для остальных). Солёные грибы готовы через 30–40 дней.',
|
||||||
|
'Сушка — лучший способ для белых грибов. Нарежьте ломтиками и сушите при 50–60°C в духовке или электросушилке.',
|
||||||
|
'Заморозка — быстрый способ заготовки. Отварите грибы 10 минут, остудите, разложите по пакетам и заморозьте.',
|
||||||
|
'Грибной суп — отличный вариант для любых съедобных грибов. Особенно ароматен из сушёных белых грибов.',
|
||||||
|
],
|
||||||
|
tips: [
|
||||||
|
'Условно-съедобные грибы (грузди, свинушки) ОБЯЗАТЕЛЬНО вымачивайте и отваривайте',
|
||||||
|
'Никогда не смешивайте в одном блюде грибы разных видов до полной уверенности в каждом',
|
||||||
|
'Не храните приготовленные грибы более 24 часов при комнатной температуре',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'equipment',
|
||||||
|
title: 'Снаряжение грибника',
|
||||||
|
icon: 'Backpack',
|
||||||
|
content: [
|
||||||
|
'Корзина — главный инструмент грибника. Плетёная корзина обеспечивает вентиляцию и предохраняет грибы от повреждений.',
|
||||||
|
'Нож — для срезания грибов и очистки от грязи прямо в лесу. Лучше складной, с фиксатором лезвия.',
|
||||||
|
'Одежда — длинные брюки, закрытая обувь (резиновые сапоги идеальны), головной убор, ветровка.',
|
||||||
|
'Компас или навигатор — в горных лесах Крыма легко заблудиться, особенно в тумане.',
|
||||||
|
'Вода и перекус — грибная охота может занять несколько часов, не забывайте о гидратации.',
|
||||||
|
'Справочник грибника — книга или приложение с описанием местных видов.',
|
||||||
|
],
|
||||||
|
tips: [
|
||||||
|
'Обработайте одежду репеллентом от клещей',
|
||||||
|
'Возьмите заряженный телефон',
|
||||||
|
'Сообщите кому-нибудь маршрут и время возвращения',
|
||||||
|
'В крымских горах погода меняется быстро — берите дождевик',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'crimea-spots',
|
||||||
|
title: 'Лучшие грибные места Крыма',
|
||||||
|
icon: 'MapPin',
|
||||||
|
content: [
|
||||||
|
'Ай-Петринская яйла — сосновые и буковые леса на высоте 800–1200 м. Белые грибы, маслята, мышата, рыжики.',
|
||||||
|
'Ангарский перевал — один из самых популярных грибных маршрутов. Опята, лисички, маслята, мышата.',
|
||||||
|
'Демерджи — живописные буковые леса с богатым грибным разнообразием. Белые грибы, сыроежки, моховики.',
|
||||||
|
'Чатыр-Даг — горный массив с обширными лесами. Отличное место для белых грибов и груздей.',
|
||||||
|
'Большой каньон Крыма — влажные буковые леса. Лисички, дождевики, вёшенки.',
|
||||||
|
'Бахчисарайский район — дубовые рощи с трюфелями и шампиньонами. Одно из лучших мест для осеннего сбора.',
|
||||||
|
],
|
||||||
|
tips: [
|
||||||
|
'Не заходите на территорию заповедников без разрешения',
|
||||||
|
'Лучший сезон в Крыму — октябрь-ноябрь, после осенних дождей',
|
||||||
|
'В горах часты туманы — будьте осторожны на тропах',
|
||||||
|
'Многие грибные места доступны на общественном транспорте из Симферополя',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
475
src/data/mushrooms.ts
Normal file
475
src/data/mushrooms.ts
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
import type { Mushroom, SeasonMonth } from '@/lib/types';
|
||||||
|
|
||||||
|
function s(months: Record<number, 'r' | 'm' | 'a'>): SeasonMonth[] {
|
||||||
|
const all: SeasonMonth[] = [];
|
||||||
|
for (let i = 1; i <= 12; i++) {
|
||||||
|
const code = months[i];
|
||||||
|
all.push({
|
||||||
|
month: i as SeasonMonth['month'],
|
||||||
|
abundance: code === 'a' ? 'abundant' : code === 'm' ? 'moderate' : code === 'r' ? 'rare' : 'none',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mushrooms: Mushroom[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
slug: 'beliy-grib',
|
||||||
|
name: 'Белый гриб',
|
||||||
|
nameAlt: ['Боровик', 'Царь грибов'],
|
||||||
|
scientificName: 'Boletus edulis',
|
||||||
|
family: 'Болетовые (Boletaceae)',
|
||||||
|
edibility: 'edible',
|
||||||
|
description: 'Белый гриб — самый ценный и знаменитый съедобный гриб. В горных лесах Крыма встречается преимущественно в буковых и дубовых рощах на высоте 600–1200 м. Это крупный, мясистый гриб с превосходными вкусовыми качествами, который высоко ценится в кулинарии. В Крыму его можно найти на склонах Чатыр-Дага, Демерджи, Ай-Петри и в других горных массивах.',
|
||||||
|
cap: 'Диаметр 7–30 см, подушковидная, гладкая. Окраска от светло-коричневой до тёмно-бурой, зависит от условий произрастания. Поверхность сухая, в сырую погоду слегка слизистая.',
|
||||||
|
stem: 'Высота 8–25 см, толщина 3–10 см. Бочонковидная или булавовидная, плотная. Цвет белый или светло-коричневый с характерным белым сетчатым рисунком в верхней части.',
|
||||||
|
flesh: 'Белая, плотная, не меняет цвет на срезе. Имеет приятный грибной аромат и мягкий ореховый вкус.',
|
||||||
|
sporePrint: 'Оливково-коричневый',
|
||||||
|
habitat: 'Буковые и дубовые леса горного Крыма, реже под соснами. Образует микоризу с лиственными и хвойными деревьями. Предпочитает хорошо дренированные почвы на склонах.',
|
||||||
|
locations: ['Чатыр-Даг', 'Демерджи', 'Ай-Петри', 'Бабуган-яйла', 'леса над Алуштой', 'окрестности Ялты'],
|
||||||
|
season: s({ 6: 'r', 7: 'm', 8: 'm', 9: 'a', 10: 'a', 11: 'm' }),
|
||||||
|
lookalikes: [
|
||||||
|
{ name: 'Жёлчный гриб', difference: 'Мякоть розовеет на срезе, горький вкус, розоватый трубчатый слой', dangerous: false },
|
||||||
|
{ name: 'Сатанинский гриб', slug: 'sataninskiy-grib', difference: 'Шляпка беловатая, ножка красно-жёлтая, мякоть синеет на срезе', dangerous: true },
|
||||||
|
],
|
||||||
|
cookingTips: 'Универсален в кулинарии. Превосходен жареный, в супах, сушёный. Сушёные белые грибы имеют наиболее интенсивный аромат. Можно мариновать и солить.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1504198266287-1659872e6590?w=800&q=80',
|
||||||
|
galleryUrls: [
|
||||||
|
'https://images.unsplash.com/photo-1504198266287-1659872e6590?w=1200&q=80',
|
||||||
|
'https://images.unsplash.com/photo-1571048953845-40ee89fade10?w=1200&q=80',
|
||||||
|
],
|
||||||
|
funFact: 'Белый гриб назван так потому, что его мякоть не темнеет при сушке, в отличие от многих других трубчатых грибов.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
slug: 'lisichka',
|
||||||
|
name: 'Лисичка обыкновенная',
|
||||||
|
nameAlt: ['Лисичка настоящая', 'Петушок'],
|
||||||
|
scientificName: 'Cantharellus cibarius',
|
||||||
|
family: 'Лисичковые (Cantharellaceae)',
|
||||||
|
edibility: 'edible',
|
||||||
|
description: 'Лисичка — один из самых узнаваемых и безопасных грибов для сбора. В Крыму встречается повсеместно в лиственных и смешанных лесах. Отличительная особенность — лисички практически не бывают червивыми благодаря содержанию хиноманнозы, природного антигельминтика.',
|
||||||
|
cap: 'Диаметр 2–10 см, вначале выпуклая, затем воронковидная с волнистым краем. Яркий яично-жёлтый или оранжево-жёлтый цвет. Поверхность гладкая, матовая.',
|
||||||
|
stem: 'Высота 3–7 см, толщина 1–2 см. Сросшаяся со шляпкой, сужается книзу. Одного цвета со шляпкой, плотная.',
|
||||||
|
flesh: 'Плотная, мясистая, жёлтая на периферии, беловатая внутри. Запах приятный, слегка фруктовый, напоминает абрикос.',
|
||||||
|
sporePrint: 'Бледно-жёлтый',
|
||||||
|
habitat: 'Буковые, дубовые и смешанные леса. Растёт группами на мшистых участках, в траве. Предпочитает влажные, затенённые места.',
|
||||||
|
locations: ['Ангарский перевал', 'леса Бахчисарайского района', 'Большой каньон Крыма', 'Крымский заповедник', 'окрестности Симферополя'],
|
||||||
|
season: s({ 6: 'm', 7: 'a', 8: 'a', 9: 'a', 10: 'm', 11: 'r' }),
|
||||||
|
lookalikes: [
|
||||||
|
{ name: 'Ложная лисичка', difference: 'Более яркий оранжевый цвет, ровные (не волнистые) пластинки, растёт на гниющей древесине', dangerous: false },
|
||||||
|
],
|
||||||
|
cookingTips: 'Отлично подходит для жарки со сметаной — классическое блюдо. Можно мариновать, сушить, замораживать. Не рекомендуется варить долго — теряет вкус.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1571260899304-425eee4c7efc?w=800&q=80',
|
||||||
|
funFact: 'Лисички содержат хиноманнозу — вещество, которое отпугивает насекомых и паразитов. Поэтому эти грибы почти никогда не бывают червивыми.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
slug: 'opyonok-osenniy',
|
||||||
|
name: 'Опёнок осенний',
|
||||||
|
nameAlt: ['Опёнок настоящий'],
|
||||||
|
scientificName: 'Armillaria mellea',
|
||||||
|
family: 'Физалакриевые (Physalacriaceae)',
|
||||||
|
edibility: 'edible',
|
||||||
|
description: 'Опёнок осенний — один из самых массовых грибов Крыма. Растёт огромными колониями на пнях, стволах и корнях деревьев. Является паразитом и сапрофитом. В урожайные годы в Крыму можно собрать несколько вёдер за один поход.',
|
||||||
|
cap: 'Диаметр 3–15 см, вначале полушаровидная, затем плоская с бугорком в центре. Окраска медово-жёлтая, охристо-коричневая. Покрыта мелкими тёмными чешуйками.',
|
||||||
|
stem: 'Высота 6–18 см, толщина 1–2 см. Плотная, волокнистая, с характерным плёнчатым кольцом. Внизу часто утолщена.',
|
||||||
|
flesh: 'Белая, плотная в шляпке, волокнистая в ножке. Запах приятный грибной, вкус слегка вяжущий.',
|
||||||
|
sporePrint: 'Белый или кремовый',
|
||||||
|
habitat: 'На пнях, стволах и у основания живых и мёртвых деревьев — дубов, буков, грабов. Растёт большими сростками.',
|
||||||
|
locations: ['все горные леса Крыма', 'Ангарский перевал', 'Демерджи', 'Чатыр-Даг', 'леса Бахчисарая', 'окрестности Севастополя'],
|
||||||
|
season: s({ 9: 'm', 10: 'a', 11: 'a', 12: 'r' }),
|
||||||
|
lookalikes: [
|
||||||
|
{ name: 'Ложноопёнок серно-жёлтый', slug: 'lozhnoopenok', difference: 'Не имеет кольца на ножке, пластинки зеленовато-жёлтые, горький вкус', dangerous: true },
|
||||||
|
{ name: 'Галерина окаймлённая', difference: 'Меньше размером, растёт на хвойной древесине, без чешуек на шляпке, смертельно ядовита', dangerous: true },
|
||||||
|
],
|
||||||
|
cookingTips: 'Требует обязательного отваривания 15–20 минут перед приготовлением. Отлично подходит для маринования, жарки, супов. Ножки жёсткие — лучше использовать только шляпки.',
|
||||||
|
warnings: 'Сырые и недоваренные опята вызывают расстройство желудка. Обязательно отваривайте перед приготовлением!',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1563201515-adbe35c5e3c0?w=800&q=80',
|
||||||
|
funFact: 'Грибница опёнка тёмного в Орегоне (США) занимает площадь около 9 км² и считается самым большим живым организмом на Земле.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
slug: 'maslyonok',
|
||||||
|
name: 'Маслёнок обыкновенный',
|
||||||
|
nameAlt: ['Маслёнок поздний', 'Маслюк'],
|
||||||
|
scientificName: 'Suillus luteus',
|
||||||
|
family: 'Маслёнковые (Suillaceae)',
|
||||||
|
edibility: 'edible',
|
||||||
|
description: 'Маслёнок — очень распространённый гриб крымских сосновых лесов. Назван за характерную маслянистую, скользкую шляпку. В Крыму встречается преимущественно в посадках крымской сосны на южном берегу и в горных сосняках.',
|
||||||
|
cap: 'Диаметр 3–12 см, полушаровидная, затем подушковидная. Шоколадно-коричневая, покрыта слизистой плёнкой. Кожица легко снимается.',
|
||||||
|
stem: 'Высота 4–10 см, толщина 1–3 см. Цилиндрическая, плотная, бледно-жёлтая. Имеет белое или лиловатое плёнчатое кольцо.',
|
||||||
|
flesh: 'Мягкая, сочная, беловатая или желтоватая, не меняет цвет. Запах приятный, вкус мягкий.',
|
||||||
|
sporePrint: 'Охряно-коричневый',
|
||||||
|
habitat: 'Сосновые леса и посадки. Образует микоризу исключительно с соснами. Растёт группами на открытых местах, полянах, просеках.',
|
||||||
|
locations: ['Ялтинский горно-лесной заповедник', 'сосновые леса ЮБК', 'Ангарский перевал', 'Долгоруковская яйла', 'леса над Алуштой'],
|
||||||
|
season: s({ 5: 'r', 6: 'm', 7: 'm', 8: 'm', 9: 'a', 10: 'a', 11: 'm' }),
|
||||||
|
lookalikes: [
|
||||||
|
{ name: 'Маслёнок жёлто-бурый', difference: 'Шляпка жёлто-бурая без слизи, без кольца на ножке, тоже съедобный', dangerous: false },
|
||||||
|
],
|
||||||
|
cookingTips: 'Перед приготовлением рекомендуется снять слизистую кожицу со шляпки. Отлично подходит для маринования, жарки, супов. Один из лучших грибов для маринада.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1604229455805-3d889a159b84?w=800&q=80',
|
||||||
|
funFact: 'Маслёнки — одни из первых грибов, которые появляются после дождя. В Крыму их можно найти уже через 2–3 дня после хорошего ливня.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
slug: 'ryzhik',
|
||||||
|
name: 'Рыжик',
|
||||||
|
nameAlt: ['Рыжик настоящий', 'Рыжик сосновый'],
|
||||||
|
scientificName: 'Lactarius deliciosus',
|
||||||
|
family: 'Сыроежковые (Russulaceae)',
|
||||||
|
edibility: 'edible',
|
||||||
|
description: 'Рыжик — изысканный гриб первой категории, считающийся деликатесом. В Крыму встречается в сосновых лесах горной части полуострова. Характерная особенность — выделение оранжевого млечного сока на срезе.',
|
||||||
|
cap: 'Диаметр 4–15 см, вначале выпуклая, затем воронковидная. Рыжевато-оранжевая с концентрическими тёмными зонами. Поверхность гладкая, во влажную погоду слизистая.',
|
||||||
|
stem: 'Высота 3–8 см, толщина 1,5–3 см. Цилиндрическая, полая, ломкая. Одного цвета со шляпкой, с характерными ямками (лакунами).',
|
||||||
|
flesh: 'Оранжевая, ломкая, выделяет оранжево-красный млечный сок, который на воздухе зеленеет. Запах приятный, фруктовый.',
|
||||||
|
sporePrint: 'Бледно-охристый',
|
||||||
|
habitat: 'Сосновые леса, особенно молодые посадки. Образует микоризу с сосной. Предпочитает песчаные почвы, освещённые места.',
|
||||||
|
locations: ['сосновые леса ЮБК', 'Ай-Петри', 'Ангарский перевал', 'леса Белогорского района'],
|
||||||
|
season: s({ 7: 'r', 8: 'm', 9: 'a', 10: 'a', 11: 'm' }),
|
||||||
|
cookingTips: 'Лучший гриб для засолки — готов к употреблению через 2 недели. Можно жарить без предварительного отваривания. При засолке приобретает зеленоватый оттенок — это нормально.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1602083920685-amateur-mushroom?w=800&q=80',
|
||||||
|
funFact: 'Рыжики — единственные грибы, которые можно есть сырыми, лишь присыпав солью. Этот способ употребления практикуется грибниками веками.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
slug: 'myshata',
|
||||||
|
name: 'Рядовка серая',
|
||||||
|
nameAlt: ['Мышата', 'Мышонок', 'Серушка'],
|
||||||
|
scientificName: 'Tricholoma terreum',
|
||||||
|
family: 'Рядовковые (Tricholomataceae)',
|
||||||
|
edibility: 'edible',
|
||||||
|
description: 'Мышата — пожалуй, самый популярный и массовый гриб Крыма. Местное название «мышата» закрепилось из-за серой окраски, напоминающей мышиную шкурку. Растёт огромными группами в сосновых лесах и является символом крымской «тихой охоты». Каждую осень сотни грибников выходят на сбор мышат.',
|
||||||
|
cap: 'Диаметр 4–10 см, коническая с подвёрнутым краем, затем распростёртая с бугорком. Серая, мышино-серая или тёмно-серая. Покрыта радиальными волокнами, слегка шерстистая.',
|
||||||
|
stem: 'Высота 5–10 см, толщина 1–2 см. Белая или сероватая, цилиндрическая, плотная, волокнистая.',
|
||||||
|
flesh: 'Белая, ломкая, под кожицей сероватая. Запах слабый, мучнистый. Вкус мягкий.',
|
||||||
|
sporePrint: 'Белый',
|
||||||
|
habitat: 'Сосновые леса, особенно молодые посадки крымской сосны. Растёт группами и рядами в хвойной подстилке.',
|
||||||
|
locations: ['все сосновые леса Крыма', 'Ай-Петри', 'Ангарский перевал', 'Алушта', 'Ялта', 'Симферопольский район'],
|
||||||
|
season: s({ 9: 'r', 10: 'a', 11: 'a', 12: 'm' }),
|
||||||
|
lookalikes: [
|
||||||
|
{ name: 'Рядовка тигровая', slug: 'ryadovka-tigrovaya', difference: 'Шляпка с чёрными чешуйками на светлом фоне, более крупная. Смертельно ядовита!', dangerous: true },
|
||||||
|
{ name: 'Рядовка землистая', difference: 'Очень похожа, но мельче. Тоже съедобна, различие непринципиально', dangerous: false },
|
||||||
|
],
|
||||||
|
cookingTips: 'Великолепны жареные с луком и сметаной. Можно мариновать, солить, замораживать. Предварительное отваривание необязательно, но рекомендуется 10 минут.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1611243017008-b47e7b29f39c?w=800&q=80',
|
||||||
|
funFact: 'В Крыму мышата настолько популярны, что многие местные жители не собирают никакие другие грибы. На рынках Симферополя осенью мышата — самый ходовой товар.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '7',
|
||||||
|
slug: 'veshenka',
|
||||||
|
name: 'Вёшенка обыкновенная',
|
||||||
|
nameAlt: ['Вёшенка устричная', 'Устричный гриб'],
|
||||||
|
scientificName: 'Pleurotus ostreatus',
|
||||||
|
family: 'Вёшенковые (Pleurotaceae)',
|
||||||
|
edibility: 'edible',
|
||||||
|
description: 'Вёшенка — крупный вкусный гриб, растущий на стволах и пнях лиственных деревьев. В Крыму встречается повсеместно, особенно в буковых лесах. Уникальна тем, что плодоносит в самое холодное время года, когда других грибов нет.',
|
||||||
|
cap: 'Диаметр 5–25 см, раковинообразная или веерообразная. Серо-голубая, серая, коричневатая. Поверхность гладкая, во влажную погоду слегка маслянистая.',
|
||||||
|
stem: 'Короткая (1–4 см), боковая, плотная, белая. Часто практически отсутствует — шляпка прикреплена к субстрату боком.',
|
||||||
|
flesh: 'Белая, плотная, мясистая. Запах приятный, анисовый. Вкус мягкий, нежный.',
|
||||||
|
sporePrint: 'Белый или бледно-лиловый',
|
||||||
|
habitat: 'На стволах и пнях лиственных деревьев — буков, дубов, тополей, ив. Растёт черепитчатыми сростками, иногда очень крупными.',
|
||||||
|
locations: ['буковые леса горного Крыма', 'парки Симферополя', 'Бахчисарайский район', 'долины рек'],
|
||||||
|
season: s({ 1: 'r', 2: 'r', 3: 'm', 4: 'm', 5: 'r', 9: 'r', 10: 'm', 11: 'a', 12: 'a' }),
|
||||||
|
cookingTips: 'Используйте только молодые грибы — старые становятся жёсткими. Превосходны в жарке, супах, рагу. Ножки жёсткие — лучше удалять.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1504545102780-26774c1bb073?w=800&q=80',
|
||||||
|
funFact: 'Вёшенка — один из немногих грибов-хищников. Её грибница способна парализовать и переваривать микроскопических червей-нематод для получения азота.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '8',
|
||||||
|
slug: 'shampinion',
|
||||||
|
name: 'Шампиньон обыкновенный',
|
||||||
|
nameAlt: ['Шампиньон полевой', 'Печерица'],
|
||||||
|
scientificName: 'Agaricus campestris',
|
||||||
|
family: 'Шампиньоновые (Agaricaceae)',
|
||||||
|
edibility: 'edible',
|
||||||
|
description: 'Шампиньон — наиболее известный съедобный гриб в мире. В Крыму дикие шампиньоны обильно растут на лугах, пастбищах и в степной зоне полуострова, особенно после осенних дождей.',
|
||||||
|
cap: 'Диаметр 5–15 см, полушаровидная, затем распростёртая. Белая или слегка буроватая, шелковисто-волокнистая. При надавливании может слегка розоветь.',
|
||||||
|
stem: 'Высота 4–10 см, толщина 1–2,5 см. Белая, плотная, с тонким кольцом. Ровная, слегка утолщена у основания.',
|
||||||
|
flesh: 'Белая, на срезе розовеет. Запах приятный, анисовый. Вкус нежный, сладковатый.',
|
||||||
|
sporePrint: 'Тёмно-коричневый, шоколадный',
|
||||||
|
habitat: 'Луга, пастбища, парки, степные участки. Растёт на унавоженных и гумусных почвах. Часто встречается вдоль дорог и на выгонах.',
|
||||||
|
locations: ['степной Крым', 'окрестности Симферополя', 'Бахчисарайский район', 'предгорья', 'парки городов'],
|
||||||
|
season: s({ 5: 'r', 6: 'm', 7: 'r', 8: 'r', 9: 'm', 10: 'a', 11: 'm' }),
|
||||||
|
lookalikes: [
|
||||||
|
{ name: 'Бледная поганка', slug: 'blednaya-poganka', difference: 'Имеет вольву (мешочек) у основания ножки, пластинки белые (не розовые/коричневые), без запаха аниса', dangerous: true },
|
||||||
|
],
|
||||||
|
cookingTips: 'Универсален — жарка, супы, соусы, салаты, пицца. Молодые шампиньоны можно употреблять сырыми. Отваривание необязательно.',
|
||||||
|
warnings: 'Будьте крайне внимательны — молодые бледные поганки похожи на шампиньоны! Всегда проверяйте основание ножки на наличие вольвы.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1552825897-bb6e04076bfa?w=800&q=80',
|
||||||
|
funFact: 'Шампиньон — единственный гриб, который человечество научилось выращивать промышленно ещё в XVII веке. Первые грибные фермы появились в катакомбах Парижа.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '9',
|
||||||
|
slug: 'gruzd',
|
||||||
|
name: 'Груздь обыкновенный',
|
||||||
|
nameAlt: ['Груздь настоящий', 'Груздь белый'],
|
||||||
|
scientificName: 'Lactarius resimus',
|
||||||
|
family: 'Сыроежковые (Russulaceae)',
|
||||||
|
edibility: 'conditionally-edible',
|
||||||
|
description: 'Груздь — крупный пластинчатый гриб, традиционно ценимый в русской кухне для засолки. В Крыму встречается в лиственных лесах горной части полуострова, хотя и не так массово, как в средней полосе России.',
|
||||||
|
cap: 'Диаметр 7–20 см, плоская с вдавлением в центре, затем воронковидная. Белая или желтоватая, с опущенным мохнатым краем. Поверхность влажная.',
|
||||||
|
stem: 'Высота 3–7 см, толщина 2–5 см. Белая, полая, гладкая, цилиндрическая.',
|
||||||
|
flesh: 'Белая, плотная, ломкая. Выделяет обильный белый едкий млечный сок, желтеющий на воздухе. Запах фруктовый.',
|
||||||
|
sporePrint: 'Желтоватый',
|
||||||
|
habitat: 'Лиственные и смешанные леса, под берёзами, дубами, буками. Растёт группами, часто прячется в подстилке.',
|
||||||
|
locations: ['горные леса Крыма', 'Бабуган-яйла', 'Чатыр-Даг', 'Крымский заповедник'],
|
||||||
|
season: s({ 7: 'r', 8: 'm', 9: 'a', 10: 'm' }),
|
||||||
|
cookingTips: 'Требует обязательного вымачивания 2–3 дня со сменой воды для удаления горечи. Идеален для холодной засолки. Солёные грузди — классический русский деликатес.',
|
||||||
|
warnings: 'Нельзя употреблять без предварительной обработки! Млечный сок вызывает раздражение ЖКТ.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1573497161079-f3fd25cc6b90?w=800&q=80',
|
||||||
|
funFact: 'Название «груздь» предположительно происходит от слова «груда» — эти грибы всегда растут скоплениями. По другой версии — от «грузный», из-за массивного вида.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '10',
|
||||||
|
slug: 'syrozhka',
|
||||||
|
name: 'Сыроежка',
|
||||||
|
nameAlt: ['Сыроежка пищевая'],
|
||||||
|
scientificName: 'Russula vesca',
|
||||||
|
family: 'Сыроежковые (Russulaceae)',
|
||||||
|
edibility: 'edible',
|
||||||
|
description: 'Сыроежки — обширный род грибов, насчитывающий десятки видов. В Крыму встречается множество видов сыроежек в самых разных лесах. Сыроежка пищевая — один из наиболее вкусных и распространённых видов.',
|
||||||
|
cap: 'Диаметр 5–12 см, полушаровидная, затем распростёртая. Окраска разнообразна — розовая, красная, зелёная, фиолетовая, жёлтая. Кожица легко снимается.',
|
||||||
|
stem: 'Высота 4–8 см, толщина 1,5–3 см. Белая, плотная, хрупкая, цилиндрическая.',
|
||||||
|
flesh: 'Белая, хрупкая, губчатая. Запах слабый, вкус мягкий (у пищевой сыроежки). Некоторые виды имеют жгучий вкус.',
|
||||||
|
sporePrint: 'Белый или кремовый',
|
||||||
|
habitat: 'Все типы лесов — лиственные, хвойные, смешанные. Образует микоризу с различными деревьями.',
|
||||||
|
locations: ['все лесные массивы Крыма', 'Ангарский перевал', 'Чатыр-Даг', 'Демерджи', 'пригороды Симферополя'],
|
||||||
|
season: s({ 6: 'r', 7: 'm', 8: 'm', 9: 'a', 10: 'a', 11: 'm' }),
|
||||||
|
cookingTips: 'Можно жарить, солить, мариновать. Жгучие виды требуют предварительного отваривания. Снимайте кожицу со шляпки для лучшего вкуса.',
|
||||||
|
warnings: 'Некоторые виды сыроежек (например, жгучеедкая) имеют едкий вкус. Попробуйте кончик мякоти на вкус — если жжёт, отварите 15 минут.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1600411833196-7c1f6b1a8b90?w=800&q=80',
|
||||||
|
funFact: 'Название «сыроежка» обманчиво — не все виды можно есть сырыми. Оно скорее говорит о лёгкости приготовления: достаточно минимальной обработки.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '11',
|
||||||
|
slug: 'dozhdevik',
|
||||||
|
name: 'Дождевик грушевидный',
|
||||||
|
nameAlt: ['Дождевик', 'Пылевик', 'Дедушкин табак'],
|
||||||
|
scientificName: 'Lycoperdon pyriforme',
|
||||||
|
family: 'Шампиньоновые (Agaricaceae)',
|
||||||
|
edibility: 'edible',
|
||||||
|
description: 'Дождевик — необычный гриб шаровидной формы без шляпки и ножки в привычном понимании. В Крыму встречается на гниющей древесине в лиственных лесах. Съедобен только в молодом возрасте, пока мякоть белая.',
|
||||||
|
cap: 'Грушевидное или яйцевидное плодовое тело 2–7 см в диаметре. Белое или кремовое, поверхность мелкобородавчатая. С возрастом буреет.',
|
||||||
|
stem: 'Ложная ножка — суженная нижняя часть плодового тела, служит для крепления к субстрату.',
|
||||||
|
flesh: 'В молодости белая, упругая, однородная. С возрастом желтеет, затем становится оливково-бурой порошистой массой спор.',
|
||||||
|
sporePrint: 'Оливково-бурый (зрелые споры)',
|
||||||
|
habitat: 'На гниющей древесине, пнях, у оснований деревьев. Лиственные и смешанные леса. Растёт группами.',
|
||||||
|
locations: ['все лесные массивы Крыма', 'Большой каньон', 'Ай-Петри', 'Крымский заповедник'],
|
||||||
|
season: s({ 6: 'r', 7: 'm', 8: 'a', 9: 'a', 10: 'm', 11: 'r' }),
|
||||||
|
cookingTips: 'Собирайте только молодые экземпляры с белой мякотью. Нарезать ломтиками и обжарить — по вкусу напоминает тофу или куриное филе. Можно сушить.',
|
||||||
|
warnings: 'Не собирайте грибы с пожелтевшей мякотью — они уже несъедобны.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1630400723562-61ea64a91185?w=800&q=80',
|
||||||
|
funFact: 'Зрелый дождевик при нажатии «выстреливает» облаком спор — отсюда народное название «дедушкин табак». Одно плодовое тело может содержать до 7 триллионов спор.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '12',
|
||||||
|
slug: 'mohovik',
|
||||||
|
name: 'Моховик зелёный',
|
||||||
|
nameAlt: ['Моховик', 'Козляк'],
|
||||||
|
scientificName: 'Xerocomus subtomentosus',
|
||||||
|
family: 'Болетовые (Boletaceae)',
|
||||||
|
edibility: 'edible',
|
||||||
|
description: 'Моховик зелёный — часто встречающийся трубчатый гриб крымских лесов. Растёт как в хвойных, так и в лиственных лесах на мшистых участках, за что и получил своё название.',
|
||||||
|
cap: 'Диаметр 4–12 см, подушковидная, бархатистая. Оливково-бурая или жёлто-коричневая, сухая, иногда с трещинами.',
|
||||||
|
stem: 'Высота 4–10 см, толщина 1–2 см. Цилиндрическая, к основанию суженная. Желтоватая, часто с бурыми продольными волокнами.',
|
||||||
|
flesh: 'Белая или желтоватая, на срезе слегка синеет. Запах приятный, вкус мягкий.',
|
||||||
|
sporePrint: 'Оливково-коричневый',
|
||||||
|
habitat: 'Лиственные и хвойные леса, на мшистых участках, вдоль тропинок. Образует микоризу с дубом, буком, сосной.',
|
||||||
|
locations: ['Чатыр-Даг', 'Демерджи', 'горные леса над Ялтой и Алуштой', 'Бахчисарайский район'],
|
||||||
|
season: s({ 6: 'r', 7: 'm', 8: 'm', 9: 'a', 10: 'm', 11: 'r' }),
|
||||||
|
cookingTips: 'Хорош в жарке и супах. Перед приготовлением не требует отваривания. При сушке и мариновании может темнеть — это нормально.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1567360425618-1594206637d2?w=800&q=80',
|
||||||
|
funFact: 'Моховики растут преимущественно на замшелых участках леса, образуя тесную связь с мхами. Мох удерживает влагу, создавая идеальные условия.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '13',
|
||||||
|
slug: 'gornyj-belyj',
|
||||||
|
name: 'Говорушка гигантская',
|
||||||
|
nameAlt: ['Горный белый гриб', 'Свинуха гигантская'],
|
||||||
|
scientificName: 'Leucopaxillus giganteus',
|
||||||
|
family: 'Рядовковые (Tricholomataceae)',
|
||||||
|
edibility: 'edible',
|
||||||
|
description: 'В Крыму этот гриб часто называют «горный белый» из-за крупных размеров и белой окраски, хотя к настоящим белым грибам он отношения не имеет. Это крупный, мясистый пластинчатый гриб, растущий на лесных полянах горного Крыма.',
|
||||||
|
cap: 'Диаметр 10–30 см (иногда до 40), вначале выпуклая, затем воронковидная. Белая или кремовая, гладкая.',
|
||||||
|
stem: 'Высота 5–10 см, толщина 2–4 см. Плотная, белая, цилиндрическая, иногда эксцентрическая.',
|
||||||
|
flesh: 'Белая, плотная, толстая. Запах мучнистый, приятный. Вкус мягкий.',
|
||||||
|
sporePrint: 'Белый',
|
||||||
|
habitat: 'Лесные поляны, опушки, просеки в горных лесах. Растёт группами, часто образуя «ведьмины кольца».',
|
||||||
|
locations: ['горные поляны Ай-Петри', 'Чатыр-Даг', 'Бабуган-яйла', 'горные луга над Ялтой'],
|
||||||
|
season: s({ 7: 'r', 8: 'm', 9: 'a', 10: 'a', 11: 'm' }),
|
||||||
|
cookingTips: 'Хорош в любом виде — жарка, тушение, супы. Мякоть плотная и хорошо держит форму при готовке. Можно сушить.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1590080876062-58baea77beb0?w=800&q=80',
|
||||||
|
funFact: 'Говорушка гигантская может образовывать «ведьмины кольца» диаметром до 15 метров. В средневековье считалось, что внутри таких колец танцуют ведьмы.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '14',
|
||||||
|
slug: 'trufel-letnyj',
|
||||||
|
name: 'Трюфель летний',
|
||||||
|
nameAlt: ['Чёрный русский трюфель', 'Трюфель крымский'],
|
||||||
|
scientificName: 'Tuber aestivum',
|
||||||
|
family: 'Трюфелевые (Tuberaceae)',
|
||||||
|
edibility: 'edible',
|
||||||
|
description: 'Настоящий трюфель, растущий в Крыму! Летний трюфель — подземный гриб, который можно найти в дубовых и буковых лесах горного Крыма. Менее ароматный, чем знаменитый перигорский трюфель, но всё равно является деликатесом.',
|
||||||
|
cap: 'Клубневидное подземное плодовое тело 2–8 см в диаметре. Поверхность чёрно-коричневая, бородавчатая, с пирамидальными выростами.',
|
||||||
|
stem: 'Отсутствует — гриб полностью подземный.',
|
||||||
|
flesh: 'Бежево-коричневая с белыми мраморными прожилками. Плотная, с характерным ореховым ароматом.',
|
||||||
|
sporePrint: 'Тёмно-коричневый',
|
||||||
|
habitat: 'Под дубами и буками, на глубине 5–15 см в известковых почвах. Образует микоризу с лиственными деревьями.',
|
||||||
|
locations: ['дубовые леса горного Крыма', 'Бахчисарайский район', 'окрестности Белогорска', 'Предгорный Крым'],
|
||||||
|
season: s({ 6: 'r', 7: 'm', 8: 'a', 9: 'a', 10: 'm' }),
|
||||||
|
cookingTips: 'Деликатесный гриб. Используют в сыром виде, тонко нарезая на готовые блюда. Не подвергайте длительной термической обработке — теряется аромат.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1616489953498-a6e1a3e2e31a?w=800&q=80',
|
||||||
|
funFact: 'В Крыму трюфели находили ещё в XIX веке. Известный ботаник Христиан Стевен описывал их сбор в окрестностях Симферополя. Для поиска использовали специально обученных собак.',
|
||||||
|
},
|
||||||
|
// --- ЯДОВИТЫЕ ---
|
||||||
|
{
|
||||||
|
id: '15',
|
||||||
|
slug: 'blednaya-poganka',
|
||||||
|
name: 'Бледная поганка',
|
||||||
|
nameAlt: ['Мухомор зелёный'],
|
||||||
|
scientificName: 'Amanita phalloides',
|
||||||
|
family: 'Мухоморовые (Amanitaceae)',
|
||||||
|
edibility: 'poisonous',
|
||||||
|
description: 'Бледная поганка — самый опасный ядовитый гриб Крыма и мира. Содержит аматоксины, разрушающие печень. Даже небольшой кусочек может вызвать смертельное отравление. Противоядия не существует. Симптомы отравления проявляются через 6–24 часа, когда токсины уже нанесли необратимый вред.',
|
||||||
|
cap: 'Диаметр 5–15 см, вначале яйцевидная (в общей обёртке), затем плосковыпуклая. Бледно-зелёная, оливковая, реже белая или желтоватая. Гладкая, шелковистая.',
|
||||||
|
stem: 'Высота 8–16 см, толщина 1–2,5 см. Белая с зеленоватым муаровым рисунком. Имеет плёнчатое кольцо и мешковидную вольву у основания.',
|
||||||
|
flesh: 'Белая, не меняет цвет. Запах слабый, у старых грибов неприятный, сладковатый. Вкус мягкий (НЕ ПРОБОВАТЬ!).',
|
||||||
|
sporePrint: 'Белый',
|
||||||
|
habitat: 'Лиственные и смешанные леса, под дубами, буками. Предпочитает плодородные почвы, освещённые места.',
|
||||||
|
locations: ['лиственные леса горного Крыма', 'дубовые рощи', 'Бахчисарайский район', 'леса над Алуштой'],
|
||||||
|
season: s({ 7: 'r', 8: 'm', 9: 'a', 10: 'm' }),
|
||||||
|
lookalikes: [
|
||||||
|
{ name: 'Шампиньон', slug: 'shampinion', difference: 'У шампиньона пластинки розовые/коричневые (не белые), нет вольвы, мякоть розовеет', dangerous: false },
|
||||||
|
{ name: 'Сыроежка зелёная', difference: 'У сыроежки нет кольца и вольвы, ножка без рисунка', dangerous: false },
|
||||||
|
],
|
||||||
|
warnings: '☠️ СМЕРТЕЛЬНО ЯДОВИТ! Даже 1/3 шляпки достаточно для смертельного отравления. Термическая обработка НЕ разрушает яд. При подозрении на отравление НЕМЕДЛЕННО вызывайте скорую помощь!',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1572981779307-38b8cabb2407?w=800&q=80',
|
||||||
|
funFact: 'По одной из версий, римский император Клавдий был отравлен именно бледной поганкой, добавленной в блюдо из грибов.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '16',
|
||||||
|
slug: 'muhomor-krasnyj',
|
||||||
|
name: 'Мухомор красный',
|
||||||
|
nameAlt: ['Мухомор'],
|
||||||
|
scientificName: 'Amanita muscaria',
|
||||||
|
family: 'Мухоморовые (Amanitaceae)',
|
||||||
|
edibility: 'poisonous',
|
||||||
|
description: 'Мухомор красный — самый узнаваемый гриб в мире благодаря яркой красной шляпке с белыми точками. В Крыму встречается в горных лесах. Содержит мусцимол и иботеновую кислоту — психоактивные вещества, вызывающие галлюцинации и отравление.',
|
||||||
|
cap: 'Диаметр 8–20 см, полушаровидная, затем распростёртая. Ярко-красная или оранжево-красная, покрытая белыми хлопьевидными бородавками (остатки покрывала). Бородавки могут смываться дождём.',
|
||||||
|
stem: 'Высота 8–20 см, толщина 2–3,5 см. Белая, цилиндрическая, с клубневидным основанием. Кольцо широкое, свисающее, белое.',
|
||||||
|
flesh: 'Белая, под кожицей оранжеватая. Запах невыраженный. Без особого вкуса.',
|
||||||
|
sporePrint: 'Белый',
|
||||||
|
habitat: 'Хвойные и смешанные леса, под берёзами, соснами, елями. Предпочитает кислые почвы.',
|
||||||
|
locations: ['сосновые леса горного Крыма', 'Ай-Петри', 'Чатыр-Даг', 'Ангарский перевал'],
|
||||||
|
season: s({ 7: 'r', 8: 'm', 9: 'a', 10: 'm', 11: 'r' }),
|
||||||
|
warnings: '⚠️ ЯДОВИТ! Вызывает тошноту, рвоту, галлюцинации, судороги. Смертельные случаи редки, но возможны, особенно у детей.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1604511878928-1e27e8e2c1e9?w=800&q=80',
|
||||||
|
funFact: 'Название «мухомор» связано с использованием этого гриба для уничтожения мух — кусочки шляпки замачивали в молоке, и мухи, полакомившись, погибали.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '17',
|
||||||
|
slug: 'muhomor-panternyj',
|
||||||
|
name: 'Мухомор пантерный',
|
||||||
|
nameAlt: ['Мухомор серый'],
|
||||||
|
scientificName: 'Amanita pantherina',
|
||||||
|
family: 'Мухоморовые (Amanitaceae)',
|
||||||
|
edibility: 'poisonous',
|
||||||
|
description: 'Мухомор пантерный значительно опаснее красного. Содержит те же токсины, но в бо́льших концентрациях, а также дополнительные ядовитые соединения. В Крыму встречается в горных лесах и является частой причиной отравлений, так как менее узнаваем, чем красный мухомор.',
|
||||||
|
cap: 'Диаметр 5–12 см, полушаровидная, затем распростёртая. Оливково-бурая или серо-коричневая с мелкими белыми хлопьями. Край с рубчатой полоской.',
|
||||||
|
stem: 'Высота 6–13 см, толщина 1–2 см. Белая, с клубневидным основанием, окружённым кольцевидной вольвой. Кольцо гладкое, без бороздок.',
|
||||||
|
flesh: 'Белая, не меняет цвет. Запах неприятный. Вкус сладковатый (НЕ ПРОБОВАТЬ!).',
|
||||||
|
sporePrint: 'Белый',
|
||||||
|
habitat: 'Хвойные и лиственные леса, часто под соснами и дубами. Предпочитает щелочные почвы.',
|
||||||
|
locations: ['все горные леса Крыма', 'Демерджи', 'Чатыр-Даг', 'Ай-Петри'],
|
||||||
|
season: s({ 7: 'm', 8: 'a', 9: 'a', 10: 'm' }),
|
||||||
|
lookalikes: [
|
||||||
|
{ name: 'Рядовка серая (мышата)', slug: 'myshata', difference: 'У мышат нет вольвы и кольца, другая текстура шляпки (волокнистая, а не с хлопьями)', dangerous: false },
|
||||||
|
],
|
||||||
|
warnings: '⚠️ СИЛЬНО ЯДОВИТ! Опаснее красного мухомора. Отравление наступает через 1–2 часа. Может вызвать кому и смерть.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1600426993655-a1429577f934?w=800&q=80',
|
||||||
|
funFact: 'Пантерный мухомор получил название из-за «пятнистой» шляпки, напоминающей шкуру пантеры. Его токсичность в 3–4 раза выше, чем у красного мухомора.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '18',
|
||||||
|
slug: 'lozhnoopenok',
|
||||||
|
name: 'Ложноопёнок серно-жёлтый',
|
||||||
|
nameAlt: ['Ложный опёнок'],
|
||||||
|
scientificName: 'Hypholoma fasciculare',
|
||||||
|
family: 'Строфариевые (Strophariaceae)',
|
||||||
|
edibility: 'poisonous',
|
||||||
|
description: 'Ложноопёнок серно-жёлтый — ядовитый двойник осеннего опёнка. В Крыму растёт на тех же пнях и деревьях, что и настоящие опята, что делает его особенно коварным. Вызывает серьёзное желудочно-кишечное отравление.',
|
||||||
|
cap: 'Диаметр 2–7 см, колокольчатая, затем распростёртая. Серно-жёлтая в центре, зеленоватая или оранжеватая по краям. Гладкая.',
|
||||||
|
stem: 'Высота 4–10 см, толщина 0,5–1 см. Тонкая, волокнистая, жёлтая, внизу буроватая. Без кольца!',
|
||||||
|
flesh: 'Жёлтая, тонкая. Запах неприятный. Вкус горький.',
|
||||||
|
sporePrint: 'Фиолетово-коричневый',
|
||||||
|
habitat: 'На пнях и мёртвой древесине лиственных и хвойных деревьев. Растёт пучками, как и настоящие опята.',
|
||||||
|
locations: ['все лесные массивы Крыма', 'места произрастания настоящих опят'],
|
||||||
|
season: s({ 5: 'r', 6: 'm', 7: 'm', 8: 'm', 9: 'a', 10: 'a', 11: 'm' }),
|
||||||
|
lookalikes: [
|
||||||
|
{ name: 'Опёнок осенний', slug: 'opyonok-osenniy', difference: 'У настоящего опёнка есть кольцо на ножке, пластинки белые/кремовые, на шляпке чешуйки', dangerous: false },
|
||||||
|
],
|
||||||
|
warnings: '⚠️ ЯДОВИТ! Вызывает тошноту, рвоту, диарею через 1–6 часов. Главное отличие от опят — отсутствие кольца и горький вкус.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1571260899304-425eee4c7efc?w=800&q=80',
|
||||||
|
funFact: 'Ложноопёнки серно-жёлтые часто растут рядом с настоящими опятами на одном пне. Поэтому при сборе опят нужно проверять каждый гриб.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '19',
|
||||||
|
slug: 'sataninskiy-grib',
|
||||||
|
name: 'Сатанинский гриб',
|
||||||
|
nameAlt: ['Болет сатанинский'],
|
||||||
|
scientificName: 'Rubroboletus satanas',
|
||||||
|
family: 'Болетовые (Boletaceae)',
|
||||||
|
edibility: 'poisonous',
|
||||||
|
description: 'Сатанинский гриб — крупный и коварный ядовитый трубчатый гриб. В Крыму встречается в дубовых и буковых лесах. Опасен тем, что похож на съедобные трубчатые грибы (белые, дубовики). Имеет крайне неприятный запах у старых экземпляров.',
|
||||||
|
cap: 'Диаметр 8–25 см, подушковидная, серовато-белая, оливково-серая или желтоватая. Бархатистая, сухая.',
|
||||||
|
stem: 'Высота 6–15 см, толщина 3–10 см. Бочонковидная, с ярко-красной или карминно-красной сеточкой на жёлтом фоне. Вверху жёлтая.',
|
||||||
|
flesh: 'Белая или желтоватая, на срезе сначала краснеет, затем синеет. У старых грибов запах падали.',
|
||||||
|
sporePrint: 'Оливково-коричневый',
|
||||||
|
habitat: 'Лиственные леса на известковых почвах, под дубами и буками. Теплолюбивый вид.',
|
||||||
|
locations: ['дубовые леса южного Крыма', 'Бахчисарайский район', 'лес над Алуштой'],
|
||||||
|
season: s({ 7: 'm', 8: 'a', 9: 'a', 10: 'm' }),
|
||||||
|
lookalikes: [
|
||||||
|
{ name: 'Белый гриб', slug: 'beliy-grib', difference: 'У белого гриба ножка с белым сетчатым рисунком (не красным), мякоть не меняет цвет', dangerous: false },
|
||||||
|
],
|
||||||
|
warnings: '⚠️ ЯДОВИТ! Вызывает тяжёлое желудочно-кишечное отравление. Главные отличия: красная сетка на ножке и посинение мякоти.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1600426993655-a1429577f934?w=800&q=80',
|
||||||
|
funFact: 'Название «сатанинский» гриб получил за зловещую окраску ножки — красную, как «адское пламя», и за неприятный трупный запах старых экземпляров.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '20',
|
||||||
|
slug: 'ryadovka-tigrovaya',
|
||||||
|
name: 'Рядовка тигровая',
|
||||||
|
nameAlt: ['Рядовка ядовитая', 'Рядовка леопардовая'],
|
||||||
|
scientificName: 'Tricholoma pardinum',
|
||||||
|
family: 'Рядовковые (Tricholomataceae)',
|
||||||
|
edibility: 'poisonous',
|
||||||
|
description: 'Рядовка тигровая — опасный двойник съедобных рядовок, особенно популярных в Крыму мышат. Её коварство в том, что она имеет приятный запах и вкус, не вызывая подозрений. Отравление наступает быстро и протекает тяжело.',
|
||||||
|
cap: 'Диаметр 5–15 см, полушаровидная, затем распростёртая. Серовато-белая с концентрическими тёмно-серыми или чёрными чешуйками, создающими «тигровый» рисунок.',
|
||||||
|
stem: 'Высота 5–10 см, толщина 2–3 см. Белая или слегка охристая, плотная, мучнистая.',
|
||||||
|
flesh: 'Белая, плотная, не меняет цвет. Запах мучнистый, приятный. Вкус мягкий, не горький!',
|
||||||
|
sporePrint: 'Белый',
|
||||||
|
habitat: 'Лиственные и хвойные леса, на известковых почвах. Растёт группами, часто в тех же местах, что и мышата.',
|
||||||
|
locations: ['горные леса Крыма', 'те же места, что и мышата — будьте осторожны!'],
|
||||||
|
season: s({ 8: 'r', 9: 'm', 10: 'a', 11: 'm' }),
|
||||||
|
lookalikes: [
|
||||||
|
{ name: 'Рядовка серая (мышата)', slug: 'myshata', difference: 'У мышат шляпка однородно-серая с радиальными волокнами, без чешуйчатого «тигрового» рисунка', dangerous: false },
|
||||||
|
],
|
||||||
|
warnings: '⚠️ ЯДОВИТ! Вызывает сильнейшее отравление ЖКТ через 15–60 минут. Особенно опасна из-за приятного вкуса и сходства с мышатами.',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1611243017008-b47e7b29f39c?w=800&q=80',
|
||||||
|
funFact: 'Рядовка тигровая — один из самых коварных грибов: она приятна на вкус и запах, не горчит, что создаёт ложное чувство безопасности. При этом вызывает тяжелейшее отравление.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getMushroomBySlug(slug: string): Mushroom | undefined {
|
||||||
|
return mushrooms.find((m) => m.slug === slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEdibleMushrooms(): Mushroom[] {
|
||||||
|
return mushrooms.filter((m) => m.edibility === 'edible');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPoisonousMushrooms(): Mushroom[] {
|
||||||
|
return mushrooms.filter((m) => m.edibility === 'poisonous');
|
||||||
|
}
|
||||||
94
src/lib/types.ts
Normal file
94
src/lib/types.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
export type Edibility = 'edible' | 'conditionally-edible' | 'inedible' | 'poisonous';
|
||||||
|
|
||||||
|
export type Month = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||||
|
|
||||||
|
export type Abundance = 'none' | 'rare' | 'moderate' | 'abundant';
|
||||||
|
|
||||||
|
export interface SeasonMonth {
|
||||||
|
month: Month;
|
||||||
|
abundance: Abundance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Mushroom {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
nameAlt?: string[];
|
||||||
|
scientificName: string;
|
||||||
|
family: string;
|
||||||
|
edibility: Edibility;
|
||||||
|
description: string;
|
||||||
|
cap: string;
|
||||||
|
stem: string;
|
||||||
|
flesh: string;
|
||||||
|
sporePrint: string;
|
||||||
|
habitat: string;
|
||||||
|
locations: string[];
|
||||||
|
season: SeasonMonth[];
|
||||||
|
lookalikes?: Lookalike[];
|
||||||
|
cookingTips?: string;
|
||||||
|
warnings?: string;
|
||||||
|
imageUrl: string;
|
||||||
|
galleryUrls?: string[];
|
||||||
|
funFact?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Lookalike {
|
||||||
|
name: string;
|
||||||
|
slug?: string;
|
||||||
|
difference: string;
|
||||||
|
dangerous: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GuideSection {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
icon: string;
|
||||||
|
content: string[];
|
||||||
|
tips?: string[];
|
||||||
|
warning?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MONTH_NAMES: Record<Month, string> = {
|
||||||
|
1: 'Январь',
|
||||||
|
2: 'Февраль',
|
||||||
|
3: 'Март',
|
||||||
|
4: 'Апрель',
|
||||||
|
5: 'Май',
|
||||||
|
6: 'Июнь',
|
||||||
|
7: 'Июль',
|
||||||
|
8: 'Август',
|
||||||
|
9: 'Сентябрь',
|
||||||
|
10: 'Октябрь',
|
||||||
|
11: 'Ноябрь',
|
||||||
|
12: 'Декабрь',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MONTH_NAMES_SHORT: Record<Month, string> = {
|
||||||
|
1: 'Янв',
|
||||||
|
2: 'Фев',
|
||||||
|
3: 'Мар',
|
||||||
|
4: 'Апр',
|
||||||
|
5: 'Май',
|
||||||
|
6: 'Июн',
|
||||||
|
7: 'Июл',
|
||||||
|
8: 'Авг',
|
||||||
|
9: 'Сен',
|
||||||
|
10: 'Окт',
|
||||||
|
11: 'Ноя',
|
||||||
|
12: 'Дек',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EDIBILITY_LABELS: Record<Edibility, string> = {
|
||||||
|
'edible': 'Съедобный',
|
||||||
|
'conditionally-edible': 'Условно-съедобный',
|
||||||
|
'inedible': 'Несъедобный',
|
||||||
|
'poisonous': 'Ядовитый',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EDIBILITY_COLORS: Record<Edibility, { bg: string; text: string; border: string }> = {
|
||||||
|
'edible': { bg: 'bg-emerald-50', text: 'text-emerald-700', border: 'border-emerald-200' },
|
||||||
|
'conditionally-edible': { bg: 'bg-amber-50', text: 'text-amber-700', border: 'border-amber-200' },
|
||||||
|
'inedible': { bg: 'bg-slate-50', text: 'text-slate-600', border: 'border-slate-200' },
|
||||||
|
'poisonous': { bg: 'bg-red-50', text: 'text-red-700', border: 'border-red-200' },
|
||||||
|
};
|
||||||
92
src/lib/utils.ts
Normal file
92
src/lib/utils.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { clsx, type ClassValue } from 'clsx';
|
||||||
|
import type { Month, Mushroom, Abundance } from './types';
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return clsx(inputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMushroomsByMonth(mushrooms: Mushroom[], month: Month): Mushroom[] {
|
||||||
|
return mushrooms.filter((m) =>
|
||||||
|
m.season.some((s) => s.month === month && s.abundance !== 'none')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMushroomAbundance(mushroom: Mushroom, month: Month): Abundance {
|
||||||
|
const seasonEntry = mushroom.season.find((s) => s.month === month);
|
||||||
|
return seasonEntry?.abundance ?? 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentMonth(): Month {
|
||||||
|
return (new Date().getMonth() + 1) as Month;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSeasonLabel(months: Month[]): string {
|
||||||
|
if (months.length === 0) return 'Не определён';
|
||||||
|
if (months.length === 12) return 'Круглый год';
|
||||||
|
|
||||||
|
const ranges: string[] = [];
|
||||||
|
let start = months[0];
|
||||||
|
let prev = months[0];
|
||||||
|
|
||||||
|
for (let i = 1; i <= months.length; i++) {
|
||||||
|
if (i < months.length && months[i] === prev + 1) {
|
||||||
|
prev = months[i];
|
||||||
|
} else {
|
||||||
|
const monthNames: Record<number, string> = {
|
||||||
|
1: 'янв', 2: 'фев', 3: 'мар', 4: 'апр',
|
||||||
|
5: 'май', 6: 'июн', 7: 'июл', 8: 'авг',
|
||||||
|
9: 'сен', 10: 'окт', 11: 'ноя', 12: 'дек',
|
||||||
|
};
|
||||||
|
if (start === prev) {
|
||||||
|
ranges.push(monthNames[start]);
|
||||||
|
} else {
|
||||||
|
ranges.push(`${monthNames[start]}–${monthNames[prev]}`);
|
||||||
|
}
|
||||||
|
if (i < months.length) {
|
||||||
|
start = months[i];
|
||||||
|
prev = months[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveSeasonMonths(mushroom: Mushroom): Month[] {
|
||||||
|
return mushroom.season
|
||||||
|
.filter((s) => s.abundance !== 'none')
|
||||||
|
.map((s) => s.month)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterMushrooms(
|
||||||
|
mushrooms: Mushroom[],
|
||||||
|
query: string,
|
||||||
|
edibility?: string,
|
||||||
|
month?: Month
|
||||||
|
): Mushroom[] {
|
||||||
|
let filtered = mushrooms;
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
const q = query.toLowerCase();
|
||||||
|
filtered = filtered.filter(
|
||||||
|
(m) =>
|
||||||
|
m.name.toLowerCase().includes(q) ||
|
||||||
|
m.scientificName.toLowerCase().includes(q) ||
|
||||||
|
m.nameAlt?.some((n) => n.toLowerCase().includes(q)) ||
|
||||||
|
m.family.toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (edibility) {
|
||||||
|
filtered = filtered.filter((m) => m.edibility === edibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (month) {
|
||||||
|
filtered = filtered.filter((m) =>
|
||||||
|
m.season.some((s) => s.month === month && s.abundance !== 'none')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user