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

#23 using custom blocks at lab nodes

This commit is contained in:
Fedor Katurov 2021-03-22 11:09:38 +07:00
parent bee41ebfb3
commit 031de64acc
17 changed files with 330 additions and 43 deletions

View file

@ -0,0 +1,11 @@
import React, { ButtonHTMLAttributes, DetailedHTMLProps, FC, HTMLAttributes } from 'react';
import styles from '~/styles/common/markdown.module.scss';
import classNames from 'classnames';
interface IProps extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {}
const Markdown: FC<IProps> = ({ className, ...props }) => (
<div className={classNames(styles.wrapper, className)} {...props} />
);
export { Markdown };

View file

@ -0,0 +1,17 @@
import React, { FC } from 'react';
import { Group } from '~/components/containers/Group';
import { Icon } from '~/components/input/Icon';
import { INodeComponentProps } from '~/redux/node/constants';
import { Filler } from '~/components/containers/Filler';
import styles from './styles.module.scss';
import { getPrettyDate } from '~/utils/dom';
const LabBottomPanel: FC<INodeComponentProps> = ({ node }) => (
<Group horizontal className={styles.wrap}>
<div className={styles.timestamp}>{getPrettyDate(node.created_at)}</div>
<Filler />
</Group>
);
export { LabBottomPanel };

View file

@ -0,0 +1,10 @@
@import "~/styles/variables.scss";
.wrap {
padding: 0 $gap $gap;
}
.timestamp {
font: $font_12_regular;
color: darken(white, 40%);
}

View file

@ -0,0 +1,102 @@
import React, { FC, useCallback, useEffect, useState } from 'react';
import { INodeComponentProps } from '~/redux/node/constants';
import SwiperCore, { A11y, Pagination, Navigation, SwiperOptions, Keyboard } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import 'swiper/swiper.scss';
import 'swiper/components/pagination/pagination.scss';
import 'swiper/components/scrollbar/scrollbar.scss';
import 'swiper/components/zoom/zoom.scss';
import 'swiper/components/navigation/navigation.scss';
import styles from './styles.module.scss';
import { useNodeImages } from '~/utils/hooks/node/useNodeImages';
import { getURL } from '~/utils/dom';
import { PRESETS, URLS } from '~/constants/urls';
import SwiperClass from 'swiper/types/swiper-class';
import { modalShowPhotoswipe } from '~/redux/modal/actions';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router';
SwiperCore.use([Navigation, Pagination, A11y]);
interface IProps extends INodeComponentProps {}
const breakpoints: SwiperOptions['breakpoints'] = {
599: {
spaceBetween: 20,
navigation: true,
},
};
const LabImage: FC<IProps> = ({ node }) => {
const dispatch = useDispatch();
const history = useHistory();
const [controlledSwiper, setControlledSwiper] = useState<SwiperClass | undefined>(undefined);
const images = useNodeImages(node);
const updateSwiper = useCallback(() => {
if (!controlledSwiper) return;
controlledSwiper.updateSlides();
controlledSwiper.updateSize();
controlledSwiper.update();
}, [controlledSwiper]);
const resetSwiper = useCallback(() => {
if (!controlledSwiper) return;
controlledSwiper.slideTo(0, 0);
setTimeout(() => controlledSwiper.slideTo(0, 0), 300);
}, [controlledSwiper]);
useEffect(() => {
updateSwiper();
resetSwiper();
}, [images, updateSwiper, resetSwiper]);
const onClick = useCallback(() => history.push(URLS.NODE_URL(node.id)), [history, node.id]);
if (!images?.length) {
return null;
}
return (
<div className={styles.wrapper}>
<Swiper
initialSlide={0}
slidesPerView={images.length > 1 ? 1.1 : 1}
onSwiper={setControlledSwiper}
spaceBetween={10}
grabCursor
autoHeight
breakpoints={breakpoints}
observeSlideChildren
observeParents
resizeObserver
watchOverflow
updateOnImagesReady
onInit={resetSwiper}
keyboard={{
enabled: true,
onlyInViewport: false,
}}
>
{images.map(file => (
<SwiperSlide className={styles.slide} key={file.id}>
<img
className={styles.image}
src={getURL(file, PRESETS['1600'])}
alt={node.title}
onLoad={updateSwiper}
onClick={onClick}
/>
</SwiperSlide>
))}
</Swiper>
</div>
);
};
export { LabImage };

View file

@ -0,0 +1,70 @@
@import "~/styles/variables.scss";
.wrapper {
border-radius: $radius;
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
:global(.swiper-container) {
width: 100%;
}
:global(.swiper-button-next),
:global(.swiper-button-prev) {
color: white;
font-size: 10px;
&::after {
font-size: 32px;
}
}
}
.slide {
text-align: center;
text-transform: uppercase;
font: $font_32_bold;
display: flex;
border-radius: $radius;
align-items: center;
justify-content: center;
width: auto;
max-width: 100%;
opacity: 1;
filter: brightness(50%) saturate(0.5);
transition: opacity 0.5s, filter 0.5s, transform 0.5s;
&:global(.swiper-slide-active) {
opacity: 1;
filter: brightness(100%);
}
@include tablet {
padding-bottom: 0;
padding-top: 0;
}
}
.image {
max-height: calc(100vh - 70px - 70px);
max-width: 100%;
border-radius: $radius;
transition: box-shadow 1s;
box-shadow: transparentize(black, 0.7) 0 3px 5px;
:global(.swiper-slide-active) & {
box-shadow: transparentize(black, 0.9) 0 10px 5px 4px,
transparentize(black, 0.7) 0 5px 5px,
transparentize(white, 0.95) 0 -1px 2px,
transparentize(white, 0.95) 0 -1px;
}
@include tablet {
padding-bottom: 0;
max-height: 100vh;
border-radius: 0;
}
}

View file

@ -4,28 +4,17 @@ import { NodePanelInner } from '~/components/node/NodePanelInner';
import { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks';
import styles from './styles.module.scss';
import { Card } from '~/components/containers/Card';
import { NodePanelLab } from '~/components/node/NodePanelLab';
import { LabNodeTitle } from '~/components/lab/LabNodeTitle';
import { Grid } from '~/components/containers/Grid';
interface IProps {
node: INode;
}
const LabNode: FC<IProps> = ({ node }) => {
const { inline, block, head } = useNodeBlocks(node, false);
const { lab } = useNodeBlocks(node, false);
console.log(node.id, { inline, block, head });
return (
<Card seamless className={styles.wrap}>
<div className={styles.head}>
<NodePanelLab node={node} />
</div>
{head}
{block}
{inline}
</Card>
);
return <div className={styles.wrap}>{lab}</div>;
};
export { LabNode };

View file

@ -1,11 +1,11 @@
@import "~/styles/variables.scss";
.wrap {
box-shadow: transparentize(black, 0.5) 0 0 0 1px, inset transparentize(white, 0.9) 0 1px, lighten(black, 10%) 0 4px;
background-color: $content_bg;
cursor: pointer;
min-width: 0;
}
.head {
background-color: transparentize(black, 0.9);
border-radius: $radius $radius 0 0;
border-radius: $radius;
}

View file

@ -0,0 +1,23 @@
import React, { FC } from 'react';
import { INode } from '~/redux/types';
import styles from './styles.module.scss';
import { URLS } from '~/constants/urls';
import { Link } from 'react-router-dom';
interface IProps {
node: INode;
}
const LabNodeTitle: FC<IProps> = ({ node }) => {
if (!node.title) return null;
return (
<div className={styles.wrap}>
<div className={styles.title}>
<Link to={URLS.NODE_URL(node.id)}>{node.title || '...'}</Link>
</div>
</div>
);
};
export { LabNodeTitle };

View file

@ -1,7 +1,7 @@
@import "~/styles/variables.scss";
.wrap {
padding: $gap;
padding: 0 $gap;
}
.title {

View file

@ -0,0 +1,8 @@
import React, { FC } from 'react';
import styles from './styles.module.scss';
interface IProps {}
const LabPad: FC<IProps> = () => <div className={styles.pad} />;
export { LabPad };

View file

@ -0,0 +1,5 @@
@import "~/styles/variables.scss";
.pad {
height: $gap;
}

View file

@ -0,0 +1,28 @@
import React, { FC, useCallback, useMemo } from 'react';
import { Markdown } from '~/components/containers/Markdown';
import { INodeComponentProps } from '~/redux/node/constants';
import { formatTextParagraphs } from '~/utils/dom';
import { path } from 'ramda';
import styles from './styles.module.scss';
import { useHistory } from 'react-router';
import { URLS } from '~/constants/urls';
const LabText: FC<INodeComponentProps> = ({ node }) => {
const content = useMemo(() => formatTextParagraphs(path(['blocks', 0, 'text'], node) || ''), [
node.blocks,
]);
const history = useHistory();
const onClick = useCallback(() => history.push(URLS.NODE_URL(node.id)), [node.id]);
return (
<Markdown
dangerouslySetInnerHTML={{ __html: content }}
className={styles.wrap}
onClick={onClick}
/>
);
};
export { LabText };

View file

@ -0,0 +1,21 @@
@import "~/styles/variables.scss";
.wrap {
padding: 0 $gap;
@include tablet {
position: relative;
max-height: 50vh;
overflow: hidden;
&::after {
content: ' ';
position: absolute;
background: linear-gradient(transparentize($content_bg, 1), $content_bg 80%);
bottom: 0;
left: auto;
width: 100%;
height: 100px;
}
}
}

View file

@ -1,19 +0,0 @@
import React, { FC } from 'react';
import { INode } from '~/redux/types';
import styles from './styles.module.scss';
import { URLS } from '~/constants/urls';
import { Link } from 'react-router-dom';
interface IProps {
node: INode;
}
const NodePanelLab: FC<IProps> = ({ node }) => (
<div className={styles.wrap}>
<div className={styles.title}>
<Link to={URLS.NODE_URL(node.id)}>{node.title || '...'}</Link>
</div>
</div>
);
export { NodePanelLab };

View file

@ -4,5 +4,5 @@
display: grid;
grid-auto-flow: row;
grid-auto-rows: auto;
grid-row-gap: $gap;
grid-row-gap: $gap * 2;
}

View file

@ -15,6 +15,11 @@ import { IEditorComponentProps, NodeEditorProps } from '~/redux/node/types';
import { EditorFiller } from '~/components/editors/EditorFiller';
import { EditorPublicSwitch } from '~/components/editors/EditorPublicSwitch';
import { NodeImageSwiperBlock } from '~/components/node/NodeImageSwiperBlock';
import { LabNodeTitle } from '~/components/lab/LabNodeTitle';
import { LabText } from '~/components/lab/LabText';
import { LabImage } from '~/components/lab/LabImage';
import { LabBottomPanel } from '~/components/lab/LabBottomPanel';
import { LabPad } from '~/components/lab/LabPad';
const prefix = 'NODE.';
export const NODE_ACTIONS = {
@ -83,6 +88,13 @@ export type 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, LabBottomPanel],
[NODE_TYPES.VIDEO]: [NodeVideoBlock, LabPad, LabNodeTitle, LabBottomPanel],
[NODE_TYPES.AUDIO]: [LabPad, LabNodeTitle, NodeAudioImageBlock, NodeAudioBlock, LabBottomPanel],
[NODE_TYPES.TEXT]: [LabPad, LabNodeTitle, LabText, LabBottomPanel],
};
export const NODE_HEADS: INodeComponents = {
[NODE_TYPES.IMAGE]: NodeImageSwiperBlock,
};

View file

@ -3,6 +3,7 @@ import { createElement, FC, useCallback, useMemo } from 'react';
import { isNil, prop } from 'ramda';
import {
INodeComponentProps,
LAB_PREVIEW_LAYOUT,
NODE_COMPONENTS,
NODE_HEADS,
NODE_INLINES,
@ -11,11 +12,12 @@ import {
// useNodeBlocks returns head, block and inline blocks of node
export const useNodeBlocks = (node: INode, isLoading: boolean) => {
const createNodeBlock = useCallback(
(block?: FC<INodeComponentProps>) =>
(block?: FC<INodeComponentProps>, key = 0) =>
!isNil(block) &&
createElement(block, {
node,
isLoading,
key,
}),
[node, isLoading]
);
@ -35,5 +37,13 @@ export const useNodeBlocks = (node: INode, isLoading: boolean) => {
[node, createNodeBlock]
);
return { head, block, inline };
const lab = useMemo(
() =>
node?.type && prop(node.type, LAB_PREVIEW_LAYOUT)
? prop(node.type, LAB_PREVIEW_LAYOUT).map((comp, i) => createNodeBlock(comp, i))
: undefined,
[node, createNodeBlock]
);
return { head, block, inline, lab };
};