1
0
Fork 0
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:
Fedor Katurov 2022-08-14 15:00:07 +07:00
parent f26f74c35f
commit a22e38f07c
13 changed files with 224 additions and 122 deletions

View file

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Before After
Before After

View 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)`,
},
};

View file

@ -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 };

View file

@ -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 />

View file

@ -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 };

View 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 };

View 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;
}

View file

@ -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')
);

View file

@ -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>
); );
} }
} }

View file

@ -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');
} }

View file

@ -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};

View 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 };