1
0
Fork 0
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:
muerwre 2022-06-30 17:15:02 +07:00 committed by GitHub
commit c173c96b57
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 573 additions and 260 deletions

View file

@ -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/

View file

@ -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",

View file

@ -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>

View file

@ -3,7 +3,3 @@
.attaches { .attaches {
@include outer_shadow(); @include outer_shadow();
} }
.helper {
z-index: 10000 !important;
}

View file

@ -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>
); );

View file

@ -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}
/> />
); );
}; };

View file

@ -1,6 +0,0 @@
@import "src/styles/variables";
.helper {
opacity: 0.5;
z-index: 10 !important;
}

View file

@ -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}
/> />
); );
}; };

View file

@ -2,5 +2,4 @@
.helper { .helper {
opacity: 0.5; opacity: 0.5;
z-index: 10000 !important;
} }

View file

@ -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 };

View file

@ -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 };

View file

@ -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 };

View file

@ -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 };

View file

@ -1,6 +0,0 @@
@import "src/styles/variables";
.item {
z-index: 1;
box-sizing: border-box;
}

View file

@ -20,7 +20,10 @@ 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(
async event => {
event.stopPropagation();
if (file.id !== currentFile?.id) { if (file.id !== currentFile?.id) {
setFile(file); setFile(file);
setTimeout(() => void play(), 0); setTimeout(() => void play(), 0);
@ -28,7 +31,9 @@ const AudioPlayer = memo(({ file, onDelete, isEditing, onTitleChange }: Props) =
} }
status === PlayerState.PLAYING ? pause() : await play(); status === PlayerState.PLAYING ? pause() : await play();
}, [play, pause, setFile, file, currentFile, status]); },
[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>
)} )}

View 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 };

View file

@ -0,0 +1,6 @@
@import "src/styles/variables";
.overlay {
box-shadow: rgba(0, 0, 0, 0.3) 5px 5px 10px 2px;
border-radius: $radius;
}

View 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 };

View 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 };

View file

@ -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) { .dragging {
grid-template-columns: repeat(auto-fill, minmax(30vw, 1fr)); opacity: 0.1;
}
} }

View 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 };

View 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 };

View 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 };

View 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;
}

View file

@ -0,0 +1,2 @@
export * from './SortableImageGrid';
export * from './SortableAudioGrid';

View file

@ -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;
} }

View file

@ -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,

View file

@ -0,0 +1 @@
export * from './useSortableActions';

View 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 };
};

View file

@ -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

View file

@ -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

View file

@ -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"