mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
Merge pull request #119 from muerwre/feature/dnd-kit
Feature: using dnd-kit instead of react-sortable-hoc
This commit is contained in:
commit
c173c96b57
35 changed files with 573 additions and 260 deletions
|
@ -1,6 +1,6 @@
|
||||||
#NEXT_PUBLIC_API_HOST=https://pig.staging.vault48.org/
|
NEXT_PUBLIC_API_HOST=https://pig.staging.vault48.org/
|
||||||
#NEXT_PUBLIC_REMOTE_CURRENT=https://pig.staging.vault48.org/static/
|
NEXT_PUBLIC_REMOTE_CURRENT=https://pig.staging.vault48.org/static/
|
||||||
#NEXT_PUBLIC_API_HOST=http://localhost:8888/
|
#NEXT_PUBLIC_API_HOST=http://localhost:8888/
|
||||||
#NEXT_PUBLIC_REMOTE_CURRENT=http://localhost:8888/static/
|
#NEXT_PUBLIC_REMOTE_CURRENT=http://localhost:8888/static/
|
||||||
NEXT_PUBLIC_API_HOST=https://pig.vault48.org/
|
#NEXT_PUBLIC_API_HOST=https://pig.vault48.org/
|
||||||
NEXT_PUBLIC_REMOTE_CURRENT=https://pig.vault48.org/static/
|
#NEXT_PUBLIC_REMOTE_CURRENT=https://pig.vault48.org/static/
|
||||||
|
|
|
@ -3,12 +3,13 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.0.5",
|
||||||
|
"@dnd-kit/sortable": "^7.0.1",
|
||||||
"@popperjs/core": "^2.11.2",
|
"@popperjs/core": "^2.11.2",
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
"@tippyjs/react": "^4.2.6",
|
"@tippyjs/react": "^4.2.6",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
|
||||||
"autosize": "^4.0.2",
|
"autosize": "^4.0.2",
|
||||||
"axios": "^0.21.2",
|
"axios": "^0.21.2",
|
||||||
"body-scroll-lock": "^2.6.4",
|
"body-scroll-lock": "^2.6.4",
|
||||||
|
@ -36,7 +37,6 @@
|
||||||
"react-popper": "^2.2.3",
|
"react-popper": "^2.2.3",
|
||||||
"react-router": "^5.1.2",
|
"react-router": "^5.1.2",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-sortable-hoc": "^2.0.0",
|
|
||||||
"react-sticky-box": "^1.0.2",
|
"react-sticky-box": "^1.0.2",
|
||||||
"sass": "^1.49.0",
|
"sass": "^1.49.0",
|
||||||
"swiper": "^8.0.7",
|
"swiper": "^8.0.7",
|
||||||
|
@ -78,9 +78,11 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/bundle-analyzer": "^12.0.8",
|
"@next/bundle-analyzer": "^12.0.8",
|
||||||
"@next/eslint-plugin-next": "^12.0.8",
|
"@next/eslint-plugin-next": "^12.0.8",
|
||||||
|
"@types/marked": "^4.0.1",
|
||||||
|
"@types/react-router-dom": "^5.1.7",
|
||||||
|
"@types/react": "^17.0.2",
|
||||||
"@types/node": "^11.13.22",
|
"@types/node": "^11.13.22",
|
||||||
"@types/ramda": "^0.26.33",
|
"@types/ramda": "^0.26.33",
|
||||||
"@types/marked": "^4.0.1",
|
|
||||||
"@types/throttle-debounce": "^2.1.0",
|
"@types/throttle-debounce": "^2.1.0",
|
||||||
"@types/yup": "^0.29.11",
|
"@types/yup": "^0.29.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
import { SortEnd } from 'react-sortable-hoc';
|
import { SortableAudioGrid, SortableImageGrid } from '~/components/sortable';
|
||||||
|
|
||||||
import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
|
|
||||||
import { SortableImageGrid } from '~/components/editors/SortableImageGrid';
|
|
||||||
import { COMMENT_FILE_TYPES } from '~/constants/uploads';
|
import { COMMENT_FILE_TYPES } from '~/constants/uploads';
|
||||||
import { useFileDropZone } from '~/hooks';
|
import { useFileDropZone } from '~/hooks';
|
||||||
import { IFile } from '~/types';
|
import { IFile } from '~/types';
|
||||||
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
|
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
|
||||||
import { moveArrItem } from '~/utils/fn';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
@ -30,29 +26,15 @@ const CommentFormAttaches: FC = () => {
|
||||||
const hasAttaches = hasImageAttaches || hasAudioAttaches;
|
const hasAttaches = hasImageAttaches || hasAudioAttaches;
|
||||||
|
|
||||||
const onImageMove = useCallback(
|
const onImageMove = useCallback(
|
||||||
({ oldIndex, newIndex }: SortEnd) => {
|
(newFiles: IFile[]) => {
|
||||||
setFiles([
|
setFiles([...filesAudios, ...newFiles.filter(it => it)]);
|
||||||
...filesAudios,
|
|
||||||
...(moveArrItem(
|
|
||||||
oldIndex,
|
|
||||||
newIndex,
|
|
||||||
filesImages.filter(file => !!file)
|
|
||||||
) as IFile[]),
|
|
||||||
]);
|
|
||||||
},
|
},
|
||||||
[setFiles, filesImages, filesAudios]
|
[setFiles, filesImages, filesAudios]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onAudioMove = useCallback(
|
const onAudioMove = useCallback(
|
||||||
({ oldIndex, newIndex }: SortEnd) => {
|
(newFiles: IFile[]) => {
|
||||||
setFiles([
|
setFiles([...filesImages, ...newFiles]);
|
||||||
...filesImages,
|
|
||||||
...(moveArrItem(
|
|
||||||
oldIndex,
|
|
||||||
newIndex,
|
|
||||||
filesAudios.filter(file => !!file)
|
|
||||||
) as IFile[]),
|
|
||||||
]);
|
|
||||||
},
|
},
|
||||||
[setFiles, filesImages, filesAudios]
|
[setFiles, filesImages, filesAudios]
|
||||||
);
|
);
|
||||||
|
@ -83,12 +65,9 @@ const CommentFormAttaches: FC = () => {
|
||||||
<SortableImageGrid
|
<SortableImageGrid
|
||||||
onDelete={onFileDelete}
|
onDelete={onFileDelete}
|
||||||
onSortEnd={onImageMove}
|
onSortEnd={onImageMove}
|
||||||
axis="xy"
|
|
||||||
items={filesImages}
|
items={filesImages}
|
||||||
locked={pendingImages}
|
locked={pendingImages}
|
||||||
pressDelay={50}
|
size={160}
|
||||||
helperClass={styles.helper}
|
|
||||||
size={120}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -98,10 +77,7 @@ const CommentFormAttaches: FC = () => {
|
||||||
onDelete={onFileDelete}
|
onDelete={onFileDelete}
|
||||||
onTitleChange={onAudioTitleChange}
|
onTitleChange={onAudioTitleChange}
|
||||||
onSortEnd={onAudioMove}
|
onSortEnd={onAudioMove}
|
||||||
axis="y"
|
|
||||||
locked={pendingAudios}
|
locked={pendingAudios}
|
||||||
pressDelay={50}
|
|
||||||
helperClass={styles.helper}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,3 @@
|
||||||
.attaches {
|
.attaches {
|
||||||
@include outer_shadow();
|
@include outer_shadow();
|
||||||
}
|
}
|
||||||
|
|
||||||
.helper {
|
|
||||||
z-index: 10000 !important;
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,9 +2,7 @@ import React, { FC, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { UploadDropzone } from '~/components/upload/UploadDropzone';
|
import { UploadDropzone } from '~/components/upload/UploadDropzone';
|
||||||
import { UploadType } from '~/constants/uploads';
|
import { UploadType } from '~/constants/uploads';
|
||||||
import { useNodeAudios } from '~/hooks/node/useNodeAudios';
|
import { IFile } from '~/types';
|
||||||
import { useNodeFormContext } from '~/hooks/node/useNodeFormFormik';
|
|
||||||
import { useNodeImages } from '~/hooks/node/useNodeImages';
|
|
||||||
import { NodeEditorProps } from '~/types/node';
|
import { NodeEditorProps } from '~/types/node';
|
||||||
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
|
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
|
||||||
import { values } from '~/utils/ramda';
|
import { values } from '~/utils/ramda';
|
||||||
|
@ -17,11 +15,7 @@ import styles from './styles.module.scss';
|
||||||
type IProps = NodeEditorProps;
|
type IProps = NodeEditorProps;
|
||||||
|
|
||||||
const AudioEditor: FC<IProps> = () => {
|
const AudioEditor: FC<IProps> = () => {
|
||||||
const formik = useNodeFormContext();
|
const { pending, filesAudios, filesImages, setFiles, uploadFiles } = useUploaderContext()!;
|
||||||
const { pending, setFiles, uploadFiles } = useUploaderContext()!;
|
|
||||||
|
|
||||||
const images = useNodeImages(formik.values);
|
|
||||||
const audios = useNodeAudios(formik.values);
|
|
||||||
|
|
||||||
const pendingImages = useMemo(
|
const pendingImages = useMemo(
|
||||||
() => values(pending).filter(item => item.type === UploadType.Image),
|
() => values(pending).filter(item => item.type === UploadType.Image),
|
||||||
|
@ -33,15 +27,20 @@ const AudioEditor: FC<IProps> = () => {
|
||||||
[pending]
|
[pending]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setImages = useCallback(values => setFiles([...values, ...audios]), [setFiles, audios]);
|
const setImages = useCallback((values: IFile[]) => setFiles([...values, ...filesAudios]), [
|
||||||
|
setFiles,
|
||||||
const setAudios = useCallback(values => setFiles([...values, ...images]), [setFiles, images]);
|
filesAudios,
|
||||||
|
]);
|
||||||
|
const setAudios = useCallback((values: IFile[]) => setFiles([...values, ...filesImages]), [
|
||||||
|
setFiles,
|
||||||
|
filesImages,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UploadDropzone onUpload={uploadFiles} helperClassName={styles.dropzone}>
|
<UploadDropzone onUpload={uploadFiles} helperClassName={styles.dropzone}>
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<ImageGrid files={images} setFiles={setImages} locked={pendingImages} />
|
<ImageGrid files={filesImages} setFiles={setImages} locked={pendingImages} />
|
||||||
<AudioGrid files={audios} setFiles={setAudios} locked={pendingAudios} />
|
<AudioGrid files={filesAudios} setFiles={setAudios} locked={pendingAudios} />
|
||||||
</div>
|
</div>
|
||||||
</UploadDropzone>
|
</UploadDropzone>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,15 +1,8 @@
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
import { SortEnd } from 'react-sortable-hoc';
|
import { SortableAudioGrid } from '~/components/sortable';
|
||||||
|
|
||||||
import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
|
|
||||||
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
|
||||||
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
||||||
import { IFile } from '~/types';
|
import { IFile } from '~/types';
|
||||||
import { moveArrItem } from '~/utils/fn';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
files: IFile[];
|
files: IFile[];
|
||||||
|
@ -18,17 +11,9 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const AudioGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
const AudioGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
||||||
const { innerWidth } = useWindowSize();
|
|
||||||
|
|
||||||
const onMove = useCallback(
|
const onMove = useCallback(
|
||||||
({ oldIndex, newIndex }: SortEnd) => {
|
(newFiles: IFile[]) => {
|
||||||
setFiles(
|
setFiles(newFiles);
|
||||||
moveArrItem(
|
|
||||||
oldIndex,
|
|
||||||
newIndex,
|
|
||||||
files.filter(file => !!file)
|
|
||||||
) as IFile[]
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[setFiles, files]
|
[setFiles, files]
|
||||||
);
|
);
|
||||||
|
@ -56,11 +41,8 @@ const AudioGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
||||||
onDelete={onDrop}
|
onDelete={onDrop}
|
||||||
onTitleChange={onTitleChange}
|
onTitleChange={onTitleChange}
|
||||||
onSortEnd={onMove}
|
onSortEnd={onMove}
|
||||||
axis="xy"
|
|
||||||
items={files}
|
items={files}
|
||||||
locked={locked}
|
locked={locked}
|
||||||
pressDelay={innerWidth < 768 ? 200 : 0}
|
|
||||||
helperClass={styles.helper}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
@import "src/styles/variables";
|
|
||||||
|
|
||||||
.helper {
|
|
||||||
opacity: 0.5;
|
|
||||||
z-index: 10 !important;
|
|
||||||
}
|
|
|
@ -1,14 +1,9 @@
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
import { SortEnd } from 'react-sortable-hoc';
|
import { SortableImageGrid } from '~/components/sortable';
|
||||||
|
|
||||||
import { SortableImageGrid } from '~/components/editors/SortableImageGrid';
|
|
||||||
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
||||||
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
||||||
import { IFile } from '~/types';
|
import { IFile } from '~/types';
|
||||||
import { moveArrItem } from '~/utils/fn';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
files: IFile[];
|
files: IFile[];
|
||||||
|
@ -20,14 +15,8 @@ const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
||||||
const { innerWidth } = useWindowSize();
|
const { innerWidth } = useWindowSize();
|
||||||
|
|
||||||
const onMove = useCallback(
|
const onMove = useCallback(
|
||||||
({ oldIndex, newIndex }: SortEnd) => {
|
(newFiles: IFile[]) => {
|
||||||
setFiles(
|
setFiles(newFiles.filter(it => it));
|
||||||
moveArrItem(
|
|
||||||
oldIndex,
|
|
||||||
newIndex,
|
|
||||||
files.filter(file => !!file)
|
|
||||||
) as IFile[]
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[setFiles, files]
|
[setFiles, files]
|
||||||
);
|
);
|
||||||
|
@ -43,11 +32,9 @@ const ImageGrid: FC<IProps> = ({ files, setFiles, locked }) => {
|
||||||
<SortableImageGrid
|
<SortableImageGrid
|
||||||
onDelete={onDrop}
|
onDelete={onDrop}
|
||||||
onSortEnd={onMove}
|
onSortEnd={onMove}
|
||||||
axis="xy"
|
|
||||||
items={files}
|
items={files}
|
||||||
locked={locked}
|
locked={locked}
|
||||||
pressDelay={innerWidth < 768 ? 200 : 0}
|
size={innerWidth > 768 ? 220 : 160}
|
||||||
helperClass={styles.helper}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,5 +2,4 @@
|
||||||
|
|
||||||
.helper {
|
.helper {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
z-index: 10000 !important;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { SortableContainer } from 'react-sortable-hoc';
|
|
||||||
|
|
||||||
import { SortableAudioGridItem } from '~/components/editors/SortableAudioGridItem';
|
|
||||||
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
|
||||||
import { AudioUpload } from '~/components/upload/AudioUpload';
|
|
||||||
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
|
||||||
import { IFile } from '~/types';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
|
|
||||||
const SortableAudioGrid = SortableContainer(
|
|
||||||
({
|
|
||||||
items,
|
|
||||||
locked,
|
|
||||||
onDelete,
|
|
||||||
onTitleChange,
|
|
||||||
}: {
|
|
||||||
items: IFile[];
|
|
||||||
locked: UploadStatus[];
|
|
||||||
onDelete: (file_id: IFile['id']) => void;
|
|
||||||
onTitleChange: (file_id: IFile['id'], title: string) => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.grid}>
|
|
||||||
{items
|
|
||||||
.filter(file => file && file.id)
|
|
||||||
.map((file, index) => (
|
|
||||||
<SortableAudioGridItem key={file.id} index={index} collection={0}>
|
|
||||||
<AudioPlayer
|
|
||||||
file={file}
|
|
||||||
onDelete={onDelete}
|
|
||||||
onTitleChange={onTitleChange}
|
|
||||||
isEditing
|
|
||||||
/>
|
|
||||||
</SortableAudioGridItem>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{locked.map((item, index) => (
|
|
||||||
<SortableAudioGridItem key={item.id} index={index} collection={1} disabled>
|
|
||||||
<AudioUpload title={item.name} progress={item.progress} is_uploading />
|
|
||||||
</SortableAudioGridItem>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export { SortableAudioGrid };
|
|
|
@ -1,11 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { SortableElement } from 'react-sortable-hoc';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
|
|
||||||
const SortableAudioGridItem = SortableElement(({ children }) => (
|
|
||||||
<div className={styles.item}>{children}</div>
|
|
||||||
));
|
|
||||||
|
|
||||||
export { SortableAudioGridItem };
|
|
|
@ -1,54 +0,0 @@
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { SortableContainer } from 'react-sortable-hoc';
|
|
||||||
|
|
||||||
import { SortableImageGridItem } from '~/components/editors/SortableImageGridItem';
|
|
||||||
import { ImageUpload } from '~/components/upload/ImageUpload';
|
|
||||||
import { ImagePresets } from '~/constants/urls';
|
|
||||||
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
|
||||||
import { IFile } from '~/types';
|
|
||||||
import { getURL } from '~/utils/dom';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
|
|
||||||
const SortableImageGrid = SortableContainer(
|
|
||||||
({
|
|
||||||
items,
|
|
||||||
locked,
|
|
||||||
onDelete,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
items: IFile[];
|
|
||||||
locked: UploadStatus[];
|
|
||||||
onDelete: (file_id: IFile['id']) => void;
|
|
||||||
size?: number;
|
|
||||||
className?: string;
|
|
||||||
}) => {
|
|
||||||
const preventEvent = useCallback(event => event.preventDefault(), []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.grid, className)} onDropCapture={preventEvent}>
|
|
||||||
{items
|
|
||||||
.filter(file => file && file.id)
|
|
||||||
.map((file, index) => (
|
|
||||||
<SortableImageGridItem key={file.id} index={index} collection={0}>
|
|
||||||
<ImageUpload
|
|
||||||
id={file.id}
|
|
||||||
thumb={getURL(file, ImagePresets.cover)}
|
|
||||||
onDrop={onDelete}
|
|
||||||
/>
|
|
||||||
</SortableImageGridItem>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{locked.map((item, index) => (
|
|
||||||
<SortableImageGridItem key={item.id} index={index} collection={1} disabled>
|
|
||||||
<ImageUpload thumb={item.thumbnail} progress={item.progress} is_uploading />
|
|
||||||
</SortableImageGridItem>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export { SortableImageGrid };
|
|
|
@ -1,11 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { SortableElement } from 'react-sortable-hoc';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
|
|
||||||
const SortableImageGridItem = SortableElement(({ children }) => (
|
|
||||||
<div className={styles.item}>{children}</div>
|
|
||||||
));
|
|
||||||
|
|
||||||
export { SortableImageGridItem };
|
|
|
@ -1,6 +0,0 @@
|
||||||
@import "src/styles/variables";
|
|
||||||
|
|
||||||
.item {
|
|
||||||
z-index: 1;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
|
@ -20,15 +20,20 @@ type Props = {
|
||||||
const AudioPlayer = memo(({ file, onDelete, isEditing, onTitleChange }: Props) => {
|
const AudioPlayer = memo(({ file, onDelete, isEditing, onTitleChange }: Props) => {
|
||||||
const { toPercent, file: currentFile, setFile, play, status, progress, pause } = useAudioPlayer();
|
const { toPercent, file: currentFile, setFile, play, status, progress, pause } = useAudioPlayer();
|
||||||
|
|
||||||
const onPlay = useCallback(async () => {
|
const onPlay = useCallback(
|
||||||
if (file.id !== currentFile?.id) {
|
async event => {
|
||||||
setFile(file);
|
event.stopPropagation();
|
||||||
setTimeout(() => void play(), 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
status === PlayerState.PLAYING ? pause() : await play();
|
if (file.id !== currentFile?.id) {
|
||||||
}, [play, pause, setFile, file, currentFile, status]);
|
setFile(file);
|
||||||
|
setTimeout(() => void play(), 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
status === PlayerState.PLAYING ? pause() : await play();
|
||||||
|
},
|
||||||
|
[play, pause, setFile, file, currentFile, status]
|
||||||
|
);
|
||||||
|
|
||||||
const onSeek = useCallback(
|
const onSeek = useCallback(
|
||||||
event => {
|
event => {
|
||||||
|
@ -65,17 +70,32 @@ const AudioPlayer = memo(({ file, onDelete, isEditing, onTitleChange }: Props) =
|
||||||
[onTitleChange, file.id]
|
[onTitleChange, file.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const stopPropagation = useCallback(
|
||||||
|
event => {
|
||||||
|
if (!isEditing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
},
|
||||||
|
[isEditing]
|
||||||
|
);
|
||||||
|
|
||||||
const playing = currentFile?.id === file.id;
|
const playing = currentFile?.id === file.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onClick={onPlay} className={classNames(styles.wrap, { [styles.playing]: playing })}>
|
<div
|
||||||
|
className={classNames(styles.wrap, {
|
||||||
|
[styles.playing]: playing,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{onDelete && (
|
{onDelete && (
|
||||||
<div className={styles.drop} onMouseDown={onDropClick}>
|
<div className={styles.drop} onMouseDown={onDropClick}>
|
||||||
<Icon icon="close" />
|
<Icon icon="close" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.playpause}>
|
<div className={styles.playpause} onClick={onPlay} onMouseDown={stopPropagation}>
|
||||||
{playing && status === PlayerState.PLAYING ? <Icon icon="pause" /> : <Icon icon="play" />}
|
{playing && status === PlayerState.PLAYING ? <Icon icon="pause" /> : <Icon icon="play" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -85,6 +105,7 @@ const AudioPlayer = memo(({ file, onDelete, isEditing, onTitleChange }: Props) =
|
||||||
placeholder={title}
|
placeholder={title}
|
||||||
handler={onRename}
|
handler={onRename}
|
||||||
value={file.metadata && file.metadata.title}
|
value={file.metadata && file.metadata.title}
|
||||||
|
onMouseDown={stopPropagation}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -92,7 +113,12 @@ const AudioPlayer = memo(({ file, onDelete, isEditing, onTitleChange }: Props) =
|
||||||
<div className={styles.title}>{title || ''}</div>
|
<div className={styles.title}>{title || ''}</div>
|
||||||
|
|
||||||
<div className={styles.progress} onClick={onSeek}>
|
<div className={styles.progress} onClick={onSeek}>
|
||||||
<div className={styles.bar} style={{ width: `${progress.progress}%` }} />
|
<div
|
||||||
|
className={styles.bar}
|
||||||
|
style={{
|
||||||
|
width: `${progress.progress}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
15
src/components/sortable/DragOverlayItem/index.tsx
Normal file
15
src/components/sortable/DragOverlayItem/index.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
interface DragOverlayItemProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DragOverlayItem: FC<DragOverlayItemProps> = ({ className, children }) => (
|
||||||
|
<div className={classNames(styles.overlay, className)}>{children}</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { DragOverlayItem };
|
|
@ -0,0 +1,6 @@
|
||||||
|
@import "src/styles/variables";
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.3) 5px 5px 10px 2px;
|
||||||
|
border-radius: $radius;
|
||||||
|
}
|
69
src/components/sortable/SortableAudioGrid/index.tsx
Normal file
69
src/components/sortable/SortableAudioGrid/index.tsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
||||||
|
import { AudioUpload } from '~/components/upload/AudioUpload';
|
||||||
|
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
||||||
|
import { IFile } from '~/types';
|
||||||
|
|
||||||
|
import { SortableList } from '../SortableList';
|
||||||
|
|
||||||
|
type OnSortEnd = (newValue: IFile[]) => void;
|
||||||
|
|
||||||
|
interface SortableAudioGridProps {
|
||||||
|
onSortEnd: OnSortEnd;
|
||||||
|
items: IFile[];
|
||||||
|
locked: UploadStatus[];
|
||||||
|
className?: string;
|
||||||
|
onDelete: (file_id: IFile['id']) => void;
|
||||||
|
onTitleChange: (file_id: IFile['id'], title: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableAudioGrid: FC<SortableAudioGridProps> = ({
|
||||||
|
items,
|
||||||
|
locked,
|
||||||
|
onDelete,
|
||||||
|
className,
|
||||||
|
onSortEnd,
|
||||||
|
onTitleChange,
|
||||||
|
}) => {
|
||||||
|
const renderItem = useCallback<FC<{ item: IFile; key?: string | number }>>(
|
||||||
|
({ item, key }) => (
|
||||||
|
<AudioPlayer
|
||||||
|
file={item}
|
||||||
|
onDelete={onDelete}
|
||||||
|
isEditing
|
||||||
|
onTitleChange={onTitleChange}
|
||||||
|
key={key}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[onTitleChange, onDelete]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderLocked = useCallback<FC<{ locked: UploadStatus }>>(
|
||||||
|
({ locked }) => (
|
||||||
|
<AudioUpload
|
||||||
|
id={locked.id}
|
||||||
|
is_uploading
|
||||||
|
title={locked.name}
|
||||||
|
progress={locked.progress}
|
||||||
|
key={locked.id}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortableList
|
||||||
|
items={items}
|
||||||
|
locked={locked}
|
||||||
|
getID={it => it.id}
|
||||||
|
getLockedID={it => it.id}
|
||||||
|
renderItem={renderItem}
|
||||||
|
renderLocked={renderLocked}
|
||||||
|
onSortEnd={onSortEnd}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { SortableAudioGrid };
|
88
src/components/sortable/SortableGrid/index.tsx
Normal file
88
src/components/sortable/SortableGrid/index.tsx
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import React, { createElement, FC, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { closestCenter, DndContext, DragOverlay } from '@dnd-kit/core';
|
||||||
|
import { rectSortingStrategy, SortableContext } from '@dnd-kit/sortable';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { DragOverlayItem } from '~/components/sortable/DragOverlayItem';
|
||||||
|
import { useSortableActions } from '~/hooks/sortable';
|
||||||
|
import { DivProps } from '~/utils/types';
|
||||||
|
|
||||||
|
import { SortableItem } from '../SortableItem';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
interface SortableGridProps<T extends {}, R extends {}> {
|
||||||
|
items: T[];
|
||||||
|
locked: R[];
|
||||||
|
getID: (item: T) => number | string;
|
||||||
|
getLockedID: (locked: R) => number | string;
|
||||||
|
renderItem: FC<{ item: T }>;
|
||||||
|
renderLocked: FC<{ locked: R }>;
|
||||||
|
onSortEnd: (newVal: T[]) => void;
|
||||||
|
className?: string;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableGrid = <T, R>({
|
||||||
|
items,
|
||||||
|
locked,
|
||||||
|
getID,
|
||||||
|
getLockedID,
|
||||||
|
className,
|
||||||
|
renderItem,
|
||||||
|
renderLocked,
|
||||||
|
onSortEnd,
|
||||||
|
size,
|
||||||
|
}: SortableGridProps<T, R>) => {
|
||||||
|
const { sensors, onDragEnd, onDragStart, draggingItem, ids } = useSortableActions(
|
||||||
|
items,
|
||||||
|
getID,
|
||||||
|
onSortEnd
|
||||||
|
);
|
||||||
|
|
||||||
|
const gridStyle = useMemo<DivProps['style']>(
|
||||||
|
() =>
|
||||||
|
size
|
||||||
|
? { gridTemplateColumns: size && `repeat(auto-fill, minmax(${size}px, 1fr))` }
|
||||||
|
: undefined,
|
||||||
|
[size]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
>
|
||||||
|
<SortableContext items={ids} strategy={rectSortingStrategy}>
|
||||||
|
<div className={classNames(styles.grid, className)} style={gridStyle}>
|
||||||
|
{items.map(item => (
|
||||||
|
<SortableItem
|
||||||
|
key={getID(item)}
|
||||||
|
id={getID(item)}
|
||||||
|
className={
|
||||||
|
draggingItem && getID(item) === getID(draggingItem) ? styles.dragging : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{createElement(renderItem, { item })}
|
||||||
|
</SortableItem>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{locked.map(item =>
|
||||||
|
createElement(renderLocked, { locked: item, key: getLockedID(item) })
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DragOverlay>
|
||||||
|
{draggingItem ? (
|
||||||
|
<DragOverlayItem>{createElement(renderItem, { item: draggingItem })}</DragOverlayItem>
|
||||||
|
) : null}
|
||||||
|
</DragOverlay>
|
||||||
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { SortableGrid };
|
|
@ -7,8 +7,8 @@
|
||||||
grid-column-gap: $gap;
|
grid-column-gap: $gap;
|
||||||
grid-row-gap: $gap;
|
grid-row-gap: $gap;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
}
|
||||||
@media (max-width: 600px) {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(30vw, 1fr));
|
.dragging {
|
||||||
}
|
opacity: 0.1;
|
||||||
}
|
}
|
64
src/components/sortable/SortableImageGrid/index.tsx
Normal file
64
src/components/sortable/SortableImageGrid/index.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { ImageUpload } from '~/components/upload/ImageUpload';
|
||||||
|
import { ImagePresets } from '~/constants/urls';
|
||||||
|
import { UploadStatus } from '~/store/uploader/UploaderStore';
|
||||||
|
import { IFile } from '~/types';
|
||||||
|
import { getURL } from '~/utils/dom';
|
||||||
|
|
||||||
|
import { SortableGrid } from '../SortableGrid';
|
||||||
|
|
||||||
|
type OnSortEnd = (newValue: IFile[]) => void;
|
||||||
|
|
||||||
|
interface SortableImageGridProps {
|
||||||
|
onSortEnd: OnSortEnd;
|
||||||
|
items: IFile[];
|
||||||
|
locked: UploadStatus[];
|
||||||
|
onDelete: (file_id: IFile['id']) => void;
|
||||||
|
className?: string;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableImageGrid: FC<SortableImageGridProps> = ({
|
||||||
|
items,
|
||||||
|
locked,
|
||||||
|
onDelete,
|
||||||
|
className,
|
||||||
|
onSortEnd,
|
||||||
|
size,
|
||||||
|
}) => {
|
||||||
|
const renderItem = useCallback<FC<{ item: IFile }>>(
|
||||||
|
({ item }) => (
|
||||||
|
<ImageUpload id={item.id} thumb={getURL(item, ImagePresets.cover)} onDrop={onDelete} />
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderLocked = useCallback<FC<{ locked: UploadStatus }>>(
|
||||||
|
({ locked }) => (
|
||||||
|
<ImageUpload
|
||||||
|
thumb={locked.thumbnail}
|
||||||
|
onDrop={onDelete}
|
||||||
|
progress={locked.progress}
|
||||||
|
is_uploading
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortableGrid
|
||||||
|
items={items}
|
||||||
|
locked={locked}
|
||||||
|
getID={it => it.id}
|
||||||
|
getLockedID={it => it.id}
|
||||||
|
renderItem={renderItem}
|
||||||
|
renderLocked={renderLocked}
|
||||||
|
onSortEnd={onSortEnd}
|
||||||
|
className={className}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { SortableImageGrid };
|
44
src/components/sortable/SortableItem/index.tsx
Normal file
44
src/components/sortable/SortableItem/index.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
interface SortableImageGridItemProps {
|
||||||
|
id: number | string;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableItem: FC<SortableImageGridItemProps> = ({
|
||||||
|
children,
|
||||||
|
id,
|
||||||
|
disabled = false,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
||||||
|
id,
|
||||||
|
disabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
className={classNames(styles.item, className)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { SortableItem };
|
80
src/components/sortable/SortableList/index.tsx
Normal file
80
src/components/sortable/SortableList/index.tsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import React, { createElement, FC, memo } from 'react';
|
||||||
|
|
||||||
|
import { closestCenter, DndContext, DragOverlay } from '@dnd-kit/core';
|
||||||
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { DragOverlayItem } from '~/components/sortable/DragOverlayItem';
|
||||||
|
import { SortableItem } from '~/components/sortable/SortableItem';
|
||||||
|
import { useSortableActions } from '~/hooks/sortable';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
interface SortableListProps<T extends {}, R extends {}> {
|
||||||
|
items: T[];
|
||||||
|
locked: R[];
|
||||||
|
getID: (item: T) => number | string;
|
||||||
|
getLockedID: (locked: R) => number | string;
|
||||||
|
renderItem: FC<{ item: T }>;
|
||||||
|
renderLocked: FC<{ locked: R }>;
|
||||||
|
onSortEnd: (newVal: T[]) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableList = memo(
|
||||||
|
<T, R>({
|
||||||
|
items,
|
||||||
|
locked,
|
||||||
|
getID,
|
||||||
|
getLockedID,
|
||||||
|
className,
|
||||||
|
renderItem,
|
||||||
|
renderLocked,
|
||||||
|
onSortEnd,
|
||||||
|
}: SortableListProps<T, R>) => {
|
||||||
|
const { sensors, onDragEnd, onDragStart, draggingItem, ids } = useSortableActions(
|
||||||
|
items,
|
||||||
|
getID,
|
||||||
|
onSortEnd
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
>
|
||||||
|
<SortableContext items={ids} strategy={verticalListSortingStrategy}>
|
||||||
|
<div className={classNames(styles.grid, className)}>
|
||||||
|
{items.map(item => (
|
||||||
|
<SortableItem
|
||||||
|
key={getID(item)}
|
||||||
|
id={getID(item)}
|
||||||
|
className={
|
||||||
|
draggingItem && getID(item) === getID(draggingItem) ? styles.dragging : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{createElement(renderItem, { item, key: getID(item) })}
|
||||||
|
</SortableItem>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{locked.map(item =>
|
||||||
|
createElement(renderLocked, { locked: item, key: getLockedID(item) })
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DragOverlay>
|
||||||
|
{draggingItem ? (
|
||||||
|
<DragOverlayItem>
|
||||||
|
{createElement(renderItem, { item: draggingItem })}
|
||||||
|
</DragOverlayItem>
|
||||||
|
) : null}
|
||||||
|
</DragOverlay>
|
||||||
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export { SortableList };
|
15
src/components/sortable/SortableList/styles.module.scss
Normal file
15
src/components/sortable/SortableList/styles.module.scss
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
@import "src/styles/variables";
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-row-gap: $gap;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragging {
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
2
src/components/sortable/index.ts
Normal file
2
src/components/sortable/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './SortableImageGrid';
|
||||||
|
export * from './SortableAudioGrid';
|
|
@ -4,15 +4,14 @@ import classNames from 'classnames';
|
||||||
|
|
||||||
import { ArcProgress } from '~/components/input/ArcProgress';
|
import { ArcProgress } from '~/components/input/ArcProgress';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { IFile } from '~/types';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
id?: IFile['id'];
|
id?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
progress?: number;
|
progress?: number;
|
||||||
onDrop?: (file_id: IFile['id']) => void;
|
onDrop?: (file_id: string) => void;
|
||||||
|
|
||||||
is_uploading?: boolean;
|
is_uploading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { IFile } from '~/types';
|
import { IFile } from '~/types';
|
||||||
|
|
||||||
export const EMPTY_FILE: IFile = {
|
export const EMPTY_FILE: IFile = {
|
||||||
id: undefined,
|
id: 0,
|
||||||
user_id: undefined,
|
user_id: undefined,
|
||||||
node_id: undefined,
|
node_id: undefined,
|
||||||
|
|
||||||
|
|
1
src/hooks/sortable/index.ts
Normal file
1
src/hooks/sortable/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './useSortableActions';
|
57
src/hooks/sortable/useSortableActions.ts
Normal file
57
src/hooks/sortable/useSortableActions.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { DragStartEvent, MouseSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||||
|
import { DragEndEvent } from '@dnd-kit/core/dist/types';
|
||||||
|
|
||||||
|
import { moveArrItem } from '~/utils/fn';
|
||||||
|
|
||||||
|
export const useSortableActions = <T>(
|
||||||
|
items: T[],
|
||||||
|
getID: (item: T) => string | number,
|
||||||
|
onSortEnd: (items: T[]) => void
|
||||||
|
) => {
|
||||||
|
const [draggingItem, setDraggingItem] = useState<T | null>(null);
|
||||||
|
|
||||||
|
const ids = useMemo(() => items.map(getID), [items]);
|
||||||
|
|
||||||
|
const onDragEnd = useCallback(
|
||||||
|
({ active, over }: DragEndEvent) => {
|
||||||
|
setDraggingItem(null);
|
||||||
|
|
||||||
|
if (!over?.id || active.id === over.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldIndex = items.findIndex(it => getID(it) === active.id);
|
||||||
|
const newIndex = items.findIndex(it => getID(it) === over.id);
|
||||||
|
|
||||||
|
onSortEnd(moveArrItem(oldIndex, newIndex, items));
|
||||||
|
},
|
||||||
|
[items]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDragStart = useCallback(
|
||||||
|
({ active }: DragStartEvent) => {
|
||||||
|
if (!active.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeItem = items.find(it => getID(it) === active.id);
|
||||||
|
|
||||||
|
setDraggingItem(activeItem ?? null);
|
||||||
|
},
|
||||||
|
[items]
|
||||||
|
);
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(TouchSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
delay: 200,
|
||||||
|
tolerance: 5,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
useSensor(MouseSensor)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { sensors, onDragEnd, onDragStart, draggingItem, ids };
|
||||||
|
};
|
|
@ -4,8 +4,8 @@
|
||||||
@import 'mixins';
|
@import 'mixins';
|
||||||
|
|
||||||
$header_height: 64px;
|
$header_height: 64px;
|
||||||
$cell: 280px;
|
$cell: 250px;
|
||||||
$fluid_cell: 250px; // smaller cell for fluid flow
|
$fluid_cell: $cell; // smaller cell for fluid flow
|
||||||
$cell_tablet: ($fluid_cell + 5) * 3 + 10; // flow breakpoint for tablet
|
$cell_tablet: ($fluid_cell + 5) * 3 + 10; // flow breakpoint for tablet
|
||||||
$cell_mobile: ($fluid_cell + 5) * 2 + 10; // flow breakpoint for mobile
|
$cell_mobile: ($fluid_cell + 5) * 2 + 10; // flow breakpoint for mobile
|
||||||
$flow_hide_recents: $cell_tablet; // breakpoint, there recents will be hidden
|
$flow_hide_recents: $cell_tablet; // breakpoint, there recents will be hidden
|
||||||
|
|
|
@ -24,7 +24,7 @@ export type UUID = string;
|
||||||
export type IUploadType = 'image' | 'text' | 'audio' | 'video' | 'other';
|
export type IUploadType = 'image' | 'text' | 'audio' | 'video' | 'other';
|
||||||
|
|
||||||
export interface IFile {
|
export interface IFile {
|
||||||
id?: number;
|
id: number;
|
||||||
temp_id?: UUID;
|
temp_id?: UUID;
|
||||||
user_id?: UUID;
|
user_id?: UUID;
|
||||||
node_id?: UUID;
|
node_id?: UUID;
|
||||||
|
|
File diff suppressed because one or more lines are too long
56
yarn.lock
56
yarn.lock
|
@ -38,13 +38,44 @@
|
||||||
core-js-pure "^3.20.2"
|
core-js-pure "^3.20.2"
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.1.5", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.2.0", "@babel/runtime@^7.9.2":
|
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.2.0", "@babel/runtime@^7.9.2":
|
||||||
version "7.16.7"
|
version "7.16.7"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa"
|
||||||
integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==
|
integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
|
"@dnd-kit/accessibility@^3.0.0":
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz#3ccbefdfca595b0a23a5dc57d3de96bc6935641c"
|
||||||
|
integrity sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
|
"@dnd-kit/core@^6.0.5":
|
||||||
|
version "6.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.0.5.tgz#5670ad0dcc83cd51dbf2fa8c6a5c8af4ac0c1989"
|
||||||
|
integrity sha512-3nL+Zy5cT+1XwsWdlXIvGIFvbuocMyB4NBxTN74DeBaBqeWdH9JsnKwQv7buZQgAHmAH+eIENfS1ginkvW6bCw==
|
||||||
|
dependencies:
|
||||||
|
"@dnd-kit/accessibility" "^3.0.0"
|
||||||
|
"@dnd-kit/utilities" "^3.2.0"
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
|
"@dnd-kit/sortable@^7.0.1":
|
||||||
|
version "7.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-7.0.1.tgz#99c6012bbab4d8bb726c0eef7b921a338c404fdb"
|
||||||
|
integrity sha512-n77qAzJQtMMywu25sJzhz3gsHnDOUlEjTtnRl8A87rWIhnu32zuP+7zmFjwGgvqfXmRufqiHOSlH7JPC/tnJ8Q==
|
||||||
|
dependencies:
|
||||||
|
"@dnd-kit/utilities" "^3.2.0"
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
|
"@dnd-kit/utilities@^3.2.0":
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.0.tgz#b3e956ea63a1347c9d0e1316b037ddcc6140acda"
|
||||||
|
integrity sha512-h65/pn2IPCCIWwdlR2BMLqRkDxpTEONA+HQW3n765HBijLYGyrnTCLa2YQt8VVjjSQD6EfFlTE6aS2Q/b6nb2g==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
"@eslint/eslintrc@^0.4.3":
|
"@eslint/eslintrc@^0.4.3":
|
||||||
version "0.4.3"
|
version "0.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
|
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
|
||||||
|
@ -346,6 +377,15 @@
|
||||||
"@types/scheduler" "*"
|
"@types/scheduler" "*"
|
||||||
csstype "^3.0.2"
|
csstype "^3.0.2"
|
||||||
|
|
||||||
|
"@types/react@^17.0.2":
|
||||||
|
version "17.0.47"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.47.tgz#4ee71aaf4c5a9e290e03aa4d0d313c5d666b3b78"
|
||||||
|
integrity sha512-mk0BL8zBinf2ozNr3qPnlu1oyVTYq+4V7WA76RgxUAtf0Em/Wbid38KN6n4abEkvO4xMTBWmnP1FtQzgkEiJoA==
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types" "*"
|
||||||
|
"@types/scheduler" "*"
|
||||||
|
csstype "^3.0.2"
|
||||||
|
|
||||||
"@types/scheduler@*":
|
"@types/scheduler@*":
|
||||||
version "0.16.2"
|
version "0.16.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
|
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
|
||||||
|
@ -2391,12 +2431,11 @@ react-sortable-hoc@^2.0.0:
|
||||||
invariant "^2.2.4"
|
invariant "^2.2.4"
|
||||||
prop-types "^15.5.7"
|
prop-types "^15.5.7"
|
||||||
|
|
||||||
react-sticky-box@^0.9.3:
|
react-sticky-box@^1.0.2:
|
||||||
version "0.9.3"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-sticky-box/-/react-sticky-box-0.9.3.tgz#8450d4cef8e4fdd7b0351520365bc98c97da11af"
|
resolved "https://registry.yarnpkg.com/react-sticky-box/-/react-sticky-box-1.0.2.tgz#7e72a0f237bdf8270cec9254337f49519a411174"
|
||||||
integrity sha512-Y/qO7vTqAvXuRR6G6ZCW4fX2Bz0GZRwiiLTVeZN5CVz9wzs37ev0Xj3KSKF/PzF0jifwATivI4t24qXG8rSz4Q==
|
integrity sha512-Kyvtppdtv1KqJyNU4DtrSMI0unyQRgtraZvVQ0GAazVbYiTsIVpyhpr+5R0Aavzu4uJNSe1awj2rk/qI7i6Zfw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.1.5"
|
|
||||||
resize-observer-polyfill "^1.5.1"
|
resize-observer-polyfill "^1.5.1"
|
||||||
|
|
||||||
react@^17.0.2:
|
react@^17.0.2:
|
||||||
|
@ -2864,6 +2903,11 @@ tslib@^1.10.0, tslib@^1.8.1:
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||||
|
|
||||||
|
tslib@^2.0.0:
|
||||||
|
version "2.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
|
||||||
|
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
|
||||||
|
|
||||||
tslib@^2.0.3, tslib@^2.1.0:
|
tslib@^2.0.3, tslib@^2.1.0:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue