diff --git a/src/components/editors/AudioEditor/index.tsx b/src/components/editors/AudioEditor/index.tsx index a22ea0d1..9826c74c 100644 --- a/src/components/editors/AudioEditor/index.tsx +++ b/src/components/editors/AudioEditor/index.tsx @@ -3,8 +3,10 @@ import { INode } from '~/redux/types'; import { connect } from 'react-redux'; import { UPLOAD_TYPES } from '~/redux/uploads/constants'; import { ImageGrid } from '../ImageGrid'; +import { AudioGrid } from '../AudioGrid'; import * as UPLOAD_ACTIONS from '~/redux/uploads/actions'; import { selectUploads } from '~/redux/uploads/selectors'; +import * as styles from './styles.scss'; const mapStateToProps = selectUploads; const mapDispatchToProps = { @@ -25,23 +27,45 @@ const AudioEditorUnconnected: FC<IProps> = ({ data, setData, temp, statuses }) = [data.files] ); - const pending_images = useMemo(() => temp.filter(id => !!statuses[id]).map(id => statuses[id]), [ - temp, - statuses, - ]); + const pending_images = useMemo( + () => + temp + .filter(id => !!statuses[id] && statuses[id].type === UPLOAD_TYPES.IMAGE) + .map(id => statuses[id]), + [temp, statuses] + ); const audios = useMemo( () => data.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), [data.files] ); + const pending_audios = useMemo( + () => + temp + .filter(id => !!statuses[id] && statuses[id].type === UPLOAD_TYPES.AUDIO) + .map(id => statuses[id]), + [temp, statuses] + ); + const setImages = useCallback(files => setData({ ...data, files: [...files, ...audios] }), [ setData, data, audios, ]); - return <ImageGrid files={images} setFiles={setImages} locked={pending_images} />; + const setAudios = useCallback(files => setData({ ...data, files: [...files, ...images] }), [ + setData, + data, + images, + ]); + + return ( + <div className={styles.wrap}> + <ImageGrid files={images} setFiles={setImages} locked={pending_images} /> + <AudioGrid files={audios} setFiles={setAudios} locked={pending_audios} /> + </div> + ); }; const AudioEditor = connect( diff --git a/src/components/editors/AudioEditor/styles.scss b/src/components/editors/AudioEditor/styles.scss new file mode 100644 index 00000000..4909ae19 --- /dev/null +++ b/src/components/editors/AudioEditor/styles.scss @@ -0,0 +1,4 @@ +.wrap { + padding-bottom: 64px; + min-height: 200px; +} diff --git a/src/components/editors/AudioGrid/index.tsx b/src/components/editors/AudioGrid/index.tsx new file mode 100644 index 00000000..8dd8556d --- /dev/null +++ b/src/components/editors/AudioGrid/index.tsx @@ -0,0 +1,43 @@ +import React, { FC, useCallback } from 'react'; +import { SortEnd } from 'react-sortable-hoc'; +import * as styles from './styles.scss'; +import { IFile } from '~/redux/types'; +import { IUploadStatus } from '~/redux/uploads/reducer'; +import { moveArrItem } from '~/utils/fn'; +import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid'; + +interface IProps { + files: IFile[]; + setFiles: (val: IFile[]) => void; + locked: IUploadStatus[]; +} + +const AudioGrid: FC<IProps> = ({ files, setFiles, locked }) => { + const onMove = useCallback( + ({ oldIndex, newIndex }: SortEnd) => { + setFiles(moveArrItem(oldIndex, newIndex, files.filter(file => !!file)) as IFile[]); + }, + [setFiles, files] + ); + + const onDrop = useCallback( + (remove_id: IFile['id']) => { + setFiles(files.filter(file => file && file.id !== remove_id)); + }, + [setFiles, files] + ); + + return ( + <SortableAudioGrid + onDrop={onDrop} + onSortEnd={onMove} + axis="xy" + items={files} + locked={locked} + pressDelay={window.innerWidth < 768 ? 200 : 0} + helperClass={styles.helper} + /> + ); +}; + +export { AudioGrid }; diff --git a/src/components/editors/AudioGrid/styles.scss b/src/components/editors/AudioGrid/styles.scss new file mode 100644 index 00000000..7ab40804 --- /dev/null +++ b/src/components/editors/AudioGrid/styles.scss @@ -0,0 +1,4 @@ +.helper { + opacity: 0.5; + z-index: 10 !important; +} diff --git a/src/components/editors/SortableAudioGrid/index.tsx b/src/components/editors/SortableAudioGrid/index.tsx new file mode 100644 index 00000000..5aa4c293 --- /dev/null +++ b/src/components/editors/SortableAudioGrid/index.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { SortableContainer } from 'react-sortable-hoc'; +import { AudioUpload } from '~/components/upload/AudioUpload'; +import * as styles from './styles.scss'; +import { SortableImageGridItem } from '~/components/editors/SortableImageGridItem'; +import { IFile } from '~/redux/types'; +import { IUploadStatus } from '~/redux/uploads/reducer'; +import { AudioPlayer } from '~/components/media/AudioPlayer'; + +const SortableAudioGrid = SortableContainer( + ({ + items, + locked, + onDrop, + }: { + items: IFile[]; + locked: IUploadStatus[]; + onDrop: (file_id: IFile['id']) => void; + }) => ( + <div className={styles.grid}> + {items + .filter(file => file && file.id) + .map((file, index) => ( + <SortableImageGridItem key={file.id} index={index} collection={0}> + <AudioPlayer file={file} onDrop={onDrop} /> + </SortableImageGridItem> + ))} + + {locked.map((item, index) => ( + <SortableImageGridItem key={item.temp_id} index={index} collection={1} disabled> + <AudioUpload title={item.name} progress={item.progress} is_uploading /> + </SortableImageGridItem> + ))} + </div> + ) +); + +export { SortableAudioGrid }; diff --git a/src/components/editors/SortableAudioGrid/styles.scss b/src/components/editors/SortableAudioGrid/styles.scss new file mode 100644 index 00000000..7dc895db --- /dev/null +++ b/src/components/editors/SortableAudioGrid/styles.scss @@ -0,0 +1,13 @@ +.grid { + box-sizing: border-box; + + display: grid; + grid-column-gap: $gap; + grid-row-gap: $gap; + grid-template-columns: auto; + grid-template-rows: $comment_height; + + @media (max-width: 600px) { + grid-template-columns: repeat(auto-fill, minmax(30vw, 1fr)); + } +} diff --git a/src/components/editors/SortableAudioGridItem/index.tsx b/src/components/editors/SortableAudioGridItem/index.tsx new file mode 100644 index 00000000..2b7ff3d0 --- /dev/null +++ b/src/components/editors/SortableAudioGridItem/index.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { SortableElement } from 'react-sortable-hoc'; + +import * as styles from './styles.scss'; + +const SortableAudioGridItem = SortableElement(({ children }) => ( + <div className={styles.item}>{children}</div> +)); + +export { SortableAudioGridItem }; diff --git a/src/components/editors/SortableAudioGridItem/styles.scss b/src/components/editors/SortableAudioGridItem/styles.scss new file mode 100644 index 00000000..81fd01cb --- /dev/null +++ b/src/components/editors/SortableAudioGridItem/styles.scss @@ -0,0 +1,4 @@ +.item { + z-index: 1; + box-sizing: border-box; +} diff --git a/src/components/editors/SortableImageGrid/styles.scss b/src/components/editors/SortableImageGrid/styles.scss index c525238b..cbd34386 100644 --- a/src/components/editors/SortableImageGrid/styles.scss +++ b/src/components/editors/SortableImageGrid/styles.scss @@ -1,6 +1,4 @@ .grid { - min-height: 200px; - padding-bottom: 62px; box-sizing: border-box; display: grid; diff --git a/src/components/upload/AudioUpload/index.tsx b/src/components/upload/AudioUpload/index.tsx new file mode 100644 index 00000000..fd150593 --- /dev/null +++ b/src/components/upload/AudioUpload/index.tsx @@ -0,0 +1,43 @@ +import React, { FC, useCallback } from 'react'; +import classNames from 'classnames'; +import * as styles from './styles.scss'; +import { ArcProgress } from '~/components/input/ArcProgress'; +import { IFile } from '~/redux/types'; +import { Icon } from '~/components/input/Icon'; + +interface IProps { + id?: IFile['id']; + title?: string; + progress?: number; + onDrop?: (file_id: IFile['id']) => void; + + is_uploading?: boolean; +} + +const AudioUpload: FC<IProps> = ({ title, progress, is_uploading, id, onDrop }) => { + const onDropFile = useCallback(() => { + if (!id || !onDrop) return; + onDrop(id); + }, [id, onDrop]); + + return ( + <div className={styles.wrap}> + {id && onDrop && ( + <div className={styles.drop} onMouseDown={onDropFile}> + <Icon icon="close" /> + </div> + )} + + <div className={classNames(styles.thumb_wrap, { is_uploading })}> + {is_uploading && ( + <div className={styles.progress}> + <ArcProgress size={40} progress={progress} /> + </div> + )} + {title && <div className={styles.title}>{title}</div>} + </div> + </div> + ); +}; + +export { AudioUpload }; diff --git a/src/components/upload/AudioUpload/styles.scss b/src/components/upload/AudioUpload/styles.scss new file mode 100644 index 00000000..17882875 --- /dev/null +++ b/src/components/upload/AudioUpload/styles.scss @@ -0,0 +1,75 @@ +.wrap { + background: lighten($content_bg, 4%); + // padding-bottom: 100%; + border-radius: $radius; + position: relative; + user-select: none; + height: $comment_height; +} + +.thumb_wrap { + // position: absolute; + // width: 100%; + // height: 100%; + z-index: 1; + border-radius: $radius; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + flex-direction: row; + height: 100%; +} + +.title { + flex: 1; + border-radius: $radius; + display: flex; + align-items: center; + padding: 10px; + box-sizing: border-box; +} + +.progress { + flex: 0 0 $comment_height; + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 40px; + height: 40px; + fill: none; + fill: white; + } +} + +.helper { + opacity: 0.3; +} + +.drop { + width: 24px; + height: 24px; + background: #222222; + position: absolute; + right: $gap; + top: $gap; + border-radius: 12px; + z-index: 2; + transition: background-color 250ms, opacity 0.25s; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 20px; + height: 20px; + } + + &:hover { + opacity: 1; + background-color: $red; + } +} diff --git a/src/constants/errors.ts b/src/constants/errors.ts index 70a8ee47..92aea57b 100644 --- a/src/constants/errors.ts +++ b/src/constants/errors.ts @@ -7,6 +7,7 @@ export const ERRORS = { TEXT_REQUIRED: 'Text_Required', UNKNOWN_NODE_TYPE: 'Unknown_Node_Type', URL_INVALID: 'Url_Invalid', + FILES_AUDIO_REQUIRED: 'Files_Audio_Required', }; export const ERROR_LITERAL = { @@ -18,4 +19,5 @@ export const ERROR_LITERAL = { [ERRORS.TEXT_REQUIRED]: 'Нужно немного текста', [ERRORS.UNKNOWN_NODE_TYPE]: 'Неизвестный тип поста', [ERRORS.URL_INVALID]: 'Неизвестный адрес', + [ERRORS.FILES_AUDIO_REQUIRED]: 'Нужна хотя бы одна песня', }; diff --git a/src/redux/uploads/constants.ts b/src/redux/uploads/constants.ts index 74a1528b..21a03c8a 100644 --- a/src/redux/uploads/constants.ts +++ b/src/redux/uploads/constants.ts @@ -38,6 +38,7 @@ export const EMPTY_UPLOAD_STATUS: IUploadStatus = { thumbnail_url: null, type: null, temp_id: null, + name: null, }; // for targeted cancellation diff --git a/src/redux/uploads/reducer.ts b/src/redux/uploads/reducer.ts index 98915b38..38a5d310 100644 --- a/src/redux/uploads/reducer.ts +++ b/src/redux/uploads/reducer.ts @@ -12,6 +12,7 @@ export interface IUploadStatus { thumbnail_url: string; progress: number; temp_id: UUID; + name: string; } export interface IUploadState { diff --git a/src/redux/uploads/sagas.ts b/src/redux/uploads/sagas.ts index 357852e8..94868079 100644 --- a/src/redux/uploads/sagas.ts +++ b/src/redux/uploads/sagas.ts @@ -78,8 +78,10 @@ function* uploadFile({ file, temp_id, type, target }: IFileWithUUID) { { preview, is_uploading: true, - type: file.type, + // type: file.type, temp_id, + type, + name: file.name, } ) ); @@ -106,7 +108,7 @@ function* uploadFile({ file, temp_id, type, target }: IFileWithUUID) { if (error) { return yield put( - uploadSetStatus(temp_id, { is_uploading: false, error: data.detail || error }) + uploadSetStatus(temp_id, { is_uploading: false, error: data.detail || error, type }) ); } @@ -116,8 +118,10 @@ function* uploadFile({ file, temp_id, type, target }: IFileWithUUID) { error: null, uuid: data.id, url: data.full_path, + type, thumbnail_url: data.full_path, progress: 1, + name: file.name, }) );