feat: add dark mode, mobile menu, method comparison, scroll-to-top
- Add theme toggle (light/dark/system) with localStorage persistence - Implement responsive mobile hamburger menu - Create ComparePage for side-by-side comparison of up to 3 methods - Add ScrollToTop component for route changes - Update navigation with compare page link Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { HomePage } from "@/pages/HomePage";
|
|||||||
import { MethodsPage } from "@/pages/MethodsPage";
|
import { MethodsPage } from "@/pages/MethodsPage";
|
||||||
import { MethodDetailPage } from "@/pages/MethodDetailPage";
|
import { MethodDetailPage } from "@/pages/MethodDetailPage";
|
||||||
import { BmiCalculatorPage } from "@/pages/BmiCalculatorPage";
|
import { BmiCalculatorPage } from "@/pages/BmiCalculatorPage";
|
||||||
|
import { ComparePage } from "@/pages/ComparePage";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
@@ -13,6 +14,7 @@ export function App() {
|
|||||||
<Route path="methods" element={<MethodsPage />} />
|
<Route path="methods" element={<MethodsPage />} />
|
||||||
<Route path="methods/:slug" element={<MethodDetailPage />} />
|
<Route path="methods/:slug" element={<MethodDetailPage />} />
|
||||||
<Route path="calculator" element={<BmiCalculatorPage />} />
|
<Route path="calculator" element={<BmiCalculatorPage />} />
|
||||||
|
<Route path="compare" element={<ComparePage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,26 +1,37 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { Link, NavLink } from "react-router-dom";
|
import { Link, NavLink } from "react-router-dom";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
|
import { ThemeToggle } from "@/components/ThemeToggle";
|
||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ to: "/", label: "Главная" },
|
{ to: "/", label: "Главная", exact: true },
|
||||||
{ to: "/methods", label: "Методики" },
|
{ to: "/methods", label: "Методики", exact: false },
|
||||||
{ to: "/calculator", label: "Калькулятор BMI" },
|
{ to: "/compare", label: "Сравнение", exact: true },
|
||||||
|
{ to: "/calculator", label: "Калькулятор BMI", exact: true },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 border-b border-gray-200 bg-white/80 backdrop-blur-lg dark:border-gray-800 dark:bg-gray-950/80">
|
<header className="sticky top-0 z-50 border-b border-gray-200 bg-white/80 backdrop-blur-lg dark:border-gray-800 dark:bg-gray-950/80">
|
||||||
<div className="container mx-auto flex items-center justify-between px-4 py-4">
|
<div className="container mx-auto flex items-center justify-between px-4 py-4">
|
||||||
<Link to="/" className="flex items-center gap-2 text-xl font-bold text-primary-600">
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="flex items-center gap-2 text-xl font-bold text-primary-600"
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
|
>
|
||||||
<span aria-hidden="true" className="text-2xl">🌿</span>
|
<span aria-hidden="true" className="text-2xl">🌿</span>
|
||||||
HealthyWeight
|
HealthyWeight
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
{/* Desktop nav */}
|
||||||
<nav className="hidden items-center gap-1 md:flex">
|
<nav className="hidden items-center gap-1 md:flex">
|
||||||
{navLinks.map(({ to, label }) => (
|
{navLinks.map(({ to, label, exact }) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={to}
|
key={to}
|
||||||
to={to}
|
to={to}
|
||||||
|
end={exact}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
clsx(
|
clsx(
|
||||||
"rounded-lg px-4 py-2 text-sm font-medium transition-colors",
|
"rounded-lg px-4 py-2 text-sm font-medium transition-colors",
|
||||||
@@ -33,23 +44,54 @@ export function Header() {
|
|||||||
{label}
|
{label}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
|
<ThemeToggle />
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<MobileMenuButton />
|
{/* Mobile controls */}
|
||||||
|
<div className="flex items-center gap-1 md:hidden">
|
||||||
|
<ThemeToggle />
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileOpen((prev) => !prev)}
|
||||||
|
className="rounded-lg p-2 text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
|
||||||
|
aria-label={mobileOpen ? "Закрыть меню" : "Открыть меню"}
|
||||||
|
aria-expanded={mobileOpen}
|
||||||
|
>
|
||||||
|
{mobileOpen ? (
|
||||||
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile nav */}
|
||||||
|
{mobileOpen && (
|
||||||
|
<nav className="border-t border-gray-200 bg-white px-4 py-3 md:hidden dark:border-gray-800 dark:bg-gray-950">
|
||||||
|
{navLinks.map(({ to, label, exact }) => (
|
||||||
|
<NavLink
|
||||||
|
key={to}
|
||||||
|
to={to}
|
||||||
|
end={exact}
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
clsx(
|
||||||
|
"block rounded-lg px-4 py-2.5 text-sm font-medium transition-colors",
|
||||||
|
isActive
|
||||||
|
? "bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
|
||||||
|
: "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MobileMenuButton() {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="rounded-lg p-2 text-gray-600 hover:bg-gray-100 md:hidden dark:text-gray-400 dark:hover:bg-gray-800"
|
|
||||||
aria-label="Открыть меню"
|
|
||||||
>
|
|
||||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
import { Header } from "@/components/Header";
|
import { Header } from "@/components/Header";
|
||||||
import { Footer } from "@/components/Footer";
|
import { Footer } from "@/components/Footer";
|
||||||
|
import { ScrollToTop } from "@/components/ScrollToTop";
|
||||||
|
|
||||||
export function Layout() {
|
export function Layout() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col">
|
<div className="flex min-h-screen flex-col">
|
||||||
|
<ScrollToTop />
|
||||||
<Header />
|
<Header />
|
||||||
<main className="flex-1">
|
<main className="flex-1">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
12
src/components/ScrollToTop.tsx
Normal file
12
src/components/ScrollToTop.tsx
Normal file
@@ -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;
|
||||||
|
}
|
||||||
40
src/components/ThemeToggle.tsx
Normal file
40
src/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useTheme } from "@/hooks/useTheme";
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
light: (
|
||||||
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
dark: (
|
||||||
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
system: (
|
||||||
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const labels = {
|
||||||
|
light: "Светлая тема",
|
||||||
|
dark: "Тёмная тема",
|
||||||
|
system: "Системная тема",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="rounded-lg p-2 text-gray-600 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
|
||||||
|
aria-label={labels[theme]}
|
||||||
|
title={labels[theme]}
|
||||||
|
>
|
||||||
|
{icons[theme]}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
src/hooks/useTheme.ts
Normal file
38
src/hooks/useTheme.ts
Normal file
@@ -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>("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 };
|
||||||
|
}
|
||||||
142
src/pages/ComparePage.tsx
Normal file
142
src/pages/ComparePage.tsx
Normal file
@@ -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<string[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="container mx-auto px-4 py-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">
|
||||||
|
Выберите до {MAX_COMPARE} методик для сравнения
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Selector */}
|
||||||
|
<div className="mt-6 flex flex-wrap gap-2">
|
||||||
|
{methods.map((m) => {
|
||||||
|
const selected = selectedIds.includes(m.id);
|
||||||
|
const disabled = !selected && selectedIds.length >= MAX_COMPARE;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={m.id}
|
||||||
|
onClick={() => 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}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Comparison table */}
|
||||||
|
{selectedMethods.length >= 2 && (
|
||||||
|
<div className="mt-8 overflow-x-auto">
|
||||||
|
<table className="w-full border-collapse text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="border-b border-gray-200 px-4 py-3 text-left text-xs font-semibold text-gray-500 uppercase dark:border-gray-700 dark:text-gray-400">
|
||||||
|
Параметр
|
||||||
|
</th>
|
||||||
|
{selectedMethods.map((m) => (
|
||||||
|
<th
|
||||||
|
key={m.id}
|
||||||
|
className="border-b border-gray-200 px-4 py-3 text-left text-xs font-semibold text-gray-900 uppercase dark:border-gray-700 dark:text-white"
|
||||||
|
>
|
||||||
|
<Link to={`/methods/${m.slug}`} className="hover:text-primary-600">
|
||||||
|
{m.title}
|
||||||
|
</Link>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<CompareRow label="Категория" items={selectedMethods.map((m) => `${CATEGORIES[m.category].icon} ${CATEGORIES[m.category].label}`)} />
|
||||||
|
<CompareRow label="Сложность" items={selectedMethods.map((m) => DIFFICULTIES[m.difficulty].label)} />
|
||||||
|
<CompareRow label="Эффективность" items={selectedMethods.map((m) => EFFECTIVENESS_LEVELS[m.effectiveness].label)} />
|
||||||
|
<CompareRow label="Сроки" items={selectedMethods.map((m) => m.timeframe)} />
|
||||||
|
<CompareRow
|
||||||
|
label="Преимущества"
|
||||||
|
items={selectedMethods.map((m) => (
|
||||||
|
<ul className="list-inside list-disc space-y-1">
|
||||||
|
{m.pros.map((p) => <li key={p}>{p}</li>)}
|
||||||
|
</ul>
|
||||||
|
))}
|
||||||
|
/>
|
||||||
|
<CompareRow
|
||||||
|
label="Недостатки"
|
||||||
|
items={selectedMethods.map((m) => (
|
||||||
|
<ul className="list-inside list-disc space-y-1">
|
||||||
|
{m.cons.map((c) => <li key={c}>{c}</li>)}
|
||||||
|
</ul>
|
||||||
|
))}
|
||||||
|
/>
|
||||||
|
<CompareRow label="Научное обоснование" items={selectedMethods.map((m) => m.scientificBasis)} />
|
||||||
|
<CompareRow
|
||||||
|
label="Противопоказания"
|
||||||
|
items={selectedMethods.map((m) =>
|
||||||
|
m.contraindications.length > 0
|
||||||
|
? m.contraindications.join(", ")
|
||||||
|
: "Нет",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedMethods.length < 2 && selectedMethods.length > 0 && (
|
||||||
|
<div className="mt-8 rounded-xl border-2 border-dashed border-gray-300 py-12 text-center text-gray-500 dark:border-gray-700 dark:text-gray-400">
|
||||||
|
Выберите ещё {2 - selectedMethods.length} методик{selectedMethods.length === 1 ? "у" : "и"} для сравнения
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedMethods.length === 0 && (
|
||||||
|
<div className="mt-8 rounded-xl border-2 border-dashed border-gray-300 py-12 text-center text-gray-500 dark:border-gray-700 dark:text-gray-400">
|
||||||
|
Выберите методики из списка выше
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CompareRow({ label, items }: { label: string; items: React.ReactNode[] }) {
|
||||||
|
return (
|
||||||
|
<tr className="border-b border-gray-100 dark:border-gray-800">
|
||||||
|
<td className="px-4 py-3 font-medium text-gray-600 align-top dark:text-gray-400">
|
||||||
|
{label}
|
||||||
|
</td>
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<td key={i} className="px-4 py-3 text-gray-700 align-top dark:text-gray-300">
|
||||||
|
{item}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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"}
|
{"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"}
|
||||||
Reference in New Issue
Block a user