mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
#23 added superpowers switch
This commit is contained in:
parent
e38090c755
commit
756840f173
16 changed files with 178 additions and 25 deletions
27
src/components/boris/BorisSuperpowers/index.tsx
Normal file
27
src/components/boris/BorisSuperpowers/index.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { Toggle } from '~/components/input/Toggle';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
active?: boolean;
|
||||||
|
onChange?: (val: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BorisSuperpowers: FC<IProps> = ({ active, onChange }) => (
|
||||||
|
<div className={styles.wrap}>
|
||||||
|
<div className={styles.toggle}>
|
||||||
|
<Toggle value={active} handler={onChange} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.left}>
|
||||||
|
<div className={styles.title}>Суперспособности</div>
|
||||||
|
{active ? (
|
||||||
|
<div className={styles.subtitle}>Включи, чтобы видеть будущее</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.subtitle}>Ты видишь всё, что скрыто</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { BorisSuperpowers };
|
19
src/components/boris/BorisSuperpowers/styles.module.scss
Normal file
19
src/components/boris/BorisSuperpowers/styles.module.scss
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
@import "~/styles/variables";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
column-gap: $gap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font: $font_14_semibold;
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font: $font_12_regular;
|
||||||
|
color: transparentize(white, 0.5);
|
||||||
|
}
|
16
src/components/boris/Superpower/index.tsx
Normal file
16
src/components/boris/Superpower/index.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import React, { FC, memo } from 'react';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import { selectAuthIsTester, selectUser } from '~/redux/auth/selectors';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const Superpower: FC<IProps> = memo(({ children }) => {
|
||||||
|
const user = useShallowSelect(selectUser);
|
||||||
|
const is_tester = useShallowSelect(selectAuthIsTester);
|
||||||
|
|
||||||
|
if (!user || !is_tester) return null;
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
});
|
||||||
|
|
||||||
|
export { Superpower };
|
|
@ -2,6 +2,7 @@ import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||||
import { IEditorComponentProps } from '~/redux/node/types';
|
import { IEditorComponentProps } from '~/redux/node/types';
|
||||||
import { usePopper } from 'react-popper';
|
import { usePopper } from 'react-popper';
|
||||||
import { Button } from '~/components/input/Button';
|
import { Button } from '~/components/input/Button';
|
||||||
|
import { Superpower } from '~/components/boris/Superpower';
|
||||||
|
|
||||||
interface IProps extends IEditorComponentProps {}
|
interface IProps extends IEditorComponentProps {}
|
||||||
|
|
||||||
|
@ -11,24 +12,22 @@ const EditorPublicSwitch: FC<IProps> = ({ data, setData }) => {
|
||||||
setData,
|
setData,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (process.env.REACT_APP_LAB_ENABLED !== '1') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Superpower>
|
||||||
color={data.is_promoted ? 'primary' : 'secondary'}
|
<Button
|
||||||
type="button"
|
color={data.is_promoted ? 'primary' : 'secondary'}
|
||||||
iconLeft={data.is_promoted ? 'waves' : 'lab'}
|
type="button"
|
||||||
size="giant"
|
iconLeft={data.is_promoted ? 'waves' : 'lab'}
|
||||||
label={
|
size="giant"
|
||||||
data.is_promoted
|
label={
|
||||||
? 'Доступно всем на главной странице'
|
data.is_promoted
|
||||||
: 'Видно только сотрудникам в лаборатории'
|
? 'Доступно всем на главной странице'
|
||||||
}
|
: 'Видно только сотрудникам в лаборатории'
|
||||||
onClick={onChange}
|
}
|
||||||
round
|
onClick={onChange}
|
||||||
/>
|
round
|
||||||
|
/>
|
||||||
|
</Superpower>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
28
src/components/input/Toggle/index.tsx
Normal file
28
src/components/input/Toggle/index.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
value?: boolean;
|
||||||
|
handler?: (val: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Toggle: FC<IProps> = ({ value, handler }) => {
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
if (!handler) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(!value);
|
||||||
|
}, [value, handler]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classNames(styles.toggle, { [styles.active]: value })}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Toggle };
|
35
src/components/input/Toggle/styles.module.scss
Normal file
35
src/components/input/Toggle/styles.module.scss
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
height: 24px;
|
||||||
|
width: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: transparentize(white, 0.9);
|
||||||
|
display: flex;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: ' ';
|
||||||
|
position: absolute;
|
||||||
|
left: 3px;
|
||||||
|
top: 3px;
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
border-radius: 11px;
|
||||||
|
background-color: darken(white, 50%);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
transition: transform 0.25s, color 0.25s, background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $wisegreen;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
transform: translate(24px, 0);
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ import * as MODAL_ACTIONS from '~/redux/modal/actions';
|
||||||
import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
||||||
import { IState } from '~/redux/store';
|
import { IState } from '~/redux/store';
|
||||||
import isBefore from 'date-fns/isBefore';
|
import isBefore from 'date-fns/isBefore';
|
||||||
|
import { Superpower } from '~/components/boris/Superpower';
|
||||||
|
|
||||||
const mapStateToProps = (state: IState) => ({
|
const mapStateToProps = (state: IState) => ({
|
||||||
user: pick(['username', 'is_user', 'photo', 'last_seen_boris'])(selectUser(state)),
|
user: pick(['username', 'is_user', 'photo', 'last_seen_boris'])(selectUser(state)),
|
||||||
|
@ -89,14 +90,14 @@ const HeaderUnconnected: FC<IProps> = memo(
|
||||||
ФЛОУ
|
ФЛОУ
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{is_user && process.env.REACT_APP_LAB_ENABLED === '1' && (
|
<Superpower>
|
||||||
<Link
|
<Link
|
||||||
className={classNames(styles.item, { [styles.is_active]: pathname === URLS.BASE })}
|
className={classNames(styles.item, { [styles.is_active]: pathname === URLS.BASE })}
|
||||||
to={URLS.LAB}
|
to={URLS.LAB}
|
||||||
>
|
>
|
||||||
ЛАБ
|
ЛАБ
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
</Superpower>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
className={classNames(styles.item, {
|
className={classNames(styles.item, {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { FC, useEffect } from 'react';
|
import React, { FC, useCallback, useEffect } from 'react';
|
||||||
import { selectNode, selectNodeComments } from '~/redux/node/selectors';
|
import { selectNode, selectNodeComments } from '~/redux/node/selectors';
|
||||||
import { selectUser } from '~/redux/auth/selectors';
|
import { selectAuthIsTester, selectUser } from '~/redux/auth/selectors';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { NodeComments } from '~/components/node/NodeComments';
|
import { NodeComments } from '~/components/node/NodeComments';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
@ -15,11 +15,12 @@ import { Footer } from '~/components/main/Footer';
|
||||||
import { BorisStats } from '~/components/boris/BorisStats';
|
import { BorisStats } from '~/components/boris/BorisStats';
|
||||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
import { selectBorisStats } from '~/redux/boris/selectors';
|
import { selectBorisStats } from '~/redux/boris/selectors';
|
||||||
import { authSetUser } from '~/redux/auth/actions';
|
import { authSetState, authSetUser } from '~/redux/auth/actions';
|
||||||
import { nodeLoadNode } from '~/redux/node/actions';
|
import { nodeLoadNode } from '~/redux/node/actions';
|
||||||
import { borisLoadStats } from '~/redux/boris/actions';
|
import { borisLoadStats } from '~/redux/boris/actions';
|
||||||
import { Container } from '~/containers/main/Container';
|
import { Container } from '~/containers/main/Container';
|
||||||
import StickyBox from 'react-sticky-box/dist/esnext';
|
import StickyBox from 'react-sticky-box/dist/esnext';
|
||||||
|
import { BorisSuperpowers } from '~/components/boris/BorisSuperpowers';
|
||||||
|
|
||||||
type IProps = {};
|
type IProps = {};
|
||||||
|
|
||||||
|
@ -30,6 +31,7 @@ const BorisLayout: FC<IProps> = () => {
|
||||||
const user = useShallowSelect(selectUser);
|
const user = useShallowSelect(selectUser);
|
||||||
const stats = useShallowSelect(selectBorisStats);
|
const stats = useShallowSelect(selectBorisStats);
|
||||||
const comments = useShallowSelect(selectNodeComments);
|
const comments = useShallowSelect(selectNodeComments);
|
||||||
|
const is_tester = useShallowSelect(selectAuthIsTester);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const last_comment = comments[0];
|
const last_comment = comments[0];
|
||||||
|
@ -55,6 +57,13 @@ const BorisLayout: FC<IProps> = () => {
|
||||||
dispatch(borisLoadStats());
|
dispatch(borisLoadStats());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const setBetaTester = useCallback(
|
||||||
|
(is_tester: boolean) => {
|
||||||
|
dispatch(authSetState({ is_tester }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
|
@ -102,6 +111,10 @@ const BorisLayout: FC<IProps> = () => {
|
||||||
<p className="grey">// Такова жизнь.</p>
|
<p className="grey">// Такова жизнь.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{user.is_user && <BorisSuperpowers active={is_tester} onChange={setBetaTester} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={styles.stats__wrap}>
|
<div className={styles.stats__wrap}>
|
||||||
<BorisStats stats={stats} />
|
<BorisStats stats={stats} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,6 +20,11 @@ export const authSetToken = (token: IAuthState['token']) => ({
|
||||||
token,
|
token,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const authSetState = (payload: Partial<IAuthState>) => ({
|
||||||
|
type: AUTH_USER_ACTIONS.SET_STATE,
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
|
||||||
export const gotAuthPostMessage = ({ token }: { token: string }) => ({
|
export const gotAuthPostMessage = ({ token }: { token: string }) => ({
|
||||||
type: AUTH_USER_ACTIONS.GOT_AUTH_POST_MESSAGE,
|
type: AUTH_USER_ACTIONS.GOT_AUTH_POST_MESSAGE,
|
||||||
token,
|
token,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { api, cleanResult, errorMiddleware, resultMiddleware } from '~/utils/api';
|
import { api, cleanResult } from '~/utils/api';
|
||||||
import { API } from '~/constants/api';
|
import { API } from '~/constants/api';
|
||||||
import { IResultWithStatus } from '~/redux/types';
|
|
||||||
import {
|
import {
|
||||||
ApiAttachSocialRequest,
|
ApiAttachSocialRequest,
|
||||||
ApiAttachSocialResult,
|
ApiAttachSocialResult,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { IToken, IUser } from '~/redux/auth/types';
|
||||||
export const AUTH_USER_ACTIONS = {
|
export const AUTH_USER_ACTIONS = {
|
||||||
SEND_LOGIN_REQUEST: 'SEND_LOGIN_REQUEST',
|
SEND_LOGIN_REQUEST: 'SEND_LOGIN_REQUEST',
|
||||||
SET_LOGIN_ERROR: 'SET_LOGIN_ERROR',
|
SET_LOGIN_ERROR: 'SET_LOGIN_ERROR',
|
||||||
|
SET_STATE: 'SET_STATE',
|
||||||
SET_USER: 'SET_USER',
|
SET_USER: 'SET_USER',
|
||||||
SET_TOKEN: 'SET_TOKEN',
|
SET_TOKEN: 'SET_TOKEN',
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,11 @@ const setUser: ActionHandler<typeof ActionCreators.authSetUser> = (state, { prof
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setState: ActionHandler<typeof ActionCreators.authSetState> = (state, { payload }) => ({
|
||||||
|
...state,
|
||||||
|
...payload,
|
||||||
|
});
|
||||||
|
|
||||||
const setToken: ActionHandler<typeof ActionCreators.authSetToken> = (state, { token }) => ({
|
const setToken: ActionHandler<typeof ActionCreators.authSetToken> = (state, { token }) => ({
|
||||||
...state,
|
...state,
|
||||||
token,
|
token,
|
||||||
|
@ -104,6 +109,7 @@ const setRegisterSocialErrors: ActionHandler<typeof ActionCreators.authSetRegist
|
||||||
export const AUTH_USER_HANDLERS = {
|
export const AUTH_USER_HANDLERS = {
|
||||||
[AUTH_USER_ACTIONS.SET_LOGIN_ERROR]: setLoginError,
|
[AUTH_USER_ACTIONS.SET_LOGIN_ERROR]: setLoginError,
|
||||||
[AUTH_USER_ACTIONS.SET_USER]: setUser,
|
[AUTH_USER_ACTIONS.SET_USER]: setUser,
|
||||||
|
[AUTH_USER_ACTIONS.SET_STATE]: setState,
|
||||||
[AUTH_USER_ACTIONS.SET_TOKEN]: setToken,
|
[AUTH_USER_ACTIONS.SET_TOKEN]: setToken,
|
||||||
[AUTH_USER_ACTIONS.SET_PROFILE]: setProfile,
|
[AUTH_USER_ACTIONS.SET_PROFILE]: setProfile,
|
||||||
[AUTH_USER_ACTIONS.SET_UPDATES]: setUpdates,
|
[AUTH_USER_ACTIONS.SET_UPDATES]: setUpdates,
|
||||||
|
|
|
@ -10,6 +10,7 @@ const HANDLERS = {
|
||||||
const INITIAL_STATE: IAuthState = {
|
const INITIAL_STATE: IAuthState = {
|
||||||
token: '',
|
token: '',
|
||||||
user: { ...EMPTY_USER },
|
user: { ...EMPTY_USER },
|
||||||
|
is_tester: false,
|
||||||
|
|
||||||
updates: {
|
updates: {
|
||||||
last: '',
|
last: '',
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { IState } from '~/redux/store';
|
||||||
|
|
||||||
export const selectAuth = (state: IState) => state.auth;
|
export const selectAuth = (state: IState) => state.auth;
|
||||||
export const selectUser = (state: IState) => state.auth.user;
|
export const selectUser = (state: IState) => state.auth.user;
|
||||||
|
export const selectAuthIsTester = (state: IState) => state.auth.is_tester;
|
||||||
export const selectToken = (state: IState) => state.auth.token;
|
export const selectToken = (state: IState) => state.auth.token;
|
||||||
export const selectAuthLogin = (state: IState) => state.auth.login;
|
export const selectAuthLogin = (state: IState) => state.auth.login;
|
||||||
export const selectAuthProfile = (state: IState) => state.auth.profile;
|
export const selectAuthProfile = (state: IState) => state.auth.profile;
|
||||||
|
|
|
@ -37,6 +37,8 @@ export type IAuthState = Readonly<{
|
||||||
user: IUser;
|
user: IUser;
|
||||||
token: string;
|
token: string;
|
||||||
|
|
||||||
|
is_tester: boolean;
|
||||||
|
|
||||||
updates: {
|
updates: {
|
||||||
last: string;
|
last: string;
|
||||||
notifications: INotification[];
|
notifications: INotification[];
|
||||||
|
|
|
@ -46,7 +46,7 @@ import { assocPath } from 'ramda';
|
||||||
|
|
||||||
const authPersistConfig: PersistConfig = {
|
const authPersistConfig: PersistConfig = {
|
||||||
key: 'auth',
|
key: 'auth',
|
||||||
whitelist: ['token', 'user', 'updates'],
|
whitelist: ['token', 'user', 'updates', 'is_tester'],
|
||||||
storage,
|
storage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue