diff --git a/package-lock.json b/package-lock.json index fcb1eb8..3b83fa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,10 @@ "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", "classnames": "^2.5.1", + "prop-types": "^15.8.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "styled-components": "^6.1.19" }, "devDependencies": { "@types/react": "^18.2.55", @@ -313,6 +315,27 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -1600,6 +1623,12 @@ "@types/react": "*" } }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT" + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -1948,6 +1977,15 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001684", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz", @@ -2057,11 +2095,30 @@ "node": ">= 8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/data-view-buffer": { @@ -3742,7 +3799,6 @@ "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, "funding": [ { "type": "github", @@ -3996,7 +4052,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -4027,7 +4082,6 @@ "version": "8.4.49", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", - "dev": true, "funding": [ { "type": "opencollective", @@ -4052,6 +4106,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4429,6 +4489,12 @@ "node": ">= 0.4" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4475,7 +4541,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -4597,6 +4662,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-components": { + "version": "6.1.19", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz", + "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.49", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4644,6 +4743,12 @@ "node": ">=8.0" } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 222c1fd..14f861e 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,10 @@ "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", "classnames": "^2.5.1", + "prop-types": "^15.8.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "styled-components": "^6.1.19" }, "devDependencies": { "@types/react": "^18.2.55", diff --git a/src/App.jsx b/src/App.jsx index cba7ad0..a23d815 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,14 +1,20 @@ -import React from 'react' +import React from "react"; import { library } from "@fortawesome/fontawesome-svg-core"; import { fas } from "@fortawesome/free-solid-svg-icons"; import Sidebar from "./components/Sidebar"; +import { ThemeProvider } from "./theme/ThemeContext"; +import ThemeToggle from "./components/ThemeToggle"; library.add(fas); -export default class App extends React.Component{ - render () { - return ( - - ) +export default class App extends React.Component { + render() { + return ( + + + {/* В зависимости от prop color */} + + + ); } } diff --git a/src/components/Sidebar/CollapseButton.jsx b/src/components/Sidebar/CollapseButton.jsx new file mode 100644 index 0000000..0d389e4 --- /dev/null +++ b/src/components/Sidebar/CollapseButton.jsx @@ -0,0 +1,46 @@ +import styled from "styled-components"; +import PropTypes from "prop-types"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +const CollapseButtonStyled = styled.button` + position: absolute; + right: ${({ $collapsed }) => ($collapsed ? "-48px" : "-18px")}; + top: 16px; + width: 32px; + height: 32px; + border-radius: 50%; + border: none; + background: ${({ theme }) => theme.buttonBackground}; + color: ${({ theme }) => theme.textDefault}; + cursor: pointer; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + transition: right 0.3s ease, background 0.3s, color 0.3s; + z-index: 2; + &:hover { + background: ${({ theme }) => theme.buttonBackgroundActive}; + color: ${({ theme }) => theme.textHover}; + } +`; + +const CollapseButton = ({ $collapsed, theme, onClick }) => ( + + {$collapsed ? ( + + ) : ( + + )} + +); + +CollapseButton.propTypes = { + $collapsed: PropTypes.bool.isRequired, + theme: PropTypes.object.isRequired, + onClick: PropTypes.func.isRequired, +}; + +export default CollapseButton; \ No newline at end of file diff --git a/src/components/Sidebar/LogoSection.jsx b/src/components/Sidebar/LogoSection.jsx new file mode 100644 index 0000000..2a15d77 --- /dev/null +++ b/src/components/Sidebar/LogoSection.jsx @@ -0,0 +1,34 @@ +import styled from "styled-components"; +import PropTypes from "prop-types"; +import logo from "../../assets/logo.png"; + +const LogoSectionStyled = styled.div` + display: flex; + align-items: center; + padding-top: 24px; + img { + width: 32px; + margin-right: 12px; + } + span { + color: ${({ theme }) => theme.logoText}; + font-weight: bold; + font-size: 1.2rem; + opacity: ${({ $collapsed }) => ($collapsed ? "0" : "1")}; + transition: opacity 0.3s; + } +`; + +const LogoSection = ({ theme, $collapsed }) => ( + + TensorFlow logo + TensorFlow + +); + +LogoSection.propTypes = { + theme: PropTypes.object.isRequired, + $collapsed: PropTypes.bool.isRequired, +}; + +export default LogoSection; diff --git a/src/components/Sidebar/NavItem.jsx b/src/components/Sidebar/NavItem.jsx new file mode 100644 index 0000000..5d991e0 --- /dev/null +++ b/src/components/Sidebar/NavItem.jsx @@ -0,0 +1,74 @@ +import styled from "styled-components"; +import PropTypes from "prop-types"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +const NavItem = ({ + title, + icon, + path, + onClick, + collapsed, + isActive = false, + theme, +}) => ( + onClick(path)} + theme={theme} + $isActive={isActive} + $collapsed={collapsed} + > + + {title} + +); + +NavItem.propTypes = { + title: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + collapsed: PropTypes.bool, + isActive: PropTypes.bool, + theme: PropTypes.object, +}; +const RouteItem = styled.div` + min-width: 18px; + display: flex; + align-items: center; + justify-content: start; + overflow: hidden; + padding: 8px 12px; + border-radius: 14px; + background: ${({ $isActive, theme }) => + $isActive ? theme.sidebarBackgroundActive : "transparent"}; + cursor: pointer; + color: ${({ $isActive, theme }) => + $isActive ? theme.textActive : theme.textDefault}; + transition: background 0.3s, color 0.3s; + font-weight: bold; + &:hover { + background: ${({ theme }) => theme.sidebarBackgroundHover}; + color: ${({ theme }) => theme.textHover}; + + svg { + color: ${({ theme }) => theme.textHover}; + } + span { + color: ${({ theme }) => theme.textHover}; + } + } + svg { + margin-right: 20px; + color: ${({ $isActive, theme }) => + $isActive ? theme.textActive : theme.textDefault}; + transition: color 0.3s; + } + span { + opacity: ${({ $collapsed }) => ($collapsed ? 0 : 1)}; + transition: opacity 0.2s, color 0.3s; + color: ${({ $isActive, theme }) => + $isActive ? theme.textActive : theme.textDefault}; + } +`; + +export default NavItem; diff --git a/src/components/Sidebar/Sidebar.jsx b/src/components/Sidebar/Sidebar.jsx index 974966e..e538926 100644 --- a/src/components/Sidebar/Sidebar.jsx +++ b/src/components/Sidebar/Sidebar.jsx @@ -1,81 +1,106 @@ -import { useState } from 'react'; -import classnames from 'classnames'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import logo from '../../assets/logo.png'; -import PropTypes from 'prop-types'; +import PropTypes from "prop-types"; +import { useMemo, useState } from "react"; +import styled from "styled-components"; +import { BOTTOM_ROUTES, TOP_ROUTES } from "../../constants/sidebarRoutes"; +import { useTheme } from "../../theme/ThemeContext"; +import { darkTheme, lightTheme } from "../../theme/theme"; +import CollapseButton from "./CollapseButton"; +import LogoSection from "./LogoSection"; +import NavItem from "./NavItem"; -const routes = [ - { title: 'Home', icon: 'fas-solid fa-house', path: '/' }, - { title: 'Sales', icon: 'chart-line', path: '/sales' }, - { title: 'Costs', icon: 'chart-column', path: '/costs' }, - { title: 'Payments', icon: 'wallet', path: '/payments' }, - { title: 'Finances', icon: 'chart-pie', path: '/finances' }, - { title: 'Messages', icon: 'envelope', path: '/messages' }, -]; +const Sidebar = ({ color }) => { + const { theme } = useTheme(); + const currentPath = window.location.pathname; // location.pathname from "react-router-dom"; + const [isCollapsed, setIsCollapsed] = useState(true); + const localTheme = useMemo(() => { + if (color === "light") return lightTheme; + if (color === "dark") return darkTheme; + return theme; + }, [color, theme]); -const bottomRoutes = [ - { title: 'Settings', icon: 'sliders', path: '/settings' }, - { title: 'Support', icon: 'phone-volume', path: '/support' }, -]; + const goToRoute = (path) => { + console.log(`going to "${path}"`); + }; -const Sidebar = (props) => { - const { color } = props; - const [isOpened, setIsOpened] = useState(false); - const containerClassnames = classnames('sidebar', { opened: isOpened }); - - const goToRoute = (path) => { - console.log(`going to "${path}"`); - }; - - const toggleSidebar = () => { - setIsOpened(v => !v); - }; - - return ( -
-
- TensorFlow logo - TensorFlow -
- -
-
-
- { - routes.map(route => ( -
{ - goToRoute(route.path); - }} - > - - { route.title } -
- )) - } -
-
- { - bottomRoutes.map(route => ( -
{ - goToRoute(route.path); - }} - > - - { route.title } -
- )) - } -
+ return ( + + +
+ setIsCollapsed((prev) => !prev)} + /> + + + {TOP_ROUTES?.map((route) => ( + goToRoute(route.path)} + theme={localTheme} + isActive={currentPath === route.path} + collapsed={isCollapsed} + /> + ))} +
- ); + + + + {BOTTOM_ROUTES.map((route) => ( + goToRoute(route.path)} + theme={localTheme} + collapsed={isCollapsed} + /> + ))} + + +
+
+ ); }; Sidebar.propTypes = { - color: PropTypes.string, + color: PropTypes.string, }; +const SidebarContainer = styled.div` + width: ${({ $collapsed }) => ($collapsed ? "38px" : "260px")}; + background: ${({ theme }) => theme.sidebarBackground}; + color: ${({ theme }) => theme.textDefault}; + padding: 0px 24px; + display: flex; + flex-direction: column; + height: 100vh; + transition: width 0.3s ease, background 0.3s, color 0.3s; + position: relative; +`; + +const SidebarContent = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; +`; + +const RouteSection = styled.div` + margin-top: 32px; + display: flex; + flex-direction: column; + gap: 16px; +`; +const BottomSection = styled.div` + padding-bottom: 32px; + display: flex; + flex-direction: column; + gap: 16px; +`; export default Sidebar; diff --git a/src/components/ThemeToggle.jsx b/src/components/ThemeToggle.jsx new file mode 100644 index 0000000..8a4f74a --- /dev/null +++ b/src/components/ThemeToggle.jsx @@ -0,0 +1,36 @@ +import styled from "styled-components"; +import { useTheme } from "../theme/ThemeContext"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faMoon, faSun } from "@fortawesome/free-solid-svg-icons"; + +const ToggleButton = styled.button` + position: fixed; + top: 20px; + right: 20px; + top: 20px; + background: ${({ theme }) => theme.buttonBackground}; + color: ${({ theme }) => theme.textDefault}; + border: none; + border-radius: 6px; + padding: 10px 20px; + font-size: 1rem; + cursor: pointer; + margin: 0 auto; + display: block; + transition: background 0.3s, color 0.3s; + &:hover { + background: ${({ theme }) => theme.buttonBackgroundActive}; + color: ${({ theme }) => theme.textHover}; + } +`; + +const ThemeToggle = () => { + const { themeName, toggleTheme, theme } = useTheme(); + return ( + + + + ); +}; + +export default ThemeToggle; diff --git a/src/constants/sidebarRoutes.js b/src/constants/sidebarRoutes.js new file mode 100644 index 0000000..caaadf1 --- /dev/null +++ b/src/constants/sidebarRoutes.js @@ -0,0 +1,13 @@ +export const TOP_ROUTES = [ + { title: "Home", icon: "fas-solid fa-house", path: "/" }, + { title: "Sales", icon: "chart-line", path: "/sales" }, + { title: "Costs", icon: "chart-column", path: "/costs" }, + { title: "Payments", icon: "wallet", path: "/payments" }, + { title: "Finances", icon: "chart-pie", path: "/finances" }, + { title: "Messages", icon: "envelope", path: "/messages" }, +]; + +export const BOTTOM_ROUTES = [ + { title: "Settings", icon: "sliders", path: "/settings" }, + { title: "Support", icon: "phone-volume", path: "/support" }, +]; diff --git a/src/index.scss b/src/index.scss index 01632cd..e5c5f40 100644 --- a/src/index.scss +++ b/src/index.scss @@ -1,25 +1,25 @@ :root { - // dark - --color-sidebar-background-dark-default: #202127; - --color-sidebar-background-dark-hover: #2D2E34; - --color-sidebar-background-dark-active: #393A3F; - --color-text-dark-default: #f0f2ff; - --color-text-dark-hover: #f0f2ff; - --color-text-dark-active: #f0f2ff; - --color-text-logo-dark-default: #3B82F6; - --color-button-background-dark-default: #202127; - --color-button-background-dark-active: #4B5966; + // dark + --color-sidebar-background-dark-default: #202127; + --color-sidebar-background-dark-hover: #2d2e34; + --color-sidebar-background-dark-active: #393a3f; + --color-text-dark-default: #f0f2ff; + --color-text-dark-hover: #f0f2ff; + --color-text-dark-active: #f0f2ff; + --color-text-logo-dark-default: #3b82f6; + --color-button-background-dark-default: #202127; + --color-button-background-dark-active: #4b5966; - // light - --color-sidebar-background-light-default: #fff; - --color-sidebar-background-light-hover: #f0f2ff; - --color-sidebar-background-light-active: #f0f2ff; - --color-text-light-default: #97a5b9; - --color-text-light-hover: #091b31; - --color-text-light-active: #0000b5; - --color-text-logo-light-default: #0000b5; - --color-button-background-light-default: #fff; - --color-button-background-light-active: #e2e8f0; + // light + --color-sidebar-background-light-default: #fff; + --color-sidebar-background-light-hover: #f0f2ff; + --color-sidebar-background-light-active: #f0f2ff; + --color-text-light-default: #97a5b9; + --color-text-light-hover: #091b31; + --color-text-light-active: #0000b5; + --color-text-logo-light-default: #0000b5; + --color-button-background-light-default: #fff; + --color-button-background-light-active: #e2e8f0; } html { @@ -29,3 +29,7 @@ html { background-color: #e2e8f0; color: rgba(255, 255, 255, 0.87); } + +body { + margin: 0px; +} diff --git a/src/theme/ThemeContext.jsx b/src/theme/ThemeContext.jsx new file mode 100644 index 0000000..726b826 --- /dev/null +++ b/src/theme/ThemeContext.jsx @@ -0,0 +1,39 @@ +import { createContext, useContext, useState, useMemo } from "react"; +import { lightTheme, darkTheme } from "./theme"; +import PropTypes from "prop-types"; +import { getLocalStorage, setLocalStorage } from "../utils/localStorage"; + +const ThemeContext = createContext(); + +export const ThemeProvider = ({ children }) => { + const getDefaultTheme = () => { + const stored = getLocalStorage("themeName"); + if (stored) return stored; + if ( + window.matchMedia && + window.matchMedia("(prefers-color-scheme: dark)").matches + ) { + return "dark"; + } + return "light"; + }; + const [themeName, setThemeName] = useState(getDefaultTheme); + const theme = useMemo( + () => (themeName === "light" ? lightTheme : darkTheme), + [themeName] + ); + const toggleTheme = () => { + const otherTheme = themeName === "light" ? "dark" : "light"; + setThemeName(otherTheme); + setLocalStorage("themeName", otherTheme); + }; + return ( + + {children} + + ); +}; +ThemeProvider.propTypes = { + children: PropTypes.node.isRequired, +}; +export const useTheme = () => useContext(ThemeContext); diff --git a/src/theme/theme.js b/src/theme/theme.js new file mode 100644 index 0000000..072b64c --- /dev/null +++ b/src/theme/theme.js @@ -0,0 +1,23 @@ +export const lightTheme = { + sidebarBackground: "#fff", + sidebarBackgroundHover: "#f0f2ff", + sidebarBackgroundActive: "#f0f2ff", + textDefault: "#97a5b9", + textHover: "#091b31", + textActive: "#0000b5", + logoText: "#0000b5", + buttonBackground: "#fff", + buttonBackgroundActive: "#e2e8f0", +}; + +export const darkTheme = { + sidebarBackground: "#202127", + sidebarBackgroundHover: "#2D2E34", + sidebarBackgroundActive: "#393A3F", + textDefault: "#f0f2ff", + textHover: "#f0f2ff", + textActive: "#f0f2ff", + logoText: "#3B82F6", + buttonBackground: "#202127", + buttonBackgroundActive: "#4B5966", +}; diff --git a/src/utils/localStorage.js b/src/utils/localStorage.js new file mode 100644 index 0000000..a747a1d --- /dev/null +++ b/src/utils/localStorage.js @@ -0,0 +1,18 @@ +export const getLocalStorage = (key, defaultValue) => { + if (typeof window === "undefined") return defaultValue; + try { + const stored = window.localStorage.getItem(key); + return stored !== null ? JSON.parse(stored) : defaultValue; + } catch { + return defaultValue; + } +}; + +export const setLocalStorage = (key, value) => { + if (typeof window === "undefined") return; + try { + window.localStorage.setItem(key, JSON.stringify(value)); + } catch (error) { + console.error("Failed to save to localStorage:", error); + } +};