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 ( +
+ + Домашняя +
+ ); };