mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46: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,46 +1,39 @@
|
|||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
import { FC } from 'react';
|
||||
|
||||
import { UploadDropzone } from '~/components/upload/UploadDropzone';
|
||||
import { UploadType } from '~/constants/uploads';
|
||||
import { IFile } from '~/types';
|
||||
import { NodeEditorProps } from '~/types/node';
|
||||
import { useUploaderContext } from '~/utils/context/UploaderContextProvider';
|
||||
import { values } from '~/utils/ramda';
|
||||
|
||||
import { AudioGrid } from '../AudioGrid';
|
||||
import { ImageGrid } from '../ImageGrid';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
type IProps = NodeEditorProps;
|
||||
|
||||
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,
|
||||
const AudioEditor: 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} />
|
||||
<ImageGrid
|
||||
files={filesImages}
|
||||
setFiles={setImages}
|
||||
locked={pendingImages}
|
||||
/>
|
||||
|
||||
<AudioGrid
|
||||
files={filesAudios}
|
||||
setFiles={setAudios}
|
||||
locked={pendingAudios}
|
||||
/>
|
||||
</div>
|
||||
</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 { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton';
|
||||
import { ImageEditor } from '~/components/editors/ImageEditor';
|
||||
import { RoomEditor } from '~/components/editors/RoomEditor';
|
||||
import { TextEditor } from '~/components/editors/TextEditor';
|
||||
import { VideoEditor } from '~/components/editors/VideoEditor';
|
||||
import { LabAudio } from '~/components/lab/LabAudioBlock';
|
||||
|
@ -49,6 +50,7 @@ export const NODE_TYPES = {
|
|||
AUDIO: 'audio',
|
||||
VIDEO: 'video',
|
||||
TEXT: 'text',
|
||||
ROOM: 'room',
|
||||
};
|
||||
|
||||
export type INodeComponentProps = {
|
||||
|
@ -56,12 +58,22 @@ export type INodeComponentProps = {
|
|||
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>[]> = {
|
||||
[NODE_TYPES.IMAGE]: [LabImage, 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],
|
||||
};
|
||||
|
||||
|
@ -94,11 +106,23 @@ export const NODE_EDITORS: Record<
|
|||
[NODE_TYPES.TEXT]: TextEditor,
|
||||
[NODE_TYPES.VIDEO]: VideoEditor,
|
||||
[NODE_TYPES.AUDIO]: AudioEditor,
|
||||
[NODE_TYPES.ROOM]: RoomEditor,
|
||||
};
|
||||
|
||||
export const NODE_PANEL_COMPONENTS: Record<string, FC<IEditorComponentProps>[]> = {
|
||||
[NODE_TYPES.TEXT]: [EditorFiller, EditorUploadCoverButton, EditorPublicSwitch],
|
||||
[NODE_TYPES.VIDEO]: [EditorFiller, EditorUploadCoverButton, EditorPublicSwitch],
|
||||
export const NODE_PANEL_COMPONENTS: Record<
|
||||
string,
|
||||
FC<IEditorComponentProps>[]
|
||||
> = {
|
||||
[NODE_TYPES.TEXT]: [
|
||||
EditorFiller,
|
||||
EditorUploadCoverButton,
|
||||
EditorPublicSwitch,
|
||||
],
|
||||
[NODE_TYPES.VIDEO]: [
|
||||
EditorFiller,
|
||||
EditorUploadCoverButton,
|
||||
EditorPublicSwitch,
|
||||
],
|
||||
[NODE_TYPES.IMAGE]: [
|
||||
EditorImageUploadButton,
|
||||
EditorFiller,
|
||||
|
@ -112,6 +136,11 @@ export const NODE_PANEL_COMPONENTS: Record<string, FC<IEditorComponentProps>[]>
|
|||
EditorUploadCoverButton,
|
||||
EditorPublicSwitch,
|
||||
],
|
||||
[NODE_TYPES.ROOM]: [
|
||||
EditorAudioUploadButton,
|
||||
EditorImageUploadButton,
|
||||
EditorFiller,
|
||||
],
|
||||
};
|
||||
|
||||
export const NODE_EDITOR_DATA: Record<
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "src/styles/variables";
|
||||
@import 'src/styles/variables';
|
||||
|
||||
.wrap {
|
||||
position: sticky;
|
||||
|
|
|
@ -13,7 +13,7 @@ import { keys } from '~/utils/ramda';
|
|||
export const useUploader = (
|
||||
subject: UploadSubject,
|
||||
target: UploadTarget,
|
||||
initialFiles?: IFile[]
|
||||
initialFiles?: IFile[],
|
||||
) => {
|
||||
const store = useLocalObservable(() => new UploaderStore(initialFiles));
|
||||
|
||||
|
@ -26,8 +26,14 @@ export const useUploader = (
|
|||
// TODO: cancel all uploads on unmount
|
||||
|
||||
const pending = await store.addPending(id, file);
|
||||
const onProgress = ({ loaded, total }) => store.updateProgress(id, loaded, total);
|
||||
const result = await apiUploadFile({ file, target, type: pending.type, onProgress });
|
||||
const onProgress = ({ loaded, total }) =>
|
||||
store.updateProgress(id, loaded, total);
|
||||
const result = await apiUploadFile({
|
||||
file,
|
||||
target,
|
||||
type: pending.type,
|
||||
onProgress,
|
||||
});
|
||||
|
||||
store.removePending(id);
|
||||
store.addFile(result);
|
||||
|
@ -38,14 +44,14 @@ export const useUploader = (
|
|||
showErrorToast(error);
|
||||
}
|
||||
},
|
||||
[store, target]
|
||||
[store, target],
|
||||
);
|
||||
|
||||
const uploadFiles = useCallback(
|
||||
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;
|
||||
|
@ -61,5 +67,7 @@ export const useUploader = (
|
|||
pendingAudios: store.pendingAudios,
|
||||
isUploading,
|
||||
setFiles: store.setFiles,
|
||||
setImages: store.setImages,
|
||||
setAudios: store.setAudios,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,51 +1,76 @@
|
|||
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 { useAuth } from '~/hooks/auth/useAuth';
|
||||
import { useShowModal } from '~/hooks/modal/useShowModal';
|
||||
import markdown from '~/styles/common/markdown.module.scss';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
interface RoomLayoutProps {}
|
||||
|
||||
const RoomLayout: FC<RoomLayoutProps> = () => (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.room}>
|
||||
<Container>
|
||||
<div className={styles.info}>
|
||||
<div className={markdown.wrapper}>
|
||||
<h1>Рум</h1>
|
||||
const RoomLayout: FC<RoomLayoutProps> = () => {
|
||||
const createNode = useShowModal(Dialog.CreateNode);
|
||||
const { user } = useAuth();
|
||||
|
||||
<p>
|
||||
Пока ещё концепт, над которым я размышляю, ты видишь его, потому
|
||||
что включил суперсилы в <a href="/boris">Борисе</a>.
|
||||
</p>
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.room}>
|
||||
<Container>
|
||||
<div className={styles.info}>
|
||||
<div className={markdown.wrapper}>
|
||||
<h1>Рум</h1>
|
||||
|
||||
<p>
|
||||
Все идеи насчёт этого раздела можно посмотреть{' '}
|
||||
<a
|
||||
href="https://github.com/muerwre/vault-frontend/issues/158"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
на гитхабе
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
Пока ещё концепт, над которым я размышляю, ты видишь его, потому
|
||||
что включил суперсилы в <a href="/boris">Борисе</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Здесь, скорее всего, будет возможность добавить несколько песен и
|
||||
картинок для слайдшоу.
|
||||
</p>
|
||||
<p>
|
||||
Все идеи насчёт этого раздела можно посмотреть{' '}
|
||||
<a
|
||||
href="https://github.com/muerwre/vault-frontend/issues/158"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
на гитхабе
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Если помните einsam.ru или раздел Nowhere на старой версии сайта,
|
||||
то будет что-то такое.
|
||||
</p>
|
||||
<p>
|
||||
Здесь, скорее всего, будет возможность добавить несколько песен
|
||||
и картинок для слайдшоу.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Если помните einsam.ru или раздел Nowhere на старой версии
|
||||
сайта, то будет что-то такое.
|
||||
</p>
|
||||
|
||||
{user.role === Role.Admin && (
|
||||
<p>
|
||||
<br />
|
||||
|
||||
<Button
|
||||
onClick={() =>
|
||||
createNode({ isInLab: false, type: NODE_TYPES.ROOM })
|
||||
}
|
||||
>
|
||||
Добавить комнату
|
||||
</Button>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</Container>
|
||||
</div>
|
||||
<div className={styles.items}></div>
|
||||
</div>
|
||||
<div className={styles.items}></div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export { RoomLayout };
|
||||
|
|
|
@ -17,6 +17,7 @@ const BorisPage: VFC = observer(() => {
|
|||
const { node, isLoading, update } = useLoadNode(BORIS_NODE_ID);
|
||||
|
||||
const onShowImageModal = useImageModal();
|
||||
|
||||
const {
|
||||
onLoadMoreComments,
|
||||
onDelete: onDeleteComment,
|
||||
|
@ -26,6 +27,7 @@ const BorisPage: VFC = observer(() => {
|
|||
isLoading: isLoadingComments,
|
||||
isLoadingMore,
|
||||
} = useNodeComments(BORIS_NODE_ID);
|
||||
|
||||
const { title, stats, isLoadingStats } = useBoris(comments);
|
||||
|
||||
return (
|
||||
|
|
|
@ -23,7 +23,21 @@ export class UploaderStore {
|
|||
}
|
||||
|
||||
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 */
|
||||
addPending = async (id: string, file: File) => {
|
||||
|
@ -60,21 +74,25 @@ export class UploaderStore {
|
|||
|
||||
/** returns only image files */
|
||||
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 */
|
||||
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 */
|
||||
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 */
|
||||
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: [],
|
||||
isUploading: false,
|
||||
setFiles: (files: IFile[]) => files,
|
||||
setImages: (files: IFile[]) => files,
|
||||
setAudios: (files: IFile[]) => files,
|
||||
});
|
||||
|
||||
export const UploaderContextProvider: FC<{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue