mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +07:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
167c1a8aad
102 changed files with 1560 additions and 385 deletions
|
@ -95,7 +95,7 @@ const EditorDialogUnconnected: FC<IProps> = ({
|
|||
maxLength={256}
|
||||
/>
|
||||
|
||||
<Button title="Сохранить" iconRight="check" />
|
||||
<Button title="Сохранить" iconRight="check" color={data.is_promoted ? 'primary' : 'lab'} />
|
||||
</Group>
|
||||
</Padder>
|
||||
);
|
||||
|
|
|
@ -11,6 +11,7 @@ import { selectAuthRegisterSocial } from '~/redux/auth/selectors';
|
|||
import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
||||
import { useCloseOnEscape } from '~/utils/hooks';
|
||||
import { LoginSocialRegisterButtons } from '~/containers/dialogs/LoginSocialRegisterButtons';
|
||||
import { Toggle } from '~/components/input/Toggle';
|
||||
|
||||
const mapStateToProps = selectAuthRegisterSocial;
|
||||
const mapDispatchToProps = {
|
||||
|
@ -21,6 +22,12 @@ const mapDispatchToProps = {
|
|||
|
||||
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & IDialogProps & {};
|
||||
|
||||
const phrase = [
|
||||
'Сушёный кабачок особенно хорош в это время года, знаете ли.',
|
||||
'Бывало, стреляешь по кабачку, или он стреляет в тебя.',
|
||||
'Он всегда рядом, кабачок -- первый сорт! Надежда империи.',
|
||||
];
|
||||
|
||||
const LoginSocialRegisterDialogUnconnected: FC<Props> = ({
|
||||
onRequestClose,
|
||||
errors,
|
||||
|
@ -32,6 +39,7 @@ const LoginSocialRegisterDialogUnconnected: FC<Props> = ({
|
|||
}) => {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isDryingPants, setIsDryingPants] = useState(false);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(event: FormEvent) => {
|
||||
|
@ -56,7 +64,7 @@ const LoginSocialRegisterDialogUnconnected: FC<Props> = ({
|
|||
useCloseOnEscape(onRequestClose);
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<form onSubmit={onSubmit} autoComplete="new-password">
|
||||
<BetterScrollDialog
|
||||
onClose={onRequestClose}
|
||||
width={300}
|
||||
|
@ -73,6 +81,7 @@ const LoginSocialRegisterDialogUnconnected: FC<Props> = ({
|
|||
value={username}
|
||||
title="Юзернэйм"
|
||||
error={errors.username}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
|
||||
<InputText
|
||||
|
@ -81,12 +90,18 @@ const LoginSocialRegisterDialogUnconnected: FC<Props> = ({
|
|||
title="Пароль"
|
||||
type="password"
|
||||
error={errors.password}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
|
||||
<label className={styles.check}>
|
||||
<input type="checkbox" />
|
||||
<div className={styles.check} onClick={() => setIsDryingPants(!isDryingPants)}>
|
||||
<Toggle value={isDryingPants} color="primary" />
|
||||
<span>Это не мои штаны сушатся на радиаторе в третьей лаборатории</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={styles.check} onClick={() => setIsDryingPants(!isDryingPants)}>
|
||||
<Toggle value={!isDryingPants} color="primary" />
|
||||
<span>{phrase[Math.floor(Math.random() * phrase.length)]}</span>
|
||||
</div>
|
||||
</Group>
|
||||
</div>
|
||||
</Padder>
|
||||
|
|
21
src/containers/lab/LabGrid/index.tsx
Normal file
21
src/containers/lab/LabGrid/index.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React, { FC } from 'react';
|
||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import styles from './styles.module.scss';
|
||||
import { LabNode } from '~/components/lab/LabNode';
|
||||
import { selectLabListNodes } from '~/redux/lab/selectors';
|
||||
|
||||
interface IProps {}
|
||||
|
||||
const LabGrid: FC<IProps> = () => {
|
||||
const nodes = useShallowSelect(selectLabListNodes);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
{nodes.map(node => (
|
||||
<LabNode node={node} key={node.id} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { LabGrid };
|
8
src/containers/lab/LabGrid/styles.module.scss
Normal file
8
src/containers/lab/LabGrid/styles.module.scss
Normal file
|
@ -0,0 +1,8 @@
|
|||
@import "~/styles/variables.scss";
|
||||
|
||||
.wrap {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
grid-auto-rows: auto;
|
||||
grid-row-gap: $gap;
|
||||
}
|
112
src/containers/lab/LabLayout/index.tsx
Normal file
112
src/containers/lab/LabLayout/index.tsx
Normal file
|
@ -0,0 +1,112 @@
|
|||
import React, { FC, useEffect } from 'react';
|
||||
import styles from './styles.module.scss';
|
||||
import { Card } from '~/components/containers/Card';
|
||||
import { Sticky } from '~/components/containers/Sticky';
|
||||
import { Container } from '~/containers/main/Container';
|
||||
import { LabGrid } from '~/containers/lab/LabGrid';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { labGetList } from '~/redux/lab/actions';
|
||||
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||
import { Grid } from '~/components/containers/Grid';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { LabHero } from '~/components/lab/LabHero';
|
||||
import { LabBanner } from '~/components/lab/LabBanner';
|
||||
import { LabHead } from '~/components/lab/LabHead';
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
|
||||
interface IProps {}
|
||||
|
||||
const LabLayout: FC<IProps> = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(labGetList());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Container>
|
||||
<div className={styles.wrap}>
|
||||
<Group className={styles.content}>
|
||||
<LabHead />
|
||||
<LabGrid />
|
||||
</Group>
|
||||
|
||||
<div className={styles.panel}>
|
||||
<Sticky>
|
||||
<Group>
|
||||
<LabBanner />
|
||||
|
||||
<Card>
|
||||
<Group>
|
||||
<Placeholder height={36} width="100%" />
|
||||
<Group horizontal>
|
||||
<Filler />
|
||||
<Placeholder height={32} width="120px" />
|
||||
</Group>
|
||||
|
||||
<div />
|
||||
<div />
|
||||
|
||||
<Placeholder height={14} width="100px" />
|
||||
|
||||
<div />
|
||||
|
||||
<div className={styles.tags}>
|
||||
<Placeholder height={20} width="100px" />
|
||||
<Placeholder height={20} width="64px" />
|
||||
<Placeholder height={20} width="100%" />
|
||||
<Placeholder height={20} width="100px" />
|
||||
<Placeholder height={20} width="100px" />
|
||||
<Placeholder height={20} width="64px" />
|
||||
</div>
|
||||
|
||||
<div />
|
||||
<div />
|
||||
|
||||
<Placeholder height={14} width="180px" />
|
||||
|
||||
<div />
|
||||
|
||||
<Group className={styles.heroes}>
|
||||
<LabHero />
|
||||
<div />
|
||||
<LabHero />
|
||||
<div />
|
||||
<LabHero />
|
||||
<div />
|
||||
<LabHero />
|
||||
<div />
|
||||
<LabHero />
|
||||
<div />
|
||||
<LabHero />
|
||||
<div />
|
||||
<LabHero />
|
||||
</Group>
|
||||
|
||||
<div />
|
||||
<div />
|
||||
|
||||
<Group>
|
||||
<Placeholder width="100%" height={100} />
|
||||
<Placeholder width="120px" height={16} />
|
||||
</Group>
|
||||
|
||||
<div />
|
||||
|
||||
<Group>
|
||||
<Placeholder width="100%" height={100} />
|
||||
<Placeholder width="120px" height={16} />
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
</Group>
|
||||
</Sticky>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { LabLayout };
|
20
src/containers/lab/LabLayout/styles.module.scss
Normal file
20
src/containers/lab/LabLayout/styles.module.scss
Normal file
|
@ -0,0 +1,20 @@
|
|||
@import "~/styles/variables.scss";
|
||||
|
||||
.wrap {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr;
|
||||
column-gap: $gap;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin-top: -7px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
& > * {
|
||||
margin: 0 $gap $gap 0;
|
||||
}
|
||||
}
|
|
@ -8,4 +8,8 @@
|
|||
@include tablet {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media (max-width: $content_width + $gap * 4) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,14 @@ import { BorisLayout } from '~/containers/node/BorisLayout';
|
|||
import { ErrorNotFound } from '~/containers/pages/ErrorNotFound';
|
||||
import { ProfilePage } from '~/containers/profile/ProfilePage';
|
||||
import { Redirect, Route, Switch, useLocation } from 'react-router';
|
||||
import { LabLayout } from '~/containers/lab/LabLayout';
|
||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import { selectAuthUser } from '~/redux/auth/selectors';
|
||||
|
||||
interface IProps {}
|
||||
|
||||
const MainRouter: FC<IProps> = () => {
|
||||
const { is_user } = useShallowSelect(selectAuthUser);
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
|
@ -20,6 +24,12 @@ const MainRouter: FC<IProps> = () => {
|
|||
<Route path={URLS.ERRORS.NOT_FOUND} component={ErrorNotFound} />
|
||||
<Route path={URLS.PROFILE_PAGE(':username')} component={ProfilePage} />
|
||||
|
||||
{is_user && (
|
||||
<>
|
||||
<Route exact path={URLS.LAB} component={LabLayout} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Redirect to="/" />
|
||||
</Switch>
|
||||
);
|
||||
|
|
|
@ -1,25 +1,30 @@
|
|||
import React, { FC, useEffect } from 'react';
|
||||
import React, { FC, useCallback, useEffect } from 'react';
|
||||
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 { NodeComments } from '~/components/node/NodeComments';
|
||||
import styles from './styles.module.scss';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import boris from '~/sprites/boris_robot.svg';
|
||||
import { NodeNoComments } from '~/components/node/NodeNoComments';
|
||||
import { useRandomPhrase } from '~/constants/phrases';
|
||||
import { NodeCommentForm } from '~/components/node/NodeCommentForm';
|
||||
import isBefore from 'date-fns/isBefore';
|
||||
import { Card } from '~/components/containers/Card';
|
||||
import { Footer } from '~/components/main/Footer';
|
||||
import { Sticky } from '~/components/containers/Sticky';
|
||||
import { BorisStats } from '~/components/boris/BorisStats';
|
||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
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 { borisLoadStats } from '~/redux/boris/actions';
|
||||
import { Container } from '~/containers/main/Container';
|
||||
import StickyBox from 'react-sticky-box/dist/esnext';
|
||||
import { BorisComments } from '~/components/boris/BorisComments';
|
||||
import { URLS } from '~/constants/urls';
|
||||
import { Route, Switch, Link } from 'react-router-dom';
|
||||
import { BorisUIDemo } from '~/components/boris/BorisUIDemo';
|
||||
import { BorisSuperpowers } from '~/components/boris/BorisSuperpowers';
|
||||
import { Superpower } from '~/components/boris/Superpower';
|
||||
import { Tabs } from '~/components/dialogs/Tabs';
|
||||
import { Tab } from '~/components/dialogs/Tab';
|
||||
import { useHistory, useLocation } from 'react-router';
|
||||
import { Card } from '~/components/containers/Card';
|
||||
|
||||
type IProps = {};
|
||||
|
||||
|
@ -30,6 +35,7 @@ const BorisLayout: FC<IProps> = () => {
|
|||
const user = useShallowSelect(selectUser);
|
||||
const stats = useShallowSelect(selectBorisStats);
|
||||
const comments = useShallowSelect(selectNodeComments);
|
||||
const is_tester = useShallowSelect(selectAuthIsTester);
|
||||
|
||||
useEffect(() => {
|
||||
const last_comment = comments[0];
|
||||
|
@ -55,6 +61,16 @@ const BorisLayout: FC<IProps> = () => {
|
|||
dispatch(borisLoadStats());
|
||||
}, [dispatch]);
|
||||
|
||||
const setBetaTester = useCallback(
|
||||
(is_tester: boolean) => {
|
||||
dispatch(authSetState({ is_tester }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className={styles.wrap}>
|
||||
|
@ -70,26 +86,40 @@ const BorisLayout: FC<IProps> = () => {
|
|||
|
||||
<div className={styles.container}>
|
||||
<Card className={styles.content}>
|
||||
<Group className={styles.grid}>
|
||||
{user.is_user && <NodeCommentForm isBefore nodeId={node.current.id} />}
|
||||
<Superpower>
|
||||
<Tabs>
|
||||
<Tab
|
||||
active={location.pathname === URLS.BORIS}
|
||||
onClick={() => history.push(URLS.BORIS)}
|
||||
>
|
||||
Комментарии
|
||||
</Tab>
|
||||
|
||||
{node.is_loading_comments ? (
|
||||
<NodeNoComments is_loading count={7} />
|
||||
) : (
|
||||
<NodeComments
|
||||
comments={comments}
|
||||
count={node.comment_count}
|
||||
user={user}
|
||||
order="ASC"
|
||||
<Tab
|
||||
active={location.pathname === `${URLS.BORIS}/ui`}
|
||||
onClick={() => history.push(`${URLS.BORIS}/ui`)}
|
||||
>
|
||||
UI Demo
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Superpower>
|
||||
|
||||
{
|
||||
<Switch>
|
||||
<Route path={`${URLS.BORIS}/ui`} component={BorisUIDemo} />
|
||||
|
||||
<BorisComments
|
||||
isLoadingComments={node.is_loading_comments}
|
||||
commentCount={node.comment_count}
|
||||
node={node.current}
|
||||
comments={node.comments}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Footer />
|
||||
</Switch>
|
||||
}
|
||||
</Card>
|
||||
|
||||
<Group className={styles.stats}>
|
||||
<Sticky>
|
||||
<StickyBox className={styles.sticky} offsetTop={72} offsetBottom={10}>
|
||||
<Group className={styles.stats__container}>
|
||||
<div className={styles.stats__about}>
|
||||
<h4>Господи-боженьки, где это я?</h4>
|
||||
|
@ -102,11 +132,15 @@ const BorisLayout: FC<IProps> = () => {
|
|||
<p className="grey">// Такова жизнь.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{user.is_user && <BorisSuperpowers active={is_tester} onChange={setBetaTester} />}
|
||||
</div>
|
||||
|
||||
<div className={styles.stats__wrap}>
|
||||
<BorisStats stats={stats} />
|
||||
</div>
|
||||
</Group>
|
||||
</Sticky>
|
||||
</StickyBox>
|
||||
</Group>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,22 +7,6 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 4;
|
||||
z-index: 2;
|
||||
border-radius: $radius;
|
||||
padding: 0;
|
||||
background: $node_bg;
|
||||
box-shadow: inset transparentize(mix($wisegreen, white, 60%), 0.6) 0 1px;
|
||||
|
||||
@include desktop {
|
||||
flex: 2.5;
|
||||
}
|
||||
|
||||
@media(max-width: 1024px) {
|
||||
flex: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.grid {
|
||||
padding: $gap;
|
||||
|
@ -36,7 +20,7 @@
|
|||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: 50% 0% no-repeat url('../../../sprites/boris_bg.svg');
|
||||
background: 50% 0 no-repeat url('../../../sprites/boris_bg.svg');
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
|
@ -167,3 +151,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 3;
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ const NodeLayout: FC<IProps> = memo(
|
|||
const { head, block } = useNodeBlocks(current, is_loading);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.wrap}>
|
||||
{head}
|
||||
|
||||
<Container>
|
||||
|
@ -64,7 +64,7 @@ const NodeLayout: FC<IProps> = memo(
|
|||
</Container>
|
||||
|
||||
<SidebarRouter prefix="/post:id" />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
.content {
|
||||
align-items: stretch !important;
|
||||
|
||||
@include vertical_at_tablet;
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,9 @@ const ProfileLayoutUnconnected: FC<IProps> = ({ history, nodeSetCoverImage }) =>
|
|||
useEffect(() => {
|
||||
if (user && user.id && user.cover) {
|
||||
nodeSetCoverImage(user.cover);
|
||||
return () => nodeSetCoverImage(null);
|
||||
return () => {
|
||||
nodeSetCoverImage(undefined);
|
||||
};
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ import React, { FC, useCallback } from 'react';
|
|||
import styles from './styles.module.scss';
|
||||
import classNames from 'classnames';
|
||||
import { IAuthState } from '~/redux/auth/types';
|
||||
import { Tabs } from '~/components/dialogs/Tabs';
|
||||
import { Tab } from '~/components/dialogs/Tab';
|
||||
|
||||
interface IProps {
|
||||
tab: string;
|
||||
|
@ -20,28 +22,20 @@ const ProfileTabs: FC<IProps> = ({ tab, is_own, setTab }) => {
|
|||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<div
|
||||
className={classNames(styles.tab, { [styles.active]: tab === 'profile' })}
|
||||
onClick={changeTab('profile')}
|
||||
>
|
||||
Профиль
|
||||
</div>
|
||||
<div
|
||||
className={classNames(styles.tab, { [styles.active]: tab === 'messages' })}
|
||||
onClick={changeTab('messages')}
|
||||
>
|
||||
Сообщения
|
||||
</div>
|
||||
{is_own && (
|
||||
<>
|
||||
<div
|
||||
className={classNames(styles.tab, { [styles.active]: tab === 'settings' })}
|
||||
onClick={changeTab('settings')}
|
||||
>
|
||||
<Tabs>
|
||||
<Tab active={tab === 'profile'} onClick={changeTab('profile')}>
|
||||
Профиль
|
||||
</Tab>
|
||||
|
||||
<Tab active={tab === 'messages'} onClick={changeTab('messages')}>
|
||||
Сообщения
|
||||
</Tab>
|
||||
{is_own && (
|
||||
<Tab active={tab === 'settings'} onClick={changeTab('settings')}>
|
||||
Настройки
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Tab>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,24 +1,6 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
margin: $gap * 2 0 0 0;
|
||||
padding: 0 $gap;
|
||||
}
|
||||
|
||||
.tab {
|
||||
@include outer_shadow();
|
||||
|
||||
padding: $gap;
|
||||
margin-right: $gap;
|
||||
border-radius: $radius $radius 0 0;
|
||||
font: $font_14_semibold;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background: lighten($content_bg, 4%);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue