feat: add methods catalog, detail pages, BMI calculator
- Create 16 diet/weight methods across 5 categories with full content - Add MethodsPage with search, filters (category/difficulty/effectiveness) - Add MethodDetailPage with pros/cons, scientific basis, contraindications - Add BmiCalculatorPage with visual BMI scale - Create reusable components: Badge, DifficultyBadge, EffectivenessIndicator, BookmarkButton, MethodCard, SearchBar, FilterPanel - Add useLocalStorage and useBookmarks hooks for localStorage persistence - Set up React Router with all routes Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,12 +1,18 @@
|
|||||||
import { Routes, Route } from "react-router-dom";
|
import { Routes, Route } from "react-router-dom";
|
||||||
import { Layout } from "@/components/Layout";
|
import { Layout } from "@/components/Layout";
|
||||||
import { HomePage } from "@/pages/HomePage";
|
import { HomePage } from "@/pages/HomePage";
|
||||||
|
import { MethodsPage } from "@/pages/MethodsPage";
|
||||||
|
import { MethodDetailPage } from "@/pages/MethodDetailPage";
|
||||||
|
import { BmiCalculatorPage } from "@/pages/BmiCalculatorPage";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<Layout />}>
|
<Route element={<Layout />}>
|
||||||
<Route index element={<HomePage />} />
|
<Route index element={<HomePage />} />
|
||||||
|
<Route path="methods" element={<MethodsPage />} />
|
||||||
|
<Route path="methods/:slug" element={<MethodDetailPage />} />
|
||||||
|
<Route path="calculator" element={<BmiCalculatorPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|||||||
30
src/components/Badge.tsx
Normal file
30
src/components/Badge.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { clsx } from "clsx";
|
||||||
|
|
||||||
|
interface BadgeProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
variant?: "default" | "emerald" | "blue" | "violet" | "amber" | "rose";
|
||||||
|
size?: "sm" | "md";
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
default: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300",
|
||||||
|
emerald: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300",
|
||||||
|
blue: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300",
|
||||||
|
violet: "bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300",
|
||||||
|
amber: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300",
|
||||||
|
rose: "bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-300",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function Badge({ children, variant = "default", size = "sm" }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
"inline-flex items-center rounded-full font-medium",
|
||||||
|
variantClasses[variant],
|
||||||
|
size === "sm" ? "px-2.5 py-0.5 text-xs" : "px-3 py-1 text-sm",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
src/components/BookmarkButton.tsx
Normal file
41
src/components/BookmarkButton.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { clsx } from "clsx";
|
||||||
|
|
||||||
|
interface BookmarkButtonProps {
|
||||||
|
isBookmarked: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
size?: "sm" | "md";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BookmarkButton({ isBookmarked, onClick, size = "sm" }: BookmarkButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onClick();
|
||||||
|
}}
|
||||||
|
className={clsx(
|
||||||
|
"rounded-full transition-colors",
|
||||||
|
size === "sm" ? "p-1.5" : "p-2",
|
||||||
|
isBookmarked
|
||||||
|
? "text-amber-500 hover:text-amber-600"
|
||||||
|
: "text-gray-400 hover:text-amber-500 dark:text-gray-500",
|
||||||
|
)}
|
||||||
|
aria-label={isBookmarked ? "Убрать из закладок" : "Добавить в закладки"}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className={clsx(size === "sm" ? "h-5 w-5" : "h-6 w-6")}
|
||||||
|
fill={isBookmarked ? "currentColor" : "none"}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/components/DifficultyBadge.tsx
Normal file
21
src/components/DifficultyBadge.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { clsx } from "clsx";
|
||||||
|
import type { Difficulty } from "@/types/method";
|
||||||
|
|
||||||
|
interface DifficultyBadgeProps {
|
||||||
|
difficulty: Difficulty;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
easy: { label: "Легко", class: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300" },
|
||||||
|
medium: { label: "Средне", class: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300" },
|
||||||
|
hard: { label: "Сложно", class: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300" },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function DifficultyBadge({ difficulty }: DifficultyBadgeProps) {
|
||||||
|
const { label, class: className } = config[difficulty];
|
||||||
|
return (
|
||||||
|
<span className={clsx("inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium", className)}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
src/components/EffectivenessIndicator.tsx
Normal file
35
src/components/EffectivenessIndicator.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { clsx } from "clsx";
|
||||||
|
import type { Effectiveness } from "@/types/method";
|
||||||
|
|
||||||
|
interface EffectivenessIndicatorProps {
|
||||||
|
effectiveness: Effectiveness;
|
||||||
|
}
|
||||||
|
|
||||||
|
const levels = {
|
||||||
|
low: { label: "Низкая", filled: 1 },
|
||||||
|
medium: { label: "Средняя", filled: 2 },
|
||||||
|
high: { label: "Высокая", filled: 3 },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function EffectivenessIndicator({ effectiveness }: EffectivenessIndicatorProps) {
|
||||||
|
const { label, filled } = levels[effectiveness];
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1.5" title={`Эффективность: ${label}`}>
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">Эффективность:</span>
|
||||||
|
<div className="flex gap-0.5">
|
||||||
|
{[1, 2, 3].map((level) => (
|
||||||
|
<div
|
||||||
|
key={level}
|
||||||
|
className={clsx(
|
||||||
|
"h-2.5 w-5 rounded-sm",
|
||||||
|
level <= filled
|
||||||
|
? "bg-primary-500"
|
||||||
|
: "bg-gray-200 dark:bg-gray-700",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-medium text-gray-600 dark:text-gray-300">{label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
107
src/components/FilterPanel.tsx
Normal file
107
src/components/FilterPanel.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { clsx } from "clsx";
|
||||||
|
import { CATEGORIES, DIFFICULTIES, EFFECTIVENESS_LEVELS } from "@/types/method";
|
||||||
|
import type { CategoryId, Difficulty, Effectiveness } from "@/types/method";
|
||||||
|
|
||||||
|
interface FilterPanelProps {
|
||||||
|
selectedCategories: CategoryId[];
|
||||||
|
selectedDifficulties: Difficulty[];
|
||||||
|
selectedEffectiveness: Effectiveness[];
|
||||||
|
onToggleCategory: (id: CategoryId) => void;
|
||||||
|
onToggleDifficulty: (id: Difficulty) => void;
|
||||||
|
onToggleEffectiveness: (id: Effectiveness) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FilterPanel({
|
||||||
|
selectedCategories,
|
||||||
|
selectedDifficulties,
|
||||||
|
selectedEffectiveness,
|
||||||
|
onToggleCategory,
|
||||||
|
onToggleDifficulty,
|
||||||
|
onToggleEffectiveness,
|
||||||
|
onReset,
|
||||||
|
}: FilterPanelProps) {
|
||||||
|
const hasFilters =
|
||||||
|
selectedCategories.length > 0 ||
|
||||||
|
selectedDifficulties.length > 0 ||
|
||||||
|
selectedEffectiveness.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Фильтры</h3>
|
||||||
|
{hasFilters && (
|
||||||
|
<button
|
||||||
|
onClick={onReset}
|
||||||
|
className="text-xs text-primary-600 hover:text-primary-700 dark:text-primary-400"
|
||||||
|
>
|
||||||
|
Сбросить
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Categories */}
|
||||||
|
<div>
|
||||||
|
<p className="mb-2 text-xs font-medium text-gray-500 uppercase dark:text-gray-400">Категория</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{Object.values(CATEGORIES).map((cat) => (
|
||||||
|
<button
|
||||||
|
key={cat.id}
|
||||||
|
onClick={() => onToggleCategory(cat.id as CategoryId)}
|
||||||
|
className={clsx(
|
||||||
|
"rounded-lg px-3 py-1.5 text-xs font-medium transition-colors",
|
||||||
|
selectedCategories.includes(cat.id as CategoryId)
|
||||||
|
? "bg-primary-600 text-white"
|
||||||
|
: "bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{cat.icon} {cat.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Difficulty */}
|
||||||
|
<div>
|
||||||
|
<p className="mb-2 text-xs font-medium text-gray-500 uppercase dark:text-gray-400">Сложность</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{Object.values(DIFFICULTIES).map((diff) => (
|
||||||
|
<button
|
||||||
|
key={diff.id}
|
||||||
|
onClick={() => onToggleDifficulty(diff.id as Difficulty)}
|
||||||
|
className={clsx(
|
||||||
|
"rounded-lg px-3 py-1.5 text-xs font-medium transition-colors",
|
||||||
|
selectedDifficulties.includes(diff.id as Difficulty)
|
||||||
|
? "bg-primary-600 text-white"
|
||||||
|
: "bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{diff.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Effectiveness */}
|
||||||
|
<div>
|
||||||
|
<p className="mb-2 text-xs font-medium text-gray-500 uppercase dark:text-gray-400">Эффективность</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{Object.values(EFFECTIVENESS_LEVELS).map((eff) => (
|
||||||
|
<button
|
||||||
|
key={eff.id}
|
||||||
|
onClick={() => onToggleEffectiveness(eff.id as Effectiveness)}
|
||||||
|
className={clsx(
|
||||||
|
"rounded-lg px-3 py-1.5 text-xs font-medium transition-colors",
|
||||||
|
selectedEffectiveness.includes(eff.id as Effectiveness)
|
||||||
|
? "bg-primary-600 text-white"
|
||||||
|
: "bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{eff.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
src/components/MethodCard.tsx
Normal file
58
src/components/MethodCard.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { clsx } from "clsx";
|
||||||
|
import type { DietMethod } from "@/types/method";
|
||||||
|
import { CATEGORIES } from "@/types/method";
|
||||||
|
import { Badge } from "@/components/Badge";
|
||||||
|
import { DifficultyBadge } from "@/components/DifficultyBadge";
|
||||||
|
import { EffectivenessIndicator } from "@/components/EffectivenessIndicator";
|
||||||
|
import { BookmarkButton } from "@/components/BookmarkButton";
|
||||||
|
|
||||||
|
interface MethodCardProps {
|
||||||
|
method: DietMethod;
|
||||||
|
isBookmarked: boolean;
|
||||||
|
onToggleBookmark: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MethodCard({ method, isBookmarked, onToggleBookmark }: MethodCardProps) {
|
||||||
|
const category = CATEGORIES[method.category];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={`/methods/${method.slug}`}
|
||||||
|
className={clsx(
|
||||||
|
"group relative flex flex-col rounded-2xl border border-gray-200 bg-white p-6 transition-all",
|
||||||
|
"hover:border-primary-300 hover:shadow-lg hover:shadow-primary-500/5",
|
||||||
|
"dark:border-gray-800 dark:bg-gray-900 dark:hover:border-primary-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="absolute right-4 top-4">
|
||||||
|
<BookmarkButton
|
||||||
|
isBookmarked={isBookmarked}
|
||||||
|
onClick={() => onToggleBookmark(method.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-2xl" aria-hidden="true">{category.icon}</span>
|
||||||
|
<Badge variant={category.color}>{category.label}</Badge>
|
||||||
|
<DifficultyBadge difficulty={method.difficulty} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="mt-3 text-lg font-semibold text-gray-900 group-hover:text-primary-600 dark:text-white dark:group-hover:text-primary-400">
|
||||||
|
{method.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="mt-2 flex-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{method.shortDescription}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<EffectivenessIndicator effectiveness={method.effectiveness} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
{method.timeframe}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
src/components/SearchBar.tsx
Normal file
32
src/components/SearchBar.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
interface SearchBarProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchBar({ value, onChange, placeholder = "Поиск методик..." }: SearchBarProps) {
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<svg
|
||||||
|
className="absolute left-3.5 top-1/2 h-5 w-5 -translate-y-1/2 text-gray-400 dark:text-gray-500"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="w-full rounded-xl border border-gray-300 bg-white py-3 pl-11 pr-4 text-sm text-gray-900 outline-none transition-colors placeholder:text-gray-400 focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 dark:border-gray-700 dark:bg-gray-900 dark:text-white dark:placeholder:text-gray-500 dark:focus:border-primary-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
528
src/data/methods.ts
Normal file
528
src/data/methods.ts
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
import type { DietMethod } from "@/types/method";
|
||||||
|
|
||||||
|
export const methods: DietMethod[] = [
|
||||||
|
// === ДИЕТЫ ===
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
slug: "intermittent-fasting",
|
||||||
|
title: "Интервальное голодание",
|
||||||
|
shortDescription: "Чередование периодов приёма пищи и голодания (16/8, 5:2 и др.)",
|
||||||
|
fullDescription:
|
||||||
|
"Интервальное голодание — это режим питания, при котором вы чередуете окна приёма пищи с периодами голодания. Самая популярная схема — 16/8: вы едите в течение 8 часов и голодаете 16 часов. Другой вариант — 5:2, при котором 5 дней вы питаетесь нормально, а 2 дня ограничиваете калории до 500–600. Метод не предписывает конкретные продукты, а фокусируется на времени приёма пищи. Исследования показывают положительное влияние на метаболизм, чувствительность к инсулину и аутофагию.",
|
||||||
|
category: "diet",
|
||||||
|
difficulty: "medium",
|
||||||
|
effectiveness: "high",
|
||||||
|
timeframe: "Результаты через 2–4 недели",
|
||||||
|
pros: [
|
||||||
|
"Не требует подсчёта калорий",
|
||||||
|
"Гибкость в выборе продуктов",
|
||||||
|
"Улучшает чувствительность к инсулину",
|
||||||
|
"Стимулирует аутофагию (клеточное обновление)",
|
||||||
|
"Бесплатно — не нужны добавки или спецпродукты",
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
"Первые дни могут быть тяжёлыми из-за голода",
|
||||||
|
"Не подходит людям с расстройствами пищевого поведения",
|
||||||
|
"Может вызвать переедание в окно питания",
|
||||||
|
"Не рекомендуется беременным и кормящим",
|
||||||
|
],
|
||||||
|
scientificBasis:
|
||||||
|
"Мета-анализ 2020 г. (New England Journal of Medicine) подтвердил эффективность для снижения веса и улучшения метаболических маркеров. Исследования на людях показывают снижение веса на 3–8% за 3–24 недели.",
|
||||||
|
contraindications: [
|
||||||
|
"Сахарный диабет 1 типа",
|
||||||
|
"Расстройства пищевого поведения",
|
||||||
|
"Беременность и кормление грудью",
|
||||||
|
"Дефицит массы тела",
|
||||||
|
],
|
||||||
|
samplePlan:
|
||||||
|
"Схема 16/8: завтрак в 12:00, обед в 15:00, ужин до 20:00. Между приёмами пищи — вода, чай, кофе без сахара.",
|
||||||
|
tags: ["голодание", "метаболизм", "инсулин", "аутофагия"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
slug: "mediterranean-diet",
|
||||||
|
title: "Средиземноморская диета",
|
||||||
|
shortDescription: "Питание на основе овощей, фруктов, оливкового масла и рыбы",
|
||||||
|
fullDescription:
|
||||||
|
"Средиземноморская диета основана на традиционном питании жителей Греции, Италии и Испании. Основу составляют овощи, фрукты, цельные злаки, бобовые, орехи, оливковое масло и рыба. Красное мясо и сладости ограничены. Это не столько диета для похудения, сколько долгосрочный здоровый стиль питания, который снижает риск сердечно-сосудистых заболеваний, диабета 2 типа и некоторых видов рака.",
|
||||||
|
category: "diet",
|
||||||
|
difficulty: "easy",
|
||||||
|
effectiveness: "high",
|
||||||
|
timeframe: "Постепенное снижение веса, устойчивый результат за 3–6 месяцев",
|
||||||
|
pros: [
|
||||||
|
"Научно доказанная польза для сердца",
|
||||||
|
"Разнообразное и вкусное питание",
|
||||||
|
"Не требует строгих ограничений",
|
||||||
|
"Снижает риск хронических заболеваний",
|
||||||
|
"Подходит для длительного применения",
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
"Относительно дорогие продукты (рыба, оливковое масло, орехи)",
|
||||||
|
"Медленное снижение веса",
|
||||||
|
"Требует навыков приготовления пищи",
|
||||||
|
],
|
||||||
|
scientificBasis:
|
||||||
|
"Исследование PREDIMED (2013, NEJM) с 7447 участниками показало снижение сердечно-сосудистых рисков на 30%. Признана лучшей диетой по рейтингу U.S. News & World Report несколько лет подряд.",
|
||||||
|
contraindications: [
|
||||||
|
"Аллергия на морепродукты или орехи (можно адаптировать)",
|
||||||
|
],
|
||||||
|
samplePlan:
|
||||||
|
"Завтрак: овсянка с ягодами и орехами. Обед: салат с тунцом и оливковым маслом. Ужин: запечённая рыба с овощами гриль.",
|
||||||
|
tags: ["средиземноморье", "сердце", "оливковое масло", "рыба"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
slug: "keto-diet",
|
||||||
|
title: "Кетогенная диета (кето)",
|
||||||
|
shortDescription: "Высокожировая, низкоуглеводная диета для введения организма в кетоз",
|
||||||
|
fullDescription:
|
||||||
|
"Кетогенная диета предполагает резкое сокращение углеводов (до 20–50 г в день) и увеличение потребления жиров. Когда организм исчерпывает запасы гликогена, он переходит в состояние кетоза — начинает использовать жиры как основной источник энергии, расщепляя их до кетоновых тел. Типичное соотношение макронутриентов: 70–80% жиров, 15–20% белков, 5–10% углеводов.",
|
||||||
|
category: "diet",
|
||||||
|
difficulty: "hard",
|
||||||
|
effectiveness: "high",
|
||||||
|
timeframe: "Быстрое снижение веса в первые 2 недели, устойчивое — за 1–3 месяца",
|
||||||
|
pros: [
|
||||||
|
"Быстрое снижение веса на начальном этапе",
|
||||||
|
"Снижение аппетита за счёт кетоза",
|
||||||
|
"Стабильный уровень сахара в крови",
|
||||||
|
"Эффективна при эпилепсии и некоторых неврологических заболеваниях",
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
"Трудно соблюдать длительное время",
|
||||||
|
"«Кето-грипп» в первые дни адаптации",
|
||||||
|
"Ограничение фруктов и многих овощей",
|
||||||
|
"Потенциальные проблемы с холестерином",
|
||||||
|
"Социальные трудности (ограниченный выбор в ресторанах)",
|
||||||
|
],
|
||||||
|
scientificBasis:
|
||||||
|
"Мета-анализ 13 РКИ (British Journal of Nutrition, 2013) показал, что кетогенная диета приводит к большему снижению веса, чем низкожировая диета в долгосрочной перспективе.",
|
||||||
|
contraindications: [
|
||||||
|
"Заболевания печени и поджелудочной железы",
|
||||||
|
"Дефицит карнитина",
|
||||||
|
"Порфирия",
|
||||||
|
"Беременность",
|
||||||
|
],
|
||||||
|
samplePlan:
|
||||||
|
"Завтрак: яичница с авокадо и беконом. Обед: салат с курицей и оливковым маслом. Ужин: стейк из лосося с брокколи в сливочном соусе.",
|
||||||
|
tags: ["кетоз", "низкоуглеводная", "жиры", "LCHF"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
slug: "calorie-deficit",
|
||||||
|
title: "Дефицит калорий (CICO)",
|
||||||
|
shortDescription: "Контроль калорийности: потреблять меньше, чем расходуешь",
|
||||||
|
fullDescription:
|
||||||
|
"CICO (Calories In, Calories Out) — это фундаментальный принцип снижения веса. Вы создаёте дефицит калорий, потребляя меньше энергии, чем тратите. Рекомендуемый дефицит — 300–500 ккал в день для безопасного снижения веса 0.5–1 кг в неделю. Метод не ограничивает продукты, но требует подсчёта калорий и понимания энергетической ценности пищи.",
|
||||||
|
category: "diet",
|
||||||
|
difficulty: "medium",
|
||||||
|
effectiveness: "high",
|
||||||
|
timeframe: "0.5–1 кг в неделю при дефиците 500 ккал/день",
|
||||||
|
pros: [
|
||||||
|
"Научно доказанный принцип — работает всегда",
|
||||||
|
"Гибкость в выборе продуктов",
|
||||||
|
"Подходит для любого бюджета",
|
||||||
|
"Обучает пониманию калорийности продуктов",
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
"Требует постоянного подсчёта калорий",
|
||||||
|
"Может игнорировать качество питания",
|
||||||
|
"Утомительно в долгосрочной перспективе",
|
||||||
|
"Не учитывает гормональный фон",
|
||||||
|
],
|
||||||
|
scientificBasis:
|
||||||
|
"Закон термодинамики подтверждён тысячами исследований. Мета-анализ в American Journal of Clinical Nutrition показывает, что дефицит калорий — единственный обязательный фактор для снижения веса.",
|
||||||
|
contraindications: [
|
||||||
|
"Расстройства пищевого поведения",
|
||||||
|
"Дефицит массы тела",
|
||||||
|
],
|
||||||
|
samplePlan:
|
||||||
|
"Рассчитайте свою норму калорий (TDEE), вычтите 400–500 ккал. Используйте приложение для отслеживания (MyFitnessPal, FatSecret).",
|
||||||
|
tags: ["калории", "подсчёт", "CICO", "дефицит"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
slug: "plant-based",
|
||||||
|
title: "Растительное питание",
|
||||||
|
shortDescription: "Диета на основе растительной пищи с минимумом продуктов животного происхождения",
|
||||||
|
fullDescription:
|
||||||
|
"Растительное питание (plant-based diet) делает акцент на овощах, фруктах, бобовых, цельных злаках, орехах и семенах. В отличие от строгого веганства, допускает небольшое количество продуктов животного происхождения. Такой подход естественным образом снижает калорийность за счёт высокого содержания клетчатки и воды в растительных продуктах.",
|
||||||
|
category: "diet",
|
||||||
|
difficulty: "medium",
|
||||||
|
effectiveness: "medium",
|
||||||
|
timeframe: "Устойчивое снижение за 2–6 месяцев",
|
||||||
|
pros: [
|
||||||
|
"Высокое содержание клетчатки и витаминов",
|
||||||
|
"Снижение риска сердечно-сосудистых заболеваний",
|
||||||
|
"Экологически более устойчивый подход",
|
||||||
|
"Естественное снижение калорийности",
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
"Риск дефицита B12, железа, цинка",
|
||||||
|
"Требует планирования для получения полного белка",
|
||||||
|
"Может быть непривычным для любителей мяса",
|
||||||
|
],
|
||||||
|
scientificBasis:
|
||||||
|
"Исследование в Journal of the American Heart Association (2019) показало снижение риска сердечно-сосудистых заболеваний на 16% у приверженцев растительного питания.",
|
||||||
|
contraindications: [
|
||||||
|
"Тяжёлая анемия (без контроля врача)",
|
||||||
|
"Детский возраст (без контроля педиатра)",
|
||||||
|
],
|
||||||
|
tags: ["растительная", "вегетарианская", "клетчатка", "овощи"],
|
||||||
|
},
|
||||||
|
|
||||||
|
// === ФИЗИЧЕСКАЯ АКТИВНОСТЬ ===
|
||||||
|
{
|
||||||
|
id: "6",
|
||||||
|
slug: "hiit-training",
|
||||||
|
title: "HIIT (Высокоинтенсивные интервальные тренировки)",
|
||||||
|
shortDescription: "Короткие взрывные упражнения с периодами отдыха",
|
||||||
|
fullDescription:
|
||||||
|
"HIIT (High-Intensity Interval Training) — метод тренировок, чередующий короткие периоды максимальной нагрузки (20–60 секунд) с периодами отдыха или низкой интенсивности. Тренировка обычно длится 15–30 минут, но по эффективности сжигания калорий сопоставима с 45–60 минутами кардио. Ключевой эффект — EPOC (избыточное потребление кислорода после тренировки), которое повышает метаболизм на несколько часов после занятия.",
|
||||||
|
category: "activity",
|
||||||
|
difficulty: "hard",
|
||||||
|
effectiveness: "high",
|
||||||
|
timeframe: "Видимые результаты через 3–4 недели при 3 тренировках в неделю",
|
||||||
|
pros: [
|
||||||
|
"Эффективное сжигание калорий за короткое время",
|
||||||
|
"Повышение метаболизма на часы после тренировки (EPOC)",
|
||||||
|
"Не требует оборудования",
|
||||||
|
"Сохраняет мышечную массу при похудении",
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
"Высокий риск травм при неправильной технике",
|
||||||
|
"Не подходит начинающим без подготовки",
|
||||||
|
"Тяжело психологически",
|
||||||
|
"Требует полноценного восстановления между тренировками",
|
||||||
|
],
|
||||||
|
scientificBasis:
|
||||||
|
"Мета-анализ в British Journal of Sports Medicine (2019) показал, что HIIT на 28.5% эффективнее традиционного кардио для снижения жировой массы.",
|
||||||
|
contraindications: [
|
||||||
|
"Сердечно-сосудистые заболевания",
|
||||||
|
"Проблемы с суставами",
|
||||||
|
"Нулевая физическая подготовка (сначала базовые тренировки)",
|
||||||
|
],
|
||||||
|
samplePlan:
|
||||||
|
"Табата 4 мин: 20 сек — берпи, 10 сек — отдых, 8 раундов. Повторить 4 раза с отдыхом 1 мин между блоками.",
|
||||||
|
tags: ["интервальные", "кардио", "жиросжигание", "EPOC"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
slug: "walking-10k",
|
||||||
|
title: "Ходьба 10 000 шагов",
|
||||||
|
shortDescription: "Ежедневная ходьба как простой и безопасный способ увеличить расход калорий",
|
||||||
|
fullDescription:
|
||||||
|
"Ходьба — самая доступная и безопасная форма физической активности. 10 000 шагов в день (примерно 7–8 км) позволяют сжигать 300–500 дополнительных калорий. Ходьба не создаёт нагрузки на суставы, не требует специальной подготовки и оборудования, и легко вписывается в повседневную жизнь.",
|
||||||
|
category: "activity",
|
||||||
|
difficulty: "easy",
|
||||||
|
effectiveness: "medium",
|
||||||
|
timeframe: "Постепенное снижение веса за 2–3 месяца",
|
||||||
|
pros: [
|
||||||
|
"Подходит всем возрастам и уровням подготовки",
|
||||||
|
"Минимальный риск травм",
|
||||||
|
"Улучшает настроение и снижает стресс",
|
||||||
|
"Не требует спортзала и оборудования",
|
||||||
|
"Легко сделать привычкой",
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
"Медленное снижение веса",
|
||||||
|
"Может быть скучно",
|
||||||
|
"Зависимость от погоды (для прогулок на улице)",
|
||||||
|
"Без диеты эффект ограничен",
|
||||||
|
],
|
||||||
|
scientificBasis:
|
||||||
|
"Исследование в JAMA Internal Medicine (2019) с 16 741 участниками показало, что увеличение количества шагов до 7 500 в день снижает смертность на 40%.",
|
||||||
|
contraindications: [
|
||||||
|
"Тяжёлые заболевания опорно-двигательного аппарата (консультация врача)",
|
||||||
|
],
|
||||||
|
samplePlan:
|
||||||
|
"Начните с 5 000 шагов и увеличивайте на 500 шагов каждую неделю. Используйте шагомер или фитнес-браслет.",
|
||||||
|
tags: ["ходьба", "шаги", "кардио", "повседневная"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "8",
|
||||||
|
slug: "strength-training",
|
||||||
|
title: "Силовые тренировки",
|
||||||
|
shortDescription: "Упражнения с отягощениями для роста мышц и ускорения метаболизма",
|
||||||
|
fullDescription:
|
||||||
|
"Силовые тренировки (с гантелями, штангой, тренажёрами или собственным весом) увеличивают мышечную массу, что повышает базовый метаболизм. Каждый килограмм мышц сжигает примерно 13 ккал в сутки в покое (против 4.5 ккал для жира). Помимо похудения, силовые тренировки укрепляют кости, суставы, улучшают осанку и функциональную силу.",
|
||||||
|
category: "activity",
|
||||||
|
difficulty: "medium",
|
||||||
|
effectiveness: "high",
|
||||||
|
timeframe: "Изменение состава тела заметно через 6–8 недель",
|
||||||
|
pros: [
|
||||||
|
"Ускоряет базовый метаболизм",
|
||||||
|
"Формирует подтянутую фигуру",
|
||||||
|
"Укрепляет кости (профилактика остеопороза)",
|
||||||
|
"Эффект сохраняется даже в покое",
|
||||||
|
"Улучшает осанку и функциональность",
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
"Требует обучения технике",
|
||||||
|
"Нужен доступ к оборудованию или спортзалу",
|
||||||
|
"Вес на весах может не снижаться (рост мышц)",
|
||||||
|
"Риск травм при неправильной технике",
|
||||||
|
],
|
||||||
|
scientificBasis:
|
||||||
|
"Мета-анализ в Obesity Reviews (2021) показал, что силовые тренировки значимо снижают процент жира в теле, даже без изменения диеты.",
|
||||||
|
contraindications: [
|
||||||
|
"Острые травмы опорно-двигательного аппарата",
|
||||||
|
"Нестабильная гипертония",
|
||||||
|
"Недавно перенесённые операции",
|
||||||
|
],
|
||||||
|
samplePlan:
|
||||||
|
"3 тренировки в неделю: понедельник — верх тела, среда — низ тела, пятница — всё тело. 3–4 подхода по 8–12 повторений.",
|
||||||
|
tags: ["мышцы", "тренажёрный зал", "метаболизм", "сила"],
|
||||||
|
},
|
||||||
|
|
||||||
|
// === ОБРАЗ ЖИЗНИ ===
|
||||||
|
{
|
||||||
|
id: "9",
|
||||||
|
slug: "sleep-optimization",
|
||||||
|
title: "Оптимизация сна",
|
||||||
|
shortDescription: "Качественный сон 7–9 часов как основа здорового метаболизма",
|
||||||
|
fullDescription:
|
||||||
|
"Недосыпание (менее 7 часов) повышает уровень грелина (гормон голода) и снижает лептин (гормон сытости), что приводит к перееданию. Кроме того, недостаток сна повышает кортизол и снижает чувствительность к инсулину. Оптимизация сна — это один из самых недооценённых инструментов для контроля веса.",
|
||||||
|
category: "lifestyle",
|
||||||
|
difficulty: "easy",
|
||||||
|
effectiveness: "medium",
|
||||||
|
timeframe: "Улучшение метаболизма за 1–2 недели нормализации сна",
|
||||||
|
pros: [
|
||||||
|
"Бесплатно",
|
||||||
|
"Улучшает не только вес, но и общее здоровье",
|
||||||
|
"Снижает тягу к сладкому и жирному",
|
||||||
|
"Повышает энергию для тренировок",
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
"Может быть сложно при загруженном графике",
|
||||||
|
"Требует дисциплины и изменения привычек",
|
||||||
|
"Результат не мгновенный",
|
||||||
|
],
|
||||||
|
scientificBasis:
|
||||||
|
"Исследование в Annals of Internal Medicine (2010) показало, что при недосыпании 55% потери веса приходится на мышцы вместо жира.",
|
||||||
|
contraindications: [
|
||||||
|
"Расстройства сна требуют консультации врача (апноэ, бессонница)",
|
||||||
|
],
|
||||||
|
samplePlan:
|
||||||
|
"Ложитесь и вставайте в одно время. За 1 час до сна: нет экранов, нет кофеина после 14:00, прохладная комната (18–20°C).",
|
||||||
|
tags: ["сон", "восстановление", "гормоны", "грелин"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "10",
|
||||||
|
slug: "mindful-eating",
|
||||||
|
title: "Осознанное питание",
|
||||||
|
shortDescription: "Практика внимательного отношения к еде: есть медленно, чувствовать сигналы тела",
|
||||||
|
fullDescription:
|
||||||
|
"Осознанное питание (mindful eating) — это практика полного присутствия во время еды. Вы едите медленно, обращаете внимание на вкус, текстуру, запах пищи, и учитесь различать физический голод от эмоционального. Метод помогает естественным образом уменьшить порции без ощущения ограничения.",
|
||||||
|
category: "lifestyle",
|
||||||
|
difficulty: "easy",
|
||||||
|
effectiveness: "medium",
|
||||||
|
timeframe: "Формирование привычки за 3–4 недели, снижение веса за 2–3 месяца",
|
||||||
|
pros: [
|
||||||
|
"Не требует ограничений в продуктах",
|
||||||
|
"Улучшает отношения с едой",
|
||||||
|
"Снижает эмоциональное переедание",
|
||||||
|
"Помогает наслаждаться едой больше",
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
"Требует постоянной практики",
|
||||||
|
"Медленный эффект на вес",
|
||||||
|
"Трудно практиковать в спешке",
|
||||||
|
],
|
||||||
|
scientificBasis:
|
||||||
|
"Систематический обзор в Obesity Reviews (2014) показал, что практики осознанного питания снижают частоту переедания и эмоционального еды.",
|
||||||
|
contraindications: [],
|
||||||
|
samplePlan:
|
||||||
|
"Ешьте без телефона и ТВ. Жуйте каждый кусочек 20–30 раз. Делайте паузу в середине приёма пищи, чтобы оценить голод.",
|
||||||
|
tags: ["осознанность", "медитация", "порции", "привычки"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "11",
|
||||||
|
slug: "water-intake",
|
||||||
|
title: "Водный режим",
|
||||||
|
shortDescription: "Достаточное потребление воды для поддержания метаболизма и контроля аппетита",
|
||||||
|
fullDescription:
|
||||||
|
"Вода участвует во всех метаболических процессах. Стакан воды перед едой снижает количество потребляемых калорий на 75–90 ккал за приём пищи. Часто жажда маскируется под голод, и достаточное потребление воды (30 мл на кг веса) помогает избежать лишних перекусов. Холодная вода дополнительно сжигает калории на нагрев.",
|
||||||
|
category: "lifestyle",
|
||||||
|
difficulty: "easy",
|
||||||
|
effectiveness: "low",
|
||||||
|
timeframe: "Вспомогательный метод, усиливает другие подходы",
|
||||||
|
pros: [
|
||||||
|
"Максимально просто и доступно",
|
||||||
|
"Помогает отличить голод от жажды",
|
||||||
|
"Улучшает состояние кожи и пищеварение",
|
||||||
|
"Усиливает эффект других методов",
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
"Сам по себе не приведёт к значительному похудению",
|
||||||
|
"Избыток воды может быть вреден",
|
||||||
|
"Частые походы в туалет",
|
||||||
|
],
|
||||||
|
scientificBasis:
|
||||||
|
"Исследование в Obesity (2010): 500 мл воды перед каждым приёмом пищи помогло участникам похудеть на 44% больше за 12 недель.",
|
||||||
|
contraindications: [
|
||||||
|
"Заболевания почек (консультация врача о нормах)",
|
||||||
|
"Сердечная недостаточность",
|
||||||
|
],
|
||||||
|
samplePlan:
|
||||||
|
"Выпивайте стакан воды сразу после пробуждения. Пейте 500 мл за 30 мин до каждого приёма пищи. Норма: 30 мл × вес тела в кг.",
|
||||||
|
tags: ["вода", "гидратация", "метаболизм", "аппетит"],
|
||||||
|
},
|
||||||
|
|
||||||
|
// === ПСИХОЛОГИЯ ===
|
||||||
|
{
|
||||||
|
id: "12",
|
||||||
|
slug: "cognitive-behavioral-therapy",
|
||||||
|
title: "Когнитивно-поведенческая терапия (КПТ)",
|
||||||
|
shortDescription: "Работа с пищевыми привычками и мышлением через психотерапию",
|
||||||
|
fullDescription:
|
||||||
|
"КПТ для управления весом фокусируется на выявлении и изменении деструктивных мыслей и поведенческих паттернов, связанных с едой. Терапевт помогает осознать триггеры переедания, изменить отношение к еде и телу, сформировать устойчивые здоровые привычки. Это один из самых эффективных подходов для долгосрочного поддержания результата.",
|
||||||
|
category: "psychology",
|
||||||
|
difficulty: "medium",
|
||||||
|
effectiveness: "high",
|
||||||
|
timeframe: "12–20 сеансов, устойчивый эффект до нескольких лет",
|
||||||
|
pros: [
|
||||||
|
"Работает с корневыми причинами переедания",
|
||||||
|
"Долгосрочный устойчивый результат",
|
||||||
|
"Улучшает общее психическое здоровье",
|
||||||
|
"Научно доказанная эффективность",
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
"Требует работы со специалистом",
|
||||||
|
"Стоимость терапии",
|
||||||
|
"Требует времени и усилий",
|
||||||
|
"Не даёт быстрого результата на весах",
|
||||||
|
],
|
||||||
|
scientificBasis:
|
||||||
|
"Кокрановский обзор (2005, обновлён 2018) подтверждает эффективность КПТ для лечения переедания и поддержания здорового веса в долгосрочной перспективе.",
|
||||||
|
contraindications: [],
|
||||||
|
samplePlan:
|
||||||
|
"Найдите психолога, специализирующегося на КПТ и пищевом поведении. Курс обычно 12–20 еженедельных сеансов.",
|
||||||
|
tags: ["психология", "терапия", "привычки", "переедание"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "13",
|
||||||
|
slug: "habit-stacking",
|
||||||
|
title: "Наслаивание привычек (Habit Stacking)",
|
||||||
|
shortDescription: "Привязка новых здоровых привычек к уже существующим",
|
||||||
|
fullDescription:
|
||||||
|
"Метод наслаивания привычек основан на нейробиологии формирования привычек. Вы привязываете новое желаемое действие к уже существующей привычке: «После того как я [существующая привычка], я сделаю [новая привычка]». Например: «После того как я налью утренний кофе, я сделаю 10 приседаний». Этот метод использует уже сформированные нейронные пути, что значительно облегчает формирование новых привычек.",
|
||||||
|
category: "psychology",
|
||||||
|
difficulty: "easy",
|
||||||
|
effectiveness: "medium",
|
||||||
|
timeframe: "Формирование устойчивой привычки за 21–66 дней",
|
||||||
|
pros: [
|
||||||
|
"Очень просто начать",
|
||||||
|
"Использует уже существующие привычки",
|
||||||
|
"Постепенное наращивание без стресса",
|
||||||
|
"Научное обоснование (нейропластичность)",
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
"Медленное накопление эффекта",
|
||||||
|
"Требует последовательности",
|
||||||
|
"Не даёт быстрого результата",
|
||||||
|
],
|
||||||
|
scientificBasis:
|
||||||
|
"Основан на исследованиях нейропластичности и работах BJ Fogg (Stanford). Исследование в European Journal of Social Psychology (2009) показало, что среднее время формирования привычки — 66 дней.",
|
||||||
|
contraindications: [],
|
||||||
|
samplePlan:
|
||||||
|
"Составьте список из 3 привычек: 1) После пробуждения — стакан воды. 2) После обеда — 10-минутная прогулка. 3) После ужина — 5 минут растяжки.",
|
||||||
|
tags: ["привычки", "мотивация", "психология", "постепенность"],
|
||||||
|
},
|
||||||
|
|
||||||
|
// === МЕДИЦИНСКИЕ ===
|
||||||
|
{
|
||||||
|
id: "14",
|
||||||
|
slug: "medical-supervision",
|
||||||
|
title: "Похудение под наблюдением врача",
|
||||||
|
shortDescription: "Программа снижения веса с медицинским контролем и анализами",
|
||||||
|
fullDescription:
|
||||||
|
"Медицинская программа снижения веса включает: обследование (анализы крови, оценка гормонов, УЗИ), составление индивидуального плана питания врачом-диетологом, мониторинг прогресса. Может включать фармакотерапию (например, препараты GLP-1: семаглутид, лираглутид). Подход рекомендуется при ожирении (ИМТ > 30) или при наличии сопутствующих заболеваний.",
|
||||||
|
category: "medical",
|
||||||
|
difficulty: "medium",
|
||||||
|
effectiveness: "high",
|
||||||
|
timeframe: "Индивидуально, обычно программа на 3–12 месяцев",
|
||||||
|
pros: [
|
||||||
|
"Индивидуальный подход",
|
||||||
|
"Контроль здоровья на каждом этапе",
|
||||||
|
"Выявление скрытых причин лишнего веса",
|
||||||
|
"Безопасность при наличии хронических заболеваний",
|
||||||
|
"Возможность медикаментозной поддержки",
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
"Высокая стоимость",
|
||||||
|
"Требует регулярных визитов к врачу",
|
||||||
|
"Зависимость от препаратов (при фармакотерапии)",
|
||||||
|
],
|
||||||
|
scientificBasis:
|
||||||
|
"Рандомизированные исследования STEP (2021) показали снижение веса до 15% на семаглутиде за 68 недель. Медицинский контроль снижает риски в 3–5 раз по сравнению с самостоятельным похудением при ожирении.",
|
||||||
|
contraindications: [
|
||||||
|
"Зависит от выбранных препаратов и методов — определяет врач",
|
||||||
|
],
|
||||||
|
samplePlan:
|
||||||
|
"Запишитесь к эндокринологу. Сдайте: общий анализ крови, ТТГ, инсулин, HbA1c, липидный профиль. Обсудите индивидуальный план.",
|
||||||
|
tags: ["врач", "эндокринолог", "анализы", "семаглутид"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "15",
|
||||||
|
slug: "elimination-diet",
|
||||||
|
title: "Элиминационная диета",
|
||||||
|
shortDescription: "Исключение потенциальных пищевых аллергенов для выявления проблемных продуктов",
|
||||||
|
fullDescription:
|
||||||
|
"Элиминационная диета — это диагностический инструмент, при котором из рациона на 2–4 недели исключаются потенциально проблемные продукты (глютен, молочные продукты, яйца, соя, орехи, сахар). Затем продукты по одному возвращаются, с отслеживанием реакции организма. Хроническое воспаление от пищевой непереносимости может замедлять метаболизм и способствовать задержке жидкости.",
|
||||||
|
category: "medical",
|
||||||
|
difficulty: "hard",
|
||||||
|
effectiveness: "medium",
|
||||||
|
timeframe: "Диагностический период 6–8 недель, далее индивидуально",
|
||||||
|
pros: [
|
||||||
|
"Помогает выявить скрытые пищевые непереносимости",
|
||||||
|
"Уменьшает воспаление и отёки",
|
||||||
|
"Улучшает пищеварение и самочувствие",
|
||||||
|
"Персонализированный подход к питанию",
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
"Очень ограничительная на начальном этапе",
|
||||||
|
"Требует дисциплины и ведения дневника",
|
||||||
|
"Социально затруднительна",
|
||||||
|
"Риск дефицита нутриентов без контроля",
|
||||||
|
],
|
||||||
|
scientificBasis:
|
||||||
|
"NICE guidelines рекомендуют элиминационные диеты для диагностики пищевой непереносимости. Исследования показывают, что до 20% населения имеют ту или иную пищевую непереносимость.",
|
||||||
|
contraindications: [
|
||||||
|
"Расстройства пищевого поведения",
|
||||||
|
"Дефицит массы тела",
|
||||||
|
"Дети без контроля педиатра",
|
||||||
|
],
|
||||||
|
samplePlan:
|
||||||
|
"Фаза 1 (2–4 недели): исключите глютен, молочные, сахар, яйца, сою. Фаза 2: возвращайте по одному продукту каждые 3 дня, записывая реакции.",
|
||||||
|
tags: ["аллергия", "воспаление", "глютен", "непереносимость"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "16",
|
||||||
|
slug: "stress-management",
|
||||||
|
title: "Управление стрессом",
|
||||||
|
shortDescription: "Техники снижения стресса для нормализации кортизола и пищевого поведения",
|
||||||
|
fullDescription:
|
||||||
|
"Хронический стресс повышает уровень кортизола — гормона, который стимулирует отложение жира в области живота и усиливает тягу к калорийной пище. Методы управления стрессом (медитация, дыхательные упражнения, йога, прогулки на природе) помогают нормализовать гормональный фон и снизить эмоциональное переедание.",
|
||||||
|
category: "psychology",
|
||||||
|
difficulty: "easy",
|
||||||
|
effectiveness: "medium",
|
||||||
|
timeframe: "Снижение кортизола за 2–4 недели регулярной практики",
|
||||||
|
pros: [
|
||||||
|
"Улучшает общее качество жизни",
|
||||||
|
"Снижает кортизол и висцеральный жир",
|
||||||
|
"Множество бесплатных методов",
|
||||||
|
"Помогает от эмоционального переедания",
|
||||||
|
],
|
||||||
|
cons: [
|
||||||
|
"Не работает как единственный метод похудения",
|
||||||
|
"Требует регулярной практики",
|
||||||
|
"Эффект на вес косвенный",
|
||||||
|
],
|
||||||
|
scientificBasis:
|
||||||
|
"Исследование в Obesity (2017) показало, что программа снижения стресса с медитацией привела к значимому уменьшению висцерального жира без изменения диеты.",
|
||||||
|
contraindications: [],
|
||||||
|
samplePlan:
|
||||||
|
"Утро: 10 минут медитации (приложение Headspace/Calm). Обед: 5 минут дыхания 4-7-8. Вечер: 15 минут прогулки.",
|
||||||
|
tags: ["стресс", "кортизол", "медитация", "йога"],
|
||||||
|
},
|
||||||
|
];
|
||||||
22
src/hooks/useBookmarks.ts
Normal file
22
src/hooks/useBookmarks.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||||
|
|
||||||
|
export function useBookmarks() {
|
||||||
|
const [bookmarks, setBookmarks] = useLocalStorage<string[]>("bookmarks", []);
|
||||||
|
|
||||||
|
const toggleBookmark = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
setBookmarks((prev) =>
|
||||||
|
prev.includes(id) ? prev.filter((b) => b !== id) : [...prev, id],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[setBookmarks],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isBookmarked = useCallback(
|
||||||
|
(id: string) => bookmarks.includes(id),
|
||||||
|
[bookmarks],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { bookmarks, toggleBookmark, isBookmarked };
|
||||||
|
}
|
||||||
25
src/hooks/useLocalStorage.ts
Normal file
25
src/hooks/useLocalStorage.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { useState, useCallback } from "react";
|
||||||
|
|
||||||
|
export function useLocalStorage<T>(key: string, initialValue: T) {
|
||||||
|
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||||
|
try {
|
||||||
|
const item = window.localStorage.getItem(key);
|
||||||
|
return item ? (JSON.parse(item) as T) : initialValue;
|
||||||
|
} catch {
|
||||||
|
return initialValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setValue = useCallback(
|
||||||
|
(value: T | ((val: T) => T)) => {
|
||||||
|
setStoredValue((prev) => {
|
||||||
|
const valueToStore = value instanceof Function ? value(prev) : value;
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||||
|
return valueToStore;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[key],
|
||||||
|
);
|
||||||
|
|
||||||
|
return [storedValue, setValue] as const;
|
||||||
|
}
|
||||||
149
src/pages/BmiCalculatorPage.tsx
Normal file
149
src/pages/BmiCalculatorPage.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { clsx } from "clsx";
|
||||||
|
import { calculateBmi, type BmiResult } from "@/utils/bmi";
|
||||||
|
|
||||||
|
export function BmiCalculatorPage() {
|
||||||
|
const [weight, setWeight] = useState("");
|
||||||
|
const [height, setHeight] = useState("");
|
||||||
|
const [result, setResult] = useState<BmiResult | null>(null);
|
||||||
|
|
||||||
|
const handleCalculate = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const w = parseFloat(weight);
|
||||||
|
const h = parseFloat(height);
|
||||||
|
if (w > 0 && h > 0) {
|
||||||
|
setResult(calculateBmi(w, h));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setWeight("");
|
||||||
|
setHeight("");
|
||||||
|
setResult(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="mx-auto max-w-2xl">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Калькулятор BMI
|
||||||
|
</h1>
|
||||||
|
<p className="mt-2 text-gray-500 dark:text-gray-400">
|
||||||
|
Индекс массы тела (BMI) — показатель соотношения веса и роста. Помогает
|
||||||
|
оценить, находится ли ваш вес в здоровом диапазоне.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form onSubmit={handleCalculate} className="mt-8 space-y-6">
|
||||||
|
<div className="grid gap-6 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="weight"
|
||||||
|
className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
Вес (кг)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="weight"
|
||||||
|
type="number"
|
||||||
|
min="20"
|
||||||
|
max="300"
|
||||||
|
step="0.1"
|
||||||
|
value={weight}
|
||||||
|
onChange={(e) => setWeight(e.target.value)}
|
||||||
|
placeholder="70"
|
||||||
|
required
|
||||||
|
className="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-900 outline-none transition-colors focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 dark:border-gray-700 dark:bg-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="height"
|
||||||
|
className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
Рост (см)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="height"
|
||||||
|
type="number"
|
||||||
|
min="100"
|
||||||
|
max="250"
|
||||||
|
step="0.1"
|
||||||
|
value={height}
|
||||||
|
onChange={(e) => setHeight(e.target.value)}
|
||||||
|
placeholder="175"
|
||||||
|
required
|
||||||
|
className="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-900 outline-none transition-colors focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 dark:border-gray-700 dark:bg-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="rounded-xl bg-primary-600 px-8 py-3 text-sm font-semibold text-white shadow-lg shadow-primary-500/25 transition hover:bg-primary-700"
|
||||||
|
>
|
||||||
|
Рассчитать
|
||||||
|
</button>
|
||||||
|
{result && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleReset}
|
||||||
|
className="rounded-xl border border-gray-300 px-6 py-3 text-sm font-medium text-gray-600 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
Сбросить
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Result */}
|
||||||
|
{result && (
|
||||||
|
<div className="mt-8 rounded-2xl border border-gray-200 bg-white p-8 dark:border-gray-800 dark:bg-gray-900">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Ваш BMI</p>
|
||||||
|
<p className={clsx("mt-1 text-5xl font-extrabold", result.color)}>
|
||||||
|
{result.value}
|
||||||
|
</p>
|
||||||
|
<p className={clsx("mt-2 text-lg font-semibold", result.color)}>
|
||||||
|
{result.category}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 rounded-xl bg-gray-50 p-4 text-sm text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||||
|
{result.recommendation}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* BMI Scale */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<p className="mb-3 text-xs font-medium text-gray-500 uppercase dark:text-gray-400">
|
||||||
|
Шкала BMI
|
||||||
|
</p>
|
||||||
|
<div className="flex h-3 overflow-hidden rounded-full">
|
||||||
|
<div className="w-[18.5%] bg-blue-400" title="Дефицит (<18.5)" />
|
||||||
|
<div className="w-[25%] bg-green-400" title="Норма (18.5–25)" />
|
||||||
|
<div className="w-[20%] bg-yellow-400" title="Избыток (25–30)" />
|
||||||
|
<div className="w-[20%] bg-orange-400" title="Ожирение I (30–35)" />
|
||||||
|
<div className="w-[16.5%] bg-red-400" title="Ожирение II–III (35+)" />
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 flex justify-between text-xs text-gray-400">
|
||||||
|
<span>16</span>
|
||||||
|
<span>18.5</span>
|
||||||
|
<span>25</span>
|
||||||
|
<span>30</span>
|
||||||
|
<span>35</span>
|
||||||
|
<span>40+</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div className="mt-8 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-200">
|
||||||
|
<strong>Важно:</strong> BMI — приблизительный показатель. Он не учитывает
|
||||||
|
мышечную массу, возраст, пол и распределение жира. Для полной оценки
|
||||||
|
здоровья обратитесь к врачу.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
161
src/pages/MethodDetailPage.tsx
Normal file
161
src/pages/MethodDetailPage.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { useParams, Link } from "react-router-dom";
|
||||||
|
import { methods } from "@/data/methods";
|
||||||
|
import { CATEGORIES } from "@/types/method";
|
||||||
|
import { useBookmarks } from "@/hooks/useBookmarks";
|
||||||
|
import { Badge } from "@/components/Badge";
|
||||||
|
import { DifficultyBadge } from "@/components/DifficultyBadge";
|
||||||
|
import { EffectivenessIndicator } from "@/components/EffectivenessIndicator";
|
||||||
|
import { BookmarkButton } from "@/components/BookmarkButton";
|
||||||
|
|
||||||
|
export function MethodDetailPage() {
|
||||||
|
const { slug } = useParams<{ slug: string }>();
|
||||||
|
const { isBookmarked, toggleBookmark } = useBookmarks();
|
||||||
|
|
||||||
|
const method = methods.find((m) => m.slug === slug);
|
||||||
|
|
||||||
|
if (!method) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto flex flex-col items-center justify-center px-4 py-20">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Методика не найдена</h1>
|
||||||
|
<Link to="/methods" className="mt-4 text-primary-600 hover:underline dark:text-primary-400">
|
||||||
|
Вернуться к списку
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = CATEGORIES[method.category];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
{/* Breadcrumb */}
|
||||||
|
<nav className="mb-6 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<Link to="/" className="hover:text-primary-600">Главная</Link>
|
||||||
|
<span className="mx-2">/</span>
|
||||||
|
<Link to="/methods" className="hover:text-primary-600">Методики</Link>
|
||||||
|
<span className="mx-2">/</span>
|
||||||
|
<span className="text-gray-900 dark:text-white">{method.title}</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<article className="mx-auto max-w-4xl">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="mb-8">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-3xl" aria-hidden="true">{category.icon}</span>
|
||||||
|
<Badge variant={category.color} size="md">{category.label}</Badge>
|
||||||
|
<DifficultyBadge difficulty={method.difficulty} />
|
||||||
|
<BookmarkButton
|
||||||
|
isBookmarked={isBookmarked(method.id)}
|
||||||
|
onClick={() => toggleBookmark(method.id)}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 className="mt-4 text-3xl font-extrabold text-gray-900 sm:text-4xl dark:text-white">
|
||||||
|
{method.title}
|
||||||
|
</h1>
|
||||||
|
<p className="mt-3 text-lg text-gray-500 dark:text-gray-400">
|
||||||
|
{method.shortDescription}
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 flex flex-wrap items-center gap-4">
|
||||||
|
<EffectivenessIndicator effectiveness={method.effectiveness} />
|
||||||
|
<span className="text-sm text-gray-400 dark:text-gray-500">{method.timeframe}</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<Section title="Подробное описание">
|
||||||
|
<p className="text-gray-700 leading-relaxed dark:text-gray-300">{method.fullDescription}</p>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Pros & Cons */}
|
||||||
|
<div className="grid gap-6 sm:grid-cols-2">
|
||||||
|
<Section title="Преимущества">
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{method.pros.map((pro) => (
|
||||||
|
<li key={pro} className="flex items-start gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="mt-0.5 text-green-500">✓</span>
|
||||||
|
{pro}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Недостатки">
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{method.cons.map((con) => (
|
||||||
|
<li key={con} className="flex items-start gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="mt-0.5 text-red-500">✗</span>
|
||||||
|
{con}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scientific Basis */}
|
||||||
|
<Section title="Научное обоснование">
|
||||||
|
<div className="rounded-xl border border-blue-200 bg-blue-50 p-4 text-sm text-blue-800 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-200">
|
||||||
|
{method.scientificBasis}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Contraindications */}
|
||||||
|
{method.contraindications.length > 0 && (
|
||||||
|
<Section title="Противопоказания">
|
||||||
|
<div className="rounded-xl border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20">
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{method.contraindications.map((c) => (
|
||||||
|
<li key={c} className="flex items-start gap-2 text-sm text-red-700 dark:text-red-300">
|
||||||
|
<span className="mt-0.5">⚠</span>
|
||||||
|
{c}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sample Plan */}
|
||||||
|
{method.samplePlan && (
|
||||||
|
<Section title="Примерный план">
|
||||||
|
<div className="rounded-xl border border-gray-200 bg-gray-50 p-4 text-sm text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||||
|
{method.samplePlan}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tags */}
|
||||||
|
<div className="mt-8 flex flex-wrap gap-2">
|
||||||
|
{method.tags.map((tag) => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className="rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-500 dark:bg-gray-800 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
#{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Back link */}
|
||||||
|
<div className="mt-10 border-t border-gray-200 pt-6 dark:border-gray-800">
|
||||||
|
<Link
|
||||||
|
to="/methods"
|
||||||
|
className="inline-flex items-center gap-2 text-sm font-medium text-primary-600 hover:text-primary-700 dark:text-primary-400"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">←</span>
|
||||||
|
Все методики
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<section className="mb-8">
|
||||||
|
<h2 className="mb-4 text-xl font-bold text-gray-900 dark:text-white">{title}</h2>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
109
src/pages/MethodsPage.tsx
Normal file
109
src/pages/MethodsPage.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import { methods } from "@/data/methods";
|
||||||
|
import { useBookmarks } from "@/hooks/useBookmarks";
|
||||||
|
import { MethodCard } from "@/components/MethodCard";
|
||||||
|
import { SearchBar } from "@/components/SearchBar";
|
||||||
|
import { FilterPanel } from "@/components/FilterPanel";
|
||||||
|
import type { CategoryId, Difficulty, Effectiveness } from "@/types/method";
|
||||||
|
|
||||||
|
function toggleInArray<T>(arr: T[], item: T): T[] {
|
||||||
|
return arr.includes(item) ? arr.filter((i) => i !== item) : [...arr, item];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MethodsPage() {
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [selectedCategories, setSelectedCategories] = useState<CategoryId[]>([]);
|
||||||
|
const [selectedDifficulties, setSelectedDifficulties] = useState<Difficulty[]>([]);
|
||||||
|
const [selectedEffectiveness, setSelectedEffectiveness] = useState<Effectiveness[]>([]);
|
||||||
|
const [showBookmarkedOnly, setShowBookmarkedOnly] = useState(false);
|
||||||
|
const { isBookmarked, toggleBookmark } = useBookmarks();
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const query = search.toLowerCase().trim();
|
||||||
|
return methods.filter((m) => {
|
||||||
|
if (query) {
|
||||||
|
const searchable = [m.title, m.shortDescription, ...m.tags].join(" ").toLowerCase();
|
||||||
|
if (!searchable.includes(query)) return false;
|
||||||
|
}
|
||||||
|
if (selectedCategories.length > 0 && !selectedCategories.includes(m.category)) return false;
|
||||||
|
if (selectedDifficulties.length > 0 && !selectedDifficulties.includes(m.difficulty)) return false;
|
||||||
|
if (selectedEffectiveness.length > 0 && !selectedEffectiveness.includes(m.effectiveness)) return false;
|
||||||
|
if (showBookmarkedOnly && !isBookmarked(m.id)) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [search, selectedCategories, selectedDifficulties, selectedEffectiveness, showBookmarkedOnly, isBookmarked]);
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setSelectedCategories([]);
|
||||||
|
setSelectedDifficulties([]);
|
||||||
|
setSelectedEffectiveness([]);
|
||||||
|
setShowBookmarkedOnly(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Методики</h1>
|
||||||
|
<p className="mt-2 text-gray-500 dark:text-gray-400">
|
||||||
|
{methods.length} методик похудения и поддержания здорового веса
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-8 lg:grid-cols-[280px_1fr]">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<aside className="space-y-6">
|
||||||
|
<SearchBar value={search} onChange={setSearch} />
|
||||||
|
|
||||||
|
<FilterPanel
|
||||||
|
selectedCategories={selectedCategories}
|
||||||
|
selectedDifficulties={selectedDifficulties}
|
||||||
|
selectedEffectiveness={selectedEffectiveness}
|
||||||
|
onToggleCategory={(id) => setSelectedCategories((prev) => toggleInArray(prev, id))}
|
||||||
|
onToggleDifficulty={(id) => setSelectedDifficulties((prev) => toggleInArray(prev, id))}
|
||||||
|
onToggleEffectiveness={(id) => setSelectedEffectiveness((prev) => toggleInArray(prev, id))}
|
||||||
|
onReset={resetFilters}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label className="flex cursor-pointer items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={showBookmarkedOnly}
|
||||||
|
onChange={(e) => setShowBookmarkedOnly(e.target.checked)}
|
||||||
|
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
Только закладки
|
||||||
|
</label>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Grid */}
|
||||||
|
<div>
|
||||||
|
{filtered.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center rounded-2xl border-2 border-dashed border-gray-300 py-16 dark:border-gray-700">
|
||||||
|
<p className="text-lg text-gray-500 dark:text-gray-400">Ничего не найдено</p>
|
||||||
|
<button
|
||||||
|
onClick={resetFilters}
|
||||||
|
className="mt-2 text-sm text-primary-600 hover:text-primary-700 dark:text-primary-400"
|
||||||
|
>
|
||||||
|
Сбросить фильтры
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
|
{filtered.map((method) => (
|
||||||
|
<MethodCard
|
||||||
|
key={method.id}
|
||||||
|
method={method}
|
||||||
|
isBookmarked={isBookmarked(method.id)}
|
||||||
|
onToggleBookmark={toggleBookmark}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="mt-4 text-sm text-gray-400 dark:text-gray-500">
|
||||||
|
Показано {filtered.length} из {methods.length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
src/types/method.ts
Normal file
43
src/types/method.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
export interface DietMethod {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
shortDescription: string;
|
||||||
|
fullDescription: string;
|
||||||
|
category: CategoryId;
|
||||||
|
difficulty: Difficulty;
|
||||||
|
effectiveness: Effectiveness;
|
||||||
|
timeframe: string;
|
||||||
|
pros: string[];
|
||||||
|
cons: string[];
|
||||||
|
scientificBasis: string;
|
||||||
|
contraindications: string[];
|
||||||
|
samplePlan?: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CATEGORIES = {
|
||||||
|
diet: { id: "diet", label: "Диеты", icon: "\u{1F957}", color: "emerald" },
|
||||||
|
activity: { id: "activity", label: "Физическая активность", icon: "\u{1F3CB}\u{FE0F}", color: "blue" },
|
||||||
|
lifestyle: { id: "lifestyle", label: "Образ жизни", icon: "\u{1F331}", color: "violet" },
|
||||||
|
psychology: { id: "psychology", label: "Психология", icon: "\u{1F9E0}", color: "amber" },
|
||||||
|
medical: { id: "medical", label: "Медицинские", icon: "\u{2695}\u{FE0F}", color: "rose" },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type CategoryId = keyof typeof CATEGORIES;
|
||||||
|
|
||||||
|
export const DIFFICULTIES = {
|
||||||
|
easy: { id: "easy", label: "Легко", order: 1 },
|
||||||
|
medium: { id: "medium", label: "Средне", order: 2 },
|
||||||
|
hard: { id: "hard", label: "Сложно", order: 3 },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type Difficulty = keyof typeof DIFFICULTIES;
|
||||||
|
|
||||||
|
export const EFFECTIVENESS_LEVELS = {
|
||||||
|
low: { id: "low", label: "Низкая", order: 1 },
|
||||||
|
medium: { id: "medium", label: "Средняя", order: 2 },
|
||||||
|
high: { id: "high", label: "Высокая", order: 3 },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type Effectiveness = keyof typeof EFFECTIVENESS_LEVELS;
|
||||||
67
src/utils/bmi.ts
Normal file
67
src/utils/bmi.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
export interface BmiResult {
|
||||||
|
value: number;
|
||||||
|
category: string;
|
||||||
|
color: string;
|
||||||
|
recommendation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateBmi(weightKg: number, heightCm: number): BmiResult {
|
||||||
|
const heightM = heightCm / 100;
|
||||||
|
const bmi = weightKg / (heightM * heightM);
|
||||||
|
const value = Math.round(bmi * 10) / 10;
|
||||||
|
|
||||||
|
if (value < 16) {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
category: "Выраженный дефицит массы тела",
|
||||||
|
color: "text-red-600",
|
||||||
|
recommendation: "Необходимо срочно обратиться к врачу. Недостаточная масса тела опасна для здоровья.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (value < 18.5) {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
category: "Дефицит массы тела",
|
||||||
|
color: "text-orange-500",
|
||||||
|
recommendation: "Рекомендуется увеличить калорийность рациона и обратиться к диетологу.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (value < 25) {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
category: "Нормальная масса тела",
|
||||||
|
color: "text-green-600",
|
||||||
|
recommendation: "Отличный показатель! Поддерживайте текущий вес с помощью сбалансированного питания и активности.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (value < 30) {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
category: "Избыточная масса тела (предожирение)",
|
||||||
|
color: "text-yellow-600",
|
||||||
|
recommendation: "Рекомендуется скорректировать питание и увеличить физическую активность. Ознакомьтесь с методиками в нашем справочнике.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (value < 35) {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
category: "Ожирение I степени",
|
||||||
|
color: "text-orange-600",
|
||||||
|
recommendation: "Рекомендуется обратиться к врачу и составить план снижения веса. Изучите методики под медицинским контролем.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (value < 40) {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
category: "Ожирение II степени",
|
||||||
|
color: "text-red-500",
|
||||||
|
recommendation: "Необходимо обратиться к эндокринологу. Снижение веса под медицинским наблюдением.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
category: "Ожирение III степени (морбидное)",
|
||||||
|
color: "text-red-700",
|
||||||
|
recommendation: "Необходима медицинская помощь. Обратитесь к специалисту для составления программы лечения.",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/footer.tsx","./src/components/header.tsx","./src/components/layout.tsx","./src/pages/homepage.tsx"],"version":"5.9.3"}
|
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/badge.tsx","./src/components/bookmarkbutton.tsx","./src/components/difficultybadge.tsx","./src/components/effectivenessindicator.tsx","./src/components/filterpanel.tsx","./src/components/footer.tsx","./src/components/header.tsx","./src/components/layout.tsx","./src/components/methodcard.tsx","./src/components/searchbar.tsx","./src/data/methods.ts","./src/hooks/usebookmarks.ts","./src/hooks/uselocalstorage.ts","./src/pages/bmicalculatorpage.tsx","./src/pages/homepage.tsx","./src/pages/methoddetailpage.tsx","./src/pages/methodspage.tsx","./src/types/method.ts","./src/utils/bmi.ts"],"version":"5.9.3"}
|
||||||
Reference in New Issue
Block a user