mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +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 { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { Card } from '~/components/containers/Card';
|
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 {
|
interface IProps {
|
||||||
node: INode;
|
node: INode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LabNode: FC<IProps> = ({ node }) => {
|
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 <div className={styles.wrap}>{lab}</div>;
|
||||||
|
|
||||||
return (
|
|
||||||
<Card seamless className={styles.wrap}>
|
|
||||||
<div className={styles.head}>
|
|
||||||
<NodePanelLab node={node} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{head}
|
|
||||||
{block}
|
|
||||||
{inline}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { LabNode };
|
export { LabNode };
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
@import "~/styles/variables.scss";
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
.wrap {
|
.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;
|
min-width: 0;
|
||||||
}
|
border-radius: $radius;
|
||||||
|
|
||||||
.head {
|
|
||||||
background-color: transparentize(black, 0.9);
|
|
||||||
border-radius: $radius $radius 0 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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";
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
padding: $gap;
|
padding: 0 $gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.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;
|
display: grid;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
grid-auto-rows: auto;
|
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 { EditorFiller } from '~/components/editors/EditorFiller';
|
||||||
import { EditorPublicSwitch } from '~/components/editors/EditorPublicSwitch';
|
import { EditorPublicSwitch } from '~/components/editors/EditorPublicSwitch';
|
||||||
import { NodeImageSwiperBlock } from '~/components/node/NodeImageSwiperBlock';
|
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.';
|
const prefix = 'NODE.';
|
||||||
export const NODE_ACTIONS = {
|
export const NODE_ACTIONS = {
|
||||||
|
@ -83,6 +88,13 @@ export type INodeComponentProps = {
|
||||||
|
|
||||||
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, 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 = {
|
export const NODE_HEADS: INodeComponents = {
|
||||||
[NODE_TYPES.IMAGE]: NodeImageSwiperBlock,
|
[NODE_TYPES.IMAGE]: NodeImageSwiperBlock,
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { createElement, FC, useCallback, useMemo } from 'react';
|
||||||
import { isNil, prop } from 'ramda';
|
import { isNil, prop } from 'ramda';
|
||||||
import {
|
import {
|
||||||
INodeComponentProps,
|
INodeComponentProps,
|
||||||
|
LAB_PREVIEW_LAYOUT,
|
||||||
NODE_COMPONENTS,
|
NODE_COMPONENTS,
|
||||||
NODE_HEADS,
|
NODE_HEADS,
|
||||||
NODE_INLINES,
|
NODE_INLINES,
|
||||||
|
@ -11,11 +12,12 @@ import {
|
||||||
// useNodeBlocks returns head, block and inline blocks of node
|
// useNodeBlocks returns head, block and inline blocks of node
|
||||||
export const useNodeBlocks = (node: INode, isLoading: boolean) => {
|
export const useNodeBlocks = (node: INode, isLoading: boolean) => {
|
||||||
const createNodeBlock = useCallback(
|
const createNodeBlock = useCallback(
|
||||||
(block?: FC<INodeComponentProps>) =>
|
(block?: FC<INodeComponentProps>, key = 0) =>
|
||||||
!isNil(block) &&
|
!isNil(block) &&
|
||||||
createElement(block, {
|
createElement(block, {
|
||||||
node,
|
node,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
key,
|
||||||
}),
|
}),
|
||||||
[node, isLoading]
|
[node, isLoading]
|
||||||
);
|
);
|
||||||
|
@ -35,5 +37,13 @@ export const useNodeBlocks = (node: INode, isLoading: boolean) => {
|
||||||
[node, createNodeBlock]
|
[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