mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +07:00
floating node panel
This commit is contained in:
parent
9e25b4e2b0
commit
336582b3d6
7 changed files with 200 additions and 113 deletions
|
@ -18,9 +18,11 @@ import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
interface IProps {
|
interface IProps {
|
||||||
is_loading: boolean;
|
is_loading: boolean;
|
||||||
node: INode;
|
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 [is_animated, setIsAnimated] = useState(false);
|
||||||
const [current, setCurrent] = useState(0);
|
const [current, setCurrent] = useState(0);
|
||||||
const [height, setHeight] = useState(320);
|
const [height, setHeight] = useState(320);
|
||||||
|
@ -39,6 +41,8 @@ const NodeImageBlock: FC<IProps> = ({ node, is_loading }) => {
|
||||||
loaded,
|
loaded,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => updateLayout(), [loaded]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!refs || !refs.current[current] || !loaded[current]) return setHeight(320);
|
if (!refs || !refs.current[current] || !loaded[current]) return setHeight(320);
|
||||||
|
|
||||||
|
|
|
@ -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 * 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 { INode } from '~/redux/types';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { NodePanelInner } from '~/components/node/NodePanelInner';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
node: INode;
|
node: INode;
|
||||||
|
layout: {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const NodePanel: FC<IProps> = ({ node: { title, user } }) => (
|
const NodePanel: FC<IProps> = ({ node, layout }) => {
|
||||||
<div className={styles.wrap}>
|
const [stack, setStack] = useState(false);
|
||||||
<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}>
|
const ref = useRef(null);
|
||||||
<Icon icon="edit" size={24} />
|
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);
|
||||||
</div>
|
}, [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 };
|
export { NodePanel };
|
||||||
|
|
||||||
// <div className={styles.mark} />
|
|
||||||
|
|
|
@ -1,91 +1,6 @@
|
||||||
.wrap {
|
.place {
|
||||||
background: $node_bg;
|
height: 72px;
|
||||||
padding: $gap;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: stretch;
|
|
||||||
border-radius: $radius $radius 0 0;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: -$radius;
|
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
margin-top: -$radius;
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
|
|
39
src/components/node/NodePanelInner/index.tsx
Normal file
39
src/components/node/NodePanelInner/index.tsx
Normal 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} />
|
107
src/components/node/NodePanelInner/styles.scss
Normal file
107
src/components/node/NodePanelInner/styles.scss
Normal 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);
|
||||||
|
}
|
|
@ -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 { RouteComponentProps } from 'react-router';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
@ -41,6 +41,10 @@ const NodeLayoutUnconnected: FC<IProps> = ({
|
||||||
nodeLoadNode,
|
nodeLoadNode,
|
||||||
nodeUpdateTags,
|
nodeUpdateTags,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [layout, setLayout] = useState({});
|
||||||
|
|
||||||
|
const updateLayout = useCallback(() => setLayout({}), []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (is_loading) return;
|
if (is_loading) return;
|
||||||
nodeLoadNode(parseInt(id, 10), null);
|
nodeLoadNode(parseInt(id, 10), null);
|
||||||
|
@ -56,9 +60,9 @@ const NodeLayoutUnconnected: FC<IProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={styles.node} seamless>
|
<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>
|
<Group>
|
||||||
<Padder>
|
<Padder>
|
||||||
|
|
|
@ -57,7 +57,10 @@ export const NODE_TYPES = {
|
||||||
TEXT: 'text',
|
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 = {
|
export const NODE_COMPONENTS: INodeComponents = {
|
||||||
[NODE_TYPES.IMAGE]: NodeImageBlock,
|
[NODE_TYPES.IMAGE]: NodeImageBlock,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue