mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +07:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
c2d71ae245
56 changed files with 985 additions and 206 deletions
|
@ -78,7 +78,7 @@ steps:
|
||||||
format: markdown
|
format: markdown
|
||||||
message: >
|
message: >
|
||||||
{{#success build.status}}🤓{{else}}😨{{/success}}
|
{{#success build.status}}🤓{{else}}😨{{/success}}
|
||||||
{{ datetime build.finished "01.02.2006 15:04:05" "UTC" }} [{{repo.name}} / {{commit.branch}}]({{ build.link }})
|
[{{repo.name}} / {{commit.branch}}]({{ build.link }})
|
||||||
```
|
```
|
||||||
{{ commit.message }}
|
{{ commit.message }}
|
||||||
```
|
```
|
||||||
|
|
|
@ -28,4 +28,12 @@
|
||||||
:global(.green) {
|
:global(.green) {
|
||||||
color: $wisegreen;
|
color: $wisegreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > :last-child::after {
|
||||||
|
display: inline-block;
|
||||||
|
content: " ";
|
||||||
|
height: 1em;
|
||||||
|
width: 120px;
|
||||||
|
flex: 0 0 120px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
15
src/components/containers/Authorized/index.tsx
Normal file
15
src/components/containers/Authorized/index.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import { selectUser } from '~/redux/auth/selectors';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const Authorized: FC<IProps> = ({ children }) => {
|
||||||
|
const user = useShallowSelect(selectUser);
|
||||||
|
|
||||||
|
if (!user.is_user) return null;
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Authorized };
|
11
src/components/containers/Markdown/index.tsx
Normal file
11
src/components/containers/Markdown/index.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import React, { ButtonHTMLAttributes, DetailedHTMLProps, FC, HTMLAttributes } from 'react';
|
||||||
|
import styles from '~/styles/common/markdown.module.scss';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
interface IProps extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {}
|
||||||
|
|
||||||
|
const Markdown: FC<IProps> = ({ className, ...props }) => (
|
||||||
|
<div className={classNames(styles.wrapper, className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export { Markdown };
|
|
@ -14,29 +14,27 @@ const EditorPublicSwitch: FC<IProps> = ({ data, setData }) => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Superpower>
|
<Button
|
||||||
<Button
|
color={data.is_promoted ? 'primary' : 'lab'}
|
||||||
color={data.is_promoted ? 'primary' : 'lab'}
|
type="button"
|
||||||
type="button"
|
size="giant"
|
||||||
size="giant"
|
label={
|
||||||
label={
|
data.is_promoted
|
||||||
data.is_promoted
|
? 'Доступно всем на главной странице'
|
||||||
? 'Доступно всем на главной странице'
|
: 'Видно только сотрудникам в лаборатории'
|
||||||
: 'Видно только сотрудникам в лаборатории'
|
}
|
||||||
}
|
onClick={onChange}
|
||||||
onClick={onChange}
|
className={styles.button}
|
||||||
className={styles.button}
|
round
|
||||||
round
|
>
|
||||||
>
|
{data.is_promoted ? (
|
||||||
{data.is_promoted ? (
|
<Icon icon="waves" size={24} />
|
||||||
<Icon icon="waves" size={24} />
|
) : (
|
||||||
) : (
|
<div className={styles.lab_wrapper}>
|
||||||
<div className={styles.lab_wrapper}>
|
<Icon icon="lab" size={24} />
|
||||||
<Icon icon="lab" size={24} />
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</Button>
|
||||||
</Button>
|
|
||||||
</Superpower>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -9,12 +9,21 @@ interface IProps {}
|
||||||
const LabBanner: FC<IProps> = () => (
|
const LabBanner: FC<IProps> = () => (
|
||||||
<Card className={styles.wrap}>
|
<Card className={styles.wrap}>
|
||||||
<Group>
|
<Group>
|
||||||
<Placeholder height={32} />
|
<div className={styles.title}>Лаборатория!</div>
|
||||||
<Placeholder height={18} width="120px" />
|
|
||||||
<Placeholder height={18} width="200px" />
|
<Group className={styles.content}>
|
||||||
<Placeholder height={18} width="60px" />
|
<p>
|
||||||
<Placeholder height={18} width="180px" />
|
<strong>
|
||||||
<Placeholder height={18} width="230px" />
|
Всё, что происходит здесь — всего лишь эксперимент, о котором не узнает никто за
|
||||||
|
пределами Убежища.
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Ловим радиоактивных жуков, приручаем утконосов-вампиров, катаемся на младшем научном
|
||||||
|
сотруднике Егоре Порсифоровиче (у него как раз сейчас линька).
|
||||||
|
</p>
|
||||||
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,21 @@
|
||||||
@import "~/styles/variables.scss";
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
background: $red_gradient_alt;
|
@include lab_shadow;
|
||||||
|
|
||||||
|
background: linear-gradient(darken($dark_blue, 0%), darken($blue, 30%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font: $font_24_bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
font: $font_14_regular;
|
||||||
|
line-height: 19px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
49
src/components/lab/LabBottomPanel/index.tsx
Normal file
49
src/components/lab/LabBottomPanel/index.tsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
import { Filler } from '~/components/containers/Filler';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { getPrettyDate } from '~/utils/dom';
|
||||||
|
import { INode } from '~/redux/types';
|
||||||
|
import { Icon } from '~/components/input/Icon';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Grid } from '~/components/containers/Grid';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
import { URLS } from '~/constants/urls';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
node: INode;
|
||||||
|
isLoading?: boolean;
|
||||||
|
hasNewComments: boolean;
|
||||||
|
commentCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LabBottomPanel: FC<Props> = ({ node, hasNewComments, commentCount }) => {
|
||||||
|
const history = useHistory();
|
||||||
|
const onClick = useCallback(() => history.push(URLS.NODE_URL(node.id)), [node.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group horizontal className={styles.wrap} onClick={onClick}>
|
||||||
|
<div className={styles.timestamp}>{getPrettyDate(node.created_at)}</div>
|
||||||
|
<Filler />
|
||||||
|
|
||||||
|
{commentCount > 0 && (
|
||||||
|
<Grid
|
||||||
|
horizontal
|
||||||
|
className={classNames(styles.comments, { [styles.active]: hasNewComments })}
|
||||||
|
>
|
||||||
|
<Icon icon={hasNewComments ? 'comment_new' : 'comment'} size={16} />
|
||||||
|
<span>{commentCount}</span>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!!node.like_count && node.like_count > 0 && (
|
||||||
|
<Grid horizontal className={classNames(styles.like)}>
|
||||||
|
<Icon icon={node.is_liked ? 'heart_full' : 'heart'} size={16} />
|
||||||
|
<span>{node.like_count}</span>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabBottomPanel };
|
25
src/components/lab/LabBottomPanel/styles.module.scss
Normal file
25
src/components/lab/LabBottomPanel/styles.module.scss
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
padding: 0 $gap $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
font: $font_12_regular;
|
||||||
|
color: darken(white, 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments, .like {
|
||||||
|
flex: 0;
|
||||||
|
font: $font_14_semibold;
|
||||||
|
color: darken(white, 50%);
|
||||||
|
fill: currentColor;
|
||||||
|
stroke: none;
|
||||||
|
column-gap: $gap / 2 !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $red;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,32 +1,31 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Group } from '~/components/containers/Group';
|
import styles from './styles.module.scss';
|
||||||
import { Card } from '~/components/containers/Card';
|
import { LabHeadItem } from '~/components/lab/LabHeadItem';
|
||||||
import { Placeholder } from '~/components/placeholders/Placeholder';
|
|
||||||
import { Filler } from '~/components/containers/Filler';
|
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const LabHead: FC<IProps> = () => (
|
const LabHead: FC<IProps> = ({ isLoading }) => {
|
||||||
<Card>
|
return null;
|
||||||
<Group horizontal>
|
|
||||||
<Group horizontal style={{ flex: '0 0 auto' }}>
|
|
||||||
<Placeholder width="32px" height={32} />
|
|
||||||
<Placeholder width="96px" height={18} />
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group horizontal style={{ flex: '0 0 auto' }}>
|
return (
|
||||||
<Placeholder width="32px" height={32} />
|
<div className={styles.wrap}>
|
||||||
<Placeholder width="126px" height={18} />
|
<div className={styles.group}>
|
||||||
</Group>
|
<LabHeadItem icon="recent" active isLoading={isLoading}>
|
||||||
|
Свежие
|
||||||
|
</LabHeadItem>
|
||||||
|
|
||||||
<Group horizontal style={{ flex: '0 0 auto' }}>
|
<LabHeadItem icon="hot" isLoading={isLoading}>
|
||||||
<Placeholder width="32px" height={32} />
|
Популярные
|
||||||
<Placeholder width="96px" height={18} />
|
</LabHeadItem>
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Filler />
|
<LabHeadItem icon="star_full" isLoading={isLoading}>
|
||||||
</Group>
|
Важные
|
||||||
</Card>
|
</LabHeadItem>
|
||||||
);
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export { LabHead };
|
export { LabHead };
|
||||||
|
|
23
src/components/lab/LabHead/styles.module.scss
Normal file
23
src/components/lab/LabHead/styles.module.scss
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
@include lab_shadow;
|
||||||
|
|
||||||
|
border-radius: $radius;
|
||||||
|
background-color: $content_bg;
|
||||||
|
padding: $gap / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
margin: $gap / 2;
|
||||||
|
}
|
||||||
|
}
|
32
src/components/lab/LabHeadItem/index.tsx
Normal file
32
src/components/lab/LabHeadItem/index.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
import { Icon } from '~/components/input/Icon';
|
||||||
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
icon: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LabHeadItem: FC<IProps> = ({ icon, children, isLoading, active }) => {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Group horizontal className={styles.item}>
|
||||||
|
<Placeholder width="32px" height={32} />
|
||||||
|
<Placeholder width="96px" height={18} />
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group horizontal className={classNames(styles.item, { [styles.active]: active })}>
|
||||||
|
<Icon icon={icon} size={24} />
|
||||||
|
<span className={styles.text}>{children}</span>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabHeadItem };
|
25
src/components/lab/LabHeadItem/styles.module.scss
Normal file
25
src/components/lab/LabHeadItem/styles.module.scss
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.item {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: $gap / 2;
|
||||||
|
fill: currentColor;
|
||||||
|
color: darken(white, 50%);
|
||||||
|
transition: color 0.25s;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $blue;
|
||||||
|
background-color: lighten($content_bg, 6%);
|
||||||
|
border-radius: $radius;
|
||||||
|
padding: 0 $gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font: $font_16_semibold;
|
||||||
|
}
|
|
@ -1,22 +1,51 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import { Placeholder } from '~/components/placeholders/Placeholder';
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
import { INode } from '~/redux/types';
|
||||||
|
import { getPrettyDate } from '~/utils/dom';
|
||||||
|
import { URLS } from '~/constants/urls';
|
||||||
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {
|
||||||
|
node?: Partial<INode>;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const LabHero: FC<IProps> = () => (
|
const LabHero: FC<IProps> = ({ node, isLoading }) => {
|
||||||
<Group horizontal className={styles.wrap1}>
|
const history = useHistory();
|
||||||
<div className={styles.star}>
|
const onClick = useCallback(() => {
|
||||||
<Icon icon="star_full" size={32} />
|
history.push(URLS.NODE_URL(node?.id));
|
||||||
</div>
|
}, [history, node]);
|
||||||
|
|
||||||
<Group>
|
if (!node || isLoading) {
|
||||||
<Placeholder height={20} />
|
return (
|
||||||
<Placeholder height={12} width="100px" />
|
<Group horizontal className={styles.wrap1}>
|
||||||
|
<div className={styles.star}>
|
||||||
|
<Icon icon="star_full" size={32} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.content}>
|
||||||
|
<Placeholder height={20} />
|
||||||
|
<Placeholder height={12} width="100px" />
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group horizontal className={styles.wrap} onClick={onClick}>
|
||||||
|
<div className={styles.star}>
|
||||||
|
<Icon icon="star_full" size={32} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.title}>{node.title}</div>
|
||||||
|
<div className={styles.description}>{getPrettyDate(node.created_at)}</div>
|
||||||
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
export { LabHero };
|
export { LabHero };
|
||||||
|
|
|
@ -1,10 +1,34 @@
|
||||||
@import "~/styles/variables.scss";
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
margin-bottom: $gap;
|
min-width: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.star {
|
.star {
|
||||||
fill: #2c2c2c;
|
fill: darken(white, 76%);
|
||||||
|
flex: 0 0 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font: $font_18_semibold;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 22px;
|
||||||
|
word-break: break-all;
|
||||||
|
color: darken(white, 40%);
|
||||||
|
|
||||||
|
@include clamp(2, 22px)
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font: $font_10_regular;
|
||||||
|
color: darken(white, 50%);
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: $gap / 2 0;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
34
src/components/lab/LabHeroes/index.tsx
Normal file
34
src/components/lab/LabHeroes/index.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { INode } from '~/redux/types';
|
||||||
|
import styles from '~/containers/lab/LabStats/styles.module.scss';
|
||||||
|
import { LabHero } from '~/components/lab/LabHero';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
nodes: Partial<INode>[];
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const empty = [...new Array(5)].map((_, i) => i);
|
||||||
|
|
||||||
|
const LabHeroes: FC<IProps> = ({ nodes, isLoading }) => {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Group className={styles.heroes}>
|
||||||
|
{empty.map(i => (
|
||||||
|
<LabHero isLoading key={i} />
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group className={styles.heroes}>
|
||||||
|
{nodes.slice(0, 10).map(node => (
|
||||||
|
<LabHero node={node} key={node?.id} />
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabHeroes };
|
100
src/components/lab/LabImage/index.tsx
Normal file
100
src/components/lab/LabImage/index.tsx
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { INodeComponentProps } from '~/redux/node/constants';
|
||||||
|
import SwiperCore, { A11y, Pagination, Navigation, SwiperOptions, Keyboard } from 'swiper';
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
|
|
||||||
|
import 'swiper/swiper.scss';
|
||||||
|
import 'swiper/components/pagination/pagination.scss';
|
||||||
|
import 'swiper/components/scrollbar/scrollbar.scss';
|
||||||
|
import 'swiper/components/zoom/zoom.scss';
|
||||||
|
import 'swiper/components/navigation/navigation.scss';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { useNodeImages } from '~/utils/hooks/node/useNodeImages';
|
||||||
|
import { getURL } from '~/utils/dom';
|
||||||
|
import { PRESETS, URLS } from '~/constants/urls';
|
||||||
|
import SwiperClass from 'swiper/types/swiper-class';
|
||||||
|
import { modalShowPhotoswipe } from '~/redux/modal/actions';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
|
||||||
|
SwiperCore.use([Navigation, Pagination, A11y]);
|
||||||
|
|
||||||
|
interface IProps extends INodeComponentProps {}
|
||||||
|
|
||||||
|
const breakpoints: SwiperOptions['breakpoints'] = {
|
||||||
|
599: {
|
||||||
|
spaceBetween: 20,
|
||||||
|
navigation: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const LabImage: FC<IProps> = ({ node }) => {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const [controlledSwiper, setControlledSwiper] = useState<SwiperClass | undefined>(undefined);
|
||||||
|
|
||||||
|
const images = useNodeImages(node);
|
||||||
|
|
||||||
|
const updateSwiper = useCallback(() => {
|
||||||
|
if (!controlledSwiper) return;
|
||||||
|
|
||||||
|
controlledSwiper.updateSlides();
|
||||||
|
controlledSwiper.updateSize();
|
||||||
|
controlledSwiper.update();
|
||||||
|
}, [controlledSwiper]);
|
||||||
|
|
||||||
|
const resetSwiper = useCallback(() => {
|
||||||
|
if (!controlledSwiper) return;
|
||||||
|
controlledSwiper.slideTo(0, 0);
|
||||||
|
}, [controlledSwiper]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateSwiper();
|
||||||
|
resetSwiper();
|
||||||
|
}, [images, updateSwiper, resetSwiper]);
|
||||||
|
|
||||||
|
const onClick = useCallback(() => history.push(URLS.NODE_URL(node.id)), [history, node.id]);
|
||||||
|
|
||||||
|
if (!images?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<Swiper
|
||||||
|
initialSlide={0}
|
||||||
|
slidesPerView={images.length > 1 ? 1.1 : 1}
|
||||||
|
onSwiper={setControlledSwiper}
|
||||||
|
spaceBetween={10}
|
||||||
|
grabCursor
|
||||||
|
autoHeight
|
||||||
|
breakpoints={breakpoints}
|
||||||
|
observeSlideChildren
|
||||||
|
observeParents
|
||||||
|
resizeObserver
|
||||||
|
watchOverflow
|
||||||
|
updateOnImagesReady
|
||||||
|
onInit={resetSwiper}
|
||||||
|
keyboard={{
|
||||||
|
enabled: true,
|
||||||
|
onlyInViewport: false,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{images.map(file => (
|
||||||
|
<SwiperSlide className={styles.slide} key={file.id}>
|
||||||
|
<img
|
||||||
|
className={styles.image}
|
||||||
|
src={getURL(file, PRESETS['1600'])}
|
||||||
|
alt={node.title}
|
||||||
|
onLoad={updateSwiper}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabImage };
|
69
src/components/lab/LabImage/styles.module.scss
Normal file
69
src/components/lab/LabImage/styles.module.scss
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
border-radius: $radius;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
:global(.swiper-container) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.swiper-button-next),
|
||||||
|
:global(.swiper-button-prev) {
|
||||||
|
color: white;
|
||||||
|
font-size: 10px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide {
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font: $font_32_bold;
|
||||||
|
display: flex;
|
||||||
|
border-radius: $radius;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
opacity: 1;
|
||||||
|
filter: brightness(50%) saturate(0.5);
|
||||||
|
transition: opacity 0.5s, filter 0.5s, transform 0.5s;
|
||||||
|
|
||||||
|
&:global(.swiper-slide-active) {
|
||||||
|
opacity: 1;
|
||||||
|
filter: brightness(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
max-height: calc(100vh - 70px - 70px);
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: $radius;
|
||||||
|
transition: box-shadow 1s;
|
||||||
|
box-shadow: transparentize(black, 0.7) 0 3px 5px;
|
||||||
|
|
||||||
|
:global(.swiper-slide-active) & {
|
||||||
|
box-shadow: transparentize(black, 0.9) 0 10px 5px 4px,
|
||||||
|
transparentize(black, 0.7) 0 5px 5px,
|
||||||
|
transparentize(white, 0.95) 0 -1px 2px,
|
||||||
|
transparentize(white, 0.95) 0 -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
padding-bottom: 0;
|
||||||
|
max-height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,30 +1,36 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC, useMemo } from 'react';
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import { NodePanelInner } from '~/components/node/NodePanelInner';
|
|
||||||
import { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks';
|
import { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { Card } from '~/components/containers/Card';
|
import { LabBottomPanel } from '~/components/lab/LabBottomPanel';
|
||||||
import { NodePanelLab } from '~/components/node/NodePanelLab';
|
import { isAfter, parseISO } from 'date-fns';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
node: INode;
|
node: INode;
|
||||||
|
lastSeen: string | null;
|
||||||
|
isLoading?: boolean;
|
||||||
|
commentCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LabNode: FC<IProps> = ({ node }) => {
|
const LabNode: FC<IProps> = ({ node, isLoading, lastSeen, commentCount }) => {
|
||||||
const { inline, block, head } = useNodeBlocks(node, false);
|
const { lab } = useNodeBlocks(node, false);
|
||||||
|
|
||||||
console.log(node.id, { inline, block, head });
|
const hasNewComments = useMemo(
|
||||||
|
() =>
|
||||||
|
!!node.commented_at && !!lastSeen && isAfter(parseISO(node.commented_at), parseISO(lastSeen)),
|
||||||
|
[node.commented_at, lastSeen]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card seamless className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<div className={styles.head}>
|
{lab}
|
||||||
<NodePanelLab node={node} />
|
<LabBottomPanel
|
||||||
</div>
|
node={node}
|
||||||
|
isLoading={!!isLoading}
|
||||||
{head}
|
hasNewComments={hasNewComments}
|
||||||
{block}
|
commentCount={commentCount}
|
||||||
{inline}
|
/>
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
@import "~/styles/variables.scss";
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
|
@include lab_shadow;
|
||||||
|
|
||||||
|
background-color: $lab_post_bg;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
border-radius: $radius;
|
||||||
|
|
||||||
.head {
|
|
||||||
background-color: transparentize(black, 0.9);
|
|
||||||
border-radius: $radius $radius 0 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
34
src/components/lab/LabNodeTitle/index.tsx
Normal file
34
src/components/lab/LabNodeTitle/index.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
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';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
import { Icon } from '~/components/input/Icon';
|
||||||
|
import Tippy from '@tippy.js/react';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
node: INode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LabNodeTitle: FC<IProps> = ({ node }) => {
|
||||||
|
if (!node.title) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group horizontal className={styles.wrap}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
<Link to={URLS.NODE_URL(node.id)}>{node.title || '...'}</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{node.is_heroic && (
|
||||||
|
<Tippy content="Важный пост">
|
||||||
|
<div className={styles.star}>
|
||||||
|
<Icon icon="star_full" />
|
||||||
|
</div>
|
||||||
|
</Tippy>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabNodeTitle };
|
|
@ -1,7 +1,7 @@
|
||||||
@import "~/styles/variables.scss";
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
padding: $gap;
|
padding: 0 $gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
@ -19,6 +19,11 @@
|
||||||
@include tablet {
|
@include tablet {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
font: $font_16_semibold;
|
font: $font_20_semibold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.star {
|
||||||
|
fill: $yellow;
|
||||||
|
flex: 0 0 24px;
|
||||||
|
}
|
14
src/components/lab/LabPad/index.tsx
Normal file
14
src/components/lab/LabPad/index.tsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
import { URLS } from '~/constants/urls';
|
||||||
|
import { INodeComponentProps } from '~/redux/node/constants';
|
||||||
|
|
||||||
|
const LabPad: FC<INodeComponentProps> = ({ node }) => {
|
||||||
|
const history = useHistory();
|
||||||
|
const onClick = useCallback(() => history.push(URLS.NODE_URL(node.id)), [node.id]);
|
||||||
|
|
||||||
|
return <div className={styles.pad} onClick={onClick} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabPad };
|
5
src/components/lab/LabPad/styles.module.scss
Normal file
5
src/components/lab/LabPad/styles.module.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.pad {
|
||||||
|
height: $gap;
|
||||||
|
}
|
36
src/components/lab/LabTags/index.tsx
Normal file
36
src/components/lab/LabTags/index.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import styles from './/styles.module.scss';
|
||||||
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
|
import { ITag } from '~/redux/types';
|
||||||
|
import { Tag } from '~/components/tags/Tag';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
tags: ITag[];
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LabTags: FC<IProps> = ({ tags, isLoading }) => {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.tags}>
|
||||||
|
{tags.slice(0, 10).map(tag => (
|
||||||
|
<Tag tag={tag} key={tag.id} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabTags };
|
10
src/components/lab/LabTags/styles.module.scss
Normal file
10
src/components/lab/LabTags/styles.module.scss
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
margin: $gap / 2;
|
||||||
|
}
|
||||||
|
}
|
28
src/components/lab/LabText/index.tsx
Normal file
28
src/components/lab/LabText/index.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import React, { FC, useCallback, useMemo } from 'react';
|
||||||
|
import { Markdown } from '~/components/containers/Markdown';
|
||||||
|
import { INodeComponentProps } from '~/redux/node/constants';
|
||||||
|
import { formatTextParagraphs } from '~/utils/dom';
|
||||||
|
import { path } from 'ramda';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
import { URLS } from '~/constants/urls';
|
||||||
|
|
||||||
|
const LabText: FC<INodeComponentProps> = ({ node }) => {
|
||||||
|
const content = useMemo(() => formatTextParagraphs(path(['blocks', 0, 'text'], node) || ''), [
|
||||||
|
node.blocks,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const onClick = useCallback(() => history.push(URLS.NODE_URL(node.id)), [node.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Markdown
|
||||||
|
dangerouslySetInnerHTML={{ __html: content }}
|
||||||
|
className={styles.wrap}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabText };
|
21
src/components/lab/LabText/styles.module.scss
Normal file
21
src/components/lab/LabText/styles.module.scss
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
padding: 0 $gap;
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
position: relative;
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: ' ';
|
||||||
|
position: absolute;
|
||||||
|
background: linear-gradient(transparentize($lab_post_bg, 1), $lab_post_bg 80%);
|
||||||
|
bottom: 0;
|
||||||
|
left: auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,14 @@
|
||||||
import React, { FC, useCallback, memo, useState, useEffect, useMemo } from 'react';
|
import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { push as historyPush } from 'connected-react-router';
|
import { push as historyPush } from 'connected-react-router';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Logo } from '~/components/main/Logo';
|
import { Logo } from '~/components/main/Logo';
|
||||||
|
|
||||||
import { Filler } from '~/components/containers/Filler';
|
import { Filler } from '~/components/containers/Filler';
|
||||||
import { selectUser, selectAuthUpdates } from '~/redux/auth/selectors';
|
import { selectAuthUpdates, selectUser } from '~/redux/auth/selectors';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import { DIALOGS } from '~/redux/modal/constants';
|
import { DIALOGS } from '~/redux/modal/constants';
|
||||||
import { pick } from 'ramda';
|
import { path, pick } from 'ramda';
|
||||||
import { path } from 'ramda';
|
|
||||||
import { UserButton } from '../UserButton';
|
import { UserButton } from '../UserButton';
|
||||||
import { Notifications } from '../Notifications';
|
import { Notifications } from '../Notifications';
|
||||||
import { URLS } from '~/constants/urls';
|
import { URLS } from '~/constants/urls';
|
||||||
|
@ -21,7 +20,7 @@ import * as MODAL_ACTIONS from '~/redux/modal/actions';
|
||||||
import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
||||||
import { IState } from '~/redux/store';
|
import { IState } from '~/redux/store';
|
||||||
import isBefore from 'date-fns/isBefore';
|
import isBefore from 'date-fns/isBefore';
|
||||||
import { Superpower } from '~/components/boris/Superpower';
|
import { Authorized } from '~/components/containers/Authorized';
|
||||||
|
|
||||||
const mapStateToProps = (state: IState) => ({
|
const mapStateToProps = (state: IState) => ({
|
||||||
user: pick(['username', 'is_user', 'photo', 'last_seen_boris'])(selectUser(state)),
|
user: pick(['username', 'is_user', 'photo', 'last_seen_boris'])(selectUser(state)),
|
||||||
|
@ -80,7 +79,7 @@ const HeaderUnconnected: FC<IProps> = memo(
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Logo />
|
<Logo />
|
||||||
|
|
||||||
<Filler />
|
<Filler className={styles.filler} />
|
||||||
|
|
||||||
<div className={styles.plugs}>
|
<div className={styles.plugs}>
|
||||||
<Link
|
<Link
|
||||||
|
@ -90,14 +89,14 @@ const HeaderUnconnected: FC<IProps> = memo(
|
||||||
ФЛОУ
|
ФЛОУ
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Superpower>
|
<Authorized>
|
||||||
<Link
|
<Link
|
||||||
className={classNames(styles.item, { [styles.is_active]: pathname === URLS.BASE })}
|
className={classNames(styles.item, { [styles.is_active]: pathname === URLS.BASE })}
|
||||||
to={URLS.LAB}
|
to={URLS.LAB}
|
||||||
>
|
>
|
||||||
ЛАБ
|
ЛАБ
|
||||||
</Link>
|
</Link>
|
||||||
</Superpower>
|
</Authorized>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
className={classNames(styles.item, {
|
className={classNames(styles.item, {
|
||||||
|
|
|
@ -48,6 +48,11 @@
|
||||||
user-select: none;
|
user-select: none;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile {
|
.profile {
|
||||||
|
@ -119,3 +124,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filler {
|
||||||
|
@include tablet {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,4 +6,8 @@
|
||||||
user-select: none;
|
user-select: none;
|
||||||
color: white;
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,9 @@ const NodeImageSwiperBlock: FC<IProps> = ({ node }) => {
|
||||||
const resetSwiper = useCallback(() => {
|
const resetSwiper = useCallback(() => {
|
||||||
if (!controlledSwiper) return;
|
if (!controlledSwiper) return;
|
||||||
controlledSwiper.slideTo(0, 0);
|
controlledSwiper.slideTo(0, 0);
|
||||||
setTimeout(() => controlledSwiper.slideTo(0, 0), 300);
|
|
||||||
|
// TODO: replace with working one
|
||||||
|
// setTimeout(() => controlledSwiper.slideTo(0, 0), 300);
|
||||||
}, [controlledSwiper]);
|
}, [controlledSwiper]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -63,7 +65,7 @@ const NodeImageSwiperBlock: FC<IProps> = ({ node }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper} key={node.id}>
|
||||||
<Swiper
|
<Swiper
|
||||||
initialSlide={0}
|
initialSlide={0}
|
||||||
slidesPerView="auto"
|
slidesPerView="auto"
|
||||||
|
|
|
@ -215,6 +215,7 @@
|
||||||
transition: fill, stroke 0.25s;
|
transition: fill, stroke 0.25s;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
flex: 0 0 32px;
|
||||||
|
|
||||||
&:global(.is_liked) {
|
&:global(.is_liked) {
|
||||||
svg {
|
svg {
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
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 };
|
|
|
@ -52,5 +52,6 @@ export const API = {
|
||||||
},
|
},
|
||||||
LAB: {
|
LAB: {
|
||||||
NODES: `/lab/`,
|
NODES: `/lab/`,
|
||||||
|
STATS: '/lab/stats',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,7 +12,12 @@ const LabGrid: FC<IProps> = () => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
{nodes.map(node => (
|
{nodes.map(node => (
|
||||||
<LabNode node={node} key={node.id} />
|
<LabNode
|
||||||
|
node={node.node}
|
||||||
|
key={node.node.id}
|
||||||
|
lastSeen={node.last_seen}
|
||||||
|
commentCount={node.comment_count}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,5 +4,5 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
grid-auto-rows: auto;
|
grid-auto-rows: auto;
|
||||||
grid-row-gap: $gap;
|
grid-row-gap: $gap * 2;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Sticky } from '~/components/containers/Sticky';
|
||||||
import { Container } from '~/containers/main/Container';
|
import { Container } from '~/containers/main/Container';
|
||||||
import { LabGrid } from '~/containers/lab/LabGrid';
|
import { LabGrid } from '~/containers/lab/LabGrid';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { labGetList } from '~/redux/lab/actions';
|
import { labGetList, labGetStats } from '~/redux/lab/actions';
|
||||||
import { Placeholder } from '~/components/placeholders/Placeholder';
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
import { Grid } from '~/components/containers/Grid';
|
import { Grid } from '~/components/containers/Grid';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
|
@ -13,14 +13,19 @@ import { LabHero } from '~/components/lab/LabHero';
|
||||||
import { LabBanner } from '~/components/lab/LabBanner';
|
import { LabBanner } from '~/components/lab/LabBanner';
|
||||||
import { LabHead } from '~/components/lab/LabHead';
|
import { LabHead } from '~/components/lab/LabHead';
|
||||||
import { Filler } from '~/components/containers/Filler';
|
import { Filler } from '~/components/containers/Filler';
|
||||||
|
import { LabStats } from '~/containers/lab/LabStats';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import { selectLabList, selectLabListNodes, selectLabStatsLoading } from '~/redux/lab/selectors';
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {}
|
||||||
|
|
||||||
const LabLayout: FC<IProps> = () => {
|
const LabLayout: FC<IProps> = () => {
|
||||||
|
const { is_loading } = useShallowSelect(selectLabList);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(labGetList());
|
dispatch(labGetList());
|
||||||
|
dispatch(labGetStats());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -28,79 +33,13 @@ const LabLayout: FC<IProps> = () => {
|
||||||
<Container>
|
<Container>
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<Group className={styles.content}>
|
<Group className={styles.content}>
|
||||||
<LabHead />
|
<LabHead isLoading={is_loading} />
|
||||||
<LabGrid />
|
<LabGrid />
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<div className={styles.panel}>
|
<div className={styles.panel}>
|
||||||
<Sticky>
|
<Sticky>
|
||||||
<Group>
|
<LabStats />
|
||||||
<LabBanner />
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<Group>
|
|
||||||
<Placeholder height={36} width="100%" />
|
|
||||||
<Group horizontal>
|
|
||||||
<Filler />
|
|
||||||
<Placeholder height={32} width="120px" />
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<div />
|
|
||||||
<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 />
|
|
||||||
<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 />
|
|
||||||
<div />
|
|
||||||
|
|
||||||
<Group>
|
|
||||||
<Placeholder width="100%" height={100} />
|
|
||||||
<Placeholder width="120px" height={16} />
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<div />
|
|
||||||
|
|
||||||
<Group>
|
|
||||||
<Placeholder width="100%" height={100} />
|
|
||||||
<Placeholder width="120px" height={16} />
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
</Group>
|
|
||||||
</Sticky>
|
</Sticky>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,6 +4,12 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 3fr 1fr;
|
grid-template-columns: 3fr 1fr;
|
||||||
column-gap: $gap;
|
column-gap: $gap;
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
padding: 0 $gap / 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
|
|
60
src/containers/lab/LabStats/index.tsx
Normal file
60
src/containers/lab/LabStats/index.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { LabBanner } from '~/components/lab/LabBanner';
|
||||||
|
import { Card } from '~/components/containers/Card';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
|
import { Filler } from '~/components/containers/Filler';
|
||||||
|
import { LabHero } from '~/components/lab/LabHero';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import {
|
||||||
|
selectLabStatsHeroes,
|
||||||
|
selectLabStatsLoading,
|
||||||
|
selectLabStatsTags,
|
||||||
|
} from '~/redux/lab/selectors';
|
||||||
|
import { LabTags } from '~/components/lab/LabTags';
|
||||||
|
import { LabHeroes } from '~/components/lab/LabHeroes';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const LabStats: FC<IProps> = () => {
|
||||||
|
const tags = useShallowSelect(selectLabStatsTags);
|
||||||
|
const heroes = useShallowSelect(selectLabStatsHeroes);
|
||||||
|
const isLoading = useShallowSelect(selectLabStatsLoading);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
<LabBanner />
|
||||||
|
|
||||||
|
<div className={styles.card}>
|
||||||
|
<Group>
|
||||||
|
{isLoading ? (
|
||||||
|
<Placeholder height={14} width="100px" />
|
||||||
|
) : (
|
||||||
|
tags.length && <div className={styles.title}>Тэги</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.tags}>
|
||||||
|
<LabTags tags={tags} isLoading={isLoading} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<Placeholder height={14} width="100px" />
|
||||||
|
) : (
|
||||||
|
heroes.length > 0 && <div className={styles.title}>Важные</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.heroes}>
|
||||||
|
<LabHeroes nodes={heroes} isLoading={isLoading} />
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabStats };
|
25
src/containers/lab/LabStats/styles.module.scss
Normal file
25
src/containers/lab/LabStats/styles.module.scss
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font: $font_14_semibold;
|
||||||
|
color: darken(white, 50%);
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0 $gap / 2;
|
||||||
|
padding-bottom: $gap / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags.tags {
|
||||||
|
margin: 0 -$gap / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroes {
|
||||||
|
margin-top: -$gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
@include lab_shadow;
|
||||||
|
|
||||||
|
border-radius: $radius;
|
||||||
|
background-color: $comment_bg;
|
||||||
|
padding: $gap;
|
||||||
|
}
|
|
@ -10,3 +10,12 @@ export const labSetList = (list: Partial<ILabState['list']>) => ({
|
||||||
type: LAB_ACTIONS.SET_LIST,
|
type: LAB_ACTIONS.SET_LIST,
|
||||||
list,
|
list,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const labGetStats = () => ({
|
||||||
|
type: LAB_ACTIONS.GET_STATS,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const labSetStats = (stats: Partial<ILabState['stats']>) => ({
|
||||||
|
type: LAB_ACTIONS.SET_STATS,
|
||||||
|
stats,
|
||||||
|
});
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { api, cleanResult } from '~/utils/api';
|
import { api, cleanResult } from '~/utils/api';
|
||||||
import { API } from '~/constants/api';
|
import { API } from '~/constants/api';
|
||||||
import { GetLabNodesRequest, GetLabNodesResult } from '~/redux/lab/types';
|
import { GetLabNodesRequest, GetLabNodesResult, GetLabStatsResult } from '~/redux/lab/types';
|
||||||
|
|
||||||
export const getLabNodes = ({ after }: GetLabNodesRequest) =>
|
export const getLabNodes = ({ after }: GetLabNodesRequest) =>
|
||||||
api
|
api
|
||||||
.get<GetLabNodesResult>(API.LAB.NODES, { params: { after } })
|
.get<GetLabNodesResult>(API.LAB.NODES, { params: { after } })
|
||||||
.then(cleanResult);
|
.then(cleanResult);
|
||||||
|
|
||||||
|
export const getLabStats = () => api.get<GetLabStatsResult>(API.LAB.STATS).then(cleanResult);
|
||||||
|
|
|
@ -3,4 +3,7 @@ const prefix = 'LAB.';
|
||||||
export const LAB_ACTIONS = {
|
export const LAB_ACTIONS = {
|
||||||
GET_LIST: `${prefix}GET_LIST`,
|
GET_LIST: `${prefix}GET_LIST`,
|
||||||
SET_LIST: `${prefix}SET_LIST`,
|
SET_LIST: `${prefix}SET_LIST`,
|
||||||
|
|
||||||
|
GET_STATS: `${prefix}GET_STATS`,
|
||||||
|
SET_STATS: `${prefix}SET_STATS`,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
||||||
import { labSetList } from '~/redux/lab/actions';
|
import { labSetList, labSetStats } from '~/redux/lab/actions';
|
||||||
import { ILabState } from '~/redux/lab/types';
|
import { ILabState } from '~/redux/lab/types';
|
||||||
|
|
||||||
type LabHandler<T extends (...args: any) => any> = (
|
type LabHandler<T extends (...args: any) => any> = (
|
||||||
|
@ -15,6 +15,15 @@ const setList: LabHandler<typeof labSetList> = (state, { list }) => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setStats: LabHandler<typeof labSetStats> = (state, { stats }) => ({
|
||||||
|
...state,
|
||||||
|
stats: {
|
||||||
|
...state.stats,
|
||||||
|
...stats,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const LAB_HANDLERS = {
|
export const LAB_HANDLERS = {
|
||||||
[LAB_ACTIONS.SET_LIST]: setList,
|
[LAB_ACTIONS.SET_LIST]: setList,
|
||||||
|
[LAB_ACTIONS.SET_STATS]: setStats,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { createReducer } from '~/utils/reducer';
|
import { createReducer } from '~/utils/reducer';
|
||||||
import { LAB_HANDLERS } from '~/redux/lab/handlers';
|
import { LAB_HANDLERS } from '~/redux/lab/handlers';
|
||||||
import { ILabState } from '~/redux/lab/types';
|
import { ILabState } from '~/redux/lab/types';
|
||||||
|
import { INode, ITag } from '~/redux/types';
|
||||||
|
|
||||||
const INITIAL_STATE: ILabState = {
|
const INITIAL_STATE: ILabState = {
|
||||||
list: {
|
list: {
|
||||||
|
@ -9,6 +10,12 @@ const INITIAL_STATE: ILabState = {
|
||||||
count: 0,
|
count: 0,
|
||||||
error: '',
|
error: '',
|
||||||
},
|
},
|
||||||
|
stats: {
|
||||||
|
is_loading: false,
|
||||||
|
heroes: [],
|
||||||
|
tags: [],
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createReducer(INITIAL_STATE, LAB_HANDLERS);
|
export default createReducer(INITIAL_STATE, LAB_HANDLERS);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { takeLeading, call, put } from 'redux-saga/effects';
|
import { takeLeading, call, put } from 'redux-saga/effects';
|
||||||
import { labGetList, labSetList } from '~/redux/lab/actions';
|
import { labGetList, labSetList, labSetStats } from '~/redux/lab/actions';
|
||||||
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
||||||
import { Unwrap } from '~/redux/types';
|
import { Unwrap } from '~/redux/types';
|
||||||
import { getLabNodes } from '~/redux/lab/api';
|
import { getLabNodes, getLabStats } from '~/redux/lab/api';
|
||||||
|
|
||||||
function* getList({ after = '' }: ReturnType<typeof labGetList>) {
|
function* getList({ after = '' }: ReturnType<typeof labGetList>) {
|
||||||
try {
|
try {
|
||||||
|
@ -16,6 +16,19 @@ function* getList({ after = '' }: ReturnType<typeof labGetList>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function* getStats() {
|
||||||
|
try {
|
||||||
|
yield put(labSetStats({ is_loading: true }));
|
||||||
|
const { heroes, tags }: Unwrap<typeof getLabStats> = yield call(getLabStats);
|
||||||
|
yield put(labSetStats({ heroes, tags }));
|
||||||
|
} catch (error) {
|
||||||
|
yield put(labSetStats({ error: error.message }));
|
||||||
|
} finally {
|
||||||
|
yield put(labSetStats({ is_loading: false }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function* labSaga() {
|
export default function* labSaga() {
|
||||||
yield takeLeading(LAB_ACTIONS.GET_LIST, getList);
|
yield takeLeading(LAB_ACTIONS.GET_LIST, getList);
|
||||||
|
yield takeLeading(LAB_ACTIONS.GET_STATS, getStats);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,3 +2,7 @@ import { IState } from '~/redux/store';
|
||||||
|
|
||||||
export const selectLab = (state: IState) => state.lab;
|
export const selectLab = (state: IState) => state.lab;
|
||||||
export const selectLabListNodes = (state: IState) => state.lab.list.nodes;
|
export const selectLabListNodes = (state: IState) => state.lab.list.nodes;
|
||||||
|
export const selectLabList = (state: IState) => state.lab.list;
|
||||||
|
export const selectLabStatsHeroes = (state: IState) => state.lab.stats.heroes;
|
||||||
|
export const selectLabStatsTags = (state: IState) => state.lab.stats.tags;
|
||||||
|
export const selectLabStatsLoading = (state: IState) => state.lab.stats.is_loading;
|
||||||
|
|
|
@ -1,19 +1,36 @@
|
||||||
import { IError, INode } from '~/redux/types';
|
import { IError, INode, ITag } from '~/redux/types';
|
||||||
|
|
||||||
export type ILabState = Readonly<{
|
export type ILabState = Readonly<{
|
||||||
list: {
|
list: {
|
||||||
is_loading: boolean;
|
is_loading: boolean;
|
||||||
nodes: INode[];
|
nodes: ILabNode[];
|
||||||
count: number;
|
count: number;
|
||||||
error: IError;
|
error: IError;
|
||||||
};
|
};
|
||||||
|
stats: {
|
||||||
|
is_loading: boolean;
|
||||||
|
heroes: Partial<INode>[];
|
||||||
|
tags: ITag[];
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type GetLabNodesRequest = {
|
export type GetLabNodesRequest = {
|
||||||
after?: string;
|
after?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ILabNode {
|
||||||
|
node: INode;
|
||||||
|
last_seen: string | null;
|
||||||
|
comment_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type GetLabNodesResult = {
|
export type GetLabNodesResult = {
|
||||||
nodes: INode[];
|
nodes: ILabNode[];
|
||||||
count: number;
|
count: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetLabStatsResult = {
|
||||||
|
heroes: INode[];
|
||||||
|
tags: ITag[];
|
||||||
|
};
|
||||||
|
|
|
@ -15,6 +15,11 @@ 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 { EditorPublicSwitch } from '~/components/editors/EditorPublicSwitch';
|
||||||
import { NodeImageSwiperBlock } from '~/components/node/NodeImageSwiperBlock';
|
import { NodeImageSwiperBlock } from '~/components/node/NodeImageSwiperBlock';
|
||||||
|
import { LabNodeTitle } from '~/components/lab/LabNodeTitle';
|
||||||
|
import { LabText } from '~/components/lab/LabText';
|
||||||
|
import { LabImage } from '~/components/lab/LabImage';
|
||||||
|
import { LabBottomPanel } from '~/components/lab/LabBottomPanel';
|
||||||
|
import { LabPad } from '~/components/lab/LabPad';
|
||||||
|
|
||||||
const prefix = 'NODE.';
|
const prefix = 'NODE.';
|
||||||
export const NODE_ACTIONS = {
|
export const NODE_ACTIONS = {
|
||||||
|
@ -83,6 +88,13 @@ export type INodeComponentProps = {
|
||||||
|
|
||||||
export type INodeComponents = Record<ValueOf<typeof NODE_TYPES>, FC<INodeComponentProps>>;
|
export type INodeComponents = Record<ValueOf<typeof NODE_TYPES>, FC<INodeComponentProps>>;
|
||||||
|
|
||||||
|
export const LAB_PREVIEW_LAYOUT: Record<string, FC<INodeComponentProps>[]> = {
|
||||||
|
[NODE_TYPES.IMAGE]: [LabImage, LabPad, LabNodeTitle],
|
||||||
|
[NODE_TYPES.VIDEO]: [NodeVideoBlock, LabPad, LabNodeTitle],
|
||||||
|
[NODE_TYPES.AUDIO]: [LabPad, LabNodeTitle, LabPad, NodeAudioImageBlock, NodeAudioBlock, LabPad],
|
||||||
|
[NODE_TYPES.TEXT]: [LabPad, LabNodeTitle, LabPad, LabText, LabPad],
|
||||||
|
};
|
||||||
|
|
||||||
export const NODE_HEADS: INodeComponents = {
|
export const NODE_HEADS: INodeComponents = {
|
||||||
[NODE_TYPES.IMAGE]: NodeImageSwiperBlock,
|
[NODE_TYPES.IMAGE]: NodeImageSwiperBlock,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
const Sprites: FC<{}> = () => (
|
const Sprites: FC = () => (
|
||||||
<svg width={0} height={0} viewBox="0 0 24 24">
|
<svg width={0} height={0} viewBox="0 0 24 24">
|
||||||
<defs>
|
<defs>
|
||||||
<pattern
|
<pattern
|
||||||
|
@ -270,6 +270,16 @@ const Sprites: FC<{}> = () => (
|
||||||
<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" />
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
|
<g id="comment">
|
||||||
|
<path fill="none" d="M0 0h24v24H0V0z" stroke="none" />
|
||||||
|
<path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="comment_new">
|
||||||
|
<path fill="none" d="M0 0h24v24H0V0z" stroke="none" />
|
||||||
|
<path d="M22,6.98V16c0,1.1-0.9,2-2,2H6l-4,4V4c0-1.1,0.9-2,2-2h10.1C14.04,2.32,14,2.66,14,3c0,2.76,2.24,5,5,5 C20.13,8,21.16,7.61,22,6.98z M16,3c0,1.66,1.34,3,3,3s3-1.34,3-3s-1.34-3-3-3S16,1.34,16,3z" />
|
||||||
|
</g>
|
||||||
|
|
||||||
<g id="youtube" stroke="none">
|
<g id="youtube" stroke="none">
|
||||||
<path fill="none" d="M0 0h24v24H0V0z" />
|
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||||
<g transform="scale(0.1) translate(-30 -30)">
|
<g transform="scale(0.1) translate(-30 -30)">
|
||||||
|
@ -283,6 +293,22 @@ const Sprites: FC<{}> = () => (
|
||||||
transform="scale(0.011) translate(120, 120)"
|
transform="scale(0.011) translate(120, 120)"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
|
<g id="recent">
|
||||||
|
<path fill="none" d="M0 0h24v24H0V0z" stroke="none" />
|
||||||
|
<path
|
||||||
|
stroke="none"
|
||||||
|
d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="hot">
|
||||||
|
<path fill="none" d="M0 0h24v24H0V0z" stroke="none" />
|
||||||
|
<path
|
||||||
|
stroke="none"
|
||||||
|
d="M13.5.67s.74 2.65.74 4.8c0 2.06-1.35 3.73-3.41 3.73-2.07 0-3.63-1.67-3.63-3.73l.03-.36C5.21 7.51 4 10.62 4 14c0 4.42 3.58 8 8 8s8-3.58 8-8C20 8.61 17.41 3.8 13.5.67zM11.71 19c-1.78 0-3.22-1.4-3.22-3.14 0-1.62 1.05-2.76 2.81-3.12 1.77-.36 3.6-1.21 4.62-2.58.39 1.29.59 2.65.59 4.04 0 2.65-2.15 4.8-4.8 4.8z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// $red: #ff3344;
|
// $red: #ff3344;
|
||||||
$red: #ff3344;
|
$red: #ff3344;
|
||||||
$yellow: #ffd60f;
|
$yellow: #ffd60f;
|
||||||
$dark_blue: #3c75ff;
|
$dark_blue: #592071;
|
||||||
$blue: #582cd0;
|
$blue: #582cd0;
|
||||||
$green: #00d2b9;
|
$green: #00d2b9;
|
||||||
//$green: #00503c;
|
//$green: #00503c;
|
||||||
|
@ -16,7 +16,7 @@ $primary: $red;
|
||||||
$secondary: $wisegreen;
|
$secondary: $wisegreen;
|
||||||
|
|
||||||
$red_gradient: linear-gradient(165deg, $orange -50%, $red 150%);
|
$red_gradient: linear-gradient(165deg, $orange -50%, $red 150%);
|
||||||
$blue_gradient: linear-gradient(170deg, $green, $dark_blue);
|
$blue_gradient: linear-gradient(170deg, $blue, $dark_blue);
|
||||||
$green_gradient: linear-gradient(
|
$green_gradient: linear-gradient(
|
||||||
170deg,
|
170deg,
|
||||||
lighten(adjust_hue($wisegreen, 15deg), 10%) 0%,
|
lighten(adjust_hue($wisegreen, 15deg), 10%) 0%,
|
||||||
|
@ -36,6 +36,7 @@ $main_text_color: white;
|
||||||
|
|
||||||
$content_bg: darken($main_bg_color, 0%);
|
$content_bg: darken($main_bg_color, 0%);
|
||||||
$content_bg_secondary: darken($content_bg, 2%);
|
$content_bg_secondary: darken($content_bg, 2%);
|
||||||
|
$lab_post_bg: lighten($content_bg, 4%);
|
||||||
|
|
||||||
$cell_bg: lighten($main_bg_color, 0%);
|
$cell_bg: lighten($main_bg_color, 0%);
|
||||||
$card_bg: lighten($main_bg_color, 0%);
|
$card_bg: lighten($main_bg_color, 0%);
|
||||||
|
|
|
@ -219,3 +219,9 @@ $sidebar_border: transparentize(white, 0.95);
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin lab_shadow {
|
||||||
|
box-shadow: transparentize(black, 0.5) 0 0 0 1px,
|
||||||
|
inset transparentize(white, 0.9) 0 1px,
|
||||||
|
lighten(black, 10%) 0 4px;
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { createElement, FC, useCallback, useMemo } from 'react';
|
||||||
import { isNil, prop } from 'ramda';
|
import { isNil, prop } from 'ramda';
|
||||||
import {
|
import {
|
||||||
INodeComponentProps,
|
INodeComponentProps,
|
||||||
|
LAB_PREVIEW_LAYOUT,
|
||||||
NODE_COMPONENTS,
|
NODE_COMPONENTS,
|
||||||
NODE_HEADS,
|
NODE_HEADS,
|
||||||
NODE_INLINES,
|
NODE_INLINES,
|
||||||
|
@ -11,11 +12,12 @@ import {
|
||||||
// useNodeBlocks returns head, block and inline blocks of node
|
// useNodeBlocks returns head, block and inline blocks of node
|
||||||
export const useNodeBlocks = (node: INode, isLoading: boolean) => {
|
export const useNodeBlocks = (node: INode, isLoading: boolean) => {
|
||||||
const createNodeBlock = useCallback(
|
const createNodeBlock = useCallback(
|
||||||
(block?: FC<INodeComponentProps>) =>
|
(block?: FC<INodeComponentProps>, key = 0) =>
|
||||||
!isNil(block) &&
|
!isNil(block) &&
|
||||||
createElement(block, {
|
createElement(block, {
|
||||||
node,
|
node,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
key,
|
||||||
}),
|
}),
|
||||||
[node, isLoading]
|
[node, isLoading]
|
||||||
);
|
);
|
||||||
|
@ -35,5 +37,13 @@ export const useNodeBlocks = (node: INode, isLoading: boolean) => {
|
||||||
[node, createNodeBlock]
|
[node, createNodeBlock]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { head, block, inline };
|
const lab = useMemo(
|
||||||
|
() =>
|
||||||
|
node?.type && prop(node.type, LAB_PREVIEW_LAYOUT)
|
||||||
|
? prop(node.type, LAB_PREVIEW_LAYOUT).map((comp, i) => createNodeBlock(comp, i))
|
||||||
|
: undefined,
|
||||||
|
[node, createNodeBlock]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { head, block, inline, lab };
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const useNodeCoverImage = (node: INode) => {
|
||||||
dispatch(nodeSetCoverImage(node.cover));
|
dispatch(nodeSetCoverImage(node.cover));
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
nodeSetCoverImage(undefined);
|
dispatch(nodeSetCoverImage(undefined));
|
||||||
};
|
};
|
||||||
}, [dispatch, node.cover, node.id]);
|
}, [dispatch, node.cover, node.id]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,6 @@ export const canLikeNode = (node: Partial<INode>, user: Partial<IUser>): boolean
|
||||||
path(['role'], user) && path(['role'], user) !== USER_ROLES.GUEST;
|
path(['role'], user) && path(['role'], user) !== USER_ROLES.GUEST;
|
||||||
|
|
||||||
export const canStarNode = (node: Partial<INode>, user: Partial<IUser>): boolean =>
|
export const canStarNode = (node: Partial<INode>, user: Partial<IUser>): boolean =>
|
||||||
node.type === NODE_TYPES.IMAGE &&
|
(node.type === NODE_TYPES.IMAGE || node.is_promoted === false) &&
|
||||||
path(['role'], user) &&
|
path(['role'], user) &&
|
||||||
path(['role'], user) === USER_ROLES.ADMIN;
|
path(['role'], user) === USER_ROLES.ADMIN;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue