diff --git a/package-lock.json b/package-lock.json index 557dc3ae..e301783e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2655,6 +2655,12 @@ "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", "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": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", @@ -2698,6 +2704,16 @@ "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": { "version": "0.10.5", "resolved": "https://registry.npmjs.org/@types/redux-saga/-/redux-saga-0.10.5.tgz", diff --git a/package.json b/package.json index 4cc1de33..7b728943 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,10 @@ "url": "https://github.com/muerwre/my-empty-react-project" }, "devDependencies": { - "@babel/types": "7.5.5", "@babel/cli": "^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", "awesome-typescript-loader": "^5.2.1", "babel-core": "^6.26.3", diff --git a/src/components/flow/Cell/index.tsx b/src/components/flow/Cell/index.tsx index 32cf673d..6c1cf034 100644 --- a/src/components/flow/Cell/index.tsx +++ b/src/components/flow/Cell/index.tsx @@ -1,10 +1,11 @@ import React, { FC, useState, useCallback } from 'react'; import { NavLink } from 'react-router-dom'; import { INode } from '~/redux/types'; -import * as styles from './styles.scss'; +import { URLS } from '~/constants/urls'; import { getImageSize } from '~/utils/dom'; import classNames = require('classnames'); -import { URLS } from '~/constants/urls'; + +import * as styles from './styles.scss'; interface IProps { node: INode; @@ -13,20 +14,23 @@ interface IProps { // title?: string; // is_hero?: boolean; // is_stamp?: boolean; + onSelect: (id: INode['id']) => void; is_text?: boolean; } -const Cell: FC = ({ node: { id, title, brief }, is_text = false }) => { +const Cell: FC = ({ node: { id, title, brief }, onSelect, is_text = false }) => { const [is_loaded, setIsLoaded] = useState(false); const onImageLoad = useCallback(() => { setIsLoaded(true); }, [setIsLoaded]); + const onClick = useCallback(() => onSelect(id), [onSelect, id]); + return ( -
{title &&
{title}
}
@@ -41,7 +45,7 @@ const Cell: FC = ({ node: { id, title, brief }, is_text = false }) => { )} -
+ ); }; diff --git a/src/components/flow/FlowGrid/index.tsx b/src/components/flow/FlowGrid/index.tsx index 1a23154a..18a4ab27 100644 --- a/src/components/flow/FlowGrid/index.tsx +++ b/src/components/flow/FlowGrid/index.tsx @@ -3,17 +3,20 @@ import { Cell } from '~/components/flow/Cell'; import * as styles from './styles.scss'; import { IFlowState } from '~/redux/flow/reducer'; +import { INode } from '~/redux/types'; -type IProps = Partial & {}; +type IProps = Partial & { + onSelect: (id: INode['id']) => void; +}; -export const FlowGrid: FC = ({ nodes }) => ( +export const FlowGrid: FC = ({ nodes, onSelect }) => (
HERO
STAMP
{nodes.map(node => ( - + ))}
diff --git a/src/components/input/LoaderCircle/index.tsx b/src/components/input/LoaderCircle/index.tsx index c1e7c96c..c99578db 100644 --- a/src/components/input/LoaderCircle/index.tsx +++ b/src/components/input/LoaderCircle/index.tsx @@ -1,5 +1,7 @@ import React, { FC } from 'react'; import * as styles from './styles.scss'; +import { Icon } from '../Icon'; +import { describeArc } from '~/utils/dom'; interface IProps { size?: number; @@ -7,31 +9,9 @@ interface IProps { export const LoaderCircle: FC = ({ size = 24 }) => (
- - - { - [...new Array(8)].map((el, i) => ( - - )) - } - + + +
); - -/* -
- - - - -
- */ diff --git a/src/components/input/LoaderCircle/styles.scss b/src/components/input/LoaderCircle/styles.scss index 97a8132a..1c4bb8d4 100644 --- a/src/components/input/LoaderCircle/styles.scss +++ b/src/components/input/LoaderCircle/styles.scss @@ -4,16 +4,26 @@ } @keyframes spin { - 0% { transform: rotate(0); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0); + } + 100% { + transform: rotate(360deg); + } } @keyframes fade { - 0% { opacity: 1; transform: scale(1); } - 100% { opacity: 0.1; transform: scale(4); } + 0% { + opacity: 1; + transform: scale(1); + } + 100% { + opacity: 0.1; + transform: scale(4); + } } .wrap { - animation: spin infinite steps(9, end) 1s; + animation: spin infinite 1s linear; display: inline-flex; } diff --git a/src/components/node/NodeComments/index.tsx b/src/components/node/NodeComments/index.tsx new file mode 100644 index 00000000..f8ca0fc9 --- /dev/null +++ b/src/components/node/NodeComments/index.tsx @@ -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 = ({ comments }) => ( +
+ {range(1, 6).map(el => ( + + ))} +
+); + +export { NodeComments }; diff --git a/src/components/node/NodeImageBlock/index.tsx b/src/components/node/NodeImageBlock/index.tsx new file mode 100644 index 00000000..07380f11 --- /dev/null +++ b/src/components/node/NodeImageBlock/index.tsx @@ -0,0 +1,21 @@ +import React, { FC } from 'react'; +import { ImageSwitcher } from '../ImageSwitcher'; +import * as styles from './styles.scss'; + +interface IProps {} + +const NodeImageBlock: FC = ({}) => ( +
+ + +
+ +
+
+); + +export { NodeImageBlock }; diff --git a/src/components/node/NodeImageBlock/styles.scss b/src/components/node/NodeImageBlock/styles.scss new file mode 100644 index 00000000..5a664443 --- /dev/null +++ b/src/components/node/NodeImageBlock/styles.scss @@ -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; + } +} diff --git a/src/components/node/NodeImageBlockPlaceholder/index.tsx b/src/components/node/NodeImageBlockPlaceholder/index.tsx new file mode 100644 index 00000000..c63e9479 --- /dev/null +++ b/src/components/node/NodeImageBlockPlaceholder/index.tsx @@ -0,0 +1,11 @@ +import React, { FC } from 'react'; +import * as styles from './styles.scss'; +import { LoaderCircle } from '~/components/input/LoaderCircle'; + +const NodeImageBlockPlaceholder: FC<{}> = () => ( +
+ +
+); + +export { NodeImageBlockPlaceholder }; diff --git a/src/components/node/NodeImageBlockPlaceholder/styles.scss b/src/components/node/NodeImageBlockPlaceholder/styles.scss new file mode 100644 index 00000000..9efeba7e --- /dev/null +++ b/src/components/node/NodeImageBlockPlaceholder/styles.scss @@ -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); + } +} diff --git a/src/components/node/NodeTags/index.tsx b/src/components/node/NodeTags/index.tsx new file mode 100644 index 00000000..40e8b8a9 --- /dev/null +++ b/src/components/node/NodeTags/index.tsx @@ -0,0 +1,18 @@ +import React, { FC } from 'react'; +import { Tags } from '../Tags'; + +interface IProps {} + +const NodeTags: FC = ({}) => ( + +); + +export { NodeTags }; diff --git a/src/constants/urls.ts b/src/constants/urls.ts index 17317ff6..73f4c091 100644 --- a/src/constants/urls.ts +++ b/src/constants/urls.ts @@ -7,5 +7,5 @@ export const URLS = { EDITOR: '/examples/edit', IMAGE: '/examples/image', }, - NODE_URL: (id: number) => `/post${id}`, + NODE_URL: (id: number | string) => `/post${id}`, }; diff --git a/src/containers/App.tsx b/src/containers/App.tsx index bcde7474..d937a6da 100644 --- a/src/containers/App.tsx +++ b/src/containers/App.tsx @@ -14,6 +14,7 @@ import { URLS } from '~/constants/urls'; import { Modal } from '~/containers/dialogs/Modal'; import { selectModal } from '~/redux/modal/selectors'; import { BlurWrapper } from '~/components/containers/BlurWrapper'; +import { NodeLayout } from './node/NodeLayout'; const mapStateToProps = selectModal; const mapDispatchToProps = {}; @@ -23,19 +24,20 @@ type IProps = typeof mapDispatchToProps & ReturnType & { const Component: FC = ({ is_shown }) => ( - - - + + + - - - - - + + + + + + - - - + + + ); diff --git a/src/containers/flow/FlowLayout/index.tsx b/src/containers/flow/FlowLayout/index.tsx index 56bd9c3e..317b6289 100644 --- a/src/containers/flow/FlowLayout/index.tsx +++ b/src/containers/flow/FlowLayout/index.tsx @@ -2,14 +2,17 @@ import React, { FC } from 'react'; import { connect } from 'react-redux'; import { FlowGrid } from '~/components/flow/FlowGrid'; import { selectFlow } from '~/redux/flow/selectors'; +import * as NODE_ACTIONS from '~/redux/node/actions'; const mapStateToProps = selectFlow; -const mapDispatchToProps = {}; +const mapDispatchToProps = { nodeLoadNode: NODE_ACTIONS.nodeLoadNode }; type IProps = ReturnType & typeof mapDispatchToProps & {}; -const FlowLayoutUnconnected: FC = ({ nodes }) => ; +const FlowLayoutUnconnected: FC = ({ nodes, nodeLoadNode }) => ( + +); const FlowLayout = connect( mapStateToProps, diff --git a/src/containers/node/NodeLayout/index.tsx b/src/containers/node/NodeLayout/index.tsx index e7dda895..98960da8 100644 --- a/src/containers/node/NodeLayout/index.tsx +++ b/src/containers/node/NodeLayout/index.tsx @@ -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 { 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 = () => ( -
-); +type IProps = ReturnType & + typeof mapDispatchToProps & + RouteComponentProps<{ id: string }> & {}; + +const NodeLayout: FC = ({ + 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 ( + + {view && createElement(view, { node })} + + + + + + + + + + + + + +
+ + + + + + + +
+
+
+
+
+ ); +}; export { NodeLayout }; diff --git a/src/containers/node/NodeLayout/styles.scss b/src/containers/node/NodeLayout/styles.scss index e69de29b..14d1de5e 100644 --- a/src/containers/node/NodeLayout/styles.scss +++ b/src/containers/node/NodeLayout/styles.scss @@ -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; +} diff --git a/src/redux/node/actions.ts b/src/redux/node/actions.ts index 4498a279..a55c2761 100644 --- a/src/redux/node/actions.ts +++ b/src/redux/node/actions.ts @@ -1,5 +1,6 @@ import { INode, IValidationErrors } from '../types'; import { NODE_ACTIONS } from './constants'; +import { INodeState } from './reducer'; export const nodeSave = (node: INode) => ({ node, @@ -10,3 +11,13 @@ export const nodeSetSaveErrors = (errors: IValidationErrors) => ({ 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, +}); diff --git a/src/redux/node/constants.ts b/src/redux/node/constants.ts index 9d485985..afbd2019 100644 --- a/src/redux/node/constants.ts +++ b/src/redux/node/constants.ts @@ -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 = { - SAVE: 'NODE.SAVE', - SET_SAVE_ERRORS: 'NODE.SET_SAVE_ERRORS', + SAVE: `${prefix}NODE.SAVE`, + LOAD_NODE: `${prefix}LOAD_NODE`, + + SET_SAVE_ERRORS: `${prefix}NODE.SET_SAVE_ERRORS`, + SET_LOADING: `${prefix}NODE.SET_LOADING`, }; 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, + Record<'component' | 'placeholder', FC<{ node: INode }>> +>; + +export const NODE_COMPONENTS: INodeComponents = { + [NODE_TYPES.IMAGE]: { + component: NodeImageBlock, + placeholder: NodeImageBlockPlaceholder, + }, +}; diff --git a/src/redux/node/handlers.ts b/src/redux/node/handlers.ts index 3f2a05fa..96c0549c 100644 --- a/src/redux/node/handlers.ts +++ b/src/redux/node/handlers.ts @@ -1,10 +1,14 @@ import assocPath from 'ramda/es/assocPath'; import { NODE_ACTIONS } from './constants'; -import { nodeSetSaveErrors } from './actions'; +import { nodeSetSaveErrors, nodeSetLoading } from './actions'; import { INodeState } from './reducer'; -const setSaveErrors = (state: INodeState, { errors }: ReturnType) => assocPath(['errors'], errors, state); +const setSaveErrors = (state: INodeState, { errors }: ReturnType) => + assocPath(['errors'], errors, state); +const setLoading = (state: INodeState, { is_loading }: ReturnType) => + assocPath(['is_loading'], is_loading, state); export const NODE_HANDLERS = { [NODE_ACTIONS.SAVE]: setSaveErrors, + [NODE_ACTIONS.SET_LOADING]: setLoading, }; diff --git a/src/redux/node/reducer.ts b/src/redux/node/reducer.ts index 6d25408d..d4512cb4 100644 --- a/src/redux/node/reducer.ts +++ b/src/redux/node/reducer.ts @@ -8,6 +8,7 @@ import { EMPTY_FILE } from '../uploads/constants'; export type INodeState = Readonly<{ is_loading: boolean; editor: INode; + current: INode; error: string; errors: Record; }>; @@ -19,6 +20,7 @@ const INITIAL_STATE: INodeState = { blocks: [], files: [], }, + current: null, is_loading: false, error: null, errors: {}, diff --git a/src/redux/node/sagas.ts b/src/redux/node/sagas.ts index 689a7956..4a0b4b1f 100644 --- a/src/redux/node/sagas.ts +++ b/src/redux/node/sagas.ts @@ -1,12 +1,14 @@ import { takeLatest, call, put, select } from 'redux-saga/effects'; import { NODE_ACTIONS } from './constants'; -import { nodeSave, nodeSetSaveErrors } from './actions'; +import { nodeSave, nodeSetSaveErrors, nodeLoadNode, nodeSetLoading } from './actions'; import { postNode } from './api'; import { reqWrapper } from '../auth/sagas'; import { flowSetNodes } from '../flow/actions'; import { ERRORS } from '~/constants/errors'; import { modalSetShown } from '../modal/actions'; import { selectFlowNodes } from '../flow/selectors'; +import { push } from 'connected-react-router'; +import { URLS } from '~/constants/urls'; function* onNodeSave({ node }: ReturnType) { yield put(nodeSetSaveErrors({})); @@ -28,6 +30,14 @@ function* onNodeSave({ node }: ReturnType) { return yield put(modalSetShown(false)); } +function* onNodeLoad({ id }: ReturnType) { + yield put(nodeSetLoading(true)); + yield put(nodeSetSaveErrors({})); + + yield put(push(URLS.NODE_URL(id))); +} + export default function* nodeSaga() { yield takeLatest(NODE_ACTIONS.SAVE, onNodeSave); + yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad); } diff --git a/src/sprites/Sprites.tsx b/src/sprites/Sprites.tsx index 64b6116b..e90d9c58 100644 --- a/src/sprites/Sprites.tsx +++ b/src/sprites/Sprites.tsx @@ -64,6 +64,11 @@ const Sprites: FC<{}> = () => ( + + + + + );