diff --git a/src/App.tsx b/src/App.tsx
index 216ac75..13fd8ab 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -4,6 +4,7 @@ import { HomePage } from "@/pages/HomePage";
import { MethodsPage } from "@/pages/MethodsPage";
import { MethodDetailPage } from "@/pages/MethodDetailPage";
import { BmiCalculatorPage } from "@/pages/BmiCalculatorPage";
+import { ComparePage } from "@/pages/ComparePage";
export function App() {
return (
@@ -13,6 +14,7 @@ export function App() {
} />
} />
} />
+ } />
);
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index a8d66d8..d737a3b 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -1,26 +1,37 @@
+import { useState } from "react";
import { Link, NavLink } from "react-router-dom";
import { clsx } from "clsx";
+import { ThemeToggle } from "@/components/ThemeToggle";
const navLinks = [
- { to: "/", label: "Главная" },
- { to: "/methods", label: "Методики" },
- { to: "/calculator", label: "Калькулятор BMI" },
+ { to: "/", label: "Главная", exact: true },
+ { to: "/methods", label: "Методики", exact: false },
+ { to: "/compare", label: "Сравнение", exact: true },
+ { to: "/calculator", label: "Калькулятор BMI", exact: true },
] as const;
export function Header() {
+ const [mobileOpen, setMobileOpen] = useState(false);
+
return (
);
}
-
-function MobileMenuButton() {
- return (
-
-
-
-
-
- );
-}
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
index 97a429a..b081aec 100644
--- a/src/components/Layout.tsx
+++ b/src/components/Layout.tsx
@@ -1,10 +1,12 @@
import { Outlet } from "react-router-dom";
import { Header } from "@/components/Header";
import { Footer } from "@/components/Footer";
+import { ScrollToTop } from "@/components/ScrollToTop";
export function Layout() {
return (
+
diff --git a/src/components/ScrollToTop.tsx b/src/components/ScrollToTop.tsx
new file mode 100644
index 0000000..bf31b9e
--- /dev/null
+++ b/src/components/ScrollToTop.tsx
@@ -0,0 +1,12 @@
+import { useEffect } from "react";
+import { useLocation } from "react-router-dom";
+
+export function ScrollToTop() {
+ const { pathname } = useLocation();
+
+ useEffect(() => {
+ window.scrollTo(0, 0);
+ }, [pathname]);
+
+ return null;
+}
diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx
new file mode 100644
index 0000000..2bd77ac
--- /dev/null
+++ b/src/components/ThemeToggle.tsx
@@ -0,0 +1,40 @@
+import { useTheme } from "@/hooks/useTheme";
+
+const icons = {
+ light: (
+
+
+
+ ),
+ dark: (
+
+
+
+ ),
+ system: (
+
+
+
+ ),
+} as const;
+
+const labels = {
+ light: "Светлая тема",
+ dark: "Тёмная тема",
+ system: "Системная тема",
+} as const;
+
+export function ThemeToggle() {
+ const { theme, toggleTheme } = useTheme();
+
+ return (
+
+ {icons[theme]}
+
+ );
+}
diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts
new file mode 100644
index 0000000..bbacabe
--- /dev/null
+++ b/src/hooks/useTheme.ts
@@ -0,0 +1,38 @@
+import { useCallback, useEffect } from "react";
+import { useLocalStorage } from "@/hooks/useLocalStorage";
+
+type Theme = "light" | "dark" | "system";
+
+function getSystemTheme(): "light" | "dark" {
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
+}
+
+function applyTheme(theme: Theme) {
+ const resolved = theme === "system" ? getSystemTheme() : theme;
+ document.documentElement.classList.toggle("dark", resolved === "dark");
+}
+
+export function useTheme() {
+ const [theme, setTheme] = useLocalStorage("theme", "system");
+
+ useEffect(() => {
+ applyTheme(theme);
+
+ if (theme === "system") {
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
+ const handler = () => applyTheme("system");
+ mq.addEventListener("change", handler);
+ return () => mq.removeEventListener("change", handler);
+ }
+ }, [theme]);
+
+ const toggleTheme = useCallback(() => {
+ setTheme((prev) => {
+ if (prev === "light") return "dark";
+ if (prev === "dark") return "system";
+ return "light";
+ });
+ }, [setTheme]);
+
+ return { theme, setTheme, toggleTheme };
+}
diff --git a/src/pages/ComparePage.tsx b/src/pages/ComparePage.tsx
new file mode 100644
index 0000000..898f611
--- /dev/null
+++ b/src/pages/ComparePage.tsx
@@ -0,0 +1,142 @@
+import { useState, useMemo } from "react";
+import { Link } from "react-router-dom";
+import { clsx } from "clsx";
+import { methods } from "@/data/methods";
+import { CATEGORIES, DIFFICULTIES, EFFECTIVENESS_LEVELS } from "@/types/method";
+import type { DietMethod } from "@/types/method";
+
+const MAX_COMPARE = 3;
+
+export function ComparePage() {
+ const [selectedIds, setSelectedIds] = useState([]);
+
+ const selectedMethods = useMemo(
+ () => selectedIds.map((id) => methods.find((m) => m.id === id)).filter(Boolean) as DietMethod[],
+ [selectedIds],
+ );
+
+ const toggleMethod = (id: string) => {
+ setSelectedIds((prev) => {
+ if (prev.includes(id)) return prev.filter((i) => i !== id);
+ if (prev.length >= MAX_COMPARE) return prev;
+ return [...prev, id];
+ });
+ };
+
+ return (
+
+
Сравнение методик
+
+ Выберите до {MAX_COMPARE} методик для сравнения
+
+
+ {/* Selector */}
+
+ {methods.map((m) => {
+ const selected = selectedIds.includes(m.id);
+ const disabled = !selected && selectedIds.length >= MAX_COMPARE;
+ return (
+ toggleMethod(m.id)}
+ disabled={disabled}
+ className={clsx(
+ "rounded-lg px-3 py-1.5 text-xs font-medium transition-colors",
+ selected
+ ? "bg-primary-600 text-white"
+ : disabled
+ ? "cursor-not-allowed bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-600"
+ : "bg-gray-100 text-gray-700 hover:bg-primary-100 hover:text-primary-700 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-primary-900/30",
+ )}
+ >
+ {CATEGORIES[m.category].icon} {m.title}
+
+ );
+ })}
+
+
+ {/* Comparison table */}
+ {selectedMethods.length >= 2 && (
+
+
+
+
+
+ Параметр
+
+ {selectedMethods.map((m) => (
+
+
+ {m.title}
+
+
+ ))}
+
+
+
+ `${CATEGORIES[m.category].icon} ${CATEGORIES[m.category].label}`)} />
+ DIFFICULTIES[m.difficulty].label)} />
+ EFFECTIVENESS_LEVELS[m.effectiveness].label)} />
+ m.timeframe)} />
+ (
+
+ {m.pros.map((p) => {p} )}
+
+ ))}
+ />
+ (
+
+ {m.cons.map((c) => {c} )}
+
+ ))}
+ />
+ m.scientificBasis)} />
+
+ m.contraindications.length > 0
+ ? m.contraindications.join(", ")
+ : "Нет",
+ )}
+ />
+
+
+
+ )}
+
+ {selectedMethods.length < 2 && selectedMethods.length > 0 && (
+
+ Выберите ещё {2 - selectedMethods.length} методик{selectedMethods.length === 1 ? "у" : "и"} для сравнения
+
+ )}
+
+ {selectedMethods.length === 0 && (
+
+ Выберите методики из списка выше
+
+ )}
+
+ );
+}
+
+function CompareRow({ label, items }: { label: string; items: React.ReactNode[] }) {
+ return (
+
+
+ {label}
+
+ {items.map((item, i) => (
+
+ {item}
+
+ ))}
+
+ );
+}
diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo
index 2786e98..54a4c8d 100644
--- a/tsconfig.app.tsbuildinfo
+++ b/tsconfig.app.tsbuildinfo
@@ -1 +1 @@
-{"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"}
\ No newline at end of file
+{"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/scrolltotop.tsx","./src/components/searchbar.tsx","./src/components/themetoggle.tsx","./src/data/methods.ts","./src/hooks/usebookmarks.ts","./src/hooks/uselocalstorage.ts","./src/hooks/usetheme.ts","./src/pages/bmicalculatorpage.tsx","./src/pages/comparepage.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"}
\ No newline at end of file