1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 12:56:41 +07:00

removed redux completely

This commit is contained in:
Fedor Katurov 2022-01-09 19:03:01 +07:00
parent 26e6d8d41b
commit a4bb07e9cf
323 changed files with 2464 additions and 3348 deletions

View file

@ -1,6 +1,4 @@
import React, { VFC } from 'react';
import { ConnectedRouter } from 'connected-react-router';
import { history } from '~/redux/store';
import { MainLayout } from '~/containers/main/MainLayout';
import { Sprites } from '~/sprites/Sprites';
import { Modal } from '~/containers/dialogs/Modal';
@ -16,12 +14,14 @@ import { SearchProvider } from '~/utils/providers/SearchProvider';
import { ToastProvider } from '~/utils/providers/ToastProvider';
import { AudioPlayerProvider } from '~/utils/providers/AudioPlayerProvider';
import { MetadataProvider } from '~/utils/providers/MetadataProvider';
import { AuthProvider } from '~/utils/providers/AuthProvider';
import { BrowserRouter } from 'react-router-dom';
const App: VFC = observer(() => {
useGlobalLoader();
return (
<ConnectedRouter history={history}>
<BrowserRouter>
<SWRConfigProvider>
<UserContextProvider>
<DragDetectorProvider>
@ -29,14 +29,16 @@ const App: VFC = observer(() => {
<SearchProvider>
<AudioPlayerProvider>
<MetadataProvider>
<MainLayout>
<ToastProvider />
<Modal />
<Sprites />
<AuthProvider>
<MainLayout>
<ToastProvider />
<Modal />
<Sprites />
<MainRouter />
</MainLayout>
<BottomContainer />
<MainRouter />
</MainLayout>
<BottomContainer />
</AuthProvider>
</MetadataProvider>
</AudioPlayerProvider>
</SearchProvider>
@ -44,7 +46,7 @@ const App: VFC = observer(() => {
</DragDetectorProvider>
</UserContextProvider>
</SWRConfigProvider>
</ConnectedRouter>
</BrowserRouter>
);
});

View file

@ -1,18 +1,21 @@
import React, { FC } from "react";
import styles from "./styles.module.scss";
import { Group } from "~/components/containers/Group";
import { NodeCommentForm } from "~/components/node/NodeCommentForm";
import { NodeNoComments } from "~/components/node/NodeNoComments";
import { NodeComments } from "~/containers/node/NodeComments";
import { Footer } from "~/components/main/Footer";
import { CommentContextProvider, useCommentContext } from "~/utils/context/CommentContextProvider";
import { useUserContext } from "~/utils/context/UserContextProvider";
import { useNodeContext } from "~/utils/context/NodeContextProvider";
import React, { FC } from 'react';
import styles from './styles.module.scss';
import { Group } from '~/components/containers/Group';
import { NodeCommentForm } from '~/components/node/NodeCommentForm';
import { NodeNoComments } from '~/components/node/NodeNoComments';
import { NodeComments } from '~/containers/node/NodeComments';
import { Footer } from '~/components/main/Footer';
import { CommentContextProvider, useCommentContext } from '~/utils/context/CommentContextProvider';
import { useUserContext } from '~/utils/context/UserContextProvider';
import { useNodeContext } from '~/utils/context/NodeContextProvider';
import { useAuth } from '~/hooks/auth/useAuth';
interface IProps {}
const BorisComments: FC<IProps> = () => {
const user = useUserContext();
const { isUser } = useAuth();
const {
isLoading,
comments,
@ -27,9 +30,7 @@ const BorisComments: FC<IProps> = () => {
return (
<>
<Group className={styles.grid}>
{user.is_user && (
<NodeCommentForm user={user} nodeId={node.id} saveComment={onSaveComment} />
)}
{isUser && <NodeCommentForm user={user} nodeId={node.id} saveComment={onSaveComment} />}
{isLoading ? (
<NodeNoComments is_loading count={7} />

View file

@ -1,73 +0,0 @@
import React, { FC, MouseEventHandler, useEffect, useRef } from "react";
import styles from "./styles.module.scss";
import { clearAllBodyScrollLocks, disableBodyScroll } from "body-scroll-lock";
import { Icon } from "~/components/input/Icon";
import { LoaderCircle } from "~/components/input/LoaderCircle";
interface IProps {
children: React.ReactChild;
header?: JSX.Element;
footer?: JSX.Element;
backdrop?: JSX.Element;
size?: 'medium' | 'big';
width?: number;
error?: string;
is_loading?: boolean;
overlay?: JSX.Element;
onOverlayClick?: MouseEventHandler<HTMLDivElement>;
onRefCapture?: (ref: any) => void;
onClose?: () => void;
}
const BetterScrollDialog: FC<IProps> = ({
children,
header,
footer,
backdrop,
width = 600,
error,
onClose,
is_loading,
overlay = null,
}) => {
const ref = useRef(null);
useEffect(() => {
disableBodyScroll(ref.current, { reserveScrollBarGap: true });
return () => clearAllBodyScrollLocks();
}, [ref]);
return (
<div className={styles.wrap} ref={ref}>
{backdrop && <div className={styles.backdrop}>{backdrop}</div>}
<div className={styles.container} style={{ maxWidth: width }}>
{onClose && (
<div className={styles.close} onClick={onClose}>
<Icon icon="close" />
</div>
)}
{header && <div className={styles.header}>{header}</div>}
<div className={styles.body}>
{children}
{error && <div className={styles.error}>{error}</div>}
</div>
{!!is_loading && (
<div className={styles.shade}>
<LoaderCircle size={48} />
</div>
)}
{overlay}
{footer && <div className={styles.footer}>{footer}</div>}
</div>
</div>
);
};
export { BetterScrollDialog };

View file

@ -1,144 +0,0 @@
@import "src/styles/variables";
.wrap {
width: 100vw;
height: 100vh;
background: transparentize(darken($content_bg, 4%), 0.5);
display: flex;
align-items: center;
justify-content: center;
padding: 70px 20px 40px 20px;
box-sizing: border-box;
@include tablet {
padding: 70px 5px 5px 5px;
}
}
.container {
display: flex;
align-items: stretch;
justify-content: center;
flex-direction: column;
min-width: $cell;
max-width: 400px;
max-height: 100%;
max-height: calc(100vh - 75px);
width: 100%;
position: relative;
box-sizing: border-box;
& > div:nth-child(2) {
border-top-left-radius: $dialog_radius;
border-top-right-radius: $dialog_radius;
}
& > div:last-child {
border-bottom-left-radius: $dialog_radius;
border-bottom-right-radius: $dialog_radius;
}
}
.header,
.footer {
@include outer_shadow();
// padding: 10px;
background: darken($content_bg, 2%);
}
.body {
@include outer_shadow();
position: relative;
overflow: auto;
flex: 1;
background: $content_bg;
}
@keyframes appear {
0% {
top: -48px;
}
100% {
top: -58px;
}
}
.close {
background: darken($content_bg, 2%);
width: 48px;
height: 48px;
position: absolute;
top: -58px;
right: 50%;
transform: translate(50%, 0);
display: flex;
align-items: center;
justify-content: center;
border-radius: 100%;
cursor: pointer;
transition: transform 0.25s, background-color 0.25s;
animation: appear 0.5s forwards;
&:hover {
background-color: $red;
transform: translate(50%, -5px);
}
svg {
width: 24px;
height: 24px;
}
}
.error {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 40px;
pointer-events: none;
background: linear-gradient(0deg, $red 50%, transparentize($red, 1));
display: flex;
align-items: center;
justify-content: center;
border-radius: 0 0 $radius $radius;
z-index: 11;
}
.backdrop {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
@keyframes appear {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.shade {
position: absolute;
background: transparentize($content_bg, 0.3);
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
border-radius: $radius;
animation: appear 1s forwards;
svg {
fill: white;
}
}

View file

@ -3,7 +3,7 @@ import { EMPTY_NODE, NODE_TYPES } from '~/constants/node';
import { EditorDialog } from '~/containers/dialogs/EditorDialog';
import { useHistory, useRouteMatch } from 'react-router';
import { values } from 'ramda';
import { INode } from '~/redux/types';
import { INode } from '~/types';
import { useCreateNode } from '~/hooks/node/useCreateNode';
const EditorCreateDialog: FC = () => {

View file

@ -1,22 +1,22 @@
import React, { createElement, FC, useCallback, useMemo, useState } from "react";
import styles from "./styles.module.scss";
import { NODE_EDITORS } from "~/constants/node";
import { BetterScrollDialog } from "../BetterScrollDialog";
import { CoverBackdrop } from "~/components/containers/CoverBackdrop";
import { prop } from "ramda";
import { useNodeFormFormik } from "~/hooks/node/useNodeFormFormik";
import { EditorButtons } from "~/components/editors/EditorButtons";
import { UploadSubject, UploadTarget } from "~/constants/uploads";
import { FormikProvider } from "formik";
import { INode } from "~/redux/types";
import { ModalWrapper } from "~/components/dialogs/ModalWrapper";
import { useTranslatedError } from "~/hooks/data/useTranslatedError";
import { useCloseOnEscape } from "~/hooks";
import { EditorConfirmClose } from "~/components/editors/EditorConfirmClose";
import { DialogComponentProps } from "~/types/modal";
import { useUploader } from "~/hooks/data/useUploader";
import { UploaderContextProvider } from "~/utils/context/UploaderContextProvider";
import { observer } from "mobx-react-lite";
import React, { createElement, FC, useCallback, useMemo, useState } from 'react';
import styles from './styles.module.scss';
import { NODE_EDITORS } from '~/constants/node';
import { BetterScrollDialog } from '../../../components/dialogs/BetterScrollDialog';
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
import { prop } from 'ramda';
import { useNodeFormFormik } from '~/hooks/node/useNodeFormFormik';
import { EditorButtons } from '~/components/editors/EditorButtons';
import { UploadSubject, UploadTarget } from '~/constants/uploads';
import { FormikProvider } from 'formik';
import { INode } from '~/types';
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
import { useTranslatedError } from '~/hooks/data/useTranslatedError';
import { useCloseOnEscape } from '~/hooks';
import { EditorConfirmClose } from '~/components/editors/EditorConfirmClose';
import { DialogComponentProps } from '~/types/modal';
import { useUploader } from '~/hooks/data/useUploader';
import { UploaderContextProvider } from '~/utils/context/UploaderContextProvider';
import { observer } from 'mobx-react-lite';
interface Props extends DialogComponentProps {
node: INode;

View file

@ -6,7 +6,7 @@ import { LoaderCircle } from '~/components/input/LoaderCircle';
import styles from './styles.module.scss';
import { useLoadNode } from '~/hooks/node/useLoadNode';
import { useUpdateNode } from '~/hooks/node/useUpdateNode';
import { INode } from '~/redux/types';
import { INode } from '~/types';
import { observer } from 'mobx-react';
const EditorEditDialog: FC = observer(() => {

View file

@ -1,62 +1,29 @@
import React, { FC, FormEvent, useCallback, useEffect, useState } from 'react';
import { connect } from 'react-redux';
import React, { FC, useCallback } from 'react';
import { useCloseOnEscape } from '~/hooks';
import { Group } from '~/components/containers/Group';
import { InputText } from '~/components/input/InputText';
import { Button } from '~/components/input/Button';
import { Padder } from '~/components/containers/Padder';
import { selectAuthLogin } from '~/redux/auth/selectors';
import { API } from '~/constants/api';
import { BetterScrollDialog } from '../BetterScrollDialog';
import { BetterScrollDialog } from '~/components/dialogs/BetterScrollDialog';
import styles from './styles.module.scss';
import * as ACTIONS from '~/redux/auth/actions';
import { ISocialProvider } from '~/redux/auth/types';
import { pick } from 'ramda';
import { LoginDialogButtons } from '~/containers/dialogs/LoginDialogButtons';
import { OAUTH_EVENT_TYPES } from '~/redux/types';
import { LoginDialogButtons } from '~/components/auth/login/LoginDialogButtons';
import { DialogTitle } from '~/components/dialogs/DialogTitle';
import { useTranslatedError } from '~/hooks/data/useTranslatedError';
import { DialogComponentProps } from '~/types/modal';
import { useShowModal } from '~/hooks/modal/useShowModal';
import { Dialog } from '~/constants/modal';
import { useAuth } from '~/hooks/auth/useAuth';
import { useLoginForm } from '~/hooks/auth/useLoginForm';
import { useOAuth } from '~/hooks/auth/useOAuth';
const mapStateToProps = state => ({
...pick(['error', 'is_registering'], selectAuthLogin(state)),
});
type LoginDialogProps = DialogComponentProps & {};
const mapDispatchToProps = {
userSendLoginRequest: ACTIONS.userSendLoginRequest,
userSetLoginError: ACTIONS.userSetLoginError,
authLoginWithSocial: ACTIONS.authLoginWithSocial,
authGotOauthLoginEvent: ACTIONS.authGotOauthLoginEvent,
};
type IProps = ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps &
DialogComponentProps & {};
const LoginDialogUnconnected: FC<IProps> = ({
error,
onRequestClose,
userSendLoginRequest,
userSetLoginError,
authGotOauthLoginEvent,
}) => {
const [username, setUserName] = useState('');
const [password, setPassword] = useState('');
const LoginDialog: FC<LoginDialogProps> = ({ onRequestClose }) => {
useCloseOnEscape(onRequestClose);
const { login } = useAuth();
const { openOauthWindow } = useOAuth();
const showRestoreDialog = useShowModal(Dialog.RestoreRequest);
const onSubmit = useCallback(
(event: FormEvent) => {
event.preventDefault();
userSendLoginRequest({ username, password });
},
[userSendLoginRequest, username, password]
);
const onRestoreRequest = useCallback(
event => {
event.preventDefault();
@ -65,44 +32,13 @@ const LoginDialogUnconnected: FC<IProps> = ({
[showRestoreDialog]
);
const openOauthWindow = useCallback(
(provider: ISocialProvider) => () => {
window.open(API.USER.OAUTH_WINDOW(provider), '', 'width=600,height=400');
},
[]
);
const onMessage = useCallback(
(event: MessageEvent) => {
if (!event?.data?.type || !Object.values(OAUTH_EVENT_TYPES).includes(event.data.type)) {
return;
}
authGotOauthLoginEvent(event.data);
},
[authGotOauthLoginEvent]
);
useEffect(() => {
if (error) userSetLoginError('');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [username, password]);
useEffect(() => {
window.addEventListener('message', onMessage);
return () => window.removeEventListener('message', onMessage);
}, [onMessage]);
useCloseOnEscape(onRequestClose);
const translatedError = useTranslatedError(error);
const { values, errors, handleSubmit, handleChange } = useLoginForm(login, onRequestClose);
return (
<form onSubmit={onSubmit}>
<form onSubmit={handleSubmit}>
<div>
<BetterScrollDialog
width={300}
error={translatedError}
onClose={onRequestClose}
footer={<LoginDialogButtons openOauthWindow={openOauthWindow} />}
backdrop={<div className={styles.backdrop} />}
@ -112,9 +48,21 @@ const LoginDialogUnconnected: FC<IProps> = ({
<Group>
<DialogTitle>Решительно войти</DialogTitle>
<InputText title="Логин" handler={setUserName} value={username} autoFocus />
<InputText
title="Логин"
handler={handleChange('username')}
value={values.username}
error={errors.username}
autoFocus
/>
<InputText title="Пароль" handler={setPassword} value={password} type="password" />
<InputText
title="Пароль"
handler={handleChange('password')}
value={values.password}
error={errors.password}
type="password"
/>
<Button
color="link"
@ -133,6 +81,4 @@ const LoginDialogUnconnected: FC<IProps> = ({
);
};
const LoginDialog = connect(mapStateToProps, mapDispatchToProps)(LoginDialogUnconnected);
export { LoginDialog };

View file

@ -1,28 +0,0 @@
import React, { FC, MouseEventHandler } from "react";
import { Button } from "~/components/input/Button";
import { Grid } from "~/components/containers/Grid";
import { Group } from "~/components/containers/Group";
import styles from "./styles.module.scss";
import { ISocialProvider } from "~/redux/auth/types";
interface IProps {
openOauthWindow: (provider: ISocialProvider) => MouseEventHandler;
}
const LoginDialogButtons: FC<IProps> = ({ openOauthWindow }) => (
<Group className={styles.footer}>
<Button>Войти</Button>
<Grid columns="repeat(2, 1fr)">
<Button color="outline" iconLeft="google" type="button" onClick={openOauthWindow('google')}>
<span>Google</span>
</Button>
<Button color="outline" iconLeft="vk" type="button" onClick={openOauthWindow('vkontakte')}>
<span>Вконтакте</span>
</Button>
</Grid>
</Group>
);
export { LoginDialogButtons };

View file

@ -1,5 +0,0 @@
@import "src/styles/variables";
.footer {
padding: $gap;
}

View file

@ -1,13 +0,0 @@
import React, { FC } from "react";
import { Button } from "~/components/input/Button";
import styles from "./styles.module.scss";
interface IProps {}
const LoginSocialRegisterButtons: FC<IProps> = () => (
<div className={styles.wrap}>
<Button>Впустите меня!</Button>
</div>
);
export { LoginSocialRegisterButtons };

View file

@ -1,9 +0,0 @@
@import "src/styles/variables";
.wrap {
padding: $gap $gap * 2;
button {
width: 100%;
}
}

View file

@ -1,76 +1,52 @@
import React, { FC, FormEvent, useCallback, useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { BetterScrollDialog } from '~/containers/dialogs/BetterScrollDialog';
import React, { FC, useCallback, useState } from 'react';
import { BetterScrollDialog } from '~/components/dialogs/BetterScrollDialog';
import { Padder } from '~/components/containers/Padder';
import { DialogTitle } from '~/components/dialogs/DialogTitle';
import { Group } from '~/components/containers/Group';
import { InputText } from '~/components/input/InputText';
import styles from './styles.module.scss';
import { selectAuthRegisterSocial } from '~/redux/auth/selectors';
import * as AUTH_ACTIONS from '~/redux/auth/actions';
import { useCloseOnEscape } from '~/hooks';
import { LoginSocialRegisterButtons } from '~/containers/dialogs/LoginSocialRegisterButtons';
import { LoginSocialRegisterButtons } from '~/components/auth/oauth/LoginSocialRegisterButtons';
import { Toggle } from '~/components/input/Toggle';
import { DialogComponentProps } from '~/types/modal';
import { getRandomPhrase } from '~/constants/phrases';
import { useSocialRegisterForm } from '~/hooks/auth/useSocialRegisterForm';
import { apiLoginWithSocial } from '~/api/auth';
import { useModal } from '~/hooks/modal/useModal';
import { useAuthStore } from '~/store/auth/useAuthStore';
const mapStateToProps = selectAuthRegisterSocial;
const mapDispatchToProps = {
authSetRegisterSocialErrors: AUTH_ACTIONS.authSetRegisterSocialErrors,
authSetRegisterSocial: AUTH_ACTIONS.authSetRegisterSocial,
authSendRegisterSocial: AUTH_ACTIONS.authSendRegisterSocial,
};
type LoginSocialRegisterDialogProps = DialogComponentProps & { token: string };
type Props = ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps &
DialogComponentProps & {};
const phrase = getRandomPhrase('REGISTER');
const phrase = [
'Сушёный кабачок особенно хорош в это время года, знаете ли.',
'Бывало, стреляешь по кабачку, или он стреляет в тебя.',
'Он всегда рядом, кабачок -- первый сорт! Надежда империи.',
];
const LoginSocialRegisterDialogUnconnected: FC<Props> = ({
const LoginSocialRegisterDialog: FC<LoginSocialRegisterDialogProps> = ({
onRequestClose,
errors,
error,
authSetRegisterSocialErrors,
authSetRegisterSocial,
authSendRegisterSocial,
token,
}) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isDryingPants, setIsDryingPants] = useState(false);
useCloseOnEscape(onRequestClose);
const { hideModal } = useModal();
const auth = useAuthStore();
const onSubmit = useCallback(
(event: FormEvent) => {
event.preventDefault();
authSendRegisterSocial(username, password);
const [isDryingPants, setIsDryingPants] = useState(false);
const onSuccess = useCallback(
(loginToken: string) => {
auth.setToken(loginToken);
hideModal();
},
[username, password, authSendRegisterSocial]
[auth, hideModal]
);
useEffect(() => {
if (errors.username) authSetRegisterSocialErrors({ username: '' });
}, [authSetRegisterSocialErrors, errors.username, username]);
useEffect(() => {
if (errors.password) authSetRegisterSocialErrors({ password: '' });
}, [authSetRegisterSocialErrors, errors.password, password]);
useEffect(() => {
if (error) authSetRegisterSocial({ error: '' });
}, [username, password, error, authSetRegisterSocial]);
useCloseOnEscape(onRequestClose);
const { values, errors, handleChange, handleSubmit } = useSocialRegisterForm(
token,
apiLoginWithSocial,
onSuccess
);
return (
<form onSubmit={onSubmit} autoComplete="new-password">
<form onSubmit={handleSubmit} autoComplete="new-password">
<BetterScrollDialog
onClose={onRequestClose}
width={300}
error={error}
footer={<LoginSocialRegisterButtons />}
>
<Padder>
@ -79,16 +55,16 @@ const LoginSocialRegisterDialogUnconnected: FC<Props> = ({
<DialogTitle>Добро пожаловать в семью!</DialogTitle>
<InputText
handler={setUsername}
value={username}
handler={handleChange('username')}
value={values.username}
title="Юзернэйм"
error={errors.username}
autoComplete="new-password"
/>
<InputText
handler={setPassword}
value={password}
handler={handleChange('password')}
value={values.password}
title="Пароль"
type="password"
error={errors.password}
@ -102,7 +78,7 @@ const LoginSocialRegisterDialogUnconnected: FC<Props> = ({
<div className={styles.check} onClick={() => setIsDryingPants(!isDryingPants)}>
<Toggle value={!isDryingPants} color="primary" />
<span>{phrase[Math.floor(Math.random() * phrase.length)]}</span>
<span>{phrase}</span>
</div>
</Group>
</div>
@ -112,9 +88,4 @@ const LoginSocialRegisterDialogUnconnected: FC<Props> = ({
);
};
const LoginSocialRegisterDialog = connect(
mapStateToProps,
mapDispatchToProps
)(LoginSocialRegisterDialogUnconnected);
export { LoginSocialRegisterDialog };

View file

@ -1,9 +1,9 @@
import React, { FC } from "react";
import { ModalWrapper } from "~/components/dialogs/ModalWrapper";
import { DIALOG_CONTENT } from "~/constants/modal";
import { useModalStore } from "~/store/modal/useModalStore";
import { has } from "ramda";
import { observer } from "mobx-react-lite";
import React, { FC } from 'react';
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
import { DIALOG_CONTENT } from '~/constants/modal';
import { useModalStore } from '~/store/modal/useModalStore';
import { has } from 'ramda';
import { observer } from 'mobx-react-lite';
type IProps = {};

View file

@ -6,10 +6,9 @@ import { getURL } from '~/utils/dom';
import { PRESETS } from '~/constants/urls';
import styles from './styles.module.scss';
import classNames from 'classnames';
import { useBlockBackButton } from '~/hooks/navigation/useBlockBackButton';
import { useModal } from '~/hooks/modal/useModal';
import { observer } from 'mobx-react';
import { IFile } from '~/redux/types';
import { IFile } from '~/types';
import { DialogComponentProps } from '~/types/modal';
export interface PhotoSwipeProps extends DialogComponentProps {
@ -60,8 +59,6 @@ const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
});
}, [hideModal, items, index]);
useBlockBackButton(hideModal);
return (
<div className="pswp" tabIndex={-1} role="dialog" aria-hidden="true" ref={ref}>
<div className={classNames('pswp__bg', styles.bg)} />

View file

@ -1,14 +1,14 @@
import React, { FC } from 'react';
import { BetterScrollDialog } from '../BetterScrollDialog';
import { BetterScrollDialog } from '../../../components/dialogs/BetterScrollDialog';
import { ProfileInfo } from '~/containers/profile/ProfileInfo';
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
import { Tabs } from '~/components/dialogs/Tabs';
import { ProfileDescription } from '~/components/profile/ProfileDescription';
import { ProfileMessages } from '~/containers/profile/ProfileMessages';
import { ProfileSettings } from '~/components/profile/ProfileSettings';
import { ProfileAccounts } from '~/components/profile/ProfileAccounts';
import { ProfileAccounts } from '~/containers/profile/ProfileAccounts';
import { DialogComponentProps } from '~/types/modal';
import { useUser } from '~/hooks/user/userUser';
import { useUser } from '~/hooks/auth/useUser';
import { useGetProfile } from '~/hooks/profile/useGetProfile';
import { ProfileProvider } from '~/utils/providers/ProfileProvider';
@ -18,7 +18,9 @@ export interface ProfileDialogProps extends DialogComponentProps {
const ProfileDialog: FC<ProfileDialogProps> = ({ username, onRequestClose }) => {
const { isLoading, profile } = useGetProfile(username);
const { id } = useUser();
const {
user: { id },
} = useUser();
return (
<ProfileProvider username={username}>

View file

@ -1,137 +1,77 @@
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { BetterScrollDialog } from '../BetterScrollDialog';
import React, { FC, useCallback, useMemo, useState } from 'react';
import { BetterScrollDialog } from '../../../components/dialogs/BetterScrollDialog';
import { Group } from '~/components/containers/Group';
import { InputText } from '~/components/input/InputText';
import { Button } from '~/components/input/Button';
import styles from './styles.module.scss';
import * as AUTH_ACTIONS from '~/redux/auth/actions';
import { pick } from 'ramda';
import { selectAuthRestore } from '~/redux/auth/selectors';
import { ERROR_LITERAL, ERRORS } from '~/constants/errors';
import { Icon } from '~/components/input/Icon';
import { useCloseOnEscape } from '~/hooks';
import { DialogComponentProps } from '~/types/modal';
import { useRestoreCode } from '~/hooks/auth/useRestoreCode';
import { RestoreInvalidCode } from '~/components/auth/restore/RestoreInvalidCode';
import { RestoreSuccess } from '~/components/auth/restore/RestoreSuccess';
import { useRestorePasswordForm } from '~/hooks/auth/useRestorePasswordForm';
import { apiRestoreCode } from '~/api/auth';
const mapStateToProps = state => ({
restore: selectAuthRestore(state),
});
type RestorePasswordDialogProps = DialogComponentProps & {
code: string;
};
const mapDispatchToProps = pick(['authRestorePassword', 'authSetRestore'], AUTH_ACTIONS);
const RestorePasswordDialog: FC<RestorePasswordDialogProps> = ({ onRequestClose, code }) => {
useCloseOnEscape(onRequestClose);
type IProps = DialogComponentProps &
ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps & {};
const { codeUser, isLoading, error } = useRestoreCode(code);
const RestorePasswordDialogUnconnected: FC<IProps> = ({
restore: { error, is_loading, is_succesfull, user },
authSetRestore,
onRequestClose,
authRestorePassword,
}) => {
const [password, setPassword] = useState('');
const [password_again, setPasswordAgain] = useState('');
const [isSent, setIsSent] = useState(false);
const onSent = useCallback(() => setIsSent(true), [setIsSent]);
const doesnt_match = useMemo(
() => !password || !password_again || password.trim() !== password_again.trim(),
[password_again, password]
const { handleChange, handleSubmit, values, errors } = useRestorePasswordForm(
code,
apiRestoreCode,
onSent
);
const onSubmit = useCallback(
event => {
event.preventDefault();
if (doesnt_match) return;
authRestorePassword(password);
},
[doesnt_match, authRestorePassword, password]
);
useEffect(() => {
if (error || is_succesfull) {
authSetRestore({ error: '', is_succesfull: false });
}
}, [authSetRestore, error, is_succesfull, password, password_again]);
const buttons = useMemo(
() => (
<Group className={styles.buttons}>
<Button color={doesnt_match ? 'outline' : 'primary'}>Восстановить</Button>
<Button color="primary">Восстановить</Button>
</Group>
),
[doesnt_match]
[]
);
const overlay = useMemo(
() =>
is_succesfull ? (
<Group className={styles.shade}>
<Icon icon="check" size={64} />
const overlay = useMemo(() => {
if (isSent) {
return <RestoreSuccess username={codeUser?.username} onClick={onRequestClose} />;
}
<div>Пароль обновлен</div>
<div>Добро пожаловать домой, ~{user?.username}!</div>
if (error) {
return <RestoreInvalidCode onClose={onRequestClose} error={error} />;
}
<div />
<Button color="secondary" onClick={onRequestClose}>
Ура!
</Button>
</Group>
) : (
undefined
),
[is_succesfull, onRequestClose, user]
);
const not_ready = useMemo(
() => (is_loading && !user ? <div className={styles.shade} /> : undefined),
[is_loading, user]
);
const invalid_code = useMemo(
() =>
!is_loading && !user ? (
<Group className={styles.error_shade}>
<Icon icon="close" size={64} />
<div>{ERROR_LITERAL[error || ERRORS.CODE_IS_INVALID]}</div>
<div className={styles.spacer} />
<Button color="primary" onClick={onRequestClose}>
Очень жаль
</Button>
</Group>
) : (
undefined
),
[is_loading, user, error, onRequestClose]
);
useCloseOnEscape(onRequestClose);
if (isLoading) {
return <div className={styles.shade} />;
}
}, [isLoading, error, isSent, codeUser, onRequestClose]);
return (
<form onSubmit={onSubmit}>
<form onSubmit={handleSubmit}>
<BetterScrollDialog
footer={buttons}
overlay={overlay}
width={300}
onClose={onRequestClose}
is_loading={is_loading}
error={error && ERROR_LITERAL[error]}
overlay={overlay || not_ready || invalid_code}
is_loading={isLoading}
>
<div className={styles.wrap}>
<Group>
<div className={styles.header}>
Пришло время сменить пароль{user && user.username && `, ~${user.username}`}
</div>
<div className={styles.header}>Пришло время сменить пароль, {codeUser?.username}</div>
<InputText
title="Новый пароль"
value={password}
handler={setPassword}
value={values.newPassword}
handler={handleChange('newPassword')}
error={errors.newPassword}
autoFocus
type="password"
/>
@ -139,9 +79,9 @@ const RestorePasswordDialogUnconnected: FC<IProps> = ({
<InputText
title="Ещё раз"
type="password"
value={password_again}
handler={setPasswordAgain}
error={password_again && doesnt_match ? ERROR_LITERAL[ERRORS.DOESNT_MATCH] : ''}
value={values.newPasswordAgain}
handler={handleChange('newPasswordAgain')}
error={errors.newPasswordAgain}
/>
<Group className={styles.text}>
@ -158,9 +98,4 @@ const RestorePasswordDialogUnconnected: FC<IProps> = ({
);
};
const RestorePasswordDialog = connect(
mapStateToProps,
mapDispatchToProps
)(RestorePasswordDialogUnconnected);
export { RestorePasswordDialog };

View file

@ -1,58 +1,32 @@
import React, { useCallback, useEffect, useMemo, useState, VFC } from "react";
import { useDispatch } from "react-redux";
import { BetterScrollDialog } from "../BetterScrollDialog";
import { Group } from "~/components/containers/Group";
import { InputText } from "~/components/input/InputText";
import { Button } from "~/components/input/Button";
import styles from "./styles.module.scss";
import * as AUTH_ACTIONS from "~/redux/auth/actions";
import { selectAuthRestore } from "~/redux/auth/selectors";
import { ERROR_LITERAL } from "~/constants/errors";
import { Icon } from "~/components/input/Icon";
import { useCloseOnEscape } from "~/hooks";
import { DialogComponentProps } from "~/types/modal";
import { useShallowSelect } from "~/hooks/data/useShallowSelect";
import { IAuthState } from "~/redux/auth/types";
import React, { useCallback, useMemo, useState, VFC } from 'react';
import { BetterScrollDialog } from '../../../components/dialogs/BetterScrollDialog';
import { Group } from '~/components/containers/Group';
import { InputText } from '~/components/input/InputText';
import { Button } from '~/components/input/Button';
import styles from './styles.module.scss';
import { useCloseOnEscape } from '~/hooks';
import { DialogComponentProps } from '~/types/modal';
import { useRestoreRequestForm } from '~/hooks/auth/useRestoreRequestForm';
import { apiRequestRestoreCode } from '~/api/auth';
import { RestoreSent } from '~/components/auth/restore/RestoreSent';
interface RestoreRequestDialogProps extends DialogComponentProps {}
const RestoreRequestDialog: VFC<RestoreRequestDialogProps> = ({ onRequestClose }) => {
const dispatch = useDispatch();
const { error, is_loading, is_succesfull } = useShallowSelect(selectAuthRestore);
const [field, setField] = useState('');
useCloseOnEscape(onRequestClose);
const authSetRestore = useCallback(
(restore: Partial<IAuthState['restore']>) => dispatch(AUTH_ACTIONS.authSetRestore(restore)),
[dispatch]
const [isSent, setIsSent] = useState(false);
const onSent = useCallback(() => setIsSent(true), [setIsSent]);
const { isSubmitting, handleSubmit, handleChange, errors, values } = useRestoreRequestForm(
apiRequestRestoreCode,
onSent
);
const authRequestRestoreCode = useCallback(
(field: string) => dispatch(AUTH_ACTIONS.authRequestRestoreCode(field)),
[dispatch]
);
const onSubmit = useCallback(
event => {
event.preventDefault();
if (!field) return;
authRequestRestoreCode(field);
},
[authRequestRestoreCode, field]
);
useEffect(() => {
if (error || is_succesfull) {
authSetRestore({ error: '', is_succesfull: false });
}
}, [authSetRestore, error, field, is_succesfull]);
const buttons = useMemo(
() => (
<Group className={styles.buttons}>
<Button>Восстановить</Button>
<Button color="secondary">Восстановить</Button>
</Group>
),
[]
@ -60,42 +34,25 @@ const RestoreRequestDialog: VFC<RestoreRequestDialogProps> = ({ onRequestClose }
const header = useMemo(() => <div className={styles.illustration} />, []);
const overlay = useMemo(
() =>
is_succesfull ? (
<Group className={styles.shade}>
<Icon icon="check" size={64} />
<div>Проверьте почту, мы отправили на неё код</div>
<div />
<Button color="secondary" onClick={onRequestClose}>
Отлично!
</Button>
</Group>
) : (
undefined
),
[is_succesfull, onRequestClose]
);
useCloseOnEscape(onRequestClose);
return (
<form onSubmit={onSubmit}>
<form onSubmit={handleSubmit}>
<BetterScrollDialog
header={header}
footer={buttons}
width={300}
onClose={onRequestClose}
is_loading={is_loading}
error={error && ERROR_LITERAL[error]}
overlay={overlay}
is_loading={isSubmitting}
overlay={isSent ? <RestoreSent onClose={onRequestClose} /> : undefined}
>
<div className={styles.wrap}>
<Group>
<InputText title="Имя или email" value={field} handler={setField} autoFocus />
<InputText
title="Имя или email"
value={values.field}
handler={handleChange('field')}
error={errors.field}
autoFocus
/>
<div className={styles.text}>
Введите имя пользователя или адрес почты. Мы пришлем ссылку для сброса пароля.

View file

@ -21,29 +21,3 @@
justify-content: center;
font: $font_18_semibold;
}
.shade {
@include outer_shadow();
background: $content_bg;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: $radius;
padding: $gap * 2;
box-sizing: border-box;
text-transform: uppercase;
font: $font_18_semibold;
text-align: center;
color: $wisegreen;
svg {
fill: $wisegreen;
}
}

View file

@ -1,6 +1,6 @@
import React, { FC } from "react";
import { BetterScrollDialog } from "../BetterScrollDialog";
import styles from "./styles.module.scss";
import React, { FC } from 'react';
import { BetterScrollDialog } from '../../../components/dialogs/BetterScrollDialog';
import styles from './styles.module.scss';
interface IProps {}

View file

@ -1,17 +1,17 @@
import React, { FC, FormEvent, useCallback, useMemo } from "react";
import { InputText } from "~/components/input/InputText";
import { FlowRecent } from "~/components/flow/FlowRecent";
import React, { FC, FormEvent, useCallback, useMemo } from 'react';
import { InputText } from '~/components/input/InputText';
import { FlowRecent } from '~/components/flow/FlowRecent';
import styles from "~/containers/flow/FlowStamp/styles.module.scss";
import { FlowSearchResults } from "~/components/flow/FlowSearchResults";
import { Icon } from "~/components/input/Icon";
import { Group } from "~/components/containers/Group";
import { Toggle } from "~/components/input/Toggle";
import classNames from "classnames";
import { Superpower } from "~/components/boris/Superpower";
import { experimentalFeatures } from "~/constants/features";
import { useSearchContext } from "~/utils/providers/SearchProvider";
import { useFlowContext } from "~/utils/context/FlowContextProvider";
import styles from '~/containers/flow/FlowStamp/styles.module.scss';
import { FlowSearchResults } from '~/components/flow/FlowSearchResults';
import { Icon } from '~/components/input/Icon';
import { Group } from '~/components/containers/Group';
import { Toggle } from '~/components/input/Toggle';
import classNames from 'classnames';
import { Superpower } from '~/components/boris/Superpower';
import { experimentalFeatures } from '~/constants/features';
import { useSearchContext } from '~/utils/providers/SearchProvider';
import { useFlowContext } from '~/utils/context/FlowContextProvider';
interface IProps {
isFluid: boolean;

View file

@ -1,11 +1,11 @@
import React, { FC } from "react";
import Masonry from "react-masonry-css";
import styles from "./styles.module.scss";
import { LabNode } from "~/components/lab/LabNode";
import { EMPTY_NODE, NODE_TYPES } from "~/constants/node";
import { values } from "ramda";
import { useLabContext } from "~/utils/context/LabContextProvider";
import { InfiniteScroll } from "~/components/containers/InfiniteScroll";
import React, { FC } from 'react';
import Masonry from 'react-masonry-css';
import styles from './styles.module.scss';
import { LabNode } from '~/components/lab/LabNode';
import { EMPTY_NODE, NODE_TYPES } from '~/constants/node';
import { values } from 'ramda';
import { useLabContext } from '~/utils/context/LabContextProvider';
import { InfiniteScroll } from '~/components/containers/InfiniteScroll';
interface IProps {}

View file

@ -1,12 +1,12 @@
import React, { FC } from "react";
import styles from "./styles.module.scss";
import { LabBanner } from "~/components/lab/LabBanner";
import { Group } from "~/components/containers/Group";
import { LabTags } from "~/components/lab/LabTags";
import { LabHeroes } from "~/components/lab/LabHeroes";
import { FlowRecentItem } from "~/components/flow/FlowRecentItem";
import { SubTitle } from "~/components/common/SubTitle";
import { useLabContext } from "~/utils/context/LabContextProvider";
import React, { FC } from 'react';
import styles from './styles.module.scss';
import { LabBanner } from '~/components/lab/LabBanner';
import { Group } from '~/components/containers/Group';
import { LabTags } from '~/components/lab/LabTags';
import { LabHeroes } from '~/components/lab/LabHeroes';
import { FlowRecentItem } from '~/components/flow/FlowRecentItem';
import { SubTitle } from '~/components/common/SubTitle';
import { useLabContext } from '~/utils/context/LabContextProvider';
interface IProps {}

View file

@ -1,6 +1,6 @@
import React, { FC } from "react";
import styles from "./styles.module.scss";
import { PlayerView } from "~/containers/player/PlayerView";
import React, { FC } from 'react';
import styles from './styles.module.scss';
import { PlayerView } from '~/containers/player/PlayerView';
type IProps = {};

View file

@ -1,6 +1,6 @@
import React, { FC } from "react";
import styles from "./styles.module.scss";
import classNames from "classnames";
import React, { FC } from 'react';
import styles from './styles.module.scss';
import classNames from 'classnames';
interface IProps {
className?: string;

View file

@ -0,0 +1,106 @@
import React, { FC, useCallback, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Logo } from '~/components/main/Logo';
import { Filler } from '~/components/containers/Filler';
import { UserButton } from '~/components/main/UserButton';
import { URLS } from '~/constants/urls';
import classNames from 'classnames';
import styles from './styles.module.scss';
import isBefore from 'date-fns/isBefore';
import { Authorized } from '~/components/containers/Authorized';
import { Button } from '~/components/input/Button';
import { observer } from 'mobx-react';
import { Dialog } from '~/constants/modal';
import { useGetLabStats } from '~/hooks/lab/useGetLabStats';
import { useAuth } from '~/hooks/auth/useAuth';
import { useModal } from '~/hooks/modal/useModal';
import { useScrollTop } from '~/hooks/dom/useScrollTop';
import { useFlow } from '~/hooks/flow/useFlow';
import { useUpdates } from '~/hooks/updates/useUpdates';
type HeaderProps = {};
const Header: FC<HeaderProps> = observer(() => {
const labStats = useGetLabStats();
const { logout } = useAuth();
const { showModal } = useModal();
const { isUser, user } = useAuth();
const { updates: flowUpdates } = useFlow();
const { borisCommentedAt } = useUpdates();
const openProfile = useCallback(() => {
showModal(Dialog.Profile, { username: user.username });
}, [user.username, showModal]);
const onLogin = useCallback(() => showModal(Dialog.Login, {}), [showModal]);
const top = useScrollTop();
const hasBorisUpdates = useMemo(
() =>
isUser &&
borisCommentedAt &&
(!user.last_seen_boris ||
isBefore(new Date(user.last_seen_boris), new Date(borisCommentedAt))),
[borisCommentedAt, isUser, user.last_seen_boris]
);
const hasLabUpdates = useMemo(() => labStats.updates.length > 0, [labStats.updates]);
const hasFlowUpdates = useMemo(() => flowUpdates.length > 0, [flowUpdates]);
return (
<div className={classNames(styles.wrap, { [styles.is_scrolled]: top > 10 })}>
<div className={styles.container}>
<div className={classNames(styles.logo_wrapper, { [styles.logged_in]: isUser })}>
<Logo />
</div>
<Filler className={styles.filler} />
<div className={styles.plugs}>
<Authorized>
<Link
className={classNames(styles.item, {
[styles.has_dot]: hasFlowUpdates,
})}
to={URLS.BASE}
>
ФЛОУ
</Link>
<Link
className={classNames(styles.item, styles.lab, {
[styles.has_dot]: hasLabUpdates,
})}
to={URLS.LAB}
>
ЛАБ
</Link>
<Link
className={classNames(styles.item, styles.boris, {
[styles.has_dot]: hasBorisUpdates,
})}
to={URLS.BORIS}
>
БОРИС
</Link>
</Authorized>
</div>
{isUser && <UserButton user={user} onLogout={logout} authOpenProfile={openProfile} />}
{!isUser && (
<Button className={styles.user_button} onClick={onLogin} round color="secondary">
ВДОХ
</Button>
)}
</div>
</div>
);
});
export { Header };

View file

@ -0,0 +1,152 @@
@import "../../../styles/variables";
.wrap {
height: $header_height;
z-index: 25;
position: sticky;
top: 0;
width: 100%;
display: flex;
align-items: stretch;
justify-content: center;
box-sizing: border-box;
transition: background-color 0.5s;
@include desktop {
height: 64px;
padding: 0;
}
&.is_scrolled {
@include blur();
}
}
.container {
@include container;
display: flex;
align-items: center;
justify-content: flex-end;
font-weight: 500;
box-sizing: border-box;
@include tablet {
padding: 0 $gap;
}
}
.spacer {
flex: 1;
}
.plugs {
display: flex;
user-select: none;
text-transform: uppercase;
align-items: center;
@include tablet {
flex: 1;
justify-content: flex-start;
}
}
.profile {
padding: 5px 10px;
box-shadow: white 0 0 0 1px;
border-radius: 10px;
}
.user_button {
flex: 0;
cursor: pointer;
}
.item {
font: $font_16_medium;
display: flex;
align-items: center;
position: relative;
padding: $gap $gap * 2;
cursor: pointer;
transition: color 0.25s;
text-decoration: none;
color: white;
white-space: nowrap;
&:hover {
color: $red;
}
&::before {
content: ' ';
position: absolute;
bottom: 0;
height: 3px;
width: 50%;
right: 50%;
background: white;
transform: translate(50%, 0) scaleX(0);
opacity: 0;
border-radius: 3px;
transition: transform 0.5s, opacity 0.25s;
}
&::after {
content: ' ';
position: absolute;
width: 6px;
height: 6px;
border-radius: 4px;
background: lighten($red, 10%);
right: 12px;
top: 6px;
transition: opacity 0.5s;
opacity: 0;
}
&.has_dot {
&::after {
opacity: 1;
}
}
&.lab::after {
background: lighten($blue, 10%);
}
&.boris::after {
background: lighten($wisegreen, 10%);
}
@include tablet {
padding: 0 $gap * 2 0 0;
&.notifications {
flex: 1;
justify-content: flex-end;
align-items: center;
margin-right: $gap;
}
&::after {
right: 0;
}
}
}
.filler {
@include tablet {
display: none;
}
}
.logo_wrapper {
@include tablet {
&.logged_in {
display: none;
}
}
}

View file

@ -1,6 +1,6 @@
import * as React from 'react';
import styles from './styles.module.scss';
import { Header } from '~/components/main/Header';
import { Header } from '~/containers/main/Header';
export const MainLayout = ({ children }) => (
<div className={styles.wrapper}>

View file

@ -1,19 +1,18 @@
import React, { FC } from "react";
import { URLS } from "~/constants/urls";
import { ErrorNotFound } from "~/containers/pages/ErrorNotFound";
import { Redirect, Route, Switch, useLocation } from "react-router";
import { useShallowSelect } from "~/hooks/data/useShallowSelect";
import { selectAuthUser } from "~/redux/auth/selectors";
import { ProfileLayout } from "~/layouts/ProfileLayout";
import FlowPage from "~/pages";
import BorisPage from "~/pages/boris";
import NodePage from "~/pages/node/[id]";
import LabPage from "~/pages/lab";
import React, { FC } from 'react';
import { URLS } from '~/constants/urls';
import { ErrorNotFound } from '~/containers/pages/ErrorNotFound';
import { Redirect, Route, Switch, useLocation } from 'react-router';
import { ProfileLayout } from '~/layouts/ProfileLayout';
import FlowPage from '~/pages';
import BorisPage from '~/pages/boris';
import NodePage from '~/pages/node/[id]';
import LabPage from '~/pages/lab';
import { useAuth } from '~/hooks/auth/useAuth';
interface IProps {}
const MainRouter: FC<IProps> = () => {
const { is_user } = useShallowSelect(selectAuthUser);
const { isUser } = useAuth();
const location = useLocation();
return (
@ -23,7 +22,7 @@ const MainRouter: FC<IProps> = () => {
<Route path={URLS.ERRORS.NOT_FOUND} component={ErrorNotFound} />
<Route path={URLS.PROFILE_PAGE(':username')} component={ProfileLayout} />
{is_user && <Route path={URLS.LAB} component={LabPage} />}
{isUser && <Route path={URLS.LAB} component={LabPage} />}
<Route path={URLS.BASE} component={FlowPage} />
<Redirect to="/" />

View file

@ -1,10 +1,10 @@
import React, { FC } from "react";
import { createPortal } from "react-dom";
import { Route, Switch } from "react-router";
import { TagSidebar } from "~/containers/sidebars/TagSidebar";
import { Authorized } from "~/components/containers/Authorized";
import { SubmitBar } from "~/components/bars/SubmitBar";
import { EditorCreateDialog } from "~/containers/dialogs/EditorCreateDialog";
import React, { FC } from 'react';
import { createPortal } from 'react-dom';
import { Route, Switch } from 'react-router';
import { TagSidebar } from '~/containers/sidebars/TagSidebar';
import { Authorized } from '~/components/containers/Authorized';
import { SubmitBar } from '~/components/bars/SubmitBar';
import { EditorCreateDialog } from '~/containers/dialogs/EditorCreateDialog';
interface IProps {
prefix?: string;

View file

@ -1,20 +1,21 @@
import React, { FC } from "react";
import { NodeDeletedBadge } from "~/components/node/NodeDeletedBadge";
import { Group } from "~/components/containers/Group";
import { Padder } from "~/components/containers/Padder";
import { NodeCommentForm } from "~/components/node/NodeCommentForm";
import { NodeRelatedBlock } from "~/components/node/NodeRelatedBlock";
import { useNodeBlocks } from "~/hooks/node/useNodeBlocks";
import { NodeTagsBlock } from "~/components/node/NodeTagsBlock";
import StickyBox from "react-sticky-box";
import styles from "./styles.module.scss";
import { NodeAuthorBlock } from "~/components/node/NodeAuthorBlock";
import { useNodeContext } from "~/utils/context/NodeContextProvider";
import { useCommentContext } from "~/utils/context/CommentContextProvider";
import { NodeNoComments } from "~/components/node/NodeNoComments";
import { NodeComments } from "~/containers/node/NodeComments";
import { useUserContext } from "~/utils/context/UserContextProvider";
import { useNodeRelatedContext } from "~/utils/context/NodeRelatedContextProvider";
import React, { FC } from 'react';
import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge';
import { Group } from '~/components/containers/Group';
import { Padder } from '~/components/containers/Padder';
import { NodeCommentForm } from '~/components/node/NodeCommentForm';
import { NodeRelatedBlock } from '~/components/node/NodeRelatedBlock';
import { useNodeBlocks } from '~/hooks/node/useNodeBlocks';
import { NodeTagsBlock } from '~/components/node/NodeTagsBlock';
import StickyBox from 'react-sticky-box';
import styles from './styles.module.scss';
import { NodeAuthorBlock } from '~/components/node/NodeAuthorBlock';
import { useNodeContext } from '~/utils/context/NodeContextProvider';
import { useCommentContext } from '~/utils/context/CommentContextProvider';
import { NodeNoComments } from '~/components/node/NodeNoComments';
import { NodeComments } from '~/containers/node/NodeComments';
import { useUserContext } from '~/utils/context/UserContextProvider';
import { useNodeRelatedContext } from '~/utils/context/NodeRelatedContextProvider';
import { useAuthProvider } from '~/utils/providers/AuthProvider';
interface IProps {
commentsOrder: 'ASC' | 'DESC';
@ -26,6 +27,7 @@ const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => {
const { comments, isLoading: isLoadingComments, onSaveComment } = useCommentContext();
const { related, isLoading: isLoadingRelated } = useNodeRelatedContext();
const { inline } = useNodeBlocks(node, isLoading);
const { isUser } = useAuthProvider();
if (node.deleted_at) {
return <NodeDeletedBadge />;
@ -44,7 +46,7 @@ const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => {
<NodeComments order={commentsOrder} />
)}
{user.is_user && !isLoading && (
{isUser && !isLoading && (
<NodeCommentForm nodeId={node.id} saveComment={onSaveComment} user={user} />
)}
</Group>

View file

@ -1,13 +1,13 @@
import React, { FC, memo, useMemo } from "react";
import React, { FC, memo, useMemo } from 'react';
import styles from "./styles.module.scss";
import { ICommentGroup } from "~/redux/types";
import { canEditComment } from "~/utils/node";
import { useGrouppedComments } from "~/hooks/node/useGrouppedComments";
import { useCommentContext } from "~/utils/context/CommentContextProvider";
import { Comment } from "~/components/comment/Comment";
import { useUserContext } from "~/utils/context/UserContextProvider";
import { useNodeContext } from "~/utils/context/NodeContextProvider";
import styles from './styles.module.scss';
import { ICommentGroup } from '~/types';
import { canEditComment } from '~/utils/node';
import { useGrouppedComments } from '~/hooks/node/useGrouppedComments';
import { useCommentContext } from '~/utils/context/CommentContextProvider';
import { Comment } from '~/components/comment/Comment';
import { useUserContext } from '~/utils/context/UserContextProvider';
import { useNodeContext } from '~/utils/context/NodeContextProvider';
interface IProps {
order: 'ASC' | 'DESC';

View file

@ -1,6 +1,6 @@
import React, { VFC } from "react";
import { PlayerBar } from "~/components/bars/PlayerBar";
import { useAudioPlayer } from "~/utils/providers/AudioPlayerProvider";
import React, { VFC } from 'react';
import { PlayerBar } from '~/components/bars/PlayerBar';
import { useAudioPlayer } from '~/utils/providers/AudioPlayerProvider';
interface PlayerViewProps {}

View file

@ -0,0 +1,89 @@
import React, { FC, Fragment } from 'react';
import styles from './styles.module.scss';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { Icon } from '~/components/input/Icon';
import { Button } from '~/components/input/Button';
import { Group } from '~/components/containers/Group';
import { useOAuth } from '~/hooks/auth/useOAuth';
import { SOCIAL_ICONS } from '~/constants/auth/socials';
type ProfileAccountsProps = {};
const ProfileAccounts: FC<ProfileAccountsProps> = () => {
const { isLoading, accounts, dropAccount, openOauthWindow } = useOAuth();
return (
<Group className={styles.wrap}>
<Group className={styles.info}>
<p>
Ты можешь входить в Убежище, используя аккаунты на других сайтах вместо ввода логина и
пароля.
</p>
<p>
Мы честно украдём и будем хранить твои имя, фото и адрес на этом сайте, но никому о них не
расскажем.
</p>
</Group>
{isLoading && (
<div className={styles.loader}>
{[...new Array(accounts.length || 1)].map((_, i) => (
<Fragment key={i}>
<Placeholder width="50%" />
<Placeholder width="auto" />
</Fragment>
))}
</div>
)}
{!isLoading && accounts.length > 0 && (
<div className={styles.list}>
{!isLoading &&
accounts.map(it => (
<div className={styles.account} key={`${it.provider}-${it.id}`}>
<div
className={styles.account__photo}
style={{ backgroundImage: it.photo ? `url(${it.photo})` : 'none' }}
>
<div className={styles.account__provider}>
<Icon icon={SOCIAL_ICONS[it.provider]} size={12} />
</div>
</div>
<div className={styles.account__name}>{it.name || it.id}</div>
<div className={styles.account__drop}>
<Icon icon="close" size={22} onClick={() => dropAccount(it.provider, it.id)} />
</div>
</div>
))}
</div>
)}
<Group horizontal className={styles.buttons}>
<Button
size="small"
type="button"
iconLeft="vk"
color="gray"
onClick={() => openOauthWindow('vkontakte')}
>
Вконтакте
</Button>
<Button
size="small"
type="button"
iconLeft="google"
color="gray"
onClick={() => openOauthWindow('google')}
>
Google
</Button>
</Group>
</Group>
);
};
export { ProfileAccounts };

View file

@ -0,0 +1,87 @@
@import "../../../styles/variables";
.wrap {
}
.list {
border-radius: $radius;
background: transparentize(white, 0.95);
}
.buttons {
background: transparentize(black, 0.8);
border-radius: $radius;
padding: $gap / 2;
display: flex;
align-items: center;
justify-content: flex-end;
}
.add {
//background-color: $content_bg !important;
}
.loader {
display: grid;
grid-row-gap: $gap;
grid-column-gap: $gap * 4;
grid-template-columns: 1fr 32px;
& > div {
height: 48px;
width: auto;
}
}
.account {
display: grid;
grid-template-columns: 20px auto 20px;
grid-column-gap: $gap * 1.5;
align-items: center;
border-bottom: 1px solid transparentize(white, 0.9);
padding: $gap;
&:last-child {
border-bottom: none;
}
&__photo {
width: 28px;
height: 28px;
background: 50% 50% no-repeat;
background-size: cover;
border-radius: 2px;
position: relative;
background: $content_bg;
}
&__provider {
position: absolute;
right: -2px;
bottom: -8px;
background: $content_bg;
}
&__name {
font: $font_16_semibold;
padding-left: $gap / 2;
}
&__drop {
cursor: pointer;
opacity: 0.5;
transition: opacity 0.25s;
fill: $red;
display: flex;
align-items: center;
&:hover {
opacity: 1;
}
}
}
.info {
padding: $gap $gap / 2;
font: $font_14_regular;
}

View file

@ -1,11 +1,13 @@
import React, { FC } from "react";
import styles from "./styles.module.scss";
import { Group } from "~/components/containers/Group";
import { Placeholder } from "~/components/placeholders/Placeholder";
import { getPrettyDate } from "~/utils/dom";
import { ProfileTabs } from "../ProfileTabs";
import { ProfileAvatar } from "~/components/profile/ProfileAvatar";
import { useProfileContext } from "~/utils/providers/ProfileProvider";
import React, { FC } from 'react';
import styles from './styles.module.scss';
import { Group } from '~/components/containers/Group';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { getPrettyDate } from '~/utils/dom';
import { ProfileTabs } from '../ProfileTabs';
import { ProfileAvatar } from '~/components/profile/ProfileAvatar';
import { useProfileContext } from '~/utils/providers/ProfileProvider';
import { usePatchUser } from '~/hooks/auth/usePatchUser';
import { useUser } from '~/hooks/auth/useUser';
interface IProps {
isLoading?: boolean;
@ -13,20 +15,27 @@ interface IProps {
}
const ProfileInfo: FC<IProps> = ({ isOwn }) => {
const { updatePhoto, profile, isLoading } = useProfileContext();
const { user } = useUser();
const { updatePhoto } = usePatchUser();
const { profile, isLoading } = useProfileContext();
const photo = isOwn ? user.photo : profile.photo;
const fullName = isOwn ? user.fullname : profile.fullname;
const lastSeen = isOwn ? new Date().toISOString() : profile.last_seen;
const username = isOwn ? user.username : profile.username;
return (
<div>
<Group className={styles.wrap} horizontal>
<ProfileAvatar canEdit={isOwn} onChangePhoto={updatePhoto} photo={profile.photo} />
<ProfileAvatar canEdit={isOwn} onChangePhoto={updatePhoto} photo={photo} />
<div className={styles.field}>
<div className={styles.name}>
{isLoading ? <Placeholder width="80%" /> : profile?.fullname || profile?.username}
{isLoading ? <Placeholder width="80%" /> : fullName || username}
</div>
<div className={styles.description}>
{isLoading ? <Placeholder /> : getPrettyDate(profile?.last_seen)}
{isLoading ? <Placeholder /> : getPrettyDate(lastSeen)}
</div>
</div>
</Group>

View file

@ -1,6 +1,6 @@
import React, { FC } from "react";
import styles from "./styles.module.scss";
import { LoaderCircle } from "~/components/input/LoaderCircle";
import React, { FC } from 'react';
import styles from './styles.module.scss';
import { LoaderCircle } from '~/components/input/LoaderCircle';
interface IProps {}

View file

@ -3,12 +3,12 @@ import styles from './styles.module.scss';
import { Message } from '~/components/profile/Message';
import { NodeNoComments } from '~/components/node/NodeNoComments';
import { useMessages } from '~/hooks/messages/useMessages';
import { useUser } from '~/hooks/user/userUser';
import { useUser } from '~/hooks/auth/useUser';
import { useProfileContext } from '~/utils/providers/ProfileProvider';
const ProfileMessages: FC = () => {
const { profile, isLoading: isLoadingProfile } = useProfileContext();
const user = useUser();
const { user } = useUser();
const { messages, isLoading: isLoadingMessages } = useMessages(profile?.username || '');
if (!messages.length || isLoadingProfile)

View file

@ -1,12 +1,12 @@
import React, { FC } from "react";
import { IUser } from "~/redux/auth/types";
import { formatText } from "~/utils/dom";
import { PRESETS } from "~/constants/urls";
import { Placeholder } from "~/components/placeholders/Placeholder";
import React, { FC } from 'react';
import { IUser } from '~/types/auth';
import { formatText } from '~/utils/dom';
import { PRESETS } from '~/constants/urls';
import { Placeholder } from '~/components/placeholders/Placeholder';
import styles from "./styles.module.scss";
import { Avatar } from "~/components/common/Avatar";
import { Markdown } from "~/components/containers/Markdown";
import styles from './styles.module.scss';
import { Avatar } from '~/components/common/Avatar';
import { Markdown } from '~/components/containers/Markdown';
interface IProps {
profile: IUser;

View file

@ -1,17 +1,10 @@
import React, { FC } from "react";
import styles from "./styles.module.scss";
import { StatsRow } from "~/components/common/StatsRow";
import { SubTitle } from "~/components/common/SubTitle";
import React, { FC } from 'react';
import styles from './styles.module.scss';
import { StatsRow } from '~/components/common/StatsRow';
import { SubTitle } from '~/components/common/SubTitle';
interface Props {}
const Row: FC<{ count: number; title: string; icon?: string }> = ({ count, title, icon }) => (
<div className={styles.row}>
<div className={styles.title}>{title}</div>
<div className={styles.counter}>{count > 999 ? '999+' : count}</div>
</div>
);
const ProfilePageStats: FC<Props> = () => (
<div className={styles.wrap}>
<SubTitle>Ачивментс</SubTitle>

View file

@ -1,6 +1,6 @@
import React, { FC } from "react";
import styles from "./styles.module.scss";
import { Tabs } from "~/components/dialogs/Tabs";
import React, { FC } from 'react';
import styles from './styles.module.scss';
import { Tabs } from '~/components/dialogs/Tabs';
interface IProps {
is_own: boolean;

View file

@ -1,8 +1,8 @@
import React, { FC, useEffect, useRef } from "react";
import styles from "./styles.module.scss";
import { createPortal } from "react-dom";
import { clearAllBodyScrollLocks, disableBodyScroll } from "body-scroll-lock";
import { useCloseOnEscape } from "~/hooks";
import React, { FC, useEffect, useRef } from 'react';
import styles from './styles.module.scss';
import { createPortal } from 'react-dom';
import { clearAllBodyScrollLocks, disableBodyScroll } from 'body-scroll-lock';
import { useCloseOnEscape } from '~/hooks';
interface IProps {
onClose?: () => void;

View file

@ -17,7 +17,7 @@ const TagSidebar: VFC = () => {
} = useRouteMatch<{ tag: string }>();
const history = useHistory();
const basePath = url.replace(new RegExp(`\/tag\/${tag}$`), '');
const basePath = url.replace(new RegExp(`/tag/${tag}$`), '');
const onClose = useCallback(() => history.push(basePath), [basePath, history]);
const { nodes, hasMore, isLoading, loadMore } = useTagNodes(tag);
const title = useMemo(() => decodeURIComponent(tag), [tag]);

View file

@ -1,10 +1,10 @@
import React, { FC, HTMLAttributes, useCallback, useMemo, useState } from "react";
import { TagField } from "~/components/containers/TagField";
import { ITag } from "~/redux/types";
import { uniq } from "ramda";
import { Tag } from "~/components/tags/Tag";
import { TagInput } from "~/containers/tags/TagInput";
import { separateTags } from "~/utils/tag";
import React, { FC, HTMLAttributes, useCallback, useMemo, useState } from 'react';
import { TagField } from '~/components/containers/TagField';
import { ITag } from '~/types';
import { uniq } from 'ramda';
import { Tag } from '~/components/tags/Tag';
import { TagInput } from '~/containers/tags/TagInput';
import { separateTags } from '~/utils/tag';
type IProps = HTMLAttributes<HTMLDivElement> & {
tags: Partial<ITag>[];