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:
parent
bee41ebfb3
commit
031de64acc
17 changed files with 330 additions and 43 deletions
11
src/components/containers/Markdown/index.tsx
Normal file
11
src/components/containers/Markdown/index.tsx
Normal 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 };
|
17
src/components/lab/LabBottomPanel/index.tsx
Normal file
17
src/components/lab/LabBottomPanel/index.tsx
Normal 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 };
|
10
src/components/lab/LabBottomPanel/styles.module.scss
Normal file
10
src/components/lab/LabBottomPanel/styles.module.scss
Normal file
|
@ -0,0 +1,10 @@
|
|||
@import "~/styles/variables.scss";
|
||||
|
||||
.wrap {
|
||||
padding: 0 $gap $gap;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font: $font_12_regular;
|
||||
color: darken(white, 40%);
|
||||
}
|
102
src/components/lab/LabImage/index.tsx
Normal file
102
src/components/lab/LabImage/index.tsx
Normal 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 };
|
70
src/components/lab/LabImage/styles.module.scss
Normal file
70
src/components/lab/LabImage/styles.module.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
23
src/components/lab/LabNodeTitle/index.tsx
Normal file
23
src/components/lab/LabNodeTitle/index.tsx
Normal 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 };
|
|
@ -1,7 +1,7 @@
|
|||
@import "~/styles/variables.scss";
|
||||
|
||||
.wrap {
|
||||
padding: $gap;
|
||||
padding: 0 $gap;
|
||||
}
|
||||
|
||||
.title {
|
8
src/components/lab/LabPad/index.tsx
Normal file
8
src/components/lab/LabPad/index.tsx
Normal 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 };
|
5
src/components/lab/LabPad/styles.module.scss
Normal file
5
src/components/lab/LabPad/styles.module.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
@import "~/styles/variables.scss";
|
||||
|
||||
.pad {
|
||||
height: $gap;
|
||||
}
|
28
src/components/lab/LabText/index.tsx
Normal file
28
src/components/lab/LabText/index.tsx
Normal 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 };
|
21
src/components/lab/LabText/styles.module.scss
Normal file
21
src/components/lab/LabText/styles.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 };
|
|
@ -4,5 +4,5 @@
|
|||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
grid-auto-rows: auto;
|
||||
grid-row-gap: $gap;
|
||||
grid-row-gap: $gap * 2;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue