1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-24 20:36:40 +07:00

#57 added block-based content editor

This commit is contained in:
Fedor Katurov 2021-03-26 15:50:26 +07:00
parent 4b542e0291
commit d8c379de6a
17 changed files with 302 additions and 13 deletions

View file

@ -1,5 +1,5 @@
import React, { FC, useCallback } from 'react'; import React, { FC, useCallback } from 'react';
import { INode } from '~/redux/types'; import { BlockType } from '~/redux/types';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { Textarea } from '~/components/input/Textarea'; import { Textarea } from '~/components/input/Textarea';
import { path } from 'ramda'; import { path } from 'ramda';
@ -9,7 +9,7 @@ type IProps = NodeEditorProps & {};
const TextEditor: FC<IProps> = ({ data, setData }) => { const TextEditor: FC<IProps> = ({ data, setData }) => {
const setText = useCallback( const setText = useCallback(
(text: string) => setData({ ...data, blocks: [{ type: 'text', text }] }), (text: string) => setData({ ...data, blocks: [{ type: BlockType.text, text }] }),
[data, setData] [data, setData]
); );

View file

@ -1,5 +1,5 @@
import React, { FC, useCallback, useMemo } from 'react'; import React, { FC, useCallback, useMemo } from 'react';
import { INode } from '~/redux/types'; import { BlockType } from '~/redux/types';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { path } from 'ramda'; import { path } from 'ramda';
import { InputText } from '~/components/input/InputText'; import { InputText } from '~/components/input/InputText';
@ -11,7 +11,7 @@ type IProps = NodeEditorProps & {};
const VideoEditor: FC<IProps> = ({ data, setData }) => { const VideoEditor: FC<IProps> = ({ data, setData }) => {
const setUrl = useCallback( const setUrl = useCallback(
(url: string) => setData({ ...data, blocks: [{ type: 'video', url }] }), (url: string) => setData({ ...data, blocks: [{ type: BlockType.video, url }] }),
[data, setData] [data, setData]
); );

View file

@ -0,0 +1,60 @@
import React, { FC } from 'react';
import { useRouteMatch } from 'react-router';
import { SidebarWrapper } from '~/containers/sidebars/SidebarWrapper';
import { FormikProvider } from 'formik';
import { useNodeFormFormik } from '~/utils/hooks/useNodeFormFormik';
import { EMPTY_NODE } from '~/redux/node/constants';
import { FileUploaderProvider, useFileUploader } from '~/utils/hooks/fileUploader';
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
import styles from './styles.module.scss';
import { NewEditorPanel } from '~/containers/editors/NewEditorPanel';
import { NewEditorContent } from '~/containers/editors/NewEditorContent';
import { BlockType, INode } from '~/redux/types';
type RouteParams = { type: string };
const data: INode = {
...EMPTY_NODE,
blocks: [
{
type: BlockType.text,
text: 'test',
},
{
type: BlockType.text,
text: 'test',
},
{
type: BlockType.text,
text: 'test',
},
],
};
const NewEditorDialog: FC = ({}) => {
const {
params: { type },
} = useRouteMatch<RouteParams>();
const uploader = useFileUploader(UPLOAD_SUBJECTS.COMMENT, UPLOAD_TARGETS.COMMENTS, data?.files);
const formik = useNodeFormFormik({ ...data, type }, uploader, console.log);
return (
<FileUploaderProvider value={uploader}>
<FormikProvider value={formik}>
<SidebarWrapper>
<div className={styles.content}>
<NewEditorContent />
</div>
<div className={styles.panel}>
<NewEditorPanel />
</div>
</SidebarWrapper>
</FormikProvider>
</FileUploaderProvider>
);
};
export { NewEditorDialog };

View file

@ -0,0 +1,12 @@
@import "~/styles/variables.scss";
.panel {
flex: 0 1 400px;
height: 100%;
background: transparentize($content_bg, 0.2);
}
.content {
flex: 0 3 $content_width - 400;
background-color: transparentize($content_bg, 0.5);
}

View file

@ -0,0 +1,17 @@
import React, { FC, useCallback } from 'react';
import { BlockType, IBlockComponentProps } from '~/redux/types';
import { InputText } from '~/components/input/InputText';
const NewEditorBlockText: FC<IBlockComponentProps> = ({ block, handler }) => {
const onChange = useCallback((text: string) => handler({ type: BlockType.text, text }), [
handler,
]);
return (
<div>
<InputText handler={onChange} value={block.text} />
</div>
);
};
export { NewEditorBlockText };

View file

@ -0,0 +1,27 @@
import React, { FC, useCallback, useMemo } from 'react';
import { getYoutubeThumb } from '~/utils/dom';
import styles from './styles.module.scss';
import classnames from 'classnames';
import { InputText } from '~/components/input/InputText';
import { BlockType, IBlockComponentProps } from '~/redux/types';
const NewEditorBlockVideo: FC<IBlockComponentProps> = ({ block, handler }) => {
const setUrl = useCallback((url: string) => handler({ type: BlockType.video, url }), [handler]);
const url = block.url || '';
const preview = useMemo(() => getYoutubeThumb(url), [url]);
const backgroundImage = (preview && `url("${preview}")`) || '';
return (
<div className={styles.wrap}>
<div className={styles.input_wrap}>
<div className={classnames(styles.input, { active: !!preview })}>
<InputText value={url} handler={setUrl} placeholder="Адрес видео" />
</div>
</div>
<div className={styles.preview} style={{ backgroundImage }} />
</div>
);
};
export { NewEditorBlockVideo };

View file

@ -0,0 +1,33 @@
@import "src/styles/variables";
.wrap {
background-color: red;
}
.preview {
padding-top: 56.25%;
position: relative;
border-radius: $radius;
background: 50% 50% no-repeat;
background-size: cover;
}
.input_wrap {
display: flex;
align-items: center;
justify-content: center;
}
.input {
flex: 1 0 50%;
padding: $gap;
background: lighten($content_bg, 4%);
border-radius: $radius $radius 0 0;
input {
text-align: center;
}
&:global(.active) {
background: $red;
}
}

View file

@ -0,0 +1,34 @@
import React, { createElement, FC, useCallback, useMemo } from 'react';
import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik';
import { NODE_EDITOR_BLOCKS } from '~/redux/node/constants';
import { has, prop } from 'ramda';
import styles from './styles.module.scss';
import { Group } from '~/components/containers/Group';
import { IBlock } from '~/redux/types';
interface IProps {}
const NewEditorContent: FC<IProps> = () => {
const { values, setFieldValue } = useNodeFormContext();
const onChange = useCallback(
(index: number) => (val: IBlock) =>
setFieldValue(
'blocks',
values.blocks.map((el, i) => (i === index ? val : el))
),
[setFieldValue, values.blocks]
);
return (
<Group className={styles.wrap}>
{values.blocks.map((block, i) =>
prop(block.type, NODE_EDITOR_BLOCKS)
? createElement(prop(block.type, NODE_EDITOR_BLOCKS), { block, handler: onChange(i) })
: null
)}
</Group>
);
};
export { NewEditorContent };

View file

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

View file

@ -0,0 +1,29 @@
import React, { FC } from 'react';
import styles from './styles.module.scss';
import { TextInput } from '~/components/input/TextInput';
import { Filler } from '~/components/containers/Filler';
import { Button } from '~/components/input/Button';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { Group } from '~/components/containers/Group';
interface IProps {}
const NewEditorPanel: FC<IProps> = () => (
<div className={styles.wrap}>
<Group>
<div className={styles.title}>
<TextInput onChange={console.log} label="Название" />
</div>
<Placeholder width="100%" height={60} />
<Placeholder width="100%" height={120} />
</Group>
<Filler />
<Button color="primary">Сохранить</Button>
</div>
);
export { NewEditorPanel };

View file

@ -0,0 +1,11 @@
@import "~/styles/variables.scss";
.wrap {
@include outer_shadow;
padding: $gap * 2;
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
}

View file

@ -5,6 +5,7 @@ import { TagSidebar } from '~/containers/sidebars/TagSidebar';
import { ProfileSidebar } from '~/containers/sidebars/ProfileSidebar'; import { ProfileSidebar } from '~/containers/sidebars/ProfileSidebar';
import { Authorized } from '~/components/containers/Authorized'; import { Authorized } from '~/components/containers/Authorized';
import { SubmitBar } from '~/components/bars/SubmitBar'; import { SubmitBar } from '~/components/bars/SubmitBar';
import { NewEditorDialog } from '~/containers/dialogs/NewEditorDialog';
interface IProps { interface IProps {
prefix?: string; prefix?: string;
@ -17,6 +18,7 @@ const SidebarRouter: FC<IProps> = ({ prefix = '', isLab }) => {
<Switch> <Switch>
<Route path={`${prefix}/tag/:tag`} component={TagSidebar} /> <Route path={`${prefix}/tag/:tag`} component={TagSidebar} />
<Route path={`${prefix}/~:username`} component={ProfileSidebar} /> <Route path={`${prefix}/~:username`} component={ProfileSidebar} />
<Route path={`${prefix}/create/:type`} component={NewEditorDialog} />
</Switch> </Switch>
<Authorized> <Authorized>

View file

@ -25,6 +25,7 @@ const LabLayout: FC<IProps> = () => {
return ( return (
<div> <div>
<div className={styles.blur} />
<Container> <Container>
<div className={styles.wrap}> <div className={styles.wrap}>
<Group className={styles.content}> <Group className={styles.content}>

View file

@ -24,3 +24,14 @@
margin: 0 $gap $gap 0; margin: 0 $gap $gap 0;
} }
} }
.blur {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: $blue_gradient;
z-index: -1;
opacity: 0.3;
}

View file

@ -1,5 +1,5 @@
import { FC } from 'react'; import { FC } from 'react';
import { IComment, INode, ValueOf } from '../types'; import { BlockType, IBlock, IBlockComponentProps, IComment, INode, ValueOf } from '../types';
import { NodeTextBlock } from '~/components/node/NodeTextBlock'; import { NodeTextBlock } from '~/components/node/NodeTextBlock';
import { NodeAudioBlock } from '~/components/node/NodeAudioBlock'; import { NodeAudioBlock } from '~/components/node/NodeAudioBlock';
import { NodeVideoBlock } from '~/components/node/NodeVideoBlock'; import { NodeVideoBlock } from '~/components/node/NodeVideoBlock';
@ -20,6 +20,8 @@ import { LabText } from '~/components/lab/LabText';
import { LabImage } from '~/components/lab/LabImage'; import { LabImage } from '~/components/lab/LabImage';
import { LabBottomPanel } from '~/components/lab/LabBottomPanel'; import { LabBottomPanel } from '~/components/lab/LabBottomPanel';
import { LabPad } from '~/components/lab/LabPad'; import { LabPad } from '~/components/lab/LabPad';
import { NewEditorBlockVideo } from '~/containers/editors/NewEditorBlockVideo';
import { NewEditorBlockText } from '~/containers/editors/NewEditorBlockText';
const prefix = 'NODE.'; const prefix = 'NODE.';
export const NODE_ACTIONS = { export const NODE_ACTIONS = {
@ -126,6 +128,17 @@ export const NODE_EDITORS: Record<
[NODE_TYPES.AUDIO]: AudioEditor, [NODE_TYPES.AUDIO]: AudioEditor,
}; };
export interface NodeEditorBlockConfig {
block: BlockType;
limit?: number;
initial?: boolean;
}
export const NODE_EDITOR_BLOCKS: Record<BlockType, FC<IBlockComponentProps>> = {
[BlockType.video]: NewEditorBlockVideo,
[BlockType.text]: NewEditorBlockText,
};
export const NODE_PANEL_COMPONENTS: Record<string, FC<IEditorComponentProps>[]> = { export const NODE_PANEL_COMPONENTS: Record<string, FC<IEditorComponentProps>[]> = {
[NODE_TYPES.TEXT]: [EditorFiller, EditorUploadCoverButton, EditorPublicSwitch], [NODE_TYPES.TEXT]: [EditorFiller, EditorUploadCoverButton, EditorPublicSwitch],
[NODE_TYPES.VIDEO]: [EditorFiller, EditorUploadCoverButton, EditorPublicSwitch], [NODE_TYPES.VIDEO]: [EditorFiller, EditorUploadCoverButton, EditorPublicSwitch],
@ -149,7 +162,7 @@ export const NODE_EDITOR_DATA: Record<
Partial<INode> Partial<INode>
> = { > = {
[NODE_TYPES.TEXT]: { [NODE_TYPES.TEXT]: {
blocks: [{ text: '', type: 'text' }], blocks: [{ text: '', type: BlockType.text }],
}, },
}; };

View file

@ -97,17 +97,18 @@ export interface IFileWithUUID {
onFail?: () => void; onFail?: () => void;
} }
export interface IBlockText { export enum BlockType {
type: 'text'; text = 'text',
text: string; video = 'video',
} }
export interface IBlockEmbed { export interface IBlock {
type: 'video'; type: BlockType;
url: string; text?: string;
url?: string;
} }
export type IBlock = IBlockText | IBlockEmbed; export type IBlockComponentProps = { block: IBlock; handler: (val: IBlock) => void };
export interface INode { export interface INode {
id?: number; id?: number;

View file

@ -0,0 +1,33 @@
import { IComment, INode } from '~/redux/types';
import { FileUploader } from '~/utils/hooks/fileUploader';
import { useCallback, useRef } from 'react';
import { useFormik, useFormikContext } from 'formik';
import { object } from 'yup';
const validationSchema = object().shape({});
export const useNodeFormFormik = (
values: INode,
uploader: FileUploader,
stopEditing: () => void
) => {
const onSubmit = useCallback(console.log, []);
const { current: initialValues } = useRef(values);
const onReset = useCallback(() => {
uploader.setFiles([]);
if (stopEditing) stopEditing();
}, [uploader, stopEditing]);
return useFormik<INode>({
initialValues,
validationSchema,
onSubmit,
onReset,
initialStatus: '',
validateOnChange: true,
});
};
export const useNodeFormContext = () => useFormikContext<INode>();