diff --git a/package-lock.json b/package-lock.json
index 1e76240..36414ea 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"axios": "^1.17.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
+ "react-icons": "^5.6.0",
"react-router-dom": "^7.17.0",
"tailwindcss": "^4.3.0"
},
@@ -2958,6 +2959,15 @@
"react": "^19.2.7"
}
},
+ "node_modules/react-icons": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz",
+ "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"node_modules/react-router": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz",
diff --git a/package.json b/package.json
index f78cb78..d4442d5 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"axios": "^1.17.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
+ "react-icons": "^5.6.0",
"react-router-dom": "^7.17.0",
"tailwindcss": "^4.3.0"
},
diff --git a/src/main.tsx b/src/main.tsx
index 3d8b726..5f5373c 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,9 +1,12 @@
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./app/App.tsx";
+import { ThemeInitialProvider } from "@/modules/theme-changer";
createRoot(document.getElementById("root")!).render(
-
+
+
+
,
);
diff --git a/src/modules/theme-changer/config/theme.config.ts b/src/modules/theme-changer/config/theme.config.ts
new file mode 100644
index 0000000..0214bfc
--- /dev/null
+++ b/src/modules/theme-changer/config/theme.config.ts
@@ -0,0 +1,106 @@
+export const themes = [
+ {
+ id: "light",
+ name: "Светлая",
+ description: "Чистая светлая тема",
+ type: "light",
+ colors: {
+ primary: "#4f46e5",
+ background: "#ffffff",
+ surface: "#f8fafc",
+ text: "#1f2937",
+ border: "#e5e7eb",
+ },
+ },
+ {
+ id: "dark",
+ name: "Темная",
+ description: "Элегантная темная тема",
+ type: "dark",
+ colors: {
+ primary: "#6366f1",
+ background: "#0f172a",
+ surface: "#1e293b",
+ text: "#f1f5f9",
+ border: "#334155",
+ },
+ },
+ {
+ id: "nightowl",
+ name: "Night Owl",
+ description: "Тема вдохновленная редактором кода",
+ type: "dark",
+ colors: {
+ primary: "#7dd3fc",
+ background: "#011627",
+ surface: "#011e3c",
+ text: "#d6deeb",
+ border: "#1d3b53",
+ },
+ },
+ {
+ id: "sunset",
+ name: "Закат",
+ description: "Теплые оранжевые тона",
+ type: "dark",
+ colors: {
+ primary: "#f97316",
+ background: "#1c1917",
+ surface: "#292524",
+ text: "#fafaf9",
+ border: "#57534e",
+ },
+ },
+ {
+ id: "forest",
+ name: "Лес",
+ description: "Успокаивающая зеленая тема",
+ type: "dark",
+ colors: {
+ primary: "#22c55e",
+ background: "#052e16",
+ surface: "#14532d",
+ text: "#f0fdf4",
+ border: "#166534",
+ },
+ },
+ {
+ id: "ocean",
+ name: "Океан",
+ description: "Глубокие синие тона",
+ type: "dark",
+ colors: {
+ primary: "#06b6d4",
+ background: "#164e63",
+ surface: "#0e7490",
+ text: "#f0fdfd",
+ border: "#0891b2",
+ },
+ },
+ {
+ id: "lavender",
+ name: "Лаванда",
+ description: "Нежная фиолетовая тема",
+ type: "light",
+ colors: {
+ primary: "#a855f7",
+ background: "#faf5ff",
+ surface: "#f3e8ff",
+ text: "#581c87",
+ border: "#e9d5ff",
+ },
+ },
+ {
+ id: "coffee",
+ name: "Кофе",
+ description: "Уютная коричневая тема",
+ type: "dark",
+ colors: {
+ primary: "#d97706",
+ background: "#292524",
+ surface: "#44403c",
+ text: "#f5f5f4",
+ border: "#57534e",
+ },
+ },
+];
diff --git a/src/modules/theme-changer/index.ts b/src/modules/theme-changer/index.ts
new file mode 100644
index 0000000..b217a25
--- /dev/null
+++ b/src/modules/theme-changer/index.ts
@@ -0,0 +1,2 @@
+export { ThemeInitialProvider } from "./provider/theme.initial.provider";
+export { ThemeToggle } from "./ui/Theme.toggle";
diff --git a/src/modules/theme-changer/provider/theme.initial.provider.tsx b/src/modules/theme-changer/provider/theme.initial.provider.tsx
new file mode 100644
index 0000000..a70d022
--- /dev/null
+++ b/src/modules/theme-changer/provider/theme.initial.provider.tsx
@@ -0,0 +1,13 @@
+import { useLayoutEffect } from "react";
+import { applyTheme, initializeTheme } from "../utils/apply.theme";
+
+export const ThemeInitialProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ useLayoutEffect(() => {
+ const theme = initializeTheme();
+ applyTheme(theme);
+ }, []);
+
+ return children;
+};
diff --git a/src/modules/theme-changer/types/theme.type.ts b/src/modules/theme-changer/types/theme.type.ts
new file mode 100644
index 0000000..5bc74d2
--- /dev/null
+++ b/src/modules/theme-changer/types/theme.type.ts
@@ -0,0 +1,13 @@
+export interface ITheme {
+ id: string;
+ name: string;
+ description: string;
+ type: string;
+ colors: {
+ primary: string;
+ background: string;
+ surface: string;
+ text: string;
+ border: string;
+ };
+}
diff --git a/src/modules/theme-changer/ui/Theme.toggle.tsx b/src/modules/theme-changer/ui/Theme.toggle.tsx
new file mode 100644
index 0000000..c07dc22
--- /dev/null
+++ b/src/modules/theme-changer/ui/Theme.toggle.tsx
@@ -0,0 +1,68 @@
+import React, { useState, useEffect } from "react";
+import { FiSun, FiMoon } from "react-icons/fi";
+import {
+ getCurrentTheme,
+ toggleDarkLight,
+ getSavedTheme,
+} from "../../theme-changer/utils/apply.theme";
+import { themes } from "../../theme-changer/config/theme.config";
+
+export const ThemeToggle: React.FC = () => {
+ const [currentTheme, setCurrentTheme] = useState(() =>
+ getSavedTheme(),
+ );
+
+ const currentThemeData = themes.find((t) => t.id === currentTheme);
+ const isDark = currentThemeData?.type === "dark";
+
+ const handleClick = () => {
+ const newTheme = toggleDarkLight();
+ setCurrentTheme(newTheme);
+ };
+
+ // Инициализация при монтировании
+ useEffect(() => {
+ const saved = getSavedTheme();
+ const current = getCurrentTheme() || saved;
+ setCurrentTheme(current);
+ }, []);
+
+ // Слушаем изменения темы из других компонентов
+ useEffect(() => {
+ const handleThemeChange = (e: Event) => {
+ const event = e as CustomEvent;
+ setCurrentTheme(event.detail.theme);
+ };
+ window.addEventListener("themechange", handleThemeChange);
+ return () => window.removeEventListener("themechange", handleThemeChange);
+ }, []);
+
+ return (
+
+ );
+};
diff --git a/src/modules/theme-changer/utils/apply.theme.ts b/src/modules/theme-changer/utils/apply.theme.ts
new file mode 100644
index 0000000..3d3fb26
--- /dev/null
+++ b/src/modules/theme-changer/utils/apply.theme.ts
@@ -0,0 +1,105 @@
+import { themes } from "../config/theme.config";
+
+export const applyTheme = (themeId: string) => {
+ const theme = themes.find((t) => t.id === themeId);
+ const root = document.documentElement;
+
+ if (theme) {
+ try {
+ root.setAttribute("data-theme", themeId);
+ localStorage.setItem("theme", themeId);
+ localStorage.setItem("theme-type", theme.type);
+
+ window.dispatchEvent(
+ new CustomEvent("themechange", {
+ detail: { theme: themeId, type: theme.type },
+ }),
+ );
+ } catch (error) {
+ console.error("❌ Error applying theme:", error);
+ }
+ } else {
+ console.warn(`⚠️ Theme not found: ${themeId}, falling back to light theme`);
+ applyTheme("light");
+ }
+};
+
+export const getSavedTheme = () => {
+ try {
+ return localStorage.getItem("theme") || "light";
+ } catch (error) {
+ console.error("Error reading theme from localStorage:", error);
+ return "light";
+ }
+};
+
+export const initializeTheme = () => {
+ const savedTheme = getSavedTheme();
+
+ const themeExists = themes.some((t) => t.id === savedTheme);
+ const themeToApply = themeExists ? savedTheme : "light";
+
+ applyTheme(themeToApply);
+ return themeToApply;
+};
+
+export const getCurrentTheme = () => {
+ return document.documentElement.getAttribute("data-theme") || "light";
+};
+
+export const getCurrentThemeType = () => {
+ const currentTheme = getCurrentTheme();
+ const theme = themes.find((t) => t.id === currentTheme);
+ return theme ? theme.type : "light";
+};
+
+export const toggleDarkLight = () => {
+ const currentTheme = getCurrentTheme();
+ const currentThemeData = themes.find((t) => t.id === currentTheme);
+
+ if (currentThemeData) {
+ const oppositeThemes = themes.filter(
+ (t) => t.type !== currentThemeData.type,
+ );
+ if (oppositeThemes.length > 0) {
+ applyTheme(oppositeThemes[0].id);
+ return oppositeThemes[0].id;
+ }
+ }
+
+ const newTheme = currentTheme === "light" ? "dark" : "light";
+ applyTheme(newTheme);
+ return newTheme;
+};
+
+export const getNextTheme = () => {
+ const currentTheme = getCurrentTheme();
+ const currentIndex = themes.findIndex((t) => t.id === currentTheme);
+ const nextIndex = (currentIndex + 1) % themes.length;
+ return themes[nextIndex].id;
+};
+
+export const applySystemTheme = () => {
+ if (
+ window.matchMedia &&
+ window.matchMedia("(prefers-color-scheme: dark)").matches
+ ) {
+ applyTheme("dark");
+ } else {
+ applyTheme("light");
+ }
+};
+
+export const watchSystemTheme = () => {
+ if (window.matchMedia) {
+ window
+ .matchMedia("(prefers-color-scheme: dark)")
+ .addEventListener("change", (e) => {
+ if (e.matches) {
+ applyTheme("dark");
+ } else {
+ applyTheme("light");
+ }
+ });
+ }
+};
diff --git a/src/pages/home.page.tsx b/src/pages/home.page.tsx
index 58fd7c1..df87d99 100644
--- a/src/pages/home.page.tsx
+++ b/src/pages/home.page.tsx
@@ -1,3 +1,13 @@
+import { ThemeToggle } from "@/modules/theme-changer/ui/Theme.toggle";
+
export const HomePage = () => {
- return Домашняя
;
+ return (
+
+
+ Домашняя
+
+ );
};