mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
refactor editos
This commit is contained in:
parent
03ddb1862c
commit
5e9c111e0f
149 changed files with 416 additions and 317 deletions
|
@ -1,13 +1,12 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { Grid } from '~/components/containers/Grid';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Grid } from '~/components/common/Grid';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { OAuthProvider } from '~/types/auth';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
|
||||
interface IProps {
|
||||
openOauthWindow: (provider: OAuthProvider) => void;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { FC, useCallback, useRef } from 'react';
|
||||
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Padder } from '~/components/containers/Padder';
|
||||
import { BetterScrollDialog } from '~/components/dialogs/BetterScrollDialog';
|
||||
import { DialogTitle } from '~/components/dialogs/DialogTitle';
|
||||
import { BetterScrollDialog } from '~/components/common/BetterScrollDialog';
|
||||
import { DialogTitle } from '~/components/common/DialogTitle';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Padder } from '~/components/common/Padder';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { InputText } from '~/components/input/InputText';
|
||||
import { Dialog } from '~/constants/modal';
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React, { FC, useCallback, useState } from 'react';
|
||||
|
||||
import { apiLoginWithSocial } from '~/api/auth';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Padder } from '~/components/containers/Padder';
|
||||
import { BetterScrollDialog } from '~/components/dialogs/BetterScrollDialog';
|
||||
import { DialogTitle } from '~/components/dialogs/DialogTitle';
|
||||
import { BetterScrollDialog } from '~/components/common/BetterScrollDialog';
|
||||
import { DialogTitle } from '~/components/common/DialogTitle';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Padder } from '~/components/common/Padder';
|
||||
import { InputText } from '~/components/input/InputText';
|
||||
import { Toggle } from '~/components/input/Toggle';
|
||||
import { getRandomPhrase } from '~/constants/phrases';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { VFC } from 'react';
|
||||
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { ERROR_LITERAL, ERRORS } from '~/constants/errors';
|
||||
|
@ -12,7 +12,10 @@ interface RestoreInvalidCodeProps {
|
|||
onClose: () => void;
|
||||
}
|
||||
|
||||
const RestoreInvalidCode: VFC<RestoreInvalidCodeProps> = ({ error, onClose }) => (
|
||||
const RestoreInvalidCode: VFC<RestoreInvalidCodeProps> = ({
|
||||
error,
|
||||
onClose,
|
||||
}) => (
|
||||
<Group className={styles.error_shade}>
|
||||
<Icon icon="close" size={64} />
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { VFC } from 'react';
|
||||
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { apiRestoreCode } from '~/api/auth';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { InputText } from '~/components/input/InputText';
|
||||
import { useCloseOnEscape } from '~/hooks';
|
||||
|
@ -9,7 +9,7 @@ import { useRestoreCode } from '~/hooks/auth/useRestoreCode';
|
|||
import { useRestorePasswordForm } from '~/hooks/auth/useRestorePasswordForm';
|
||||
import { DialogComponentProps } from '~/types/modal';
|
||||
|
||||
import { BetterScrollDialog } from '../../../components/dialogs/BetterScrollDialog';
|
||||
import { BetterScrollDialog } from '../../../components/common/BetterScrollDialog';
|
||||
|
||||
import { RestoreInvalidCode } from './components/RestoreInvalidCode';
|
||||
import { RestoreSuccess } from './components/RestoreSuccess';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { VFC } from 'react';
|
||||
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import React, { useCallback, useMemo, useState, VFC } from 'react';
|
||||
|
||||
import { apiRequestRestoreCode } from '~/api/auth';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { InputText } from '~/components/input/InputText';
|
||||
import { useCloseOnEscape } from '~/hooks';
|
||||
import { useRestoreRequestForm } from '~/hooks/auth/useRestoreRequestForm';
|
||||
import { DialogComponentProps } from '~/types/modal';
|
||||
|
||||
import { BetterScrollDialog } from '../../../components/dialogs/BetterScrollDialog';
|
||||
import { BetterScrollDialog } from '../../../components/common/BetterScrollDialog';
|
||||
|
||||
import { RestoreSent } from './components/RestoreSent';
|
||||
import styles from './styles.module.scss';
|
||||
|
|
|
@ -2,12 +2,12 @@ import React, { FC, useCallback, useMemo } from 'react';
|
|||
|
||||
import { TelegramUser } from '@v9v/ts-react-telegram-login';
|
||||
|
||||
import { Padder } from '~/components/containers/Padder';
|
||||
import { Padder } from '~/components/common/Padder';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { useTelegramAccount } from '~/hooks/auth/useTelegramAccount';
|
||||
import { DialogComponentProps } from '~/types/modal';
|
||||
|
||||
import { BetterScrollDialog } from '../../../components/dialogs/BetterScrollDialog';
|
||||
import { BetterScrollDialog } from '../../../components/common/BetterScrollDialog';
|
||||
import { TelegramLoginForm } from '../LoginDialog/components/TelegramLoginForm/index';
|
||||
|
||||
interface TelegramAttachDialogProps extends DialogComponentProps {}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Footer } from '~/components/main/Footer';
|
||||
import { NodeNoComments } from '~/components/node/NodeNoComments';
|
||||
import { NodeCommentFormSSR } from '~/containers/node/NodeCommentForm/ssr';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { Padder } from '~/components/containers/Padder';
|
||||
import { Padder } from '~/components/common/Padder';
|
||||
import { Button } from '~/components/input/Button';
|
||||
|
||||
import { BorisContactItem } from './components/BorisContactItem';
|
||||
|
|
|
@ -2,10 +2,10 @@ import React, { FC, ReactNode } from 'react';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Card, CardProps } from '~/components/common/Card';
|
||||
import { Filler } from '~/components/common/Filler';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { SubTitle } from '~/components/common/SubTitle';
|
||||
import { Card, CardProps } from '~/components/containers/Card';
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, { VFC } from 'react';
|
|||
import classNames from 'classnames';
|
||||
import { addYears, differenceInMonths, differenceInYears } from 'date-fns';
|
||||
|
||||
import { CardProps } from '~/components/containers/Card';
|
||||
import { CardProps } from '~/components/common/Card';
|
||||
|
||||
import { StatsCard } from '../StatsCard';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { VFC } from 'react';
|
||||
|
||||
import { CardProps } from '~/components/containers/Card';
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { CardProps } from '~/components/common/Card';
|
||||
import { Filler } from '~/components/common/Filler';
|
||||
|
||||
import { BasicCurveChart } from '../BasicCurveChart';
|
||||
import { StatsCard } from '../StatsCard';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { SuperPowersToggle } from '~/containers/auth/SuperPowersToggle';
|
||||
import { useTelegramAccount } from '~/hooks/auth/useTelegramAccount';
|
||||
import { BorisUsageStats } from '~/types/boris';
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
import { Anchor } from '~/components/common/Anchor';
|
||||
import { Card } from '~/components/containers/Card';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Card } from '~/components/common/Card';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import markdown from '~/styles/common/markdown.module.scss';
|
||||
|
||||
export interface BorisSuperpowersProps {}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { FC, useCallback, useState } from 'react';
|
|||
import { FormikProvider } from 'formik';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { Filler } from '~/components/common/Filler';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { ERROR_LITERAL } from '~/constants/errors';
|
||||
import { EMPTY_COMMENT } from '~/constants/node';
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
import { UploadDropzone } from '~/components/upload/UploadDropzone';
|
||||
import { NodeEditorProps } from '~/types/node';
|
||||
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
|
||||
|
||||
import { AudioGrid } from '../AudioGrid';
|
||||
import { ImageGrid } from '../ImageGrid';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
const AudioEditor: FC<NodeEditorProps> = () => {
|
||||
const {
|
||||
filesImages,
|
||||
filesAudios,
|
||||
uploadFiles,
|
||||
setImages,
|
||||
setAudios,
|
||||
pendingAudios,
|
||||
pendingImages,
|
||||
} = useUploaderContext()!;
|
||||
|
||||
return (
|
||||
<UploadDropzone onUpload={uploadFiles} helperClassName={styles.dropzone}>
|
||||
<div className={styles.wrap}>
|
||||
<ImageGrid
|
||||
files={filesImages}
|
||||
setFiles={setImages}
|
||||
locked={pendingImages}
|
||||
/>
|
||||
|
||||
<AudioGrid
|
||||
files={filesAudios}
|
||||
setFiles={setAudios}
|
||||
locked={pendingAudios}
|
||||
/>
|
||||
</div>
|
||||
</UploadDropzone>
|
||||
);
|
||||
};
|
||||
|
||||
export { AudioEditor };
|
|
@ -0,0 +1,6 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.wrap {
|
||||
padding-bottom: $upload_button_height + $gap;
|
||||
min-height: 200px;
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import { SortableAudioGrid } from '~/components/sortable/SortableAudioGrid';
|
||||
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
||||
import { IFile } from '~/types';
|
||||
|
||||
interface IProps {
|
||||
files: IFile[];
|
||||
setFiles: (val: IFile[]) => void;
|
||||
locked: UploadStatus[];
|
||||
}
|
||||
|
||||
const AudioGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
||||
const onMove = useCallback(
|
||||
(newFiles: IFile[]) => {
|
||||
setFiles(newFiles);
|
||||
},
|
||||
[setFiles],
|
||||
);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(remove_id: IFile['id']) => {
|
||||
setFiles(files.filter((file) => file && file.id !== remove_id));
|
||||
},
|
||||
[setFiles, files],
|
||||
);
|
||||
|
||||
const onTitleChange = useCallback(
|
||||
(changeId: IFile['id'], title: string) => {
|
||||
setFiles(
|
||||
files.map((file) =>
|
||||
file && file.id === changeId
|
||||
? { ...file, metadata: { ...file.metadata, title } }
|
||||
: file,
|
||||
),
|
||||
);
|
||||
},
|
||||
[setFiles, files],
|
||||
);
|
||||
|
||||
return (
|
||||
<SortableAudioGrid
|
||||
onDelete={onDrop}
|
||||
onTitleChange={onTitleChange}
|
||||
onSortEnd={onMove}
|
||||
items={files}
|
||||
locked={locked}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { AudioGrid };
|
|
@ -0,0 +1,18 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { UploadType } from '~/constants/uploads';
|
||||
import { EditorUploadButton } from '~/containers/dialogs/EditorDialog/components/EditorButtons/components/EditorActionsPanel/components/EditorUploadButton';
|
||||
import { IEditorComponentProps } from '~/types/node';
|
||||
|
||||
type IProps = IEditorComponentProps & {};
|
||||
|
||||
const EditorAudioUploadButton: FC<IProps> = () => (
|
||||
<EditorUploadButton
|
||||
accept="audio/*"
|
||||
icon="audio"
|
||||
type={UploadType.Audio}
|
||||
label="Добавить аудио"
|
||||
/>
|
||||
);
|
||||
|
||||
export { EditorAudioUploadButton };
|
|
@ -0,0 +1,12 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { Filler } from '~/components/common/Filler';
|
||||
import { IEditorComponentProps } from '~/types/node';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
type IProps = IEditorComponentProps & {};
|
||||
|
||||
const EditorFiller: FC<IProps> = () => <Filler className={styles.filler} />;
|
||||
|
||||
export { EditorFiller };
|
|
@ -0,0 +1,4 @@
|
|||
.filler {
|
||||
touch-action: none;
|
||||
pointer-events: none;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { UploadType } from '~/constants/uploads';
|
||||
import { IEditorComponentProps } from '~/types/node';
|
||||
|
||||
import { EditorUploadButton } from '../EditorUploadButton';
|
||||
|
||||
type IProps = IEditorComponentProps & {};
|
||||
|
||||
const EditorImageUploadButton: FC<IProps> = () => (
|
||||
<EditorUploadButton
|
||||
accept="image/*"
|
||||
icon="image"
|
||||
type={UploadType.Image}
|
||||
label="Добавить фоточек"
|
||||
/>
|
||||
);
|
||||
|
||||
export { EditorImageUploadButton };
|
|
@ -0,0 +1,45 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
||||
import { IEditorComponentProps } from '~/types/node';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface IProps extends IEditorComponentProps {}
|
||||
|
||||
const EditorPublicSwitch: FC<IProps> = () => {
|
||||
const { values, setFieldValue } = useNodeFormContext();
|
||||
|
||||
const onChange = useCallback(
|
||||
() => setFieldValue('is_promoted', !values.is_promoted),
|
||||
[values.is_promoted, setFieldValue],
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
color={values.is_promoted ? 'flow' : 'lab'}
|
||||
type="button"
|
||||
size="giant"
|
||||
label={
|
||||
values.is_promoted
|
||||
? 'Доступно всем на главной странице'
|
||||
: 'Видно только сотрудникам в лаборатории'
|
||||
}
|
||||
onClick={onChange}
|
||||
className={styles.button}
|
||||
round
|
||||
>
|
||||
{values.is_promoted ? (
|
||||
<Icon icon="waves" size={24} />
|
||||
) : (
|
||||
<div className={styles.lab_wrapper}>
|
||||
<Icon icon="lab" size={24} />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export { EditorPublicSwitch };
|
|
@ -0,0 +1,63 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
@keyframes move_1 {
|
||||
0% {
|
||||
transform: scale(0) translate(0, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1) translate(5px, -5px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1.2) translate(-5px, -10px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes move_2 {
|
||||
0% {
|
||||
transform: scale(0) translate(0, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1) translate(-5px, -5px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1.6) translate(5px, -10px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
|
||||
}
|
||||
|
||||
.lab_wrapper {
|
||||
position: relative;
|
||||
bottom: -2px;
|
||||
|
||||
.button:hover & {
|
||||
&:before,&:after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 10px;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
box-shadow: white 0 0 0 2px;
|
||||
border-radius: 4px;
|
||||
animation: move_1 0.5s infinite linear;
|
||||
}
|
||||
|
||||
&:after {
|
||||
animation: move_2 0.5s -0.25s infinite linear;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import React, { ChangeEvent, FC, useCallback } from 'react';
|
||||
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { UploadType } from '~/constants/uploads';
|
||||
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
||||
import { IEditorComponentProps } from '~/types/node';
|
||||
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
|
||||
import { getFileType } from '~/utils/uploader';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
type IProps = IEditorComponentProps & {
|
||||
accept?: string;
|
||||
icon?: string;
|
||||
type?: UploadType;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
const EditorUploadButton: FC<IProps> = ({
|
||||
accept = 'image/*',
|
||||
icon = 'plus',
|
||||
type = UploadType.Image,
|
||||
label,
|
||||
}) => {
|
||||
const { uploadFiles } = useUploaderContext()!;
|
||||
const { values } = useNodeFormContext();
|
||||
|
||||
const onInputChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const files = Array.from(event.target.files || []).filter(
|
||||
(file) => !type || getFileType(file) === type,
|
||||
);
|
||||
|
||||
uploadFiles(files);
|
||||
},
|
||||
[type, uploadFiles],
|
||||
);
|
||||
|
||||
const color = values.is_promoted ? 'flow' : 'lab';
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
round
|
||||
size="giant"
|
||||
className={styles.wrap}
|
||||
label={label}
|
||||
color={color}
|
||||
>
|
||||
<Icon icon={icon} size={24} />
|
||||
<input type="file" onChange={onInputChange} accept={accept} multiple />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export { EditorUploadButton };
|
|
@ -0,0 +1,27 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.wrap {
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import React, { ChangeEvent, FC, useCallback, useEffect } from 'react';
|
||||
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { UploadSubject, UploadTarget, UploadType } from '~/constants/uploads';
|
||||
import { imagePresets } from '~/constants/urls';
|
||||
import { useUploader } from '~/hooks/data/useUploader';
|
||||
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
||||
import { IEditorComponentProps } from '~/types/node';
|
||||
import { getURL } from '~/utils/dom';
|
||||
import { getFileType } from '~/utils/uploader';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
type IProps = IEditorComponentProps & {};
|
||||
|
||||
const EditorUploadCoverButton: FC<IProps> = () => {
|
||||
const { values, setFieldValue } = useNodeFormContext();
|
||||
const { uploadFile, files, pendingImages } = useUploader(
|
||||
UploadSubject.Editor,
|
||||
UploadTarget.Nodes,
|
||||
values.cover ? [values.cover] : [],
|
||||
);
|
||||
|
||||
const background = values.cover
|
||||
? getURL(values.cover, imagePresets['300'])
|
||||
: null;
|
||||
const preview = pendingImages?.[0]?.thumbnail || '';
|
||||
|
||||
const onDropCover = useCallback(() => {
|
||||
setFieldValue('cover', null);
|
||||
}, [setFieldValue]);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files || [])
|
||||
.filter((file) => getFileType(file) === UploadType.Image)
|
||||
.slice(0, 1);
|
||||
|
||||
const result = await uploadFile(files[0]);
|
||||
setFieldValue('cover', result);
|
||||
},
|
||||
[uploadFile, setFieldValue],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFieldValue('cover', files[files.length - 1]);
|
||||
}, [files, setFieldValue]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<div
|
||||
className={styles.preview}
|
||||
style={{ backgroundImage: `url("${preview || background}")` }}
|
||||
>
|
||||
<div className={styles.input}>
|
||||
{!values.cover && <span>ОБЛОЖКА</span>}
|
||||
<input type="file" accept="image/*" onChange={onInputChange} />
|
||||
</div>
|
||||
|
||||
{values.cover && (
|
||||
<div className={styles.button} onClick={onDropCover}>
|
||||
<Icon icon="close" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { EditorUploadCoverButton };
|
|
@ -0,0 +1,83 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.wrap {
|
||||
@include outer_shadow();
|
||||
|
||||
height: $upload_button_height;
|
||||
border-radius: ($upload_button_height * 0.5) !important;
|
||||
position: relative;
|
||||
border-radius: $radius;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.5s;
|
||||
background: $content_bg_lighter;
|
||||
flex: 0 1 $upload_button_height * 4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font: $font_16_medium;
|
||||
text-shadow: rgba(0, 0, 0, 0.5) 0 1px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
border-radius: ($upload_button_height * 0.5) !important;
|
||||
background: 50% 50% no-repeat;
|
||||
background-size: cover;
|
||||
will-change: transform;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: $upload_button_height;
|
||||
flex: 0 0 $upload_button_height;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: inset rgba(255, 255, 255, 0.05) 1px 1px, rgba(0, 0, 0, 0.3) -1px 0;
|
||||
border-radius: $upload_button_height;
|
||||
background: $content_bg_lighter;
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
fill: $color_danger;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
import { NODE_TYPES } from '~/constants/node';
|
||||
import { IEditorComponentProps } from '~/types/node';
|
||||
|
||||
import { EditorAudioUploadButton } from '../components/EditorAudioUploadButton';
|
||||
import { EditorFiller } from '../components/EditorFiller';
|
||||
import { EditorImageUploadButton } from '../components/EditorImageUploadButton';
|
||||
import { EditorPublicSwitch } from '../components/EditorPublicSwitch';
|
||||
import { EditorUploadCoverButton } from '../components/EditorUploadCoverButton';
|
||||
|
||||
export const NODE_PANEL_COMPONENTS: Record<
|
||||
string,
|
||||
FC<IEditorComponentProps>[]
|
||||
> = {
|
||||
[NODE_TYPES.TEXT]: [
|
||||
EditorFiller,
|
||||
EditorUploadCoverButton,
|
||||
EditorPublicSwitch,
|
||||
],
|
||||
[NODE_TYPES.VIDEO]: [
|
||||
EditorFiller,
|
||||
EditorUploadCoverButton,
|
||||
EditorPublicSwitch,
|
||||
],
|
||||
[NODE_TYPES.IMAGE]: [
|
||||
EditorImageUploadButton,
|
||||
EditorFiller,
|
||||
EditorUploadCoverButton,
|
||||
EditorPublicSwitch,
|
||||
],
|
||||
[NODE_TYPES.AUDIO]: [
|
||||
EditorAudioUploadButton,
|
||||
EditorImageUploadButton,
|
||||
EditorFiller,
|
||||
EditorUploadCoverButton,
|
||||
EditorPublicSwitch,
|
||||
],
|
||||
[NODE_TYPES.ROOM]: [
|
||||
EditorAudioUploadButton,
|
||||
EditorImageUploadButton,
|
||||
EditorFiller,
|
||||
],
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
import React, { createElement, FC } from 'react';
|
||||
|
||||
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
||||
import { has } from '~/utils/ramda';
|
||||
|
||||
import { NODE_PANEL_COMPONENTS } from './constants';
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
const EditorActionsPanel: FC = () => {
|
||||
const { values } = useNodeFormContext();
|
||||
|
||||
if (!values.type || !has(values.type, NODE_PANEL_COMPONENTS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
{NODE_PANEL_COMPONENTS[values.type] &&
|
||||
NODE_PANEL_COMPONENTS[values.type].map((el, key) =>
|
||||
createElement(el, { key }),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { EditorActionsPanel };
|
|
@ -0,0 +1,30 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.panel {
|
||||
height: 72px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
bottom: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: $gap;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
|
||||
& > * {
|
||||
margin: 0 $gap * 0.5;
|
||||
pointer-events: all;
|
||||
touch-action: auto;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { Filler } from '~/components/common/Filler';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Padder } from '~/components/common/Padder';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { InputText } from '~/components/input/InputText';
|
||||
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
||||
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
||||
|
||||
import { EditorActionsPanel } from './components/EditorActionsPanel';
|
||||
|
||||
const EditorButtons: FC = () => {
|
||||
const { values, handleChange, isSubmitting } = useNodeFormContext();
|
||||
const { isTablet } = useWindowSize();
|
||||
|
||||
return (
|
||||
<Padder style={{ position: 'relative' }}>
|
||||
<EditorActionsPanel />
|
||||
|
||||
<Group horizontal>
|
||||
<Filler>
|
||||
<InputText
|
||||
title="Название"
|
||||
value={values.title}
|
||||
handler={handleChange('title')}
|
||||
autoFocus={!isTablet}
|
||||
maxLength={256}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</Filler>
|
||||
|
||||
<Button
|
||||
title={isTablet ? undefined : 'Сохранить'}
|
||||
iconRight="check"
|
||||
color={values.is_promoted ? 'flow' : 'lab'}
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
/>
|
||||
</Group>
|
||||
</Padder>
|
||||
);
|
||||
};
|
||||
|
||||
export { EditorButtons };
|
|
@ -0,0 +1,37 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Button } from '~/components/input/Button';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface IProps {
|
||||
onApprove: () => void;
|
||||
onDecline: () => void;
|
||||
}
|
||||
|
||||
const EditorConfirmClose: FC<IProps> = ({ onApprove, onDecline }) => (
|
||||
<div className={styles.wrap}>
|
||||
<Group className={styles.content}>
|
||||
<div className={styles.title}>Точно закрыть?</div>
|
||||
<div className={styles.subtitle}>
|
||||
Все изменения будут потеряны, воспоминания затёрты, очевидцы умрут, над
|
||||
миром воссияет ядерный рассвет.
|
||||
</div>
|
||||
|
||||
<div />
|
||||
|
||||
<Group horizontal>
|
||||
<Button color="gray" type="button" onClick={onApprove} autoFocus>
|
||||
Да
|
||||
</Button>
|
||||
|
||||
<Button type="button" onClick={onDecline}>
|
||||
О боже, нет!
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</div>
|
||||
);
|
||||
|
||||
export { EditorConfirmClose };
|
|
@ -0,0 +1,42 @@
|
|||
@import 'src/styles/variables.scss';
|
||||
|
||||
@keyframes appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.wrap {
|
||||
@include blur;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 21;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: appear 0.25s forwards;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-transform: uppercase;
|
||||
font: $font_18_semibold;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font: $font_12_medium;
|
||||
color: $gray_50;
|
||||
max-width: 300px;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { UploadDropzone } from '~/components/upload/UploadDropzone';
|
||||
import { NodeEditorProps } from '~/types/node';
|
||||
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
|
||||
import { values } from '~/utils/ramda';
|
||||
|
||||
import { ImageGrid } from '../ImageGrid';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
type IProps = NodeEditorProps;
|
||||
|
||||
const ImageEditor: FC<IProps> = () => {
|
||||
const { pending, files, setFiles, uploadFiles } = useUploaderContext()!;
|
||||
|
||||
return (
|
||||
<UploadDropzone onUpload={uploadFiles} helperClassName={styles.dropzone}>
|
||||
<div className={styles.wrap}>
|
||||
<ImageGrid files={files} setFiles={setFiles} locked={values(pending)} />
|
||||
</div>
|
||||
</UploadDropzone>
|
||||
);
|
||||
};
|
||||
|
||||
export { ImageEditor };
|
|
@ -0,0 +1,9 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.wrap {
|
||||
min-height: 200px;
|
||||
padding-bottom: $upload_button_height + $gap;
|
||||
}
|
||||
|
||||
div.dropzone {
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import { SortableImageGrid } from '~/components/sortable/SortableImageGrid';
|
||||
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
||||
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
||||
import { IFile } from '~/types';
|
||||
|
||||
interface IProps {
|
||||
files: IFile[];
|
||||
setFiles: (val: IFile[]) => void;
|
||||
locked: UploadStatus[];
|
||||
}
|
||||
|
||||
const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
||||
const { isTablet } = useWindowSize();
|
||||
|
||||
const onMove = useCallback(
|
||||
(newFiles: IFile[]) => {
|
||||
setFiles(newFiles.filter((it) => it));
|
||||
},
|
||||
[setFiles],
|
||||
);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(id: IFile['id']) => {
|
||||
setFiles(files.filter((file) => file && file.id !== id));
|
||||
},
|
||||
[setFiles, files],
|
||||
);
|
||||
|
||||
return (
|
||||
<SortableImageGrid
|
||||
onDelete={onDrop}
|
||||
onSortEnd={onMove}
|
||||
items={files}
|
||||
locked={locked}
|
||||
size={!isTablet ? 220 : (innerWidth - 60) / 2}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { ImageGrid };
|
|
@ -0,0 +1,5 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.helper {
|
||||
opacity: 0.5;
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
import { UploadDropzone } from '~/components/upload/UploadDropzone';
|
||||
import { NodeEditorProps } from '~/types/node';
|
||||
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
|
||||
|
||||
import { AudioGrid } from '../AudioGrid';
|
||||
import { ImageGrid } from '../ImageGrid';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
const RoomEditor: FC<NodeEditorProps> = () => {
|
||||
const {
|
||||
filesImages,
|
||||
filesAudios,
|
||||
uploadFiles,
|
||||
setImages,
|
||||
setAudios,
|
||||
pendingAudios,
|
||||
pendingImages,
|
||||
} = useUploaderContext()!;
|
||||
|
||||
return (
|
||||
<UploadDropzone onUpload={uploadFiles} helperClassName={styles.dropzone}>
|
||||
<div className={styles.wrap}>
|
||||
<ImageGrid
|
||||
files={filesImages}
|
||||
setFiles={setImages}
|
||||
locked={pendingImages}
|
||||
/>
|
||||
|
||||
<AudioGrid
|
||||
files={filesAudios}
|
||||
setFiles={setAudios}
|
||||
locked={pendingAudios}
|
||||
/>
|
||||
</div>
|
||||
</UploadDropzone>
|
||||
);
|
||||
};
|
||||
|
||||
export { RoomEditor };
|
|
@ -0,0 +1,6 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.wrap {
|
||||
padding-bottom: $upload_button_height + $gap;
|
||||
min-height: 200px;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import { Textarea } from '~/components/input/Textarea';
|
||||
import { useRandomPhrase } from '~/constants/phrases';
|
||||
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
||||
import { NodeEditorProps } from '~/types/node';
|
||||
import { path } from '~/utils/ramda';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
type IProps = NodeEditorProps & {};
|
||||
|
||||
const TextEditor: FC<IProps> = () => {
|
||||
const { values, setFieldValue } = useNodeFormContext();
|
||||
const placeholder = useRandomPhrase('SIMPLE');
|
||||
|
||||
const setText = useCallback((text: string) => setFieldValue('blocks', [{ type: 'text', text }]), [
|
||||
setFieldValue,
|
||||
]);
|
||||
|
||||
const text = (path(['blocks', 0, 'text'], values) as string) || '';
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<Textarea value={text} handler={setText} minRows={6} placeholder={placeholder} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { TextEditor };
|
|
@ -0,0 +1,7 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.wrap {
|
||||
& > div {
|
||||
padding-bottom: 64px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { InputText } from '~/components/input/InputText';
|
||||
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
||||
import { NodeEditorProps } from '~/types/node';
|
||||
import { getYoutubeThumb } from '~/utils/dom';
|
||||
import { path } from '~/utils/ramda';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
type IProps = NodeEditorProps & {};
|
||||
|
||||
const VideoEditor: FC<IProps> = () => {
|
||||
const { values, setFieldValue } = useNodeFormContext();
|
||||
|
||||
const setUrl = useCallback((url: string) => setFieldValue('blocks', [{ type: 'video', url }]), [
|
||||
setFieldValue,
|
||||
]);
|
||||
|
||||
const url = (path(['blocks', 0, 'url'], values) as string) || '';
|
||||
const preview = useMemo(() => getYoutubeThumb(url), [url]);
|
||||
const backgroundImage = (preview && `url("${preview}")`) || '';
|
||||
|
||||
return (
|
||||
<div className={styles.preview} style={{ backgroundImage }}>
|
||||
<div className={styles.input_wrap}>
|
||||
<div className={classnames(styles.input, { active: !!preview })}>
|
||||
<InputText value={url} handler={setUrl} placeholder="Адрес видео" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { VideoEditor };
|
|
@ -0,0 +1,38 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.preview {
|
||||
padding-top: 56.25%;
|
||||
position: relative;
|
||||
border-radius: $radius;
|
||||
background: 50% 50% no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.input_wrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.input {
|
||||
// @include outer_shadow();
|
||||
|
||||
flex: 1 0 50%;
|
||||
padding: $gap * 2;
|
||||
border-radius: $radius;
|
||||
background: $content_bg;
|
||||
margin: 20px;
|
||||
|
||||
input {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:global(.active) {
|
||||
background: $color_danger;
|
||||
}
|
||||
}
|
31
src/containers/dialogs/EditorDialog/constants/index.ts
Normal file
31
src/containers/dialogs/EditorDialog/constants/index.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
import { NODE_TYPES } from '~/constants/node';
|
||||
import { INode } from '~/types';
|
||||
import { NodeEditorProps } from '~/types/node';
|
||||
|
||||
import { AudioEditor } from '../components/AudioEditor';
|
||||
import { ImageEditor } from '../components/ImageEditor';
|
||||
import { RoomEditor } from '../components/RoomEditor';
|
||||
import { TextEditor } from '../components/TextEditor';
|
||||
import { VideoEditor } from '../components/VideoEditor';
|
||||
|
||||
export const NODE_EDITORS: Record<
|
||||
typeof NODE_TYPES[keyof typeof NODE_TYPES],
|
||||
FC<NodeEditorProps>
|
||||
> = {
|
||||
[NODE_TYPES.IMAGE]: ImageEditor,
|
||||
[NODE_TYPES.TEXT]: TextEditor,
|
||||
[NODE_TYPES.VIDEO]: VideoEditor,
|
||||
[NODE_TYPES.AUDIO]: AudioEditor,
|
||||
[NODE_TYPES.ROOM]: RoomEditor,
|
||||
};
|
||||
|
||||
export const NODE_EDITOR_DATA: Record<
|
||||
typeof NODE_TYPES[keyof typeof NODE_TYPES],
|
||||
Partial<INode>
|
||||
> = {
|
||||
[NODE_TYPES.TEXT]: {
|
||||
blocks: [{ text: '', type: 'text' }],
|
||||
},
|
||||
};
|
|
@ -1,13 +1,16 @@
|
|||
import React, { createElement, FC, useCallback, useMemo, useState } from 'react';
|
||||
import React, {
|
||||
createElement,
|
||||
FC,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { FormikProvider } from 'formik';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
|
||||
import { BetterScrollDialog } from '~/components/dialogs/BetterScrollDialog';
|
||||
import { EditorButtons } from '~/components/editors/EditorButtons';
|
||||
import { EditorConfirmClose } from '~/components/editors/EditorConfirmClose';
|
||||
import { NODE_EDITORS } from '~/constants/node';
|
||||
import { BetterScrollDialog } from '~/components/common/BetterScrollDialog';
|
||||
import { CoverBackdrop } from '~/components/common/CoverBackdrop';
|
||||
import { UploadSubject, UploadTarget } from '~/constants/uploads';
|
||||
import { useCloseOnEscape } from '~/hooks';
|
||||
import { useUploader } from '~/hooks/data/useUploader';
|
||||
|
@ -17,6 +20,9 @@ import { DialogComponentProps } from '~/types/modal';
|
|||
import { UploaderContextProvider } from '~/utils/context/UploaderContextProvider';
|
||||
import { prop } from '~/utils/ramda';
|
||||
|
||||
import { EditorButtons } from './components/EditorButtons';
|
||||
import { EditorConfirmClose } from './components/EditorConfirmClose';
|
||||
import { NODE_EDITORS } from './constants/';
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface Props extends DialogComponentProps {
|
||||
|
@ -24,61 +30,73 @@ interface Props extends DialogComponentProps {
|
|||
onSubmit: (node: INode) => Promise<unknown>;
|
||||
}
|
||||
|
||||
const EditorDialog: FC<Props> = observer(({ node, onRequestClose, onSubmit }) => {
|
||||
const [isConfirmModalShown, setConfirmModalShown] = useState(false);
|
||||
const EditorDialog: FC<Props> = observer(
|
||||
({ node, onRequestClose, onSubmit }) => {
|
||||
const [isConfirmModalShown, setConfirmModalShown] = useState(false);
|
||||
|
||||
const uploader = useUploader(UploadSubject.Editor, UploadTarget.Nodes, node.files);
|
||||
const formik = useNodeFormFormik(node, uploader, onRequestClose, onSubmit);
|
||||
const { values, handleSubmit, dirty } = formik;
|
||||
const uploader = useUploader(
|
||||
UploadSubject.Editor,
|
||||
UploadTarget.Nodes,
|
||||
node.files,
|
||||
);
|
||||
const formik = useNodeFormFormik(node, uploader, onRequestClose, onSubmit);
|
||||
const { values, handleSubmit, dirty } = formik;
|
||||
|
||||
const component = useMemo(() => node.type && prop(node.type, NODE_EDITORS), [node.type]);
|
||||
const component = useMemo(
|
||||
() => node.type && prop(node.type, NODE_EDITORS),
|
||||
[node.type],
|
||||
);
|
||||
|
||||
const closeConfirmModal = useCallback(() => {
|
||||
setConfirmModalShown(false);
|
||||
}, [setConfirmModalShown]);
|
||||
const closeConfirmModal = useCallback(() => {
|
||||
setConfirmModalShown(false);
|
||||
}, [setConfirmModalShown]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
if (!dirty) {
|
||||
onRequestClose();
|
||||
return;
|
||||
const onClose = useCallback(() => {
|
||||
if (!dirty) {
|
||||
onRequestClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isConfirmModalShown) {
|
||||
closeConfirmModal();
|
||||
return;
|
||||
}
|
||||
|
||||
setConfirmModalShown(true);
|
||||
}, [dirty, isConfirmModalShown, onRequestClose, closeConfirmModal]);
|
||||
|
||||
useCloseOnEscape(onClose);
|
||||
|
||||
if (!component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isConfirmModalShown) {
|
||||
closeConfirmModal();
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<UploaderContextProvider value={uploader}>
|
||||
<FormikProvider value={formik}>
|
||||
<form onSubmit={handleSubmit} className={styles.form}>
|
||||
<BetterScrollDialog
|
||||
footer={<EditorButtons />}
|
||||
backdrop={<CoverBackdrop cover={values.cover} />}
|
||||
width={860}
|
||||
onClose={onClose}
|
||||
>
|
||||
<>
|
||||
{isConfirmModalShown && (
|
||||
<EditorConfirmClose
|
||||
onApprove={onRequestClose}
|
||||
onDecline={closeConfirmModal}
|
||||
/>
|
||||
)}
|
||||
|
||||
setConfirmModalShown(true);
|
||||
}, [dirty, isConfirmModalShown, onRequestClose, closeConfirmModal]);
|
||||
|
||||
useCloseOnEscape(onClose);
|
||||
|
||||
if (!component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<UploaderContextProvider value={uploader}>
|
||||
<FormikProvider value={formik}>
|
||||
<form onSubmit={handleSubmit} className={styles.form}>
|
||||
<BetterScrollDialog
|
||||
footer={<EditorButtons />}
|
||||
backdrop={<CoverBackdrop cover={values.cover} />}
|
||||
width={860}
|
||||
onClose={onClose}
|
||||
>
|
||||
<>
|
||||
{isConfirmModalShown && (
|
||||
<EditorConfirmClose onApprove={onRequestClose} onDecline={closeConfirmModal} />
|
||||
)}
|
||||
|
||||
<div className={styles.editor}>{createElement(component)}</div>
|
||||
</>
|
||||
</BetterScrollDialog>
|
||||
</form>
|
||||
</FormikProvider>
|
||||
</UploaderContextProvider>
|
||||
);
|
||||
});
|
||||
<div className={styles.editor}>{createElement(component)}</div>
|
||||
</>
|
||||
</BetterScrollDialog>
|
||||
</form>
|
||||
</FormikProvider>
|
||||
</UploaderContextProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export { EditorDialog };
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { FC, useCallback } from 'react';
|
|||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
|
||||
import { ModalWrapper } from '~/components/common/ModalWrapper';
|
||||
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
||||
import { EditorDialog } from '~/containers/dialogs/EditorDialog';
|
||||
import { useLoadNode } from '~/hooks/node/useLoadNode';
|
||||
|
@ -16,29 +16,37 @@ export interface EditorEditDialogProps extends DialogComponentProps {
|
|||
nodeId: number;
|
||||
}
|
||||
|
||||
const EditorEditDialog: FC<EditorEditDialogProps> = observer(({ nodeId, onRequestClose }) => {
|
||||
const { node, isLoading } = useLoadNode(nodeId);
|
||||
const updateNode = useUpdateNode(nodeId);
|
||||
const EditorEditDialog: FC<EditorEditDialogProps> = observer(
|
||||
({ nodeId, onRequestClose }) => {
|
||||
const { node, isLoading } = useLoadNode(nodeId);
|
||||
const updateNode = useUpdateNode(nodeId);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (node: INode) => {
|
||||
await updateNode(node);
|
||||
onRequestClose();
|
||||
},
|
||||
[updateNode, onRequestClose]
|
||||
);
|
||||
|
||||
if (isLoading || !node) {
|
||||
return (
|
||||
<ModalWrapper onOverlayClick={onRequestClose}>
|
||||
<div className={styles.loader}>
|
||||
<LoaderCircle size={64} />
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
const onSubmit = useCallback(
|
||||
async (node: INode) => {
|
||||
await updateNode(node);
|
||||
onRequestClose();
|
||||
},
|
||||
[updateNode, onRequestClose],
|
||||
);
|
||||
}
|
||||
|
||||
return <EditorDialog node={node} onRequestClose={onRequestClose} onSubmit={onSubmit} />;
|
||||
});
|
||||
if (isLoading || !node) {
|
||||
return (
|
||||
<ModalWrapper onOverlayClick={onRequestClose}>
|
||||
<div className={styles.loader}>
|
||||
<LoaderCircle size={64} />
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EditorDialog
|
||||
node={node}
|
||||
onRequestClose={onRequestClose}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export { EditorEditDialog };
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { FC } from 'react';
|
|||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
|
||||
import { ModalWrapper } from '~/components/common/ModalWrapper';
|
||||
import { DIALOG_CONTENT } from '~/constants/modal';
|
||||
import { useModalStore } from '~/store/modal/useModalStore';
|
||||
import { has } from '~/utils/ramda';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { BetterScrollDialog } from '../../../components/dialogs/BetterScrollDialog';
|
||||
import { BetterScrollDialog } from '~/components/common/BetterScrollDialog';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@ import React, { FC, FormEvent, useCallback, useMemo } from 'react';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Superpower } from '~/components/common/Superpower';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { FlowRecent } from '~/components/flow/FlowRecent';
|
||||
import { FlowSearchResults } from '~/components/flow/FlowSearchResults';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { FC, memo } from 'react';
|
||||
|
||||
import { Columns } from '~/components/common/Columns';
|
||||
import { Hoverable } from '~/components/common/Hoverable';
|
||||
import { Columns } from '~/components/containers/Columns';
|
||||
import { LabNoResults } from '~/components/lab/LabNoResults';
|
||||
import { LabNode } from '~/components/lab/LabNode';
|
||||
import { useLabContext } from '~/utils/context/LabContextProvider';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC, memo } from 'react';
|
||||
|
||||
import { Columns } from '~/components/containers/Columns';
|
||||
import { Columns } from '~/components/common/Columns';
|
||||
import { LabNode } from '~/components/lab/LabNode';
|
||||
import { EMPTY_NODE, NODE_TYPES } from '~/constants/node';
|
||||
import { values } from '~/utils/ramda';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { SubTitle } from '~/components/common/SubTitle';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { FlowRecentItem } from '~/components/flow/FlowRecentItem';
|
||||
import { LabFactoryBanner } from '~/components/lab/LabFactoryBanner';
|
||||
import { LabHeroes } from '~/components/lab/LabHeroes';
|
||||
|
|
|
@ -6,7 +6,7 @@ import { observer } from 'mobx-react-lite';
|
|||
|
||||
import { Anchor } from '~/components/common/Anchor';
|
||||
import { Authorized } from '~/components/common/Authorized';
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { Filler } from '~/components/common/Filler';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { Logo } from '~/components/main/Logo';
|
||||
import { Dialog } from '~/constants/modal';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { FC, useMemo } from 'react';
|
||||
|
||||
import { Card } from '~/components/common/Card';
|
||||
import { SubTitle } from '~/components/common/SubTitle';
|
||||
import { Card } from '~/components/containers/Card';
|
||||
import { Backlink } from '~/components/node/Backlink';
|
||||
import { NodeBackLink } from '~/types';
|
||||
import { has } from '~/utils/ramda';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Padder } from '~/components/containers/Padder';
|
||||
import { Sticky } from '~/components/containers/Sticky';
|
||||
import { Filler } from '~/components/common/Filler';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Padder } from '~/components/common/Padder';
|
||||
import { Sticky } from '~/components/common/Sticky';
|
||||
import { NodeAuthorBlock } from '~/components/node/NodeAuthorBlock';
|
||||
import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge';
|
||||
import { NodeNoComments } from '~/components/node/NodeNoComments';
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
||||
import { UploadType } from '~/constants/uploads';
|
||||
import { IComment, IFile } from '~/types';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
import { Padder } from '~/components/containers/Padder';
|
||||
import { Padder } from '~/components/common/Padder';
|
||||
import { NotificationSettingsForm } from '~/components/notifications/NotificationSettingsForm';
|
||||
import { useOAuth } from '~/hooks/auth/useOAuth';
|
||||
import { useNotificationSettings } from '~/hooks/notifications/useNotificationSettings';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC, Fragment } from 'react';
|
||||
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||
import { ProfileAvatar } from '~/components/profile/ProfileAvatar';
|
||||
import { usePatchUser } from '~/hooks/auth/usePatchUser';
|
||||
|
@ -31,7 +31,11 @@ const ProfileInfo: FC<IProps> = ({ isOwn }) => {
|
|||
<div>
|
||||
<Group className={styles.wrap} horizontal>
|
||||
<div className={styles.avatar}>
|
||||
<ProfileAvatar canEdit={isOwn} onChangePhoto={updatePhoto} photo={photo} />
|
||||
<ProfileAvatar
|
||||
canEdit={isOwn}
|
||||
onChangePhoto={updatePhoto}
|
||||
photo={photo}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
|
|
|
@ -2,8 +2,8 @@ import React, { FC } from 'react';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Filler } from '~/components/common/Filler';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { useUserActiveStatus } from '~/hooks/auth/useUserActiveStatus';
|
||||
import { IUser } from '~/types/auth';
|
||||
|
||||
|
@ -23,7 +23,9 @@ const ProfileQuickInfo: FC<ProfileQuickInfoProps> = ({ user }) => {
|
|||
<h5 className={styles.fullname}>{user.fullname || user.username}</h5>
|
||||
<div className={styles.username}>~{user.username}</div>
|
||||
|
||||
<div className={classNames(styles.status, { [styles.active]: isActive })}>
|
||||
<div
|
||||
className={classNames(styles.status, { [styles.active]: isActive })}
|
||||
>
|
||||
{isActive ? 'юнит в сознании' : 'юнит деактивирован'}
|
||||
</div>
|
||||
</Filler>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { VFC } from 'react';
|
||||
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Filler } from '~/components/common/Filler';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { ProfileAvatar } from '~/components/profile/ProfileAvatar';
|
||||
import { usePatchUser } from '~/hooks/auth/usePatchUser';
|
||||
import { useUser } from '~/hooks/auth/useUser';
|
||||
|
@ -16,11 +16,18 @@ const ProfileSidebarHead: VFC<ProfileSidebarHeadProps> = () => {
|
|||
|
||||
return (
|
||||
<Group horizontal>
|
||||
<ProfileAvatar canEdit onChangePhoto={updatePhoto} photo={user.photo} size={72} />
|
||||
<ProfileAvatar
|
||||
canEdit
|
||||
onChangePhoto={updatePhoto}
|
||||
photo={user.photo}
|
||||
size={72}
|
||||
/>
|
||||
|
||||
<Filler>
|
||||
<div className={styles.name}>{user.fullname || user.username}</div>
|
||||
<div className={styles.username}>{!!user.fullname && `~${user.username}`}</div>
|
||||
<div className={styles.username}>
|
||||
{!!user.fullname && `~${user.username}`}
|
||||
</div>
|
||||
</Filler>
|
||||
</Group>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Padder } from '~/components/containers/Padder';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Padder } from '~/components/common/Padder';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { MenuButton } from '~/components/menu/MenuButton';
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@ import React, { useCallback, VFC } from 'react';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Zone } from '~/components/containers/Zone';
|
||||
import { Filler } from '~/components/common/Filler';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Zone } from '~/components/common/Zone';
|
||||
import { VerticalMenu } from '~/components/menu/VerticalMenu';
|
||||
import { useStackContext } from '~/components/sidebar/SidebarStack';
|
||||
import { ProfileSidebarHead } from '~/containers/profile/ProfileSidebarHead';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React, { VFC } from 'react';
|
||||
|
||||
import { Card } from '~/components/common/Card';
|
||||
import { Grid } from '~/components/common/Grid';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Square } from '~/components/common/Square';
|
||||
import { Card } from '~/components/containers/Card';
|
||||
import { Grid } from '~/components/containers/Grid';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
|
||||
interface ProfileStatsProps {}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { Tabs } from '~/components/dialogs/Tabs';
|
||||
import { Tabs } from '~/components/common/Tabs';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { SuperPowersToggle } from '~/containers/auth/SuperPowersToggle';
|
||||
|
||||
interface ProfileTogglesProps {}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React, { VFC } from 'react';
|
||||
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Padder } from '~/components/containers/Padder';
|
||||
import { Filler } from '~/components/common/Filler';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Padder } from '~/components/common/Padder';
|
||||
import { FlowRecentItem } from '~/components/flow/FlowRecentItem';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { InputText } from '~/components/input/InputText';
|
||||
|
@ -32,7 +32,7 @@ const SettingsDeleted: VFC<SettingsDeletedProps> = () => {
|
|||
<br />
|
||||
|
||||
<div className={styles.grid}>
|
||||
{nodes.map(node => (
|
||||
{nodes.map((node) => (
|
||||
<div className={styles.item} key={node.id}>
|
||||
<FlowRecentItem node={node} key={node.id} />
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { FC, useCallback, useState, VFC } from 'react';
|
||||
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Filler } from '~/components/common/Filler';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { NoteCard } from '~/components/notes/NoteCard';
|
||||
import { NoteCreationForm } from '~/components/notes/NoteCreationForm';
|
||||
|
|
|
@ -2,8 +2,8 @@ import React, { FC } from 'react';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Card } from '~/components/containers/Card';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Card } from '~/components/common/Card';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Theme, themeColors } from '~/constants/themes';
|
||||
import { useTheme } from '~/utils/providers/ThemeProvider';
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
import { Filler } from '~/components/common/Filler';
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Superpower } from '~/components/common/Superpower';
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Zone } from '~/components/containers/Zone';
|
||||
import { Zone } from '~/components/common/Zone';
|
||||
import { InputText } from '~/components/input/InputText';
|
||||
import { Textarea } from '~/components/input/Textarea';
|
||||
import { ProfileAccounts } from '~/containers/profile/ProfileAccounts';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback, useEffect, useMemo, VFC } from 'react';
|
||||
|
||||
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
|
||||
import { CoverBackdrop } from '~/components/common/CoverBackdrop';
|
||||
import { ProfileSidebarNotes } from '~/components/profile/ProfileSidebarNotes';
|
||||
import { ProfileSidebarNotifications } from '~/components/profile/ProfileSidebarNotifications';
|
||||
import { ProfileSidebarSettings } from '~/components/profile/ProfileSidebarSettings';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useMemo, VFC } from 'react';
|
||||
|
||||
import { InfiniteScroll } from '~/components/containers/InfiniteScroll';
|
||||
import { InfiniteScroll } from '~/components/common/InfiniteScroll';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { SidebarStack } from '~/components/sidebar/SidebarStack';
|
||||
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import React, { FC, HTMLAttributes, useCallback, useMemo, useState } from 'react';
|
||||
import React, {
|
||||
FC,
|
||||
HTMLAttributes,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { TagField } from '~/components/containers/TagField';
|
||||
import { TagField } from '~/components/common/TagField';
|
||||
import { Tag } from '~/components/tags/Tag';
|
||||
import { TagInput } from '~/containers/tags/TagInput';
|
||||
import { ITag } from '~/types';
|
||||
|
@ -35,8 +41,10 @@ export const Tags: FC<IProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
const exist = tags.map(tag => tag.title);
|
||||
const uniqueTags = uniq([...exist, ...data, ...last]).filter(el => el) as string[];
|
||||
const exist = tags.map((tag) => tag.title);
|
||||
const uniqueTags = uniq([...exist, ...data, ...last]).filter(
|
||||
(el) => el,
|
||||
) as string[];
|
||||
|
||||
if (uniqueTags.length === exist.length) {
|
||||
return;
|
||||
|
@ -45,18 +53,18 @@ export const Tags: FC<IProps> = ({
|
|||
onTagsChange(uniqueTags);
|
||||
setData([]);
|
||||
},
|
||||
[data, onTagsChange, tags]
|
||||
[data, onTagsChange, tags],
|
||||
);
|
||||
|
||||
const onAppendTag = useCallback(
|
||||
(created: string[]) => {
|
||||
setData(
|
||||
uniq([...data, ...created]).filter(
|
||||
title => !tags.some(it => it.title?.trim() === title?.trim())
|
||||
)
|
||||
(title) => !tags.some((it) => it.title?.trim() === title?.trim()),
|
||||
),
|
||||
);
|
||||
},
|
||||
[data, setData, tags]
|
||||
[data, setData, tags],
|
||||
);
|
||||
|
||||
const onClearTag = useCallback((): string | undefined => {
|
||||
|
@ -67,13 +75,16 @@ export const Tags: FC<IProps> = ({
|
|||
}, [data, setData]);
|
||||
|
||||
const exclude = useMemo(
|
||||
() => [...(data || []), ...(tags || []).filter(el => el.title).map(({ title }) => title!)],
|
||||
[data, tags]
|
||||
() => [
|
||||
...(data || []),
|
||||
...(tags || []).filter((el) => el.title).map(({ title }) => title!),
|
||||
],
|
||||
[data, tags],
|
||||
);
|
||||
|
||||
return (
|
||||
<TagField {...props}>
|
||||
{catTags.map(tag => (
|
||||
{catTags.map((tag) => (
|
||||
<Tag
|
||||
key={tag.title}
|
||||
tag={tag}
|
||||
|
@ -83,7 +94,7 @@ export const Tags: FC<IProps> = ({
|
|||
/>
|
||||
))}
|
||||
|
||||
{ordinaryTags.map(tag => (
|
||||
{ordinaryTags.map((tag) => (
|
||||
<Tag
|
||||
key={tag.title}
|
||||
tag={tag}
|
||||
|
@ -93,7 +104,7 @@ export const Tags: FC<IProps> = ({
|
|||
/>
|
||||
))}
|
||||
|
||||
{data.map(title => (
|
||||
{data.map((title) => (
|
||||
<Tag key={title} tag={{ title }} editing />
|
||||
))}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue