mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
added theme switcher to profile
This commit is contained in:
parent
f26f74c35f
commit
a22e38f07c
13 changed files with 224 additions and 122 deletions
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 176 KiB |
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB |
30
src/constants/themes/index.ts
Normal file
30
src/constants/themes/index.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
export enum Theme {
|
||||||
|
Default = 'Default',
|
||||||
|
Horizon = 'Horizon',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ThemeColors {
|
||||||
|
colors: string[];
|
||||||
|
background: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const themeColors: Record<Theme, ThemeColors> = {
|
||||||
|
[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)`,
|
||||||
|
},
|
||||||
|
};
|
|
@ -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 (
|
|
||||||
<BrowserRouter>
|
|
||||||
<SWRConfigProvider>
|
|
||||||
<UserContextProvider>
|
|
||||||
<DragDetectorProvider>
|
|
||||||
<PageCoverProvider>
|
|
||||||
<SearchProvider>
|
|
||||||
<AudioPlayerProvider>
|
|
||||||
<MetadataProvider>
|
|
||||||
<AuthProvider>
|
|
||||||
<MainLayout>
|
|
||||||
<ToastProvider />
|
|
||||||
<Modal />
|
|
||||||
<Sprites />
|
|
||||||
|
|
||||||
<MainRouter />
|
|
||||||
</MainLayout>
|
|
||||||
<BottomContainer />
|
|
||||||
</AuthProvider>
|
|
||||||
</MetadataProvider>
|
|
||||||
</AudioPlayerProvider>
|
|
||||||
</SearchProvider>
|
|
||||||
</PageCoverProvider>
|
|
||||||
</DragDetectorProvider>
|
|
||||||
</UserContextProvider>
|
|
||||||
</SWRConfigProvider>
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export { App };
|
|
|
@ -4,13 +4,12 @@ import classNames from 'classnames';
|
||||||
|
|
||||||
import { Filler } from '~/components/containers/Filler';
|
import { Filler } from '~/components/containers/Filler';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import { Button } from '~/components/input/Button';
|
import { Zone } from '~/components/containers/Zone';
|
||||||
import { Icon } from '~/components/input/Icon';
|
|
||||||
import { MenuButton, MenuItemWithIcon } from '~/components/menu';
|
|
||||||
import { VerticalMenu } from '~/components/menu/VerticalMenu';
|
import { VerticalMenu } from '~/components/menu/VerticalMenu';
|
||||||
import { useStackContext } from '~/components/sidebar/SidebarStack';
|
import { useStackContext } from '~/components/sidebar/SidebarStack';
|
||||||
import { ProfileSidebarHead } from '~/containers/profile/ProfileSidebarHead';
|
import { ProfileSidebarHead } from '~/containers/profile/ProfileSidebarHead';
|
||||||
import { ProfileStats } from '~/containers/profile/ProfileStats';
|
import { ProfileStats } from '~/containers/profile/ProfileStats';
|
||||||
|
import { ThemeSwitcher } from '~/containers/settings/ThemeSwitcher';
|
||||||
import { useAuth } from '~/hooks/auth/useAuth';
|
import { useAuth } from '~/hooks/auth/useAuth';
|
||||||
import markdown from '~/styles/common/markdown.module.scss';
|
import markdown from '~/styles/common/markdown.module.scss';
|
||||||
|
|
||||||
|
@ -50,9 +49,15 @@ const ProfileSidebarMenu: VFC<ProfileSidebarMenuProps> = ({ onClose }) => {
|
||||||
</VerticalMenu.Item>
|
</VerticalMenu.Item>
|
||||||
</VerticalMenu>
|
</VerticalMenu>
|
||||||
|
|
||||||
<div className={styles.toggles}>
|
<Group className={styles.toggles}>
|
||||||
|
<Zone>
|
||||||
<ProfileToggles />
|
<ProfileToggles />
|
||||||
</div>
|
</Zone>
|
||||||
|
|
||||||
|
<Zone>
|
||||||
|
<ThemeSwitcher />
|
||||||
|
</Zone>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<div className={styles.stats}>
|
<div className={styles.stats}>
|
||||||
<ProfileStats />
|
<ProfileStats />
|
||||||
|
|
|
@ -7,11 +7,9 @@ import { SuperPowersToggle } from '~/containers/auth/SuperPowersToggle';
|
||||||
interface ProfileTogglesProps {}
|
interface ProfileTogglesProps {}
|
||||||
|
|
||||||
const ProfileToggles: FC<ProfileTogglesProps> = () => (
|
const ProfileToggles: FC<ProfileTogglesProps> = () => (
|
||||||
<Zone>
|
|
||||||
<Group>
|
<Group>
|
||||||
<SuperPowersToggle />
|
<SuperPowersToggle />
|
||||||
</Group>
|
</Group>
|
||||||
</Zone>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export { ProfileToggles };
|
export { ProfileToggles };
|
||||||
|
|
47
src/containers/settings/ThemeSwitcher/index.tsx
Normal file
47
src/containers/settings/ThemeSwitcher/index.tsx
Normal file
|
@ -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<ThemeSwitcherProps> = () => {
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group horizontal>
|
||||||
|
{Object.entries(themeColors).map(([id, item]) => (
|
||||||
|
<Card
|
||||||
|
key={id}
|
||||||
|
className={classNames(styles.card, {
|
||||||
|
[styles.active]: theme === id,
|
||||||
|
})}
|
||||||
|
style={{ background: item.background }}
|
||||||
|
role="button"
|
||||||
|
onClick={() => setTheme(id as Theme)}
|
||||||
|
>
|
||||||
|
<Group>
|
||||||
|
<Group horizontal>
|
||||||
|
{item.colors.map((color) => (
|
||||||
|
<div
|
||||||
|
key={color}
|
||||||
|
className={styles.sample}
|
||||||
|
style={{ background: color }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
<div className={styles.title}>{item.name}</div>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ThemeSwitcher };
|
30
src/containers/settings/ThemeSwitcher/styles.module.scss
Normal file
30
src/containers/settings/ThemeSwitcher/styles.module.scss
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -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(
|
|
||||||
<StoreContextProvider store={mobxStore}>
|
|
||||||
<App />
|
|
||||||
</StoreContextProvider>,
|
|
||||||
document.getElementById('app')
|
|
||||||
);
|
|
|
@ -23,6 +23,7 @@ import { ToastProvider } from '~/utils/providers/ToastProvider';
|
||||||
|
|
||||||
import '~/styles/main.scss';
|
import '~/styles/main.scss';
|
||||||
import 'tippy.js/dist/tippy.css';
|
import 'tippy.js/dist/tippy.css';
|
||||||
|
import { ThemeProvider } from '~/utils/providers/ThemeProvider';
|
||||||
|
|
||||||
const mobxStore = getMOBXStore();
|
const mobxStore = getMOBXStore();
|
||||||
|
|
||||||
|
@ -30,9 +31,11 @@ export default class MyApp extends App {
|
||||||
render() {
|
render() {
|
||||||
const { Component, pageProps, router } = this.props;
|
const { Component, pageProps, router } = this.props;
|
||||||
const canonicalURL =
|
const canonicalURL =
|
||||||
!!CONFIG.publicHost && new URL(router.asPath, CONFIG.publicHost).toString();
|
!!CONFIG.publicHost &&
|
||||||
|
new URL(router.asPath, CONFIG.publicHost).toString();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
<StoreContextProvider store={mobxStore}>
|
<StoreContextProvider store={mobxStore}>
|
||||||
<SWRConfigProvider>
|
<SWRConfigProvider>
|
||||||
<UserContextProvider>
|
<UserContextProvider>
|
||||||
|
@ -49,7 +52,9 @@ export default class MyApp extends App {
|
||||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=0"
|
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!!canonicalURL && <link rel="canonical" href={canonicalURL} />}
|
{!!canonicalURL && (
|
||||||
|
<link rel="canonical" href={canonicalURL} />
|
||||||
|
)}
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
|
@ -69,6 +74,7 @@ export default class MyApp extends App {
|
||||||
</UserContextProvider>
|
</UserContextProvider>
|
||||||
</SWRConfigProvider>
|
</SWRConfigProvider>
|
||||||
</StoreContextProvider>
|
</StoreContextProvider>
|
||||||
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,9 +73,8 @@ $_brown: #23201f;
|
||||||
--gray_90: #{transparentize(white, 0.95)};
|
--gray_90: #{transparentize(white, 0.95)};
|
||||||
|
|
||||||
// page background
|
// page background
|
||||||
--page-background: url('../../../src/sprites/noise.png') 0% 0% #{$_brown};
|
--page-background: url('/images/noise.png') 0% 0% #{$_brown};
|
||||||
--page-background-top: 600px 600px url('../../../src/sprites/noise_top.png')
|
--page-background-top: 600px 600px url('/images/noise_top.png') 0% 0%;
|
||||||
0% 0%;
|
|
||||||
--boris-background: 50% 0 / cover no-repeat
|
--boris-background: 50% 0 / cover no-repeat
|
||||||
url('../../sprites/boris_bg.svg');
|
url('../../sprites/boris_bg.svg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ $_lemon: #fab795;
|
||||||
$_ocean: #25b0bc;
|
$_ocean: #25b0bc;
|
||||||
|
|
||||||
@mixin apply {
|
@mixin apply {
|
||||||
:root.theme_horizon {
|
:root.theme-horizon {
|
||||||
// main definitions (move to --vars)
|
// main definitions (move to --vars)
|
||||||
--color_primary: #{$_accent};
|
--color_primary: #{$_accent};
|
||||||
--color_danger: #{$_red};
|
--color_danger: #{$_red};
|
||||||
|
|
59
src/utils/providers/ThemeProvider.tsx
Normal file
59
src/utils/providers/ThemeProvider.tsx
Normal file
|
@ -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, string> = {
|
||||||
|
[Theme.Default]: '',
|
||||||
|
[Theme.Horizon]: 'theme-horizon',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThemeProvider: FC<ProvidersProps> = ({ 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 (
|
||||||
|
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTheme = () => useContext(ThemeContext);
|
||||||
|
|
||||||
|
export { ThemeProvider };
|
Loading…
Add table
Add a link
Reference in a new issue