1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-24 20:36:40 +07:00

add room editor dialog

This commit is contained in:
Fedor Katurov 2023-10-26 19:23:27 +06:00
parent f809ab40b7
commit 7698d17ed3
11 changed files with 207 additions and 81 deletions

1
.gitignore vendored
View file

@ -9,3 +9,4 @@
.DS_Store .DS_Store
/tsconfig.tsbuildinfo /tsconfig.tsbuildinfo
.env.local .env.local
.tsconfig.tsbuildinfo

View file

@ -1,46 +1,39 @@
import React, { FC, useCallback, useMemo } from 'react'; import { FC } from 'react';
import { UploadDropzone } from '~/components/upload/UploadDropzone'; import { UploadDropzone } from '~/components/upload/UploadDropzone';
import { UploadType } from '~/constants/uploads';
import { IFile } from '~/types';
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 { AudioGrid } from '../AudioGrid'; import { AudioGrid } from '../AudioGrid';
import { ImageGrid } from '../ImageGrid'; import { ImageGrid } from '../ImageGrid';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
type IProps = NodeEditorProps; const AudioEditor: FC<NodeEditorProps> = () => {
const {
const AudioEditor: FC<IProps> = () => {
const { pending, filesAudios, filesImages, setFiles, uploadFiles } = useUploaderContext()!;
const pendingImages = useMemo(
() => values(pending).filter(item => item.type === UploadType.Image),
[pending]
);
const pendingAudios = useMemo(
() => values(pending).filter(item => item.type === UploadType.Audio),
[pending]
);
const setImages = useCallback((values: IFile[]) => setFiles([...values, ...filesAudios]), [
setFiles,
filesAudios,
]);
const setAudios = useCallback((values: IFile[]) => setFiles([...values, ...filesImages]), [
setFiles,
filesImages, filesImages,
]); filesAudios,
uploadFiles,
setImages,
setAudios,
pendingAudios,
pendingImages,
} = useUploaderContext()!;
return ( return (
<UploadDropzone onUpload={uploadFiles} helperClassName={styles.dropzone}> <UploadDropzone onUpload={uploadFiles} helperClassName={styles.dropzone}>
<div className={styles.wrap}> <div className={styles.wrap}>
<ImageGrid files={filesImages} setFiles={setImages} locked={pendingImages} /> <ImageGrid
<AudioGrid files={filesAudios} setFiles={setAudios} locked={pendingAudios} /> files={filesImages}
setFiles={setImages}
locked={pendingImages}
/>
<AudioGrid
files={filesAudios}
setFiles={setAudios}
locked={pendingAudios}
/>
</div> </div>
</UploadDropzone> </UploadDropzone>
); );

View file

@ -0,0 +1,42 @@
import { FC } from 'react';
import { UploadDropzone } from '~/components/upload/UploadDropzone';
import { NodeEditorProps } from '~/types/node';
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
import { AudioGrid } from '../AudioGrid';
import { ImageGrid } from '../ImageGrid';
import styles from './styles.module.scss';
const RoomEditor: FC<NodeEditorProps> = () => {
const {
filesImages,
filesAudios,
uploadFiles,
setImages,
setAudios,
pendingAudios,
pendingImages,
} = useUploaderContext()!;
return (
<UploadDropzone onUpload={uploadFiles} helperClassName={styles.dropzone}>
<div className={styles.wrap}>
<ImageGrid
files={filesImages}
setFiles={setImages}
locked={pendingImages}
/>
<AudioGrid
files={filesAudios}
setFiles={setAudios}
locked={pendingAudios}
/>
</div>
</UploadDropzone>
);
};
export { RoomEditor };

View file

@ -0,0 +1,6 @@
@import "src/styles/variables";
.wrap {
padding-bottom: $upload_button_height + $gap;
min-height: 200px;
}

View file

@ -7,6 +7,7 @@ import { EditorImageUploadButton } from '~/components/editors/EditorImageUploadB
import { EditorPublicSwitch } from '~/components/editors/EditorPublicSwitch'; import { EditorPublicSwitch } from '~/components/editors/EditorPublicSwitch';
import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton'; import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton';
import { ImageEditor } from '~/components/editors/ImageEditor'; import { ImageEditor } from '~/components/editors/ImageEditor';
import { RoomEditor } from '~/components/editors/RoomEditor';
import { TextEditor } from '~/components/editors/TextEditor'; import { TextEditor } from '~/components/editors/TextEditor';
import { VideoEditor } from '~/components/editors/VideoEditor'; import { VideoEditor } from '~/components/editors/VideoEditor';
import { LabAudio } from '~/components/lab/LabAudioBlock'; import { LabAudio } from '~/components/lab/LabAudioBlock';
@ -49,6 +50,7 @@ export const NODE_TYPES = {
AUDIO: 'audio', AUDIO: 'audio',
VIDEO: 'video', VIDEO: 'video',
TEXT: 'text', TEXT: 'text',
ROOM: 'room',
}; };
export type INodeComponentProps = { export type INodeComponentProps = {
@ -56,12 +58,22 @@ export type INodeComponentProps = {
isLoading: boolean; isLoading: boolean;
}; };
export type INodeComponents = Record<ValueOf<typeof NODE_TYPES>, FC<INodeComponentProps>>; export type INodeComponents = Record<
ValueOf<typeof NODE_TYPES>,
FC<INodeComponentProps>
>;
export const LAB_PREVIEW_LAYOUT: Record<string, FC<INodeComponentProps>[]> = { export const LAB_PREVIEW_LAYOUT: Record<string, FC<INodeComponentProps>[]> = {
[NODE_TYPES.IMAGE]: [LabImage, LabPad, LabNodeTitle, LabDescription], [NODE_TYPES.IMAGE]: [LabImage, LabPad, LabNodeTitle, LabDescription],
[NODE_TYPES.VIDEO]: [LabVideo, LabPad, LabNodeTitle, LabDescription], [NODE_TYPES.VIDEO]: [LabVideo, LabPad, LabNodeTitle, LabDescription],
[NODE_TYPES.AUDIO]: [LabPad, LabNodeTitle, LabPad, NodeAudioImageBlock, LabAudio, LabPad], [NODE_TYPES.AUDIO]: [
LabPad,
LabNodeTitle,
LabPad,
NodeAudioImageBlock,
LabAudio,
LabPad,
],
[NODE_TYPES.TEXT]: [LabPad, LabNodeTitle, LabPad, LabText, LabPad], [NODE_TYPES.TEXT]: [LabPad, LabNodeTitle, LabPad, LabText, LabPad],
}; };
@ -94,11 +106,23 @@ export const NODE_EDITORS: Record<
[NODE_TYPES.TEXT]: TextEditor, [NODE_TYPES.TEXT]: TextEditor,
[NODE_TYPES.VIDEO]: VideoEditor, [NODE_TYPES.VIDEO]: VideoEditor,
[NODE_TYPES.AUDIO]: AudioEditor, [NODE_TYPES.AUDIO]: AudioEditor,
[NODE_TYPES.ROOM]: RoomEditor,
}; };
export const NODE_PANEL_COMPONENTS: Record<string, FC<IEditorComponentProps>[]> = { export const NODE_PANEL_COMPONENTS: Record<
[NODE_TYPES.TEXT]: [EditorFiller, EditorUploadCoverButton, EditorPublicSwitch], string,
[NODE_TYPES.VIDEO]: [EditorFiller, EditorUploadCoverButton, EditorPublicSwitch], FC<IEditorComponentProps>[]
> = {
[NODE_TYPES.TEXT]: [
EditorFiller,
EditorUploadCoverButton,
EditorPublicSwitch,
],
[NODE_TYPES.VIDEO]: [
EditorFiller,
EditorUploadCoverButton,
EditorPublicSwitch,
],
[NODE_TYPES.IMAGE]: [ [NODE_TYPES.IMAGE]: [
EditorImageUploadButton, EditorImageUploadButton,
EditorFiller, EditorFiller,
@ -112,6 +136,11 @@ export const NODE_PANEL_COMPONENTS: Record<string, FC<IEditorComponentProps>[]>
EditorUploadCoverButton, EditorUploadCoverButton,
EditorPublicSwitch, EditorPublicSwitch,
], ],
[NODE_TYPES.ROOM]: [
EditorAudioUploadButton,
EditorImageUploadButton,
EditorFiller,
],
}; };
export const NODE_EDITOR_DATA: Record< export const NODE_EDITOR_DATA: Record<

View file

@ -1,4 +1,4 @@
@import "src/styles/variables"; @import 'src/styles/variables';
.wrap { .wrap {
position: sticky; position: sticky;

View file

@ -13,7 +13,7 @@ import { keys } from '~/utils/ramda';
export const useUploader = ( export const useUploader = (
subject: UploadSubject, subject: UploadSubject,
target: UploadTarget, target: UploadTarget,
initialFiles?: IFile[] initialFiles?: IFile[],
) => { ) => {
const store = useLocalObservable(() => new UploaderStore(initialFiles)); const store = useLocalObservable(() => new UploaderStore(initialFiles));
@ -26,8 +26,14 @@ export const useUploader = (
// TODO: cancel all uploads on unmount // TODO: cancel all uploads on unmount
const pending = await store.addPending(id, file); const pending = await store.addPending(id, file);
const onProgress = ({ loaded, total }) => store.updateProgress(id, loaded, total); const onProgress = ({ loaded, total }) =>
const result = await apiUploadFile({ file, target, type: pending.type, onProgress }); store.updateProgress(id, loaded, total);
const result = await apiUploadFile({
file,
target,
type: pending.type,
onProgress,
});
store.removePending(id); store.removePending(id);
store.addFile(result); store.addFile(result);
@ -38,14 +44,14 @@ export const useUploader = (
showErrorToast(error); showErrorToast(error);
} }
}, },
[store, target] [store, target],
); );
const uploadFiles = useCallback( const uploadFiles = useCallback(
async (files: File[]) => { async (files: File[]) => {
await Promise.any(files.map(file => uploadFile(file))); await Promise.any(files.map((file) => uploadFile(file)));
}, },
[uploadFile] [uploadFile],
); );
const isUploading = keys(store.pending).length > 0; const isUploading = keys(store.pending).length > 0;
@ -61,5 +67,7 @@ export const useUploader = (
pendingAudios: store.pendingAudios, pendingAudios: store.pendingAudios,
isUploading, isUploading,
setFiles: store.setFiles, setFiles: store.setFiles,
setImages: store.setImages,
setAudios: store.setAudios,
}; };
}; };

View file

@ -1,12 +1,22 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { Button } from '~/components/input/Button';
import { Role } from '~/constants/auth';
import { Dialog } from '~/constants/modal';
import { NODE_TYPES } from '~/constants/node';
import { Container } from '~/containers/main/Container'; import { Container } from '~/containers/main/Container';
import { useAuth } from '~/hooks/auth/useAuth';
import { useShowModal } from '~/hooks/modal/useShowModal';
import markdown from '~/styles/common/markdown.module.scss'; import markdown from '~/styles/common/markdown.module.scss';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
interface RoomLayoutProps {} interface RoomLayoutProps {}
const RoomLayout: FC<RoomLayoutProps> = () => ( const RoomLayout: FC<RoomLayoutProps> = () => {
const createNode = useShowModal(Dialog.CreateNode);
const { user } = useAuth();
return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.room}> <div className={styles.room}>
<Container> <Container>
@ -32,20 +42,35 @@ const RoomLayout: FC<RoomLayoutProps> = () => (
</p> </p>
<p> <p>
Здесь, скорее всего, будет возможность добавить несколько песен и Здесь, скорее всего, будет возможность добавить несколько песен
картинок для слайдшоу. и картинок для слайдшоу.
</p> </p>
<p> <p>
Если помните einsam.ru или раздел Nowhere на старой версии сайта, Если помните einsam.ru или раздел Nowhere на старой версии
то будет что-то такое. сайта, то будет что-то такое.
</p> </p>
{user.role === Role.Admin && (
<p>
<br />
<Button
onClick={() =>
createNode({ isInLab: false, type: NODE_TYPES.ROOM })
}
>
Добавить комнату
</Button>
</p>
)}
</div> </div>
</div> </div>
</Container> </Container>
</div> </div>
<div className={styles.items}></div> <div className={styles.items}></div>
</div> </div>
); );
};
export { RoomLayout }; export { RoomLayout };

View file

@ -17,6 +17,7 @@ const BorisPage: VFC = observer(() => {
const { node, isLoading, update } = useLoadNode(BORIS_NODE_ID); const { node, isLoading, update } = useLoadNode(BORIS_NODE_ID);
const onShowImageModal = useImageModal(); const onShowImageModal = useImageModal();
const { const {
onLoadMoreComments, onLoadMoreComments,
onDelete: onDeleteComment, onDelete: onDeleteComment,
@ -26,6 +27,7 @@ const BorisPage: VFC = observer(() => {
isLoading: isLoadingComments, isLoading: isLoadingComments,
isLoadingMore, isLoadingMore,
} = useNodeComments(BORIS_NODE_ID); } = useNodeComments(BORIS_NODE_ID);
const { title, stats, isLoadingStats } = useBoris(comments); const { title, stats, isLoadingStats } = useBoris(comments);
return ( return (

View file

@ -23,7 +23,21 @@ export class UploaderStore {
} }
addFile = (file: IFile) => this.files.push(file); addFile = (file: IFile) => this.files.push(file);
setFiles = (files: IFile[]) => (this.files = files);
// replaces all files
setFiles = (files: IFile[]) => {
this.files = files;
};
// refreshes only images, keeping audios as they was
setImages = (newImages: IFile[]) => {
this.files = [...newImages, ...this.filesAudios];
};
// refreshes only audios, keeping audios as they was
setAudios = (newAudios: IFile[]) => {
this.files = [...newAudios, ...this.filesImages];
};
/** adds pending from file */ /** adds pending from file */
addPending = async (id: string, file: File) => { addPending = async (id: string, file: File) => {
@ -60,21 +74,25 @@ export class UploaderStore {
/** returns only image files */ /** returns only image files */
get filesImages() { get filesImages() {
return this.files.filter(file => file && file.type === UploadType.Image); return this.files.filter((file) => file && file.type === UploadType.Image);
} }
/** returns only image pending */ /** returns only image pending */
get pendingImages() { get pendingImages() {
return values(this.pending).filter(item => item.type === UploadType.Image); return values(this.pending).filter(
(item) => item.type === UploadType.Image,
);
} }
/** returns only audio files */ /** returns only audio files */
get filesAudios() { get filesAudios() {
return this.files.filter(file => file && file.type === UploadType.Audio); return this.files.filter((file) => file && file.type === UploadType.Audio);
} }
/** returns only audio pending */ /** returns only audio pending */
get pendingAudios() { get pendingAudios() {
return values(this.pending).filter(item => item.type === UploadType.Audio); return values(this.pending).filter(
(item) => item.type === UploadType.Audio,
);
} }
} }

View file

@ -17,6 +17,8 @@ const UploaderContext = createContext<Uploader>({
pendingImages: [], pendingImages: [],
isUploading: false, isUploading: false,
setFiles: (files: IFile[]) => files, setFiles: (files: IFile[]) => files,
setImages: (files: IFile[]) => files,
setAudios: (files: IFile[]) => files,
}); });
export const UploaderContextProvider: FC<{ export const UploaderContextProvider: FC<{