diff --git a/src/components/editors/TextEditor/index.tsx b/src/components/editors/TextEditor/index.tsx index 113d4df5..8e595ebd 100644 --- a/src/components/editors/TextEditor/index.tsx +++ b/src/components/editors/TextEditor/index.tsx @@ -1,5 +1,5 @@ import React, { FC, useCallback } from 'react'; -import { INode } from '~/redux/types'; +import { BlockType } from '~/redux/types'; import styles from './styles.module.scss'; import { Textarea } from '~/components/input/Textarea'; import { path } from 'ramda'; @@ -9,7 +9,7 @@ type IProps = NodeEditorProps & {}; const TextEditor: FC = ({ data, setData }) => { const setText = useCallback( - (text: string) => setData({ ...data, blocks: [{ type: 'text', text }] }), + (text: string) => setData({ ...data, blocks: [{ type: BlockType.text, text }] }), [data, setData] ); diff --git a/src/components/editors/VideoEditor/index.tsx b/src/components/editors/VideoEditor/index.tsx index fa9803f9..95cb5e98 100644 --- a/src/components/editors/VideoEditor/index.tsx +++ b/src/components/editors/VideoEditor/index.tsx @@ -1,5 +1,5 @@ import React, { FC, useCallback, useMemo } from 'react'; -import { INode } from '~/redux/types'; +import { BlockType } from '~/redux/types'; import styles from './styles.module.scss'; import { path } from 'ramda'; import { InputText } from '~/components/input/InputText'; @@ -11,7 +11,7 @@ type IProps = NodeEditorProps & {}; const VideoEditor: FC = ({ data, setData }) => { const setUrl = useCallback( - (url: string) => setData({ ...data, blocks: [{ type: 'video', url }] }), + (url: string) => setData({ ...data, blocks: [{ type: BlockType.video, url }] }), [data, setData] ); diff --git a/src/containers/dialogs/NewEditorDialog/index.tsx b/src/containers/dialogs/NewEditorDialog/index.tsx new file mode 100644 index 00000000..3001b6ce --- /dev/null +++ b/src/containers/dialogs/NewEditorDialog/index.tsx @@ -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(); + + const uploader = useFileUploader(UPLOAD_SUBJECTS.COMMENT, UPLOAD_TARGETS.COMMENTS, data?.files); + + const formik = useNodeFormFormik({ ...data, type }, uploader, console.log); + + return ( + + + +
+ +
+ +
+ +
+
+
+
+ ); +}; + +export { NewEditorDialog }; diff --git a/src/containers/dialogs/NewEditorDialog/styles.module.scss b/src/containers/dialogs/NewEditorDialog/styles.module.scss new file mode 100644 index 00000000..be6d6208 --- /dev/null +++ b/src/containers/dialogs/NewEditorDialog/styles.module.scss @@ -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); +} diff --git a/src/containers/editors/NewEditorBlockText/index.tsx b/src/containers/editors/NewEditorBlockText/index.tsx new file mode 100644 index 00000000..17bd961a --- /dev/null +++ b/src/containers/editors/NewEditorBlockText/index.tsx @@ -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 = ({ block, handler }) => { + const onChange = useCallback((text: string) => handler({ type: BlockType.text, text }), [ + handler, + ]); + + return ( +
+ +
+ ); +}; + +export { NewEditorBlockText }; diff --git a/src/containers/editors/NewEditorBlockVideo/index.tsx b/src/containers/editors/NewEditorBlockVideo/index.tsx new file mode 100644 index 00000000..deddb2ce --- /dev/null +++ b/src/containers/editors/NewEditorBlockVideo/index.tsx @@ -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 = ({ 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 ( +
+
+
+ +
+
+ +
+
+ ); +}; +export { NewEditorBlockVideo }; diff --git a/src/containers/editors/NewEditorBlockVideo/styles.module.scss b/src/containers/editors/NewEditorBlockVideo/styles.module.scss new file mode 100644 index 00000000..3ce34884 --- /dev/null +++ b/src/containers/editors/NewEditorBlockVideo/styles.module.scss @@ -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; + } +} diff --git a/src/containers/editors/NewEditorContent/index.tsx b/src/containers/editors/NewEditorContent/index.tsx new file mode 100644 index 00000000..6b55aaeb --- /dev/null +++ b/src/containers/editors/NewEditorContent/index.tsx @@ -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 = () => { + 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 ( + + {values.blocks.map((block, i) => + prop(block.type, NODE_EDITOR_BLOCKS) + ? createElement(prop(block.type, NODE_EDITOR_BLOCKS), { block, handler: onChange(i) }) + : null + )} + + ); +}; + +export { NewEditorContent }; diff --git a/src/containers/editors/NewEditorContent/styles.module.scss b/src/containers/editors/NewEditorContent/styles.module.scss new file mode 100644 index 00000000..b8335b3e --- /dev/null +++ b/src/containers/editors/NewEditorContent/styles.module.scss @@ -0,0 +1,5 @@ +@import "~/styles/variables.scss"; + +.wrap { + padding: $gap; +} diff --git a/src/containers/editors/NewEditorPanel/index.tsx b/src/containers/editors/NewEditorPanel/index.tsx new file mode 100644 index 00000000..01e9ee66 --- /dev/null +++ b/src/containers/editors/NewEditorPanel/index.tsx @@ -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 = () => ( +
+ +
+ +
+ + + + +
+ + + + +
+); + +export { NewEditorPanel }; diff --git a/src/containers/editors/NewEditorPanel/styles.module.scss b/src/containers/editors/NewEditorPanel/styles.module.scss new file mode 100644 index 00000000..ab3c42a6 --- /dev/null +++ b/src/containers/editors/NewEditorPanel/styles.module.scss @@ -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; +} diff --git a/src/containers/main/SidebarRouter/index.tsx b/src/containers/main/SidebarRouter/index.tsx index 47cc641e..bc8431cc 100644 --- a/src/containers/main/SidebarRouter/index.tsx +++ b/src/containers/main/SidebarRouter/index.tsx @@ -5,6 +5,7 @@ import { TagSidebar } from '~/containers/sidebars/TagSidebar'; import { ProfileSidebar } from '~/containers/sidebars/ProfileSidebar'; import { Authorized } from '~/components/containers/Authorized'; import { SubmitBar } from '~/components/bars/SubmitBar'; +import { NewEditorDialog } from '~/containers/dialogs/NewEditorDialog'; interface IProps { prefix?: string; @@ -17,6 +18,7 @@ const SidebarRouter: FC = ({ prefix = '', isLab }) => { + diff --git a/src/layouts/LabLayout/index.tsx b/src/layouts/LabLayout/index.tsx index 314d6c0c..a8d6a502 100644 --- a/src/layouts/LabLayout/index.tsx +++ b/src/layouts/LabLayout/index.tsx @@ -25,6 +25,7 @@ const LabLayout: FC = () => { return (
+
diff --git a/src/layouts/LabLayout/styles.module.scss b/src/layouts/LabLayout/styles.module.scss index 17f70529..8346df40 100644 --- a/src/layouts/LabLayout/styles.module.scss +++ b/src/layouts/LabLayout/styles.module.scss @@ -24,3 +24,14 @@ 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; +} diff --git a/src/redux/node/constants.ts b/src/redux/node/constants.ts index 8755a016..c43abaa0 100644 --- a/src/redux/node/constants.ts +++ b/src/redux/node/constants.ts @@ -1,5 +1,5 @@ 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 { NodeAudioBlock } from '~/components/node/NodeAudioBlock'; import { NodeVideoBlock } from '~/components/node/NodeVideoBlock'; @@ -20,6 +20,8 @@ import { LabText } from '~/components/lab/LabText'; import { LabImage } from '~/components/lab/LabImage'; import { LabBottomPanel } from '~/components/lab/LabBottomPanel'; import { LabPad } from '~/components/lab/LabPad'; +import { NewEditorBlockVideo } from '~/containers/editors/NewEditorBlockVideo'; +import { NewEditorBlockText } from '~/containers/editors/NewEditorBlockText'; const prefix = 'NODE.'; export const NODE_ACTIONS = { @@ -126,6 +128,17 @@ export const NODE_EDITORS: Record< [NODE_TYPES.AUDIO]: AudioEditor, }; +export interface NodeEditorBlockConfig { + block: BlockType; + limit?: number; + initial?: boolean; +} + +export const NODE_EDITOR_BLOCKS: Record> = { + [BlockType.video]: NewEditorBlockVideo, + [BlockType.text]: NewEditorBlockText, +}; + export const NODE_PANEL_COMPONENTS: Record[]> = { [NODE_TYPES.TEXT]: [EditorFiller, EditorUploadCoverButton, EditorPublicSwitch], [NODE_TYPES.VIDEO]: [EditorFiller, EditorUploadCoverButton, EditorPublicSwitch], @@ -149,7 +162,7 @@ export const NODE_EDITOR_DATA: Record< Partial > = { [NODE_TYPES.TEXT]: { - blocks: [{ text: '', type: 'text' }], + blocks: [{ text: '', type: BlockType.text }], }, }; diff --git a/src/redux/types.ts b/src/redux/types.ts index f78309a3..3940a704 100644 --- a/src/redux/types.ts +++ b/src/redux/types.ts @@ -97,17 +97,18 @@ export interface IFileWithUUID { onFail?: () => void; } -export interface IBlockText { - type: 'text'; - text: string; +export enum BlockType { + text = 'text', + video = 'video', } -export interface IBlockEmbed { - type: 'video'; - url: string; +export interface IBlock { + type: BlockType; + text?: string; + url?: string; } -export type IBlock = IBlockText | IBlockEmbed; +export type IBlockComponentProps = { block: IBlock; handler: (val: IBlock) => void }; export interface INode { id?: number; diff --git a/src/utils/hooks/useNodeFormFormik.ts b/src/utils/hooks/useNodeFormFormik.ts new file mode 100644 index 00000000..2433a599 --- /dev/null +++ b/src/utils/hooks/useNodeFormFormik.ts @@ -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({ + initialValues, + validationSchema, + onSubmit, + onReset, + initialStatus: '', + validateOnChange: true, + }); +}; + +export const useNodeFormContext = () => useFormikContext();