From 72e07dad3da2da79c70ae25359ac0d8cb769e19d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B5=D0=BD=D0=B8=D1=81=20=D0=A8=D0=BA=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=82=D1=83=D1=80?= Date: Wed, 11 Feb 2026 13:05:24 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=93=D1=80=D0=B8=D0=B1=D1=8B=20=D0=9A?= =?UTF-8?q?=D1=80=D1=8B=D0=BC=D0=B0=20=E2=80=94=20=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D0=B0=D1=8F=20=D1=8D=D0=BD=D1=86=D0=B8=D0=BA=D0=BB=D0=BE=D0=BF?= =?UTF-8?q?=D0=B5=D0=B4=D0=B8=D1=8F=20=D0=B8=20=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BE=D1=87=D0=BD=D0=B8=D0=BA=20=D0=B3=D1=80=D0=B8=D0=B1?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Энциклопедия 20 видов грибов Крыма с детальными описаниями - Интерактивный календарь грибника по месяцам - Справочник: правила сбора, первая помощь, кулинария - Поиск и фильтрация по съедобности и сезону - Адаптивный дизайн, природная цветовая палитра - Docker-конфигурация для деплоя Tech: Next.js 15, TypeScript, Tailwind CSS 4, React 19 Co-authored-by: Cursor --- .cursor/rules/component-patterns.mdc | 29 ++ .cursor/rules/data-patterns.mdc | 22 ++ .cursor/rules/project-conventions.mdc | 26 ++ .dockerignore | 8 + .gitignore | 24 +- Dockerfile | 44 +++ PROGRESS.md | 92 +++++ docker-compose.yml | 8 + next.config.ts | 12 +- package-lock.json | 20 ++ package.json | 2 + src/app/calendar/page.tsx | 233 +++++++++++++ src/app/encyclopedia/[slug]/page.tsx | 259 ++++++++++++++ src/app/encyclopedia/page.tsx | 186 ++++++++++ src/app/globals.css | 140 +++++++- src/app/guide/page.tsx | 149 ++++++++ src/app/layout.tsx | 47 ++- src/app/not-found.tsx | 24 ++ src/app/page.tsx | 269 +++++++++++---- src/components/EdibilityBadge.tsx | 42 +++ src/components/Footer.tsx | 70 ++++ src/components/Header.tsx | 98 ++++++ src/components/MushroomCard.tsx | 58 ++++ src/components/SeasonBar.tsx | 67 ++++ src/data/guide.ts | 112 ++++++ src/data/mushrooms.ts | 475 ++++++++++++++++++++++++++ src/lib/types.ts | 94 +++++ src/lib/utils.ts | 92 +++++ 28 files changed, 2598 insertions(+), 104 deletions(-) create mode 100644 .cursor/rules/component-patterns.mdc create mode 100644 .cursor/rules/data-patterns.mdc create mode 100644 .cursor/rules/project-conventions.mdc create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 PROGRESS.md create mode 100644 docker-compose.yml create mode 100644 src/app/calendar/page.tsx create mode 100644 src/app/encyclopedia/[slug]/page.tsx create mode 100644 src/app/encyclopedia/page.tsx create mode 100644 src/app/guide/page.tsx create mode 100644 src/app/not-found.tsx create mode 100644 src/components/EdibilityBadge.tsx create mode 100644 src/components/Footer.tsx create mode 100644 src/components/Header.tsx create mode 100644 src/components/MushroomCard.tsx create mode 100644 src/components/SeasonBar.tsx create mode 100644 src/data/guide.ts create mode 100644 src/data/mushrooms.ts create mode 100644 src/lib/types.ts create mode 100644 src/lib/utils.ts diff --git a/.cursor/rules/component-patterns.mdc b/.cursor/rules/component-patterns.mdc new file mode 100644 index 0000000..890b044 --- /dev/null +++ b/.cursor/rules/component-patterns.mdc @@ -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 diff --git a/.cursor/rules/data-patterns.mdc b/.cursor/rules/data-patterns.mdc new file mode 100644 index 0000000..9e2723e --- /dev/null +++ b/.cursor/rules/data-patterns.mdc @@ -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 diff --git a/.cursor/rules/project-conventions.mdc b/.cursor/rules/project-conventions.mdc new file mode 100644 index 0000000..4f0e382 --- /dev/null +++ b/.cursor/rules/project-conventions.mdc @@ -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 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8b38ec3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +.next +.git +*.md +.cursor +.env* +Dockerfile +docker-compose.yml diff --git a/.gitignore b/.gitignore index 5ef6a52..8a31afb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies +# Dependencies /node_modules /.pnp .pnp.* @@ -10,32 +8,30 @@ !.yarn/releases !.yarn/versions -# testing -/coverage - -# next.js +# Next.js /.next/ /out/ -# production +# Production /build -# misc +# Misc .DS_Store *.pem -# debug +# Debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files (can opt-in for committing if needed) -.env* +# Env files +.env*.local +.env -# vercel +# Vercel .vercel -# typescript +# TypeScript *.tsbuildinfo next-env.d.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a8f0762 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/PROGRESS.md b/PROGRESS.md new file mode 100644 index 0000000..3d86fb0 --- /dev/null +++ b/PROGRESS.md @@ -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 месяцев, интерактивный +- При клике на месяц — список грибов сезона +- Визуальная шкала обилия (мало/средне/много) +- Текущий месяц выделен + +### Справочник +- Правила безопасного сбора +- Как отличить ядовитые от съедобных +- Первая помощь при отравлении +- Способы приготовления +- Снаряжение грибника diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..937f9d4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + grib: + build: . + ports: + - "3000:3000" + restart: unless-stopped + environment: + - NODE_ENV=production diff --git a/next.config.ts b/next.config.ts index e9ffa30..ce28622 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,15 @@ -import type { NextConfig } from "next"; +import type { NextConfig } from 'next'; const nextConfig: NextConfig = { - /* config options here */ + output: 'standalone', + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'images.unsplash.com', + }, + ], + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index f15c11c..2903450 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "grib", "version": "0.1.0", "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^0.563.0", "next": "16.1.6", "react": "19.2.3", "react-dom": "19.2.3" @@ -2576,6 +2578,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4833,6 +4844,15 @@ "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": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", diff --git a/package.json b/package.json index db12236..73928c8 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "lint": "eslint" }, "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^0.563.0", "next": "16.1.6", "react": "19.2.3", "react-dom": "19.2.3" diff --git a/src/app/calendar/page.tsx b/src/app/calendar/page.tsx new file mode 100644 index 0000000..dee9611 --- /dev/null +++ b/src/app/calendar/page.tsx @@ -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 = { + rare: 'Редко', + moderate: 'Умеренно', + abundant: 'Обильно', +}; + +const abundanceDot: Record = { + rare: 'bg-amber-300', + moderate: 'bg-forest-400', + abundant: 'bg-forest-600', +}; + +const monthDescriptions: Record = { + 1: 'Январь — самый тихий месяц для грибника. В тёплые зимы можно найти вёшенки на стволах деревьев.', + 2: 'Февраль похож на январь. Вёшенки продолжают плодоносить в буковых лесах горного Крыма.', + 3: 'Весна начинается. Появляются первые вёшенки. В лесу пока ещё прохладно для массового роста грибов.', + 4: 'Апрель — месяц первых весенних грибов. Вёшенки обильны, могут появиться первые сморчки.', + 5: 'Май — начало грибного сезона. Появляются первые маслята и шампиньоны после весенних дождей.', + 6: 'Июнь — разнообразие нарастает. Лисички, маслята, белые грибы начинают появляться в горных лесах.', + 7: 'Июль — жаркий месяц. Грибы появляются после дождей. В горных лесах можно найти белые, лисички, рыжики.', + 8: 'Август — активный месяц. Дождевики, маслята, моховики обильны. Начинают появляться осенние виды.', + 9: 'Сентябрь — разгар сезона! Максимальное разнообразие видов. Белые грибы, лисички, опята, рыжики.', + 10: 'Октябрь — пик грибного сезона в Крыму! Мышата, опята, маслята, белые — всё в изобилии.', + 11: 'Ноябрь — продолжение осеннего сезона. Мышата, опята, вёшенки в большом количестве.', + 12: 'Декабрь — завершение сезона. Мышата, опята, вёшенки — последний шанс набрать корзинку.', +}; + +export default function CalendarPage() { + const currentMonth = getCurrentMonth(); + const [selectedMonth, setSelectedMonth] = useState(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 ( +
+ {/* Header */} +
+
+
+ +
+

Календарь грибника

+
+

+ Узнайте, какие грибы растут в каждом месяце в Крыму +

+
+ + {/* Month Selector - Grid */} +
+ {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 ( + + ); + })} +
+ + {/* Selected Month Content */} +
+ {/* Month Header */} +
+ +
+

+ {MONTH_NAMES[selectedMonth]} +

+

+ {monthMushrooms.length} {monthMushrooms.length === 1 ? 'вид' : monthMushrooms.length < 5 ? 'вида' : 'видов'} грибов +

+
+ +
+ + {/* Month Description */} +
+

+ {monthDescriptions[selectedMonth]} +

+
+ + {/* Mushroom List */} +
+ {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 ( + + {/* Image */} +
+ {mushroom.name} +
+ + {/* Info */} +
+
+

+ {mushroom.name} +

+ +
+

+ {mushroom.scientificName} +

+
+ + {/* Abundance */} +
+ + + {abundanceLabel[abundance]} + +
+ + ); + }) + ) : ( +
+ +

+ В этом месяце грибной сезон не активен +

+

+ Основной сезон в Крыму — с мая по декабрь +

+
+ )} +
+
+ + {/* Legend */} +
+ Обилие: + + + Редко + + + + Умеренно + + + + Обильно + + + + Текущий месяц + +
+
+ ); +} diff --git a/src/app/encyclopedia/[slug]/page.tsx b/src/app/encyclopedia/[slug]/page.tsx new file mode 100644 index 0000000..8b046c9 --- /dev/null +++ b/src/app/encyclopedia/[slug]/page.tsx @@ -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 { + 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 ( +
+ {/* Back */} + + + Назад к энциклопедии + + + {/* Hero */} +
+ {/* Image */} +
+ {mushroom.name} +
+ + {/* Info */} +
+ +

+ {mushroom.name} +

+

+ {mushroom.scientificName} +

+ {mushroom.nameAlt && mushroom.nameAlt.length > 0 && ( +

+ Также: {mushroom.nameAlt.join(', ')} +

+ )} +

+ Семейство: {mushroom.family} +

+ +

+ {mushroom.description} +

+ + {/* Season */} +
+

+ Сезон сбора: {seasonLabel} +

+ +
+ + {/* Locations */} +
+

+ + Где искать в Крыму +

+
+ {mushroom.locations.map((loc) => ( + + {loc} + + ))} +
+
+
+
+ + {/* Details */} +
+ {/* Description cards */} + {[ + { title: 'Шляпка', content: mushroom.cap, icon: '🍄' }, + { title: 'Ножка', content: mushroom.stem, icon: '🌿' }, + { title: 'Мякоть', content: mushroom.flesh, icon: '🔬' }, + { title: 'Споровый порошок', content: mushroom.sporePrint, icon: '🌫️' }, + ].map((detail) => ( +
+

+ {detail.icon} + {detail.title} +

+

+ {detail.content} +

+
+ ))} +
+ + {/* Habitat */} +
+

+ 🌲 Места обитания +

+

+ {mushroom.habitat} +

+
+ + {/* Warnings */} + {mushroom.warnings && ( +
+

+ + Предупреждение +

+

+ {mushroom.warnings} +

+
+ )} + + {/* Lookalikes */} + {mushroom.lookalikes && mushroom.lookalikes.length > 0 && ( +
+

+ + Похожие виды (двойники) +

+
+ {mushroom.lookalikes.map((lookalike) => ( +
+
+

+ {lookalike.slug ? ( + + {lookalike.name} + + ) : ( + lookalike.name + )} +

+ {lookalike.dangerous && ( + + ОПАСНЫЙ + + )} +
+

+ Отличия: {lookalike.difference} +

+
+ ))} +
+
+ )} + + {/* Cooking */} + {mushroom.cookingTips && ( +
+

+ + Кулинарные советы +

+

+ {mushroom.cookingTips} +

+
+ )} + + {/* Fun fact */} + {mushroom.funFact && ( +
+

+ + Интересный факт +

+

+ {mushroom.funFact} +

+
+ )} + + {/* Navigation */} +
+

Другие грибы

+
+ {mushrooms + .filter((m) => m.id !== mushroom.id) + .slice(0, 4) + .map((m) => ( + +
+ {m.name} +
+

+ {m.name} +

+

+ {m.scientificName} +

+ + ))} +
+
+
+ ); +} diff --git a/src/app/encyclopedia/page.tsx b/src/app/encyclopedia/page.tsx new file mode 100644 index 0000000..6058ab9 --- /dev/null +++ b/src/app/encyclopedia/page.tsx @@ -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(''); + 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 ( +
+ {/* Header */} +
+
+
+ +
+

Энциклопедия грибов

+
+

+ {mushrooms.length} видов грибов Крымского полуострова +

+
+ + {/* Search & Filters */} +
+ {/* Search */} +
+
+ + 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 && ( + + )} +
+ +
+ + {/* Filter Panel */} + {showFilters && ( +
+
+ {/* Edibility */} +
+ +
+ {(['', 'edible', 'conditionally-edible', 'poisonous'] as const).map((val) => ( + + ))} +
+
+ + {/* Month */} +
+ + +
+
+ + {hasFilters && ( + + )} +
+ )} +
+ + {/* Results */} + {filtered.length > 0 ? ( + <> +

+ Найдено: {filtered.length} {filtered.length === 1 ? 'вид' : filtered.length < 5 ? 'вида' : 'видов'} +

+
+ {filtered.map((m) => ( + + ))} +
+ + ) : ( +
+ +

Ничего не найдено

+

+ Попробуйте изменить параметры поиска или фильтры +

+
+ )} +
+ ); +} + +export default function EncyclopediaPage() { + return ( + +
+
+
+
+ {Array.from({ length: 8 }).map((_, i) => ( +
+ ))} +
+
+
+ }> + + + ); +} diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..9d7b55c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,142 @@ @import "tailwindcss"; :root { - --background: #ffffff; - --foreground: #171717; + --color-forest-50: #f0f7ec; + --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 { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-border: var(--border); + --color-ring: var(--ring); + --color-forest-50: var(--color-forest-50); + --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 { background: var(--background); 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); } diff --git a/src/app/guide/page.tsx b/src/app/guide/page.tsx new file mode 100644 index 0000000..7b10b7f --- /dev/null +++ b/src/app/guide/page.tsx @@ -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 = { + Shield, + AlertTriangle, + Heart, + ChefHat, + Backpack, + MapPin, +}; + +const sectionColors: Record = { + 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 ( +
+ {/* Header */} +
+
+
+ +
+

Справочник грибника

+
+

+ Всё, что нужно знать для безопасного и успешного сбора грибов в Крыму +

+
+ + {/* Quick nav */} + + + {/* Sections */} +
+ {guideSections.map((section) => { + const colors = sectionColors[section.id] || sectionColors.safety; + const Icon = iconMap[section.icon] || Shield; + + return ( +
+ {/* Section Header */} +
+
+ +

{section.title}

+
+
+ + {/* Content */} +
+
    + {section.content.map((item, i) => ( +
  • + + {i + 1} + +

    {item}

    +
  • + ))} +
+ + {/* Warning */} + {section.warning && ( +
+
+ +

+ {section.warning} +

+
+
+ )} + + {/* Tips */} + {section.tips && section.tips.length > 0 && ( +
+

+ Полезные советы +

+
    + {section.tips.map((tip, i) => ( +
  • + + {tip} +
  • + ))} +
+
+ )} +
+
+ ); + })} +
+ + {/* Emergency */} +
+

+ Экстренная помощь при отравлении грибами +

+

+ 103 или 112 +

+

+ При первых признаках отравления немедленно звоните в скорую помощь. + Сохраните остатки грибов для анализа. +

+
+
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..8358b2c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,20 +1,31 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; +import { Header } from '@/components/Header'; +import { Footer } from '@/components/Footer'; +import './globals.css'; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], +const inter = Inter({ + subsets: ['latin', 'cyrillic'], + variable: '--font-inter', }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: { + default: 'Грибы Крыма — Энциклопедия грибника', + template: '%s | Грибы Крыма', + }, + description: + 'Полная энциклопедия грибов Крымского полуострова. Описания, фотографии, календарь сбора, справочник грибника. Съедобные и ядовитые грибы Крыма.', + keywords: [ + 'грибы Крыма', + 'энциклопедия грибов', + 'съедобные грибы', + 'ядовитые грибы', + 'грибной календарь', + 'тихая охота', + 'мышата', + 'белый гриб', + ], }; export default function RootLayout({ @@ -23,11 +34,11 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - - {children} + + +
+
{children}
+