mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +07:00
commit
e3454e2c66
44 changed files with 595 additions and 102 deletions
|
@ -1,42 +1,15 @@
|
||||||
import React, { DetailsHTMLAttributes, FC, useEffect, useRef } from 'react';
|
import React, { DetailsHTMLAttributes, FC } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import StickyBox from 'react-sticky-box/dist/esnext';
|
||||||
|
|
||||||
import ResizeSensor from 'resize-sensor';
|
interface IProps extends DetailsHTMLAttributes<HTMLDivElement> {
|
||||||
(window as any).ResizeSensor = ResizeSensor;
|
offsetTop?: number;
|
||||||
|
|
||||||
import StickySidebar from 'sticky-sidebar';
|
|
||||||
(window as any).StickySidebar = StickySidebar;
|
|
||||||
|
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
interface IProps extends DetailsHTMLAttributes<HTMLDivElement> {}
|
|
||||||
|
|
||||||
const Sticky: FC<IProps> = ({ children }) => {
|
|
||||||
const ref = useRef(null);
|
|
||||||
const sb = useRef<StickySidebar>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!ref.current) return;
|
|
||||||
|
|
||||||
sb.current = new StickySidebar(ref.current, {
|
|
||||||
resizeSensor: true,
|
|
||||||
topSpacing: 72,
|
|
||||||
bottomSpacing: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => sb.current?.destroy();
|
|
||||||
}, [ref.current, sb.current, children]);
|
|
||||||
|
|
||||||
if (sb) {
|
|
||||||
sb.current?.updateSticky();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Sticky: FC<IProps> = ({ children, offsetTop = 65 }) => {
|
||||||
return (
|
return (
|
||||||
<div className={classnames(styles.wrap, 'sidebar_container')}>
|
<StickyBox offsetTop={offsetTop} offsetBottom={10}>
|
||||||
<div className="sidebar" ref={ref}>
|
{children}
|
||||||
<div className={classnames(styles.sticky, 'sidebar__inner')}>{children}</div>
|
</StickyBox>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
@import "src/styles/variables";
|
|
||||||
|
|
||||||
.wrap {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
:global(.sidebar) {
|
|
||||||
will-change: min-height;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.sidebar__inner) {
|
|
||||||
transform: translate(0, 0); /* For browsers don't support translate3d. */
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
will-change: position, transform;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { EditorUploadButton } from '~/components/editors/EditorUploadButton';
|
import { EditorUploadButton } from '~/components/editors/EditorUploadButton';
|
||||||
import { INode } from '~/redux/types';
|
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
import { IEditorComponentProps } from '~/redux/node/types';
|
import { IEditorComponentProps } from '~/redux/node/types';
|
||||||
|
|
||||||
|
|
|
@ -13,11 +13,12 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
margin: 0 $gap;
|
margin: 0 $gap / 2;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
46
src/components/editors/EditorPublicSwitch/index.tsx
Normal file
46
src/components/editors/EditorPublicSwitch/index.tsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||||
|
import { IEditorComponentProps } from '~/redux/node/types';
|
||||||
|
import { usePopper } from 'react-popper';
|
||||||
|
import { Button } from '~/components/input/Button';
|
||||||
|
|
||||||
|
interface IProps extends IEditorComponentProps {}
|
||||||
|
|
||||||
|
const EditorPublicSwitch: FC<IProps> = ({ data, setData }) => {
|
||||||
|
const tooltip = useRef<HTMLDivElement>(null);
|
||||||
|
const onChange = useCallback(() => setData({ ...data, is_promoted: !data.is_promoted }), [
|
||||||
|
data,
|
||||||
|
setData,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const pop = usePopper(tooltip?.current?.parentElement, tooltip.current, {
|
||||||
|
placement: 'bottom',
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: 'offset',
|
||||||
|
options: {
|
||||||
|
offset: [0, 4],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => console.log(pop), [pop]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
color={data.is_promoted ? 'primary' : 'secondary'}
|
||||||
|
type="button"
|
||||||
|
iconLeft={data.is_promoted ? 'waves' : 'lab'}
|
||||||
|
size="giant"
|
||||||
|
label={
|
||||||
|
data.is_promoted
|
||||||
|
? 'Доступно всем на главной странице'
|
||||||
|
: 'Видно только сотрудникам в лаборатории'
|
||||||
|
}
|
||||||
|
onClick={onChange}
|
||||||
|
round
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { EditorPublicSwitch };
|
63
src/components/editors/EditorPublicSwitch/styles.module.scss
Normal file
63
src/components/editors/EditorPublicSwitch/styles.module.scss
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
@import "src/styles/variables";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
@include outer_shadow();
|
||||||
|
@include editor_round_button();
|
||||||
|
|
||||||
|
transition: all 0.5s;
|
||||||
|
fill: $content_bg;
|
||||||
|
background-color: $olive;
|
||||||
|
|
||||||
|
&.promoted {
|
||||||
|
background-color: lighten($content_bg, 4%);
|
||||||
|
fill: $red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
border-radius: 2px;
|
||||||
|
background: darken($content_bg, 6%);
|
||||||
|
padding: $gap;
|
||||||
|
position: absolute;
|
||||||
|
font: $font_12_semibold;
|
||||||
|
bottom: 100%;
|
||||||
|
right: 0;
|
||||||
|
transform: translate(0, -$gap);
|
||||||
|
text-align: center;
|
||||||
|
touch-action: none;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
width: 100px;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.1s;
|
||||||
|
}
|
|
@ -1,15 +1,15 @@
|
||||||
import React, { FC, useCallback, useEffect } from 'react';
|
import React, { FC, useCallback, useEffect } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { IFileWithUUID, INode, IFile } from '~/redux/types';
|
import { IFile, IFileWithUUID } from '~/redux/types';
|
||||||
import uuid from 'uuid4';
|
import uuid from 'uuid4';
|
||||||
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
||||||
import { assocPath } from 'ramda';
|
import { append, assocPath } from 'ramda';
|
||||||
import { append } from 'ramda';
|
|
||||||
import { selectUploads } from '~/redux/uploads/selectors';
|
import { selectUploads } from '~/redux/uploads/selectors';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { NODE_SETTINGS } from '~/redux/node/constants';
|
import { NODE_SETTINGS } from '~/redux/node/constants';
|
||||||
|
import { IEditorComponentProps } from '~/redux/node/types';
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const { statuses, files } = selectUploads(state);
|
const { statuses, files } = selectUploads(state);
|
||||||
|
@ -22,12 +22,7 @@ const mapDispatchToProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type IProps = ReturnType<typeof mapStateToProps> &
|
type IProps = ReturnType<typeof mapStateToProps> &
|
||||||
typeof mapDispatchToProps & {
|
typeof mapDispatchToProps & IEditorComponentProps & {
|
||||||
data: INode;
|
|
||||||
setData: (val: INode) => void;
|
|
||||||
temp: string[];
|
|
||||||
setTemp: (val: string[]) => void;
|
|
||||||
|
|
||||||
accept?: string;
|
accept?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
type?: typeof UPLOAD_TYPES[keyof typeof UPLOAD_TYPES];
|
type?: typeof UPLOAD_TYPES[keyof typeof UPLOAD_TYPES];
|
||||||
|
@ -82,18 +77,6 @@ const EditorUploadButtonUnconnected: FC<IProps> = ({
|
||||||
[data, setData]
|
[data, setData]
|
||||||
);
|
);
|
||||||
|
|
||||||
// const onDrop = useCallback(
|
|
||||||
// (event: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
// event.preventDefault();
|
|
||||||
|
|
||||||
// if (!event.dataTransfer || !event.dataTransfer.files || !event.dataTransfer.files.length)
|
|
||||||
// return;
|
|
||||||
|
|
||||||
// onUpload(Array.from(event.dataTransfer.files));
|
|
||||||
// },
|
|
||||||
// [onUpload]
|
|
||||||
// );
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('dragover', eventPreventer, false);
|
window.addEventListener('dragover', eventPreventer, false);
|
||||||
window.addEventListener('drop', eventPreventer, false);
|
window.addEventListener('drop', eventPreventer, false);
|
||||||
|
|
|
@ -2,17 +2,10 @@
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
@include outer_shadow();
|
@include outer_shadow();
|
||||||
|
@include editor_round_button();
|
||||||
|
|
||||||
width: $upload_button_height;
|
|
||||||
height: $upload_button_height;
|
|
||||||
border-radius: ($upload_button_height / 2) !important;
|
|
||||||
position: relative;
|
|
||||||
border-radius: $radius;
|
|
||||||
cursor: pointer;
|
|
||||||
// opacity: 0.7;
|
|
||||||
transition: opacity 0.5s;
|
transition: opacity 0.5s;
|
||||||
background: $red_gradient;
|
background: $red_gradient;
|
||||||
// box-shadow: $content_bg 0 0 5px 10px;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
|
@ -28,6 +28,7 @@ type IButtonProps = DetailedHTMLProps<
|
||||||
stretchy?: boolean;
|
stretchy?: boolean;
|
||||||
iconOnly?: boolean;
|
iconOnly?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
round?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Button: FC<IButtonProps> = memo(
|
const Button: FC<IButtonProps> = memo(
|
||||||
|
@ -48,6 +49,7 @@ const Button: FC<IButtonProps> = memo(
|
||||||
iconOnly,
|
iconOnly,
|
||||||
label,
|
label,
|
||||||
ref,
|
ref,
|
||||||
|
round,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const tooltip = useRef<HTMLSpanElement | null>(null);
|
const tooltip = useRef<HTMLSpanElement | null>(null);
|
||||||
|
@ -75,6 +77,7 @@ const Button: FC<IButtonProps> = memo(
|
||||||
icon: ((iconLeft || iconRight) && !title && !children) || iconOnly,
|
icon: ((iconLeft || iconRight) && !title && !children) || iconOnly,
|
||||||
has_icon_left: !!iconLeft,
|
has_icon_left: !!iconLeft,
|
||||||
has_icon_right: !!iconRight,
|
has_icon_right: !!iconRight,
|
||||||
|
round,
|
||||||
}),
|
}),
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
|
|
|
@ -201,14 +201,17 @@
|
||||||
.normal {
|
.normal {
|
||||||
height: 38px;
|
height: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.big {
|
.big {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.giant {
|
.giant {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
min-width: 50px;
|
min-width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled {
|
.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
22
src/components/lab/LabBanner/index.tsx
Normal file
22
src/components/lab/LabBanner/index.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { Card } from '~/components/containers/Card';
|
||||||
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const LabBanner: FC<IProps> = () => (
|
||||||
|
<Card className={styles.wrap}>
|
||||||
|
<Group>
|
||||||
|
<Placeholder height={32} />
|
||||||
|
<Placeholder height={18} width="120px" />
|
||||||
|
<Placeholder height={18} width="200px" />
|
||||||
|
<Placeholder height={18} width="60px" />
|
||||||
|
<Placeholder height={18} width="180px" />
|
||||||
|
<Placeholder height={18} width="230px" />
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { LabBanner };
|
5
src/components/lab/LabBanner/styles.module.scss
Normal file
5
src/components/lab/LabBanner/styles.module.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
background: $red_gradient_alt;
|
||||||
|
}
|
22
src/components/lab/LabHero/index.tsx
Normal file
22
src/components/lab/LabHero/index.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
import { Icon } from '~/components/input/Icon';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const LabHero: FC<IProps> = () => (
|
||||||
|
<Group horizontal className={styles.wrap1}>
|
||||||
|
<div className={styles.star}>
|
||||||
|
<Icon icon="star_full" size={32} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<Placeholder height={20} />
|
||||||
|
<Placeholder height={12} width="100px" />
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { LabHero };
|
10
src/components/lab/LabHero/styles.module.scss
Normal file
10
src/components/lab/LabHero/styles.module.scss
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
margin-bottom: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star {
|
||||||
|
fill: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
31
src/components/lab/LabNode/index.tsx
Normal file
31
src/components/lab/LabNode/index.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { INode } from '~/redux/types';
|
||||||
|
import { NodePanelInner } from '~/components/node/NodePanelInner';
|
||||||
|
import { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { Card } from '~/components/containers/Card';
|
||||||
|
import { NodePanelLab } from '~/components/node/NodePanelLab';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
node: INode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LabNode: FC<IProps> = ({ node }) => {
|
||||||
|
const { inline, block, head } = useNodeBlocks(node, false);
|
||||||
|
|
||||||
|
console.log(node.id, { inline, block, head });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card seamless className={styles.wrap}>
|
||||||
|
<div className={styles.head}>
|
||||||
|
<NodePanelLab node={node} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{head}
|
||||||
|
{block}
|
||||||
|
{inline}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabNode };
|
11
src/components/lab/LabNode/styles.module.scss
Normal file
11
src/components/lab/LabNode/styles.module.scss
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head {
|
||||||
|
background-color: transparentize(black, 0.9);
|
||||||
|
border-radius: $radius $radius 0 0;
|
||||||
|
}
|
||||||
|
|
|
@ -82,6 +82,15 @@ const HeaderUnconnected: FC<IProps> = memo(
|
||||||
<Filler />
|
<Filler />
|
||||||
|
|
||||||
<div className={styles.plugs}>
|
<div className={styles.plugs}>
|
||||||
|
{is_user && (
|
||||||
|
<Link
|
||||||
|
className={classNames(styles.item, { [styles.is_active]: pathname === URLS.BASE })}
|
||||||
|
to={URLS.LAB}
|
||||||
|
>
|
||||||
|
ЛАБ
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
className={classNames(styles.item, { [styles.is_active]: pathname === URLS.BASE })}
|
className={classNames(styles.item, { [styles.is_active]: pathname === URLS.BASE })}
|
||||||
to={URLS.BASE}
|
to={URLS.BASE}
|
||||||
|
@ -122,9 +131,6 @@ const HeaderUnconnected: FC<IProps> = memo(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const Header = connect(
|
const Header = connect(mapStateToProps, mapDispatchToProps)(HeaderUnconnected);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(HeaderUnconnected);
|
|
||||||
|
|
||||||
export { Header };
|
export { Header };
|
||||||
|
|
|
@ -4,14 +4,12 @@ import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { INodeComponentProps } from '~/redux/node/constants';
|
import { INodeComponentProps } from '~/redux/node/constants';
|
||||||
|
import { useNodeAudios } from '~/utils/hooks/node/useNodeAudios';
|
||||||
|
|
||||||
interface IProps extends INodeComponentProps {}
|
interface IProps extends INodeComponentProps {}
|
||||||
|
|
||||||
const NodeAudioBlock: FC<IProps> = ({ node }) => {
|
const NodeAudioBlock: FC<IProps> = ({ node }) => {
|
||||||
const audios = useMemo(
|
const audios = useNodeAudios(node);
|
||||||
() => node.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO),
|
|
||||||
[node.files]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
|
|
|
@ -6,14 +6,12 @@ import { path } from 'ramda';
|
||||||
import { getURL } from '~/utils/dom';
|
import { getURL } from '~/utils/dom';
|
||||||
import { PRESETS } from '~/constants/urls';
|
import { PRESETS } from '~/constants/urls';
|
||||||
import { INodeComponentProps } from '~/redux/node/constants';
|
import { INodeComponentProps } from '~/redux/node/constants';
|
||||||
|
import { useNodeImages } from '~/utils/hooks/node/useNodeImages';
|
||||||
|
|
||||||
interface IProps extends INodeComponentProps {}
|
interface IProps extends INodeComponentProps {}
|
||||||
|
|
||||||
const NodeAudioImageBlock: FC<IProps> = ({ node }) => {
|
const NodeAudioImageBlock: FC<IProps> = ({ node }) => {
|
||||||
const images = useMemo(
|
const images = useNodeImages(node);
|
||||||
() => node.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE),
|
|
||||||
[node.files]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (images.length === 0) return null;
|
if (images.length === 0) return null;
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,6 @@
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: stretch;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -88,7 +86,7 @@
|
||||||
@include tablet {
|
@include tablet {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
font: $font_20_semibold;
|
font: $font_16_semibold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
19
src/components/node/NodePanelLab/index.tsx
Normal file
19
src/components/node/NodePanelLab/index.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
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 };
|
24
src/components/node/NodePanelLab/styles.module.scss
Normal file
24
src/components/node/NodePanelLab/styles.module.scss
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
padding: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font: $font_24_semibold;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-bottom: 0;
|
||||||
|
font: $font_16_semibold;
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,4 +50,7 @@ export const API = {
|
||||||
NODES: `/tag/nodes`,
|
NODES: `/tag/nodes`,
|
||||||
AUTOCOMPLETE: `/tag/autocomplete`,
|
AUTOCOMPLETE: `/tag/autocomplete`,
|
||||||
},
|
},
|
||||||
|
LAB: {
|
||||||
|
NODES: `/lab/`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { INode } from '~/redux/types';
|
||||||
|
|
||||||
export const URLS = {
|
export const URLS = {
|
||||||
BASE: '/',
|
BASE: '/',
|
||||||
|
LAB: '/lab',
|
||||||
BORIS: '/boris',
|
BORIS: '/boris',
|
||||||
AUTH: {
|
AUTH: {
|
||||||
LOGIN: '/auth/login',
|
LOGIN: '/auth/login',
|
||||||
|
|
21
src/containers/lab/LabGrid/index.tsx
Normal file
21
src/containers/lab/LabGrid/index.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { LabNode } from '~/components/lab/LabNode';
|
||||||
|
import { selectLabListNodes } from '~/redux/lab/selectors';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const LabGrid: FC<IProps> = () => {
|
||||||
|
const nodes = useShallowSelect(selectLabListNodes);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrap}>
|
||||||
|
{nodes.map(node => (
|
||||||
|
<LabNode node={node} key={node.id} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabGrid };
|
8
src/containers/lab/LabGrid/styles.module.scss
Normal file
8
src/containers/lab/LabGrid/styles.module.scss
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
grid-auto-rows: auto;
|
||||||
|
grid-row-gap: $gap;
|
||||||
|
}
|
89
src/containers/lab/LabLayout/index.tsx
Normal file
89
src/containers/lab/LabLayout/index.tsx
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import React, { FC, useEffect } from 'react';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { Card } from '~/components/containers/Card';
|
||||||
|
import { Sticky } from '~/components/containers/Sticky';
|
||||||
|
import { Container } from '~/containers/main/Container';
|
||||||
|
import { LabGrid } from '~/containers/lab/LabGrid';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { labGetList } from '~/redux/lab/actions';
|
||||||
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
|
import { Grid } from '~/components/containers/Grid';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
import { LabHero } from '~/components/lab/LabHero';
|
||||||
|
import { LabBanner } from '~/components/lab/LabBanner';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const LabLayout: FC<IProps> = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(labGetList());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Container>
|
||||||
|
<div className={styles.wrap}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<LabGrid />
|
||||||
|
</div>
|
||||||
|
<div className={styles.panel}>
|
||||||
|
<Sticky>
|
||||||
|
<Group>
|
||||||
|
<LabBanner />
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Group>
|
||||||
|
<Placeholder height={32} width="100%" />
|
||||||
|
|
||||||
|
<div />
|
||||||
|
|
||||||
|
<Placeholder height={14} width="100px" />
|
||||||
|
|
||||||
|
<div />
|
||||||
|
|
||||||
|
<div className={styles.tags}>
|
||||||
|
<Placeholder height={20} width="100px" />
|
||||||
|
<Placeholder height={20} width="64px" />
|
||||||
|
<Placeholder height={20} width="100%" />
|
||||||
|
<Placeholder height={20} width="100px" />
|
||||||
|
<Placeholder height={20} width="100px" />
|
||||||
|
<Placeholder height={20} width="64px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div />
|
||||||
|
|
||||||
|
<Placeholder height={14} width="180px" />
|
||||||
|
|
||||||
|
<div />
|
||||||
|
|
||||||
|
<Group className={styles.heroes}>
|
||||||
|
<LabHero />
|
||||||
|
<div />
|
||||||
|
<LabHero />
|
||||||
|
<div />
|
||||||
|
<LabHero />
|
||||||
|
<div />
|
||||||
|
<LabHero />
|
||||||
|
<div />
|
||||||
|
<LabHero />
|
||||||
|
<div />
|
||||||
|
<LabHero />
|
||||||
|
<div />
|
||||||
|
<LabHero />
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<div />
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
</Group>
|
||||||
|
</Sticky>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabLayout };
|
20
src/containers/lab/LabLayout/styles.module.scss
Normal file
20
src/containers/lab/LabLayout/styles.module.scss
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 3fr 1fr;
|
||||||
|
column-gap: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
margin-top: -7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
margin: 0 $gap $gap 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,10 +6,14 @@ import { BorisLayout } from '~/containers/node/BorisLayout';
|
||||||
import { ErrorNotFound } from '~/containers/pages/ErrorNotFound';
|
import { ErrorNotFound } from '~/containers/pages/ErrorNotFound';
|
||||||
import { ProfilePage } from '~/containers/profile/ProfilePage';
|
import { ProfilePage } from '~/containers/profile/ProfilePage';
|
||||||
import { Redirect, Route, Switch, useLocation } from 'react-router';
|
import { Redirect, Route, Switch, useLocation } from 'react-router';
|
||||||
|
import { LabLayout } from '~/containers/lab/LabLayout';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import { selectAuthUser } from '~/redux/auth/selectors';
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {}
|
||||||
|
|
||||||
const MainRouter: FC<IProps> = () => {
|
const MainRouter: FC<IProps> = () => {
|
||||||
|
const { is_user } = useShallowSelect(selectAuthUser);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -20,6 +24,12 @@ const MainRouter: FC<IProps> = () => {
|
||||||
<Route path={URLS.ERRORS.NOT_FOUND} component={ErrorNotFound} />
|
<Route path={URLS.ERRORS.NOT_FOUND} component={ErrorNotFound} />
|
||||||
<Route path={URLS.PROFILE_PAGE(':username')} component={ProfilePage} />
|
<Route path={URLS.PROFILE_PAGE(':username')} component={ProfilePage} />
|
||||||
|
|
||||||
|
{is_user && (
|
||||||
|
<>
|
||||||
|
<Route exact path={URLS.LAB} component={LabLayout} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Redirect to="/" />
|
<Redirect to="/" />
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
|
12
src/redux/lab/actions.ts
Normal file
12
src/redux/lab/actions.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
||||||
|
import { ILabState } from '~/redux/lab/types';
|
||||||
|
|
||||||
|
export const labGetList = (after?: string) => ({
|
||||||
|
type: LAB_ACTIONS.GET_LIST,
|
||||||
|
after,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const labSetList = (list: Partial<ILabState['list']>) => ({
|
||||||
|
type: LAB_ACTIONS.SET_LIST,
|
||||||
|
list,
|
||||||
|
});
|
8
src/redux/lab/api.ts
Normal file
8
src/redux/lab/api.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { api, cleanResult } from '~/utils/api';
|
||||||
|
import { API } from '~/constants/api';
|
||||||
|
import { GetLabNodesRequest, GetLabNodesResult } from '~/redux/lab/types';
|
||||||
|
|
||||||
|
export const getLabNodes = ({ after }: GetLabNodesRequest) =>
|
||||||
|
api
|
||||||
|
.get<GetLabNodesResult>(API.LAB.NODES, { params: { after } })
|
||||||
|
.then(cleanResult);
|
6
src/redux/lab/constants.ts
Normal file
6
src/redux/lab/constants.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
const prefix = 'LAB.';
|
||||||
|
|
||||||
|
export const LAB_ACTIONS = {
|
||||||
|
GET_LIST: `${prefix}GET_LIST`,
|
||||||
|
SET_LIST: `${prefix}SET_LIST`,
|
||||||
|
};
|
20
src/redux/lab/handlers.ts
Normal file
20
src/redux/lab/handlers.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
||||||
|
import { labSetList } from '~/redux/lab/actions';
|
||||||
|
import { ILabState } from '~/redux/lab/types';
|
||||||
|
|
||||||
|
type LabHandler<T extends (...args: any) => any> = (
|
||||||
|
state: Readonly<ILabState>,
|
||||||
|
payload: ReturnType<T>
|
||||||
|
) => Readonly<ILabState>;
|
||||||
|
|
||||||
|
const setList: LabHandler<typeof labSetList> = (state, { list }) => ({
|
||||||
|
...state,
|
||||||
|
list: {
|
||||||
|
...state.list,
|
||||||
|
...list,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LAB_HANDLERS = {
|
||||||
|
[LAB_ACTIONS.SET_LIST]: setList,
|
||||||
|
};
|
14
src/redux/lab/index.ts
Normal file
14
src/redux/lab/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { createReducer } from '~/utils/reducer';
|
||||||
|
import { LAB_HANDLERS } from '~/redux/lab/handlers';
|
||||||
|
import { ILabState } from '~/redux/lab/types';
|
||||||
|
|
||||||
|
const INITIAL_STATE: ILabState = {
|
||||||
|
list: {
|
||||||
|
is_loading: false,
|
||||||
|
nodes: [],
|
||||||
|
count: 0,
|
||||||
|
error: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createReducer(INITIAL_STATE, LAB_HANDLERS);
|
21
src/redux/lab/sagas.ts
Normal file
21
src/redux/lab/sagas.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { takeLeading, call, put } from 'redux-saga/effects';
|
||||||
|
import { labGetList, labSetList } from '~/redux/lab/actions';
|
||||||
|
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
||||||
|
import { Unwrap } from '~/redux/types';
|
||||||
|
import { getLabNodes } from '~/redux/lab/api';
|
||||||
|
|
||||||
|
function* getList({ after = '' }: ReturnType<typeof labGetList>) {
|
||||||
|
try {
|
||||||
|
yield put(labSetList({ is_loading: true }));
|
||||||
|
const { nodes, count }: Unwrap<typeof getLabNodes> = yield call(getLabNodes, { after });
|
||||||
|
yield put(labSetList({ nodes, count }));
|
||||||
|
} catch (error) {
|
||||||
|
yield put(labSetList({ error: error.message }));
|
||||||
|
} finally {
|
||||||
|
yield put(labSetList({ is_loading: false }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function* labSaga() {
|
||||||
|
yield takeLeading(LAB_ACTIONS.GET_LIST, getList);
|
||||||
|
}
|
4
src/redux/lab/selectors.ts
Normal file
4
src/redux/lab/selectors.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { IState } from '~/redux/store';
|
||||||
|
|
||||||
|
export const selectLab = (state: IState) => state.lab;
|
||||||
|
export const selectLabListNodes = (state: IState) => state.lab.list.nodes;
|
19
src/redux/lab/types.ts
Normal file
19
src/redux/lab/types.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { IError, INode } from '~/redux/types';
|
||||||
|
|
||||||
|
export type ILabState = Readonly<{
|
||||||
|
list: {
|
||||||
|
is_loading: boolean;
|
||||||
|
nodes: INode[];
|
||||||
|
count: number;
|
||||||
|
error: IError;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type GetLabNodesRequest = {
|
||||||
|
after?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetLabNodesResult = {
|
||||||
|
nodes: INode[];
|
||||||
|
count: number;
|
||||||
|
};
|
|
@ -13,6 +13,7 @@ import { EditorAudioUploadButton } from '~/components/editors/EditorAudioUploadB
|
||||||
import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton';
|
import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton';
|
||||||
import { IEditorComponentProps, NodeEditorProps } from '~/redux/node/types';
|
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 { NodeImageSwiperBlock } from '~/components/node/NodeImageSwiperBlock';
|
import { NodeImageSwiperBlock } from '~/components/node/NodeImageSwiperBlock';
|
||||||
|
|
||||||
const prefix = 'NODE.';
|
const prefix = 'NODE.';
|
||||||
|
@ -59,6 +60,8 @@ export const EMPTY_NODE: INode = {
|
||||||
|
|
||||||
blocks: [],
|
blocks: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
|
is_public: true,
|
||||||
|
is_promoted: true,
|
||||||
|
|
||||||
flow: {
|
flow: {
|
||||||
display: 'single',
|
display: 'single',
|
||||||
|
@ -112,14 +115,20 @@ export const NODE_EDITORS: Record<
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NODE_PANEL_COMPONENTS: Record<string, FC<IEditorComponentProps>[]> = {
|
export const NODE_PANEL_COMPONENTS: Record<string, FC<IEditorComponentProps>[]> = {
|
||||||
[NODE_TYPES.TEXT]: [EditorFiller, EditorUploadCoverButton],
|
[NODE_TYPES.TEXT]: [EditorFiller, EditorUploadCoverButton, EditorPublicSwitch],
|
||||||
[NODE_TYPES.VIDEO]: [EditorFiller, EditorUploadCoverButton],
|
[NODE_TYPES.VIDEO]: [EditorFiller, EditorUploadCoverButton, EditorPublicSwitch],
|
||||||
[NODE_TYPES.IMAGE]: [EditorImageUploadButton, EditorFiller, EditorUploadCoverButton],
|
[NODE_TYPES.IMAGE]: [
|
||||||
|
EditorImageUploadButton,
|
||||||
|
EditorFiller,
|
||||||
|
EditorUploadCoverButton,
|
||||||
|
EditorPublicSwitch,
|
||||||
|
],
|
||||||
[NODE_TYPES.AUDIO]: [
|
[NODE_TYPES.AUDIO]: [
|
||||||
EditorAudioUploadButton,
|
EditorAudioUploadButton,
|
||||||
EditorImageUploadButton,
|
EditorImageUploadButton,
|
||||||
EditorFiller,
|
EditorFiller,
|
||||||
EditorUploadCoverButton,
|
EditorUploadCoverButton,
|
||||||
|
EditorPublicSwitch,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,10 @@ import nodeSaga from '~/redux/node/sagas';
|
||||||
import flow, { IFlowState } from '~/redux/flow/reducer';
|
import flow, { IFlowState } from '~/redux/flow/reducer';
|
||||||
import flowSaga from '~/redux/flow/sagas';
|
import flowSaga from '~/redux/flow/sagas';
|
||||||
|
|
||||||
|
import lab from '~/redux/lab';
|
||||||
|
import labSaga from '~/redux/lab/sagas';
|
||||||
|
import { ILabState } from '~/redux/lab/types';
|
||||||
|
|
||||||
import uploads, { IUploadState } from '~/redux/uploads/reducer';
|
import uploads, { IUploadState } from '~/redux/uploads/reducer';
|
||||||
import uploadSaga from '~/redux/uploads/sagas';
|
import uploadSaga from '~/redux/uploads/sagas';
|
||||||
|
|
||||||
|
@ -69,6 +73,7 @@ export interface IState {
|
||||||
boris: IBorisState;
|
boris: IBorisState;
|
||||||
messages: IMessagesState;
|
messages: IMessagesState;
|
||||||
tag: ITagState;
|
tag: ITagState;
|
||||||
|
lab: ILabState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sagaMiddleware = createSagaMiddleware();
|
export const sagaMiddleware = createSagaMiddleware();
|
||||||
|
@ -93,6 +98,7 @@ export const store = createStore(
|
||||||
player: persistReducer(playerPersistConfig, player),
|
player: persistReducer(playerPersistConfig, player),
|
||||||
messages,
|
messages,
|
||||||
tag: tag,
|
tag: tag,
|
||||||
|
lab: lab,
|
||||||
}),
|
}),
|
||||||
composeEnhancers(applyMiddleware(routerMiddleware(history), sagaMiddleware))
|
composeEnhancers(applyMiddleware(routerMiddleware(history), sagaMiddleware))
|
||||||
);
|
);
|
||||||
|
@ -110,6 +116,7 @@ export function configureStore(): {
|
||||||
sagaMiddleware.run(borisSaga);
|
sagaMiddleware.run(borisSaga);
|
||||||
sagaMiddleware.run(messagesSaga);
|
sagaMiddleware.run(messagesSaga);
|
||||||
sagaMiddleware.run(tagSaga);
|
sagaMiddleware.run(tagSaga);
|
||||||
|
sagaMiddleware.run(labSaga);
|
||||||
|
|
||||||
window.addEventListener('message', message => {
|
window.addEventListener('message', message => {
|
||||||
if (message && message.data && message.data.type === 'oauth_login' && message.data.token)
|
if (message && message.data && message.data.type === 'oauth_login' && message.data.token)
|
||||||
|
|
|
@ -124,6 +124,8 @@ export interface INode {
|
||||||
description?: string;
|
description?: string;
|
||||||
is_liked?: boolean;
|
is_liked?: boolean;
|
||||||
is_heroic?: boolean;
|
is_heroic?: boolean;
|
||||||
|
is_promoted?: boolean;
|
||||||
|
is_public?: boolean;
|
||||||
like_count?: number;
|
like_count?: number;
|
||||||
|
|
||||||
flow: {
|
flow: {
|
||||||
|
|
|
@ -255,6 +255,16 @@ const Sprites: FC<{}> = () => (
|
||||||
<path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z" />
|
<path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z" />
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
|
<g id="waves" stroke="none">
|
||||||
|
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||||
|
<path d="M17 16.99c-1.35 0-2.2.42-2.95.8-.65.33-1.18.6-2.05.6-.9 0-1.4-.25-2.05-.6-.75-.38-1.57-.8-2.95-.8s-2.2.42-2.95.8c-.65.33-1.17.6-2.05.6v1.95c1.35 0 2.2-.42 2.95-.8.65-.33 1.17-.6 2.05-.6s1.4.25 2.05.6c.75.38 1.57.8 2.95.8s2.2-.42 2.95-.8c.65-.33 1.18-.6 2.05-.6.9 0 1.4.25 2.05.6.75.38 1.58.8 2.95.8v-1.95c-.9 0-1.4-.25-2.05-.6-.75-.38-1.6-.8-2.95-.8zm0-4.45c-1.35 0-2.2.43-2.95.8-.65.32-1.18.6-2.05.6-.9 0-1.4-.25-2.05-.6-.75-.38-1.57-.8-2.95-.8s-2.2.43-2.95.8c-.65.32-1.17.6-2.05.6v1.95c1.35 0 2.2-.43 2.95-.8.65-.35 1.15-.6 2.05-.6s1.4.25 2.05.6c.75.38 1.57.8 2.95.8s2.2-.43 2.95-.8c.65-.35 1.15-.6 2.05-.6s1.4.25 2.05.6c.75.38 1.58.8 2.95.8v-1.95c-.9 0-1.4-.25-2.05-.6-.75-.38-1.6-.8-2.95-.8zm2.95-8.08c-.75-.38-1.58-.8-2.95-.8s-2.2.42-2.95.8c-.65.32-1.18.6-2.05.6-.9 0-1.4-.25-2.05-.6-.75-.37-1.57-.8-2.95-.8s-2.2.42-2.95.8c-.65.33-1.17.6-2.05.6v1.93c1.35 0 2.2-.43 2.95-.8.65-.33 1.17-.6 2.05-.6s1.4.25 2.05.6c.75.38 1.57.8 2.95.8s2.2-.43 2.95-.8c.65-.32 1.18-.6 2.05-.6.9 0 1.4.25 2.05.6.75.38 1.58.8 2.95.8V5.04c-.9 0-1.4-.25-2.05-.58zM17 8.09c-1.35 0-2.2.43-2.95.8-.65.35-1.15.6-2.05.6s-1.4-.25-2.05-.6c-.75-.38-1.57-.8-2.95-.8s-2.2.43-2.95.8c-.65.35-1.15.6-2.05.6v1.95c1.35 0 2.2-.43 2.95-.8.65-.32 1.18-.6 2.05-.6s1.4.25 2.05.6c.75.38 1.57.8 2.95.8s2.2-.43 2.95-.8c.65-.32 1.18-.6 2.05-.6.9 0 1.4.25 2.05.6.75.38 1.58.8 2.95.8V9.49c-.9 0-1.4-.25-2.05-.6-.75-.38-1.6-.8-2.95-.8z"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="lab" stroke="none">
|
||||||
|
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||||
|
<path d="M13,11.33L18,18H6l5-6.67V6h2 M15.96,4H8.04C7.62,4,7.39,4.48,7.65,4.81L9,6.5v4.17L3.2,18.4C2.71,19.06,3.18,20,4,20h16 c0.82,0,1.29-0.94,0.8-1.6L15,10.67V6.5l1.35-1.69C16.61,4.48,16.38,4,15.96,4L15.96,4z"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
<g id="search">
|
<g id="search">
|
||||||
<path fill="none" d="M0 0h24v24H0V0z" stroke="none" />
|
<path fill="none" d="M0 0h24v24H0V0z" stroke="none" />
|
||||||
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
|
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
|
||||||
|
|
|
@ -209,3 +209,13 @@ $sidebar_border: transparentize(white, 0.95);
|
||||||
background: transparentize($content_bg, 0.4);
|
background: transparentize($content_bg, 0.4);
|
||||||
box-shadow: transparentize(white, 0.95) -1px 0;
|
box-shadow: transparentize(white, 0.95) -1px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin editor_round_button {
|
||||||
|
width: $upload_button_height;
|
||||||
|
height: $upload_button_height;
|
||||||
|
border-radius: ($upload_button_height / 2) !important;
|
||||||
|
flex: 0 0 $upload_button_height;
|
||||||
|
position: relative;
|
||||||
|
border-radius: $radius;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,10 @@ import { useMemo } from 'react';
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
|
|
||||||
export const useNodeAudios = (node: INode) => {
|
export const useNodeAudios = (node: INode) => {
|
||||||
|
if (!node?.files) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), [
|
return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), [
|
||||||
node.files,
|
node.files,
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -3,6 +3,10 @@ import { useMemo } from 'react';
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
|
|
||||||
export const useNodeImages = (node: INode) => {
|
export const useNodeImages = (node: INode) => {
|
||||||
|
if (!node?.files) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [
|
return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [
|
||||||
node.files,
|
node.files,
|
||||||
]);
|
]);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue