mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36: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 { 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<ProfileSidebarMenuProps> = ({ onClose }) => {
|
|||
</VerticalMenu.Item>
|
||||
</VerticalMenu>
|
||||
|
||||
<div className={styles.toggles}>
|
||||
<ProfileToggles />
|
||||
</div>
|
||||
<Group className={styles.toggles}>
|
||||
<Zone>
|
||||
<ProfileToggles />
|
||||
</Zone>
|
||||
|
||||
<Zone>
|
||||
<ThemeSwitcher />
|
||||
</Zone>
|
||||
</Group>
|
||||
|
||||
<div className={styles.stats}>
|
||||
<ProfileStats />
|
||||
|
|
|
@ -7,11 +7,9 @@ import { SuperPowersToggle } from '~/containers/auth/SuperPowersToggle';
|
|||
interface ProfileTogglesProps {}
|
||||
|
||||
const ProfileToggles: FC<ProfileTogglesProps> = () => (
|
||||
<Zone>
|
||||
<Group>
|
||||
<SuperPowersToggle />
|
||||
</Group>
|
||||
</Zone>
|
||||
<Group>
|
||||
<SuperPowersToggle />
|
||||
</Group>
|
||||
);
|
||||
|
||||
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 '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 (
|
||||
<StoreContextProvider store={mobxStore}>
|
||||
<SWRConfigProvider>
|
||||
<UserContextProvider>
|
||||
<DragDetectorProvider>
|
||||
<PageCoverProvider>
|
||||
<SearchProvider>
|
||||
<AudioPlayerProvider>
|
||||
<MetadataProvider>
|
||||
<AuthProvider>
|
||||
<SidebarProvider>
|
||||
<Head>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=0"
|
||||
<ThemeProvider>
|
||||
<StoreContextProvider store={mobxStore}>
|
||||
<SWRConfigProvider>
|
||||
<UserContextProvider>
|
||||
<DragDetectorProvider>
|
||||
<PageCoverProvider>
|
||||
<SearchProvider>
|
||||
<AudioPlayerProvider>
|
||||
<MetadataProvider>
|
||||
<AuthProvider>
|
||||
<SidebarProvider>
|
||||
<Head>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=0"
|
||||
/>
|
||||
|
||||
{!!canonicalURL && <link rel="canonical" href={canonicalURL} />}
|
||||
</Head>
|
||||
{!!canonicalURL && (
|
||||
<link rel="canonical" href={canonicalURL} />
|
||||
)}
|
||||
</Head>
|
||||
|
||||
<MainLayout>
|
||||
<ToastProvider />
|
||||
<Modal />
|
||||
<Sprites />
|
||||
<Component {...pageProps} />
|
||||
</MainLayout>
|
||||
<BottomContainer />
|
||||
</SidebarProvider>
|
||||
</AuthProvider>
|
||||
</MetadataProvider>
|
||||
</AudioPlayerProvider>
|
||||
</SearchProvider>
|
||||
</PageCoverProvider>
|
||||
</DragDetectorProvider>
|
||||
</UserContextProvider>
|
||||
</SWRConfigProvider>
|
||||
</StoreContextProvider>
|
||||
<MainLayout>
|
||||
<ToastProvider />
|
||||
<Modal />
|
||||
<Sprites />
|
||||
<Component {...pageProps} />
|
||||
</MainLayout>
|
||||
<BottomContainer />
|
||||
</SidebarProvider>
|
||||
</AuthProvider>
|
||||
</MetadataProvider>
|
||||
</AudioPlayerProvider>
|
||||
</SearchProvider>
|
||||
</PageCoverProvider>
|
||||
</DragDetectorProvider>
|
||||
</UserContextProvider>
|
||||
</SWRConfigProvider>
|
||||
</StoreContextProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
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