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:
parent
f809ab40b7
commit
7698d17ed3
11 changed files with 207 additions and 81 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -9,3 +9,4 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/tsconfig.tsbuildinfo
|
/tsconfig.tsbuildinfo
|
||||||
.env.local
|
.env.local
|
||||||
|
.tsconfig.tsbuildinfo
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
42
src/components/editors/RoomEditor/index.tsx
Normal file
42
src/components/editors/RoomEditor/index.tsx
Normal 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 };
|
6
src/components/editors/RoomEditor/styles.module.scss
Normal file
6
src/components/editors/RoomEditor/styles.module.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
@import "src/styles/variables";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
padding-bottom: $upload_button_height + $gap;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
|
@ -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<
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@import "src/styles/variables";
|
@import 'src/styles/variables';
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<{
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue