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

floating node panel

This commit is contained in:
Fedor Katurov 2019-10-13 21:11:20 +07:00
parent 9e25b4e2b0
commit 336582b3d6
7 changed files with 200 additions and 113 deletions

View file

@ -18,9 +18,11 @@ import { UPLOAD_TYPES } from '~/redux/uploads/constants';
interface IProps {
is_loading: boolean;
node: INode;
layout: {};
updateLayout: () => void;
}
const NodeImageBlock: FC<IProps> = ({ node, is_loading }) => {
const NodeImageBlock: FC<IProps> = ({ node, is_loading, updateLayout }) => {
const [is_animated, setIsAnimated] = useState(false);
const [current, setCurrent] = useState(0);
const [height, setHeight] = useState(320);
@ -39,6 +41,8 @@ const NodeImageBlock: FC<IProps> = ({ node, is_loading }) => {
loaded,
]);
useEffect(() => updateLayout(), [loaded]);
useEffect(() => {
if (!refs || !refs.current[current] || !loaded[current]) return setHeight(320);

View file

@ -1,33 +1,48 @@
import React, { FC } from 'react';
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import * as styles from './styles.scss';
import { Group } from '~/components/containers/Group';
import { Filler } from '~/components/containers/Filler';
import { Icon } from '~/components/input/Icon';
import { INode } from '~/redux/types';
import { createPortal } from 'react-dom';
import { NodePanelInner } from '~/components/node/NodePanelInner';
interface IProps {
node: INode;
layout: {};
}
const NodePanel: FC<IProps> = ({ node: { title, user } }) => (
<div className={styles.wrap}>
<Group horizontal className={styles.panel}>
<Filler>
<div className={styles.title}>{title || '...'}</div>
{user && user.username && <div className={styles.name}>~ {user.username}</div>}
</Filler>
</Group>
const NodePanel: FC<IProps> = ({ node, layout }) => {
const [stack, setStack] = useState(false);
<div className={styles.buttons}>
<Icon icon="edit" size={24} />
const ref = useRef(null);
const getPlace = useCallback(() => {
if (!ref.current) return;
<div className={styles.sep} />
const { offsetTop } = ref.current;
const { height } = ref.current.getBoundingClientRect();
const { scrollY, innerHeight } = window;
<Icon icon="heart" size={24} />
setStack(offsetTop > scrollY + innerHeight - height);
}, [ref]);
useEffect(() => {
getPlace();
window.addEventListener('scroll', getPlace);
window.addEventListener('resize', getPlace);
return () => {
window.removeEventListener('scroll', getPlace);
window.removeEventListener('resize', getPlace);
};
}, [layout]);
return (
<div className={styles.place} ref={ref}>
{stack ? (
createPortal(<NodePanelInner node={node} stack />, document.body)
) : (
<NodePanelInner node={node} />
)}
</div>
</div>
);
);
};
export { NodePanel };
// <div className={styles.mark} />

View file

@ -1,91 +1,6 @@
.wrap {
background: $node_bg;
padding: $gap;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: stretch;
border-radius: $radius $radius 0 0;
.place {
height: 72px;
position: relative;
margin-top: -$radius;
z-index: 3;
}
.title {
text-transform: uppercase;
font: $font_24_semibold;
height: 24px;
padding-bottom: 6px;
}
.name {
font: $font_14_regular;
color: transparentize(white, 0.5);
}
.btn {
flex: 1;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
fill: transparentize(white, 0.5);
}
.panel {
flex: 1;
}
.buttons {
flex: 0;
padding-right: $gap;
fill: transparentize(white, 0.7);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
& > * {
margin: 0 $gap;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
//height: 54px;
//border-radius: $radius $radius 0 0;
//background: linear-gradient(176deg, #f42a00, #5c1085);
//position: absolute;
//bottom: 0;
//right: 10px;
//width: 270px;
//display: flex;
}
.mark {
flex: 0 0 32px;
position: relative;
&::after {
content: ' ';
position: absolute;
top: -38px;
right: 4px;
width: 24px;
height: 52px;
background: $green_gradient;
box-shadow: transparentize(black, 0.8) 4px 2px;
border-radius: 2px;
}
}
.sep {
flex: 0 0 6px;
height: 6px;
width: 6px;
border-radius: 4px;
background: transparentize(black, 0.7);
margin-top: -$radius;
}

View file

@ -0,0 +1,39 @@
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import * as styles from './styles.scss';
import { Group } from '~/components/containers/Group';
import { Filler } from '~/components/containers/Filler';
import { Icon } from '~/components/input/Icon';
import { INode } from '~/redux/types';
import classNames from 'classnames';
interface IProps {
node: INode;
stack?: boolean;
}
const NodePanelInner: FC<IProps> = ({ node: { title, user }, stack }) => {
return (
<div className={classNames(styles.wrap, { stack })}>
<div className={styles.content}>
<Group horizontal className={styles.panel}>
<Filler>
<div className={styles.title}>{title || '...'}</div>
{user && user.username && <div className={styles.name}>~ {user.username}</div>}
</Filler>
</Group>
<div className={styles.buttons}>
<Icon icon="edit" size={24} />
<div className={styles.sep} />
<Icon icon="heart" size={24} />
</div>
</div>
</div>
);
};
export { NodePanelInner };
// <div className={styles.mark} />

View file

@ -0,0 +1,107 @@
.wrap {
display: flex;
align-items: center;
justify-content: stretch;
position: relative;
height: 72px;
width: 100%;
flex-direction: row;
align-items: center;
justify-content: center;
&:global(.stack) {
bottom: 0;
position: fixed;
}
}
.content {
flex: 0 1 $content_width;
display: flex;
align-items: center;
justify-content: stretch;
border-radius: $radius $radius 0 0;
box-sizing: border-box;
padding: $gap;
background: $node_bg;
height: 72px;
}
.title {
text-transform: uppercase;
font: $font_24_semibold;
height: 24px;
padding-bottom: 6px;
}
.name {
font: $font_14_regular;
color: transparentize(white, 0.5);
}
.btn {
flex: 1;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
fill: transparentize(white, 0.5);
}
.panel {
flex: 1;
}
.buttons {
flex: 0;
padding-right: $gap;
fill: transparentize(white, 0.7);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
& > * {
margin: 0 $gap;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
//height: 54px;
//border-radius: $radius $radius 0 0;
//background: linear-gradient(176deg, #f42a00, #5c1085);
//position: absolute;
//bottom: 0;
//right: 10px;
//width: 270px;
//display: flex;
}
.mark {
flex: 0 0 32px;
position: relative;
&::after {
content: ' ';
position: absolute;
top: -38px;
right: 4px;
width: 24px;
height: 52px;
background: $green_gradient;
box-shadow: transparentize(black, 0.8) 4px 2px;
border-radius: 2px;
}
}
.sep {
flex: 0 0 6px;
height: 6px;
width: 6px;
border-radius: 4px;
background: transparentize(black, 0.7);
}

View file

@ -1,4 +1,4 @@
import React, { FC, createElement, useEffect, useCallback } from 'react';
import React, { FC, createElement, useEffect, useCallback, useState } from 'react';
import { RouteComponentProps } from 'react-router';
import { connect } from 'react-redux';
@ -41,6 +41,10 @@ const NodeLayoutUnconnected: FC<IProps> = ({
nodeLoadNode,
nodeUpdateTags,
}) => {
const [layout, setLayout] = useState({});
const updateLayout = useCallback(() => setLayout({}), []);
useEffect(() => {
if (is_loading) return;
nodeLoadNode(parseInt(id, 10), null);
@ -56,9 +60,9 @@ const NodeLayoutUnconnected: FC<IProps> = ({
return (
<Card className={styles.node} seamless>
{block && createElement(block, { node, is_loading })}
{block && createElement(block, { node, is_loading, updateLayout, layout })}
<NodePanel node={node} />
<NodePanel node={node} layout={layout} />
<Group>
<Padder>

View file

@ -57,7 +57,10 @@ export const NODE_TYPES = {
TEXT: 'text',
};
type INodeComponents = Record<ValueOf<typeof NODE_TYPES>, FC<{ node: INode; is_loading: boolean }>>;
type INodeComponents = Record<
ValueOf<typeof NODE_TYPES>,
FC<{ node: INode; is_loading: boolean; layout: {}; updateLayout: () => void }>
>;
export const NODE_COMPONENTS: INodeComponents = {
[NODE_TYPES.IMAGE]: NodeImageBlock,