diff --git a/src/sprites/noise.png b/public/images/noise.png similarity index 100% rename from src/sprites/noise.png rename to public/images/noise.png diff --git a/src/sprites/noise_top.png b/public/images/noise_top.png similarity index 100% rename from src/sprites/noise_top.png rename to public/images/noise_top.png diff --git a/src/constants/themes/index.ts b/src/constants/themes/index.ts new file mode 100644 index 00000000..de1a915c --- /dev/null +++ b/src/constants/themes/index.ts @@ -0,0 +1,30 @@ +export enum Theme { + Default = 'Default', + Horizon = 'Horizon', +} + +interface ThemeColors { + colors: string[]; + background: string; + name: string; +} + +export const themeColors: Record = { + [Theme.Default]: { + name: 'Волт', + colors: [ + 'linear-gradient(170deg, #00ac35 -50%, #007962 150%)', + 'linear-gradient(165deg, #ff7549 -50%, #ff3344 150%)', + ], + background: `url('/images/noise_top.png') 0% 0% #23201f`, + }, + [Theme.Horizon]: { + name: 'Веспер', + colors: [ + 'linear-gradient(170deg, #f09483 -150%, #e95678 100%)', + 'linear-gradient(165deg, #fab795 -50%, #fab795 150%)', + 'linear-gradient(170deg, #25b0bc, #7693d6)', + ], + background: `url("/images/horizon_bg.svg") 50% 50% / cover rgb(28, 30, 38)`, + }, +}; diff --git a/src/containers/App.tsx b/src/containers/App.tsx deleted file mode 100644 index a7e657a2..00000000 --- a/src/containers/App.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { VFC } from 'react'; - -import { observer } from 'mobx-react-lite'; -import { BrowserRouter } from 'react-router-dom'; - -import { PageCoverProvider } from '~/components/containers/PageCoverProvider'; -import { Modal } from '~/containers/dialogs/Modal'; -import { BottomContainer } from '~/containers/main/BottomContainer'; -import { MainRouter } from '~/containers/main/MainRouter'; -import { DragDetectorProvider } from '~/hooks/dom/useDragDetector'; -import { useGlobalLoader } from '~/hooks/dom/useGlobalLoader'; -import { MainLayout } from '~/layouts/MainLayout'; -import { Sprites } from '~/sprites/Sprites'; -import { UserContextProvider } from '~/utils/context/UserContextProvider'; -import { AudioPlayerProvider } from '~/utils/providers/AudioPlayerProvider'; -import { AuthProvider } from '~/utils/providers/AuthProvider'; -import { MetadataProvider } from '~/utils/providers/MetadataProvider'; -import { SWRConfigProvider } from '~/utils/providers/SWRConfigProvider'; -import { SearchProvider } from '~/utils/providers/SearchProvider'; -import { ToastProvider } from '~/utils/providers/ToastProvider'; - -const App: VFC = observer(() => { - useGlobalLoader(); - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}); - -export { App }; diff --git a/src/containers/profile/ProfileSidebarMenu/index.tsx b/src/containers/profile/ProfileSidebarMenu/index.tsx index 148461b2..4cf8ae07 100644 --- a/src/containers/profile/ProfileSidebarMenu/index.tsx +++ b/src/containers/profile/ProfileSidebarMenu/index.tsx @@ -4,13 +4,12 @@ import classNames from 'classnames'; import { Filler } from '~/components/containers/Filler'; import { Group } from '~/components/containers/Group'; -import { Button } from '~/components/input/Button'; -import { Icon } from '~/components/input/Icon'; -import { MenuButton, MenuItemWithIcon } from '~/components/menu'; +import { Zone } from '~/components/containers/Zone'; import { VerticalMenu } from '~/components/menu/VerticalMenu'; import { useStackContext } from '~/components/sidebar/SidebarStack'; import { ProfileSidebarHead } from '~/containers/profile/ProfileSidebarHead'; import { ProfileStats } from '~/containers/profile/ProfileStats'; +import { ThemeSwitcher } from '~/containers/settings/ThemeSwitcher'; import { useAuth } from '~/hooks/auth/useAuth'; import markdown from '~/styles/common/markdown.module.scss'; @@ -50,9 +49,15 @@ const ProfileSidebarMenu: VFC = ({ onClose }) => { -
- -
+ + + + + + + + +
diff --git a/src/containers/profile/ProfileToggles/index.tsx b/src/containers/profile/ProfileToggles/index.tsx index 135d52ea..6a25bf96 100644 --- a/src/containers/profile/ProfileToggles/index.tsx +++ b/src/containers/profile/ProfileToggles/index.tsx @@ -7,11 +7,9 @@ import { SuperPowersToggle } from '~/containers/auth/SuperPowersToggle'; interface ProfileTogglesProps {} const ProfileToggles: FC = () => ( - - - - - + + + ); export { ProfileToggles }; diff --git a/src/containers/settings/ThemeSwitcher/index.tsx b/src/containers/settings/ThemeSwitcher/index.tsx new file mode 100644 index 00000000..25138e2e --- /dev/null +++ b/src/containers/settings/ThemeSwitcher/index.tsx @@ -0,0 +1,47 @@ +import React, { FC } from 'react'; + +import classNames from 'classnames'; + +import { Card } from '~/components/containers/Card'; +import { Group } from '~/components/containers/Group'; +import { Theme, themeColors } from '~/constants/themes'; +import { useTheme } from '~/utils/providers/ThemeProvider'; + +import styles from './styles.module.scss'; + +interface ThemeSwitcherProps {} + +const ThemeSwitcher: FC = () => { + const { theme, setTheme } = useTheme(); + + return ( + + {Object.entries(themeColors).map(([id, item]) => ( + setTheme(id as Theme)} + > + + + {item.colors.map((color) => ( +
+ ))} + +
{item.name}
+ + + ))} + + ); +}; + +export { ThemeSwitcher }; diff --git a/src/containers/settings/ThemeSwitcher/styles.module.scss b/src/containers/settings/ThemeSwitcher/styles.module.scss new file mode 100644 index 00000000..9df40937 --- /dev/null +++ b/src/containers/settings/ThemeSwitcher/styles.module.scss @@ -0,0 +1,30 @@ +@import 'src/styles/variables'; + +.button { + flex: 1; +} + +.card { + padding: $gap; + flex: 1; + cursor: pointer; + + &.active { + outline: 1px solid $color_primary; + } +} + +.title { + font: $font_12_semibold; + text-align: left; + text-transform: uppercase; + color: $gray_50; +} + +.sample { + @include outer_shadow; + + border-radius: 100%; + flex: 0 1 20px; + height: 20px; +} diff --git a/src/index.tsx b/src/index.tsx deleted file mode 100644 index 4cae79f3..00000000 --- a/src/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react'; - -import { render } from 'react-dom'; - -import { App } from '~/containers/App'; -import '~/styles/main.scss'; -import { getMOBXStore } from '~/store'; -import { StoreContextProvider } from '~/utils/context/StoreContextProvider'; - -const mobxStore = getMOBXStore(); - -render( - - - , - document.getElementById('app') -); diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index b2f6b5e4..bc9a9b64 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -20,9 +20,10 @@ import { SWRConfigProvider } from '~/utils/providers/SWRConfigProvider'; import { SearchProvider } from '~/utils/providers/SearchProvider'; import { SidebarProvider } from '~/utils/providers/SidebarProvider'; import { ToastProvider } from '~/utils/providers/ToastProvider'; - + import '~/styles/main.scss'; import 'tippy.js/dist/tippy.css'; +import { ThemeProvider } from '~/utils/providers/ThemeProvider'; const mobxStore = getMOBXStore(); @@ -30,45 +31,50 @@ export default class MyApp extends App { render() { const { Component, pageProps, router } = this.props; const canonicalURL = - !!CONFIG.publicHost && new URL(router.asPath, CONFIG.publicHost).toString(); + !!CONFIG.publicHost && + new URL(router.asPath, CONFIG.publicHost).toString(); return ( - - - - - - - - - - - - + + + + + + + + + + + + - {!!canonicalURL && } - + {!!canonicalURL && ( + + )} + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + ); } } diff --git a/src/styles/themes/_default.scss b/src/styles/themes/_default.scss index 4012cd42..ebe869d9 100644 --- a/src/styles/themes/_default.scss +++ b/src/styles/themes/_default.scss @@ -73,9 +73,8 @@ $_brown: #23201f; --gray_90: #{transparentize(white, 0.95)}; // page background - --page-background: url('../../../src/sprites/noise.png') 0% 0% #{$_brown}; - --page-background-top: 600px 600px url('../../../src/sprites/noise_top.png') - 0% 0%; + --page-background: url('/images/noise.png') 0% 0% #{$_brown}; + --page-background-top: 600px 600px url('/images/noise_top.png') 0% 0%; --boris-background: 50% 0 / cover no-repeat url('../../sprites/boris_bg.svg'); } diff --git a/src/styles/themes/_horizon.scss b/src/styles/themes/_horizon.scss index 061f7110..f968cd33 100644 --- a/src/styles/themes/_horizon.scss +++ b/src/styles/themes/_horizon.scss @@ -18,7 +18,7 @@ $_lemon: #fab795; $_ocean: #25b0bc; @mixin apply { - :root.theme_horizon { + :root.theme-horizon { // main definitions (move to --vars) --color_primary: #{$_accent}; --color_danger: #{$_red}; diff --git a/src/utils/providers/ThemeProvider.tsx b/src/utils/providers/ThemeProvider.tsx new file mode 100644 index 00000000..4064a824 --- /dev/null +++ b/src/utils/providers/ThemeProvider.tsx @@ -0,0 +1,59 @@ +import React, { + createContext, + FC, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; + +import { keys } from 'ramda'; + +import { Theme } from '~/constants/themes'; + +interface ProvidersProps {} + +const ThemeContext = createContext({ + theme: Theme.Default, + setTheme: (theme: Theme) => {}, +}); + +const themeClass: Record = { + [Theme.Default]: '', + [Theme.Horizon]: 'theme-horizon', +}; + +const ThemeProvider: FC = ({ children }) => { + const [theme, setTheme] = useState(Theme.Default); + const value = useMemo(() => ({ theme, setTheme }), [theme, setTheme]); + + useEffect(() => { + const stored = localStorage.getItem('vault__theme'); + if (!stored || !keys(themeClass).includes(stored as Theme)) { + return; + } + setTheme(stored as Theme); + }, []); + + useEffect(() => { + if (!themeClass[theme]) { + return; + } + + document.documentElement.classList.add(themeClass[theme]); + + try { + localStorage.setItem('vault__theme', theme); + } catch {} + + return () => document.documentElement.classList.remove(themeClass[theme]); + }, [theme]); + + return ( + {children} + ); +}; + +export const useTheme = () => useContext(ThemeContext); + +export { ThemeProvider };