mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-26 13:26:40 +07:00
refactor editos
This commit is contained in:
parent
03ddb1862c
commit
5e9c111e0f
149 changed files with 416 additions and 317 deletions
|
@ -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';
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue