mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
NodeLayout
This commit is contained in:
parent
684a4f4474
commit
b154277de8
23 changed files with 332 additions and 65 deletions
16
package-lock.json
generated
16
package-lock.json
generated
|
@ -2655,6 +2655,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
|
||||||
"integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag=="
|
"integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag=="
|
||||||
},
|
},
|
||||||
|
"@types/history": {
|
||||||
|
"version": "4.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.3.tgz",
|
||||||
|
"integrity": "sha512-cS5owqtwzLN5kY+l+KgKdRJ/Cee8tlmQoGQuIE9tWnSmS3JMKzmxo2HIAk2wODMifGwO20d62xZQLYz+RLfXmw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/json-schema": {
|
"@types/json-schema": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz",
|
||||||
|
@ -2698,6 +2704,16 @@
|
||||||
"csstype": "^2.2.0"
|
"csstype": "^2.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-router": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-j2Gge5cvxca+5lK9wxovmGPgpVJMwjyu5lTA/Cd6fLGoPq7FXcUE1jFkEdxeyqGGz8VfHYSHCn5Lcn24BzaNKA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/history": "*",
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/redux-saga": {
|
"@types/redux-saga": {
|
||||||
"version": "0.10.5",
|
"version": "0.10.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/redux-saga/-/redux-saga-0.10.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/redux-saga/-/redux-saga-0.10.5.tgz",
|
||||||
|
|
|
@ -14,9 +14,10 @@
|
||||||
"url": "https://github.com/muerwre/my-empty-react-project"
|
"url": "https://github.com/muerwre/my-empty-react-project"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/types": "7.5.5",
|
|
||||||
"@babel/cli": "^7.0.0-rc.1",
|
"@babel/cli": "^7.0.0-rc.1",
|
||||||
"@babel/preset-env": "^7.0.0-rc.1",
|
"@babel/preset-env": "^7.0.0-rc.1",
|
||||||
|
"@babel/types": "7.5.5",
|
||||||
|
"@types/react-router": "^5.0.3",
|
||||||
"autoresponsive-react": "^1.1.31",
|
"autoresponsive-react": "^1.1.31",
|
||||||
"awesome-typescript-loader": "^5.2.1",
|
"awesome-typescript-loader": "^5.2.1",
|
||||||
"babel-core": "^6.26.3",
|
"babel-core": "^6.26.3",
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import React, { FC, useState, useCallback } from 'react';
|
import React, { FC, useState, useCallback } from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import * as styles from './styles.scss';
|
import { URLS } from '~/constants/urls';
|
||||||
import { getImageSize } from '~/utils/dom';
|
import { getImageSize } from '~/utils/dom';
|
||||||
import classNames = require('classnames');
|
import classNames = require('classnames');
|
||||||
import { URLS } from '~/constants/urls';
|
|
||||||
|
import * as styles from './styles.scss';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
node: INode;
|
node: INode;
|
||||||
|
@ -13,20 +14,23 @@ interface IProps {
|
||||||
// title?: string;
|
// title?: string;
|
||||||
// is_hero?: boolean;
|
// is_hero?: boolean;
|
||||||
// is_stamp?: boolean;
|
// is_stamp?: boolean;
|
||||||
|
onSelect: (id: INode['id']) => void;
|
||||||
is_text?: boolean;
|
is_text?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Cell: FC<IProps> = ({ node: { id, title, brief }, is_text = false }) => {
|
const Cell: FC<IProps> = ({ node: { id, title, brief }, onSelect, is_text = false }) => {
|
||||||
const [is_loaded, setIsLoaded] = useState(false);
|
const [is_loaded, setIsLoaded] = useState(false);
|
||||||
|
|
||||||
const onImageLoad = useCallback(() => {
|
const onImageLoad = useCallback(() => {
|
||||||
setIsLoaded(true);
|
setIsLoaded(true);
|
||||||
}, [setIsLoaded]);
|
}, [setIsLoaded]);
|
||||||
|
|
||||||
|
const onClick = useCallback(() => onSelect(id), [onSelect, id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<div
|
||||||
to={URLS.NODE_URL(id)}
|
|
||||||
className={classNames(styles.cell, 'vert-1', 'hor-1', { is_text: false })}
|
className={classNames(styles.cell, 'vert-1', 'hor-1', { is_text: false })}
|
||||||
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className={styles.face}>{title && <div className={styles.title}>{title}</div>}</div>
|
<div className={styles.face}>{title && <div className={styles.title}>{title}</div>}</div>
|
||||||
|
|
||||||
|
@ -41,7 +45,7 @@ const Cell: FC<IProps> = ({ node: { id, title, brief }, is_text = false }) => {
|
||||||
<img src={getImageSize(brief.thumbnail, 'medium')} onLoad={onImageLoad} alt="" />
|
<img src={getImageSize(brief.thumbnail, 'medium')} onLoad={onImageLoad} alt="" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</NavLink>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,17 +3,20 @@ import { Cell } from '~/components/flow/Cell';
|
||||||
|
|
||||||
import * as styles from './styles.scss';
|
import * as styles from './styles.scss';
|
||||||
import { IFlowState } from '~/redux/flow/reducer';
|
import { IFlowState } from '~/redux/flow/reducer';
|
||||||
|
import { INode } from '~/redux/types';
|
||||||
|
|
||||||
type IProps = Partial<IFlowState> & {};
|
type IProps = Partial<IFlowState> & {
|
||||||
|
onSelect: (id: INode['id']) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export const FlowGrid: FC<IProps> = ({ nodes }) => (
|
export const FlowGrid: FC<IProps> = ({ nodes, onSelect }) => (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.grid_test}>
|
<div className={styles.grid_test}>
|
||||||
<div className={styles.hero}>HERO</div>
|
<div className={styles.hero}>HERO</div>
|
||||||
<div className={styles.stamp}>STAMP</div>
|
<div className={styles.stamp}>STAMP</div>
|
||||||
|
|
||||||
{nodes.map(node => (
|
{nodes.map(node => (
|
||||||
<Cell key={node.id} node={node} />
|
<Cell key={node.id} node={node} onSelect={onSelect} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,37 +1,17 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import * as styles from './styles.scss';
|
import * as styles from './styles.scss';
|
||||||
|
import { Icon } from '../Icon';
|
||||||
|
import { describeArc } from '~/utils/dom';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
size?: number;
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LoaderCircle: FC<IProps> = ({ size = 24 }) => (
|
export const LoaderCircle: FC<IProps> = ({ size = 24 }) => (
|
||||||
<div className={styles.wrap}>
|
|
||||||
<svg width={size} height={size} viewBox="0 0 24 24">
|
|
||||||
<g strokeWidth={0.5}>
|
|
||||||
{
|
|
||||||
[...new Array(8)].map((el, i) => (
|
|
||||||
<path
|
|
||||||
d="M11,2 L11,6 C11,6.55228475 11.4477153,7 12,7 C12.5522847,7 13,6.55228475 13,6 L13,2 C13,1.44771525 12.5522847,1 12,1 C11.4477153,1 11,1.44771525 11,2 Z"
|
|
||||||
opacity={0.125 * (8 - i)}
|
|
||||||
transform={`rotate(${Math.floor(45 * i)} 12 12)`}
|
|
||||||
// style={{
|
|
||||||
// animationDelay: `${-100 * (8 - i)}ms`,
|
|
||||||
// }}
|
|
||||||
key={i}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<svg className={styles.icon} width={size} height={size}>
|
<svg className={styles.icon} width={size} height={size}>
|
||||||
<path d={describeArc(size / 2, size / 2, size / 2, 0, 90)} />
|
<path d={describeArc(size / 2, size / 2, size / 2, 0, 90)} />
|
||||||
<path d={describeArc(size / 2, size / 2, size / 2, 180, 270)} />
|
<path d={describeArc(size / 2, size / 2, size / 2, 180, 270)} />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
*/
|
);
|
||||||
|
|
|
@ -4,16 +4,26 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade {
|
@keyframes fade {
|
||||||
0% { opacity: 1; transform: scale(1); }
|
0% {
|
||||||
100% { opacity: 0.1; transform: scale(4); }
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.1;
|
||||||
|
transform: scale(4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
animation: spin infinite steps(9, end) 1s;
|
animation: spin infinite 1s linear;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
18
src/components/node/NodeComments/index.tsx
Normal file
18
src/components/node/NodeComments/index.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import range from 'ramda/es/range';
|
||||||
|
import { Comment } from '../Comment';
|
||||||
|
import { INode } from '~/redux/types';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
comments?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeComments: FC<IProps> = ({ comments }) => (
|
||||||
|
<div>
|
||||||
|
{range(1, 6).map(el => (
|
||||||
|
<Comment key={el} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { NodeComments };
|
21
src/components/node/NodeImageBlock/index.tsx
Normal file
21
src/components/node/NodeImageBlock/index.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { ImageSwitcher } from '../ImageSwitcher';
|
||||||
|
import * as styles from './styles.scss';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const NodeImageBlock: FC<IProps> = ({}) => (
|
||||||
|
<div>
|
||||||
|
<ImageSwitcher total={5} current={2} />
|
||||||
|
|
||||||
|
<div className={styles.image_container}>
|
||||||
|
<img
|
||||||
|
className={styles.image}
|
||||||
|
src="http://37.192.131.144/full/attached/2019/08/e4fb2a1d0a2e20d499aaa1f5f83a7115.jpg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { NodeImageBlock };
|
15
src/components/node/NodeImageBlock/styles.scss
Normal file
15
src/components/node/NodeImageBlock/styles.scss
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
.image_container {
|
||||||
|
width: 100%;
|
||||||
|
background: $node_image_bg;
|
||||||
|
border-radius: $panel_radius 0 0 $panel_radius;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.image {
|
||||||
|
max-height: 800px;
|
||||||
|
opacity: 1;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: $radius $radius 0 0;
|
||||||
|
}
|
||||||
|
}
|
11
src/components/node/NodeImageBlockPlaceholder/index.tsx
Normal file
11
src/components/node/NodeImageBlockPlaceholder/index.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import * as styles from './styles.scss';
|
||||||
|
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
||||||
|
|
||||||
|
const NodeImageBlockPlaceholder: FC<{}> = () => (
|
||||||
|
<div className={styles.placeholder}>
|
||||||
|
<LoaderCircle size={64} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { NodeImageBlockPlaceholder };
|
14
src/components/node/NodeImageBlockPlaceholder/styles.scss
Normal file
14
src/components/node/NodeImageBlockPlaceholder/styles.scss
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
.placeholder {
|
||||||
|
height: 33vw;
|
||||||
|
background: transparentize(black, 0.8);
|
||||||
|
border: $radius $radius 0 0;
|
||||||
|
@include outer_shadow();
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: transparentize(white, 0.95);
|
||||||
|
}
|
||||||
|
}
|
18
src/components/node/NodeTags/index.tsx
Normal file
18
src/components/node/NodeTags/index.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Tags } from '../Tags';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const NodeTags: FC<IProps> = ({}) => (
|
||||||
|
<Tags
|
||||||
|
tags={[
|
||||||
|
{ title: 'Избранный', feature: 'red' },
|
||||||
|
{ title: 'Плейлист', feature: 'green' },
|
||||||
|
{ title: 'Просто' },
|
||||||
|
{ title: '+ фото', feature: 'black' },
|
||||||
|
{ title: '+ с музыкой', feature: 'black' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { NodeTags };
|
|
@ -7,5 +7,5 @@ export const URLS = {
|
||||||
EDITOR: '/examples/edit',
|
EDITOR: '/examples/edit',
|
||||||
IMAGE: '/examples/image',
|
IMAGE: '/examples/image',
|
||||||
},
|
},
|
||||||
NODE_URL: (id: number) => `/post${id}`,
|
NODE_URL: (id: number | string) => `/post${id}`,
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { URLS } from '~/constants/urls';
|
||||||
import { Modal } from '~/containers/dialogs/Modal';
|
import { Modal } from '~/containers/dialogs/Modal';
|
||||||
import { selectModal } from '~/redux/modal/selectors';
|
import { selectModal } from '~/redux/modal/selectors';
|
||||||
import { BlurWrapper } from '~/components/containers/BlurWrapper';
|
import { BlurWrapper } from '~/components/containers/BlurWrapper';
|
||||||
|
import { NodeLayout } from './node/NodeLayout';
|
||||||
|
|
||||||
const mapStateToProps = selectModal;
|
const mapStateToProps = selectModal;
|
||||||
const mapDispatchToProps = {};
|
const mapDispatchToProps = {};
|
||||||
|
@ -32,6 +33,7 @@ const Component: FC<IProps> = ({ is_shown }) => (
|
||||||
<Route path={URLS.EXAMPLES.IMAGE} component={ImageExample} />
|
<Route path={URLS.EXAMPLES.IMAGE} component={ImageExample} />
|
||||||
<Route path={URLS.EXAMPLES.EDITOR} component={EditorExample} />
|
<Route path={URLS.EXAMPLES.EDITOR} component={EditorExample} />
|
||||||
<Route path="/examples/horizontal" component={HorizontalExample} />
|
<Route path="/examples/horizontal" component={HorizontalExample} />
|
||||||
|
<Route path="/post:id" component={NodeLayout} />
|
||||||
|
|
||||||
<Redirect to="/" />
|
<Redirect to="/" />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
|
@ -2,14 +2,17 @@ import React, { FC } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { FlowGrid } from '~/components/flow/FlowGrid';
|
import { FlowGrid } from '~/components/flow/FlowGrid';
|
||||||
import { selectFlow } from '~/redux/flow/selectors';
|
import { selectFlow } from '~/redux/flow/selectors';
|
||||||
|
import * as NODE_ACTIONS from '~/redux/node/actions';
|
||||||
|
|
||||||
const mapStateToProps = selectFlow;
|
const mapStateToProps = selectFlow;
|
||||||
|
|
||||||
const mapDispatchToProps = {};
|
const mapDispatchToProps = { nodeLoadNode: NODE_ACTIONS.nodeLoadNode };
|
||||||
|
|
||||||
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||||
|
|
||||||
const FlowLayoutUnconnected: FC<IProps> = ({ nodes }) => <FlowGrid nodes={nodes} />;
|
const FlowLayoutUnconnected: FC<IProps> = ({ nodes, nodeLoadNode }) => (
|
||||||
|
<FlowGrid nodes={nodes} onSelect={nodeLoadNode} />
|
||||||
|
);
|
||||||
|
|
||||||
const FlowLayout = connect(
|
const FlowLayout = connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
|
|
|
@ -1,10 +1,70 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC, createElement } from 'react';
|
||||||
|
import { RouteComponentProps } from 'react-router';
|
||||||
|
import range from 'ramda/es/range';
|
||||||
|
import { selectNode } from '~/redux/node/selectors';
|
||||||
|
import { Card } from '~/components/containers/Card';
|
||||||
|
import { ImageSwitcher } from '~/components/node/ImageSwitcher';
|
||||||
|
import { NodePanel } from '~/components/node/NodePanel';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
import { Padder } from '~/components/containers/Padder';
|
||||||
|
import { NodeNoComments } from '~/components/node/NodeNoComments';
|
||||||
|
import { Comment } from '~/components/node/Comment';
|
||||||
|
|
||||||
|
import { Tags } from '~/components/node/Tags';
|
||||||
|
import { NodeRelated } from '~/components/node/NodeRelated';
|
||||||
import * as styles from './styles.scss';
|
import * as styles from './styles.scss';
|
||||||
|
import { NodeComments } from '~/components/node/NodeComments';
|
||||||
|
import { NodeTags } from '~/components/node/NodeTags';
|
||||||
|
import { NodeImageBlockPlaceholder } from '~/components/node/NodeImageBlockPlaceholder';
|
||||||
|
import { NODE_COMPONENTS } from '~/redux/node/constants';
|
||||||
|
|
||||||
interface IProps {}
|
const mapStateToProps = selectNode;
|
||||||
|
const mapDispatchToProps = {};
|
||||||
|
|
||||||
const NodeLayout: FC<IProps> = () => (
|
type IProps = ReturnType<typeof mapStateToProps> &
|
||||||
<div />
|
typeof mapDispatchToProps &
|
||||||
|
RouteComponentProps<{ id: string }> & {};
|
||||||
|
|
||||||
|
const NodeLayout: FC<IProps> = ({
|
||||||
|
match: {
|
||||||
|
params: { id },
|
||||||
|
},
|
||||||
|
is_loading,
|
||||||
|
current: node,
|
||||||
|
}) => {
|
||||||
|
const block = node && node.type && NODE_COMPONENTS[node.type] && NODE_COMPONENTS[node.type];
|
||||||
|
const view = block && block[is_loading ? 'placeholder' : 'component'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={styles.node} seamless>
|
||||||
|
{view && createElement(view, { node })}
|
||||||
|
|
||||||
|
<NodeImageBlockPlaceholder />
|
||||||
|
|
||||||
|
<NodePanel />
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<Padder>
|
||||||
|
<Group horizontal className={styles.content}>
|
||||||
|
<Group className={styles.comments}>
|
||||||
|
<NodeNoComments />
|
||||||
|
<NodeComments />
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<div className={styles.panel}>
|
||||||
|
<Group style={{ flex: 1 }}>
|
||||||
|
<NodeTags />
|
||||||
|
|
||||||
|
<NodeRelated title="First album" />
|
||||||
|
|
||||||
|
<NodeRelated title="Second album" />
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Padder>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export { NodeLayout };
|
export { NodeLayout };
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
.content {
|
||||||
|
align-items: stretch !important;
|
||||||
|
|
||||||
|
@include vertical_at_tablet;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments {
|
||||||
|
flex: 3 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
flex: 1 3;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-left: $gap / 2;
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.node {
|
||||||
|
background: $node_bg;
|
||||||
|
box-shadow: $node_shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
background: $node_buttons_bg;
|
||||||
|
flex: 1;
|
||||||
|
border-radius: $panel_radius;
|
||||||
|
box-shadow: $comment_shadow;
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import { INode, IValidationErrors } from '../types';
|
import { INode, IValidationErrors } from '../types';
|
||||||
import { NODE_ACTIONS } from './constants';
|
import { NODE_ACTIONS } from './constants';
|
||||||
|
import { INodeState } from './reducer';
|
||||||
|
|
||||||
export const nodeSave = (node: INode) => ({
|
export const nodeSave = (node: INode) => ({
|
||||||
node,
|
node,
|
||||||
|
@ -10,3 +11,13 @@ export const nodeSetSaveErrors = (errors: IValidationErrors) => ({
|
||||||
errors,
|
errors,
|
||||||
type: NODE_ACTIONS.SET_SAVE_ERRORS,
|
type: NODE_ACTIONS.SET_SAVE_ERRORS,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const nodeLoadNode = (id: string | number) => ({
|
||||||
|
id,
|
||||||
|
type: NODE_ACTIONS.LOAD_NODE,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const nodeSetLoading = (is_loading: INodeState['is_loading']) => ({
|
||||||
|
is_loading,
|
||||||
|
type: NODE_ACTIONS.SET_LOADING,
|
||||||
|
});
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
import { IBlock, INode } from '../types';
|
import { IBlock, INode, ValueOf } from '../types';
|
||||||
|
import { NodeImageBlock } from '~/components/node/NodeImageBlock';
|
||||||
|
import { NodeImageBlockPlaceholder } from '~/components/node/NodeImageBlockPlaceholder';
|
||||||
|
import { ReactElement, FC } from 'react';
|
||||||
|
|
||||||
|
const prefix = 'NODE.';
|
||||||
export const NODE_ACTIONS = {
|
export const NODE_ACTIONS = {
|
||||||
SAVE: 'NODE.SAVE',
|
SAVE: `${prefix}NODE.SAVE`,
|
||||||
SET_SAVE_ERRORS: 'NODE.SET_SAVE_ERRORS',
|
LOAD_NODE: `${prefix}LOAD_NODE`,
|
||||||
|
|
||||||
|
SET_SAVE_ERRORS: `${prefix}NODE.SET_SAVE_ERRORS`,
|
||||||
|
SET_LOADING: `${prefix}NODE.SET_LOADING`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EMPTY_BLOCK: IBlock = {
|
export const EMPTY_BLOCK: IBlock = {
|
||||||
|
@ -32,3 +39,22 @@ export const EMPTY_NODE: INode = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const NODE_TYPES = {
|
||||||
|
IMAGE: 'image',
|
||||||
|
AUDIO: 'audio',
|
||||||
|
VIDEO: 'video',
|
||||||
|
TEXT: 'text',
|
||||||
|
};
|
||||||
|
|
||||||
|
type INodeComponents = Record<
|
||||||
|
ValueOf<typeof NODE_TYPES>,
|
||||||
|
Record<'component' | 'placeholder', FC<{ node: INode }>>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const NODE_COMPONENTS: INodeComponents = {
|
||||||
|
[NODE_TYPES.IMAGE]: {
|
||||||
|
component: NodeImageBlock,
|
||||||
|
placeholder: NodeImageBlockPlaceholder,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
import assocPath from 'ramda/es/assocPath';
|
import assocPath from 'ramda/es/assocPath';
|
||||||
import { NODE_ACTIONS } from './constants';
|
import { NODE_ACTIONS } from './constants';
|
||||||
import { nodeSetSaveErrors } from './actions';
|
import { nodeSetSaveErrors, nodeSetLoading } from './actions';
|
||||||
import { INodeState } from './reducer';
|
import { INodeState } from './reducer';
|
||||||
|
|
||||||
const setSaveErrors = (state: INodeState, { errors }: ReturnType<typeof nodeSetSaveErrors>) => assocPath(['errors'], errors, state);
|
const setSaveErrors = (state: INodeState, { errors }: ReturnType<typeof nodeSetSaveErrors>) =>
|
||||||
|
assocPath(['errors'], errors, state);
|
||||||
|
const setLoading = (state: INodeState, { is_loading }: ReturnType<typeof nodeSetLoading>) =>
|
||||||
|
assocPath(['is_loading'], is_loading, state);
|
||||||
|
|
||||||
export const NODE_HANDLERS = {
|
export const NODE_HANDLERS = {
|
||||||
[NODE_ACTIONS.SAVE]: setSaveErrors,
|
[NODE_ACTIONS.SAVE]: setSaveErrors,
|
||||||
|
[NODE_ACTIONS.SET_LOADING]: setLoading,
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { EMPTY_FILE } from '../uploads/constants';
|
||||||
export type INodeState = Readonly<{
|
export type INodeState = Readonly<{
|
||||||
is_loading: boolean;
|
is_loading: boolean;
|
||||||
editor: INode;
|
editor: INode;
|
||||||
|
current: INode;
|
||||||
error: string;
|
error: string;
|
||||||
errors: Record<string, string>;
|
errors: Record<string, string>;
|
||||||
}>;
|
}>;
|
||||||
|
@ -19,6 +20,7 @@ const INITIAL_STATE: INodeState = {
|
||||||
blocks: [],
|
blocks: [],
|
||||||
files: [],
|
files: [],
|
||||||
},
|
},
|
||||||
|
current: null,
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
errors: {},
|
errors: {},
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { takeLatest, call, put, select } from 'redux-saga/effects';
|
import { takeLatest, call, put, select } from 'redux-saga/effects';
|
||||||
import { NODE_ACTIONS } from './constants';
|
import { NODE_ACTIONS } from './constants';
|
||||||
import { nodeSave, nodeSetSaveErrors } from './actions';
|
import { nodeSave, nodeSetSaveErrors, nodeLoadNode, nodeSetLoading } from './actions';
|
||||||
import { postNode } from './api';
|
import { postNode } from './api';
|
||||||
import { reqWrapper } from '../auth/sagas';
|
import { reqWrapper } from '../auth/sagas';
|
||||||
import { flowSetNodes } from '../flow/actions';
|
import { flowSetNodes } from '../flow/actions';
|
||||||
import { ERRORS } from '~/constants/errors';
|
import { ERRORS } from '~/constants/errors';
|
||||||
import { modalSetShown } from '../modal/actions';
|
import { modalSetShown } from '../modal/actions';
|
||||||
import { selectFlowNodes } from '../flow/selectors';
|
import { selectFlowNodes } from '../flow/selectors';
|
||||||
|
import { push } from 'connected-react-router';
|
||||||
|
import { URLS } from '~/constants/urls';
|
||||||
|
|
||||||
function* onNodeSave({ node }: ReturnType<typeof nodeSave>) {
|
function* onNodeSave({ node }: ReturnType<typeof nodeSave>) {
|
||||||
yield put(nodeSetSaveErrors({}));
|
yield put(nodeSetSaveErrors({}));
|
||||||
|
@ -28,6 +30,14 @@ function* onNodeSave({ node }: ReturnType<typeof nodeSave>) {
|
||||||
return yield put(modalSetShown(false));
|
return yield put(modalSetShown(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function* onNodeLoad({ id }: ReturnType<typeof nodeLoadNode>) {
|
||||||
|
yield put(nodeSetLoading(true));
|
||||||
|
yield put(nodeSetSaveErrors({}));
|
||||||
|
|
||||||
|
yield put(push(URLS.NODE_URL(id)));
|
||||||
|
}
|
||||||
|
|
||||||
export default function* nodeSaga() {
|
export default function* nodeSaga() {
|
||||||
yield takeLatest(NODE_ACTIONS.SAVE, onNodeSave);
|
yield takeLatest(NODE_ACTIONS.SAVE, onNodeSave);
|
||||||
|
yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad);
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,11 @@ const Sprites: FC<{}> = () => (
|
||||||
<path fill="none" d="M0 0h24v24H0V0z" />
|
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||||
<path d="M12 6.5c3.79 0 7.17 2.13 8.82 5.5-1.65 3.37-5.02 5.5-8.82 5.5S4.83 15.37 3.18 12C4.83 8.63 8.21 6.5 12 6.5m0-2C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zm0 5c1.38 0 2.5 1.12 2.5 2.5s-1.12 2.5-2.5 2.5-2.5-1.12-2.5-2.5 1.12-2.5 2.5-2.5m0-2c-2.48 0-4.5 2.02-4.5 4.5s2.02 4.5 4.5 4.5 4.5-2.02 4.5-4.5-2.02-4.5-4.5-4.5z" />
|
<path d="M12 6.5c3.79 0 7.17 2.13 8.82 5.5-1.65 3.37-5.02 5.5-8.82 5.5S4.83 15.37 3.18 12C4.83 8.63 8.21 6.5 12 6.5m0-2C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zm0 5c1.38 0 2.5 1.12 2.5 2.5s-1.12 2.5-2.5 2.5-2.5-1.12-2.5-2.5 1.12-2.5 2.5-2.5m0-2c-2.48 0-4.5 2.02-4.5 4.5s2.02 4.5 4.5 4.5 4.5-2.02 4.5-4.5-2.02-4.5-4.5-4.5z" />
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
|
<g id="hourglass" stroke="none">
|
||||||
|
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||||
|
<path d="M 434.86328 468.69141 A 12 12 0 0 0 426.20117 472.20508 L 434.6875 480.68945 L 443.17188 472.20508 A 12 12 0 0 0 434.86328 468.69141 z M 434.6875 480.68945 L 426.20117 489.17578 A 12 12 0 0 0 434.6875 492.68945 A 12 12 0 0 0 443.17188 489.17578 L 434.6875 480.68945 z " />
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue