mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +07:00
#23 lab stats
This commit is contained in:
parent
9745b895f1
commit
11fd582453
23 changed files with 328 additions and 101 deletions
|
@ -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,19 @@
|
||||||
@import "~/styles/variables.scss";
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
background: $red_gradient_alt;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
const history = useHistory();
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
history.push(URLS.NODE_URL(node?.id));
|
||||||
|
}, [history, node]);
|
||||||
|
|
||||||
|
if (!node || isLoading) {
|
||||||
|
return (
|
||||||
<Group horizontal className={styles.wrap1}>
|
<Group horizontal className={styles.wrap1}>
|
||||||
<div className={styles.star}>
|
<div className={styles.star}>
|
||||||
<Icon icon="star_full" size={32} />
|
<Icon icon="star_full" size={32} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Group>
|
<div className={styles.content}>
|
||||||
<Placeholder height={20} />
|
<Placeholder height={20} />
|
||||||
<Placeholder height={12} width="100px" />
|
<Placeholder height={12} width="100px" />
|
||||||
|
</div>
|
||||||
</Group>
|
</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>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
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, 7).map(node => (
|
||||||
|
<LabHero node={node} key={node?.id} />
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabHeroes };
|
|
@ -1,11 +1,7 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } 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 { LabNodeTitle } from '~/components/lab/LabNodeTitle';
|
|
||||||
import { Grid } from '~/components/containers/Grid';
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
node: INode;
|
node: INode;
|
||||||
|
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,5 +52,6 @@ export const API = {
|
||||||
},
|
},
|
||||||
LAB: {
|
LAB: {
|
||||||
NODES: `/lab/`,
|
NODES: `/lab/`,
|
||||||
|
STATS: '/lab/stats',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,6 +13,7 @@ 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';
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {}
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ const LabLayout: FC<IProps> = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(labGetList());
|
dispatch(labGetList());
|
||||||
|
dispatch(labGetStats());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -34,73 +36,7 @@ const LabLayout: FC<IProps> = () => {
|
||||||
|
|
||||||
<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>
|
||||||
|
|
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 />
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Group>
|
||||||
|
{isLoading ? (
|
||||||
|
<Placeholder height={14} width="100px" />
|
||||||
|
) : (
|
||||||
|
<div className={styles.title}>Тэги</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.tags}>
|
||||||
|
<LabTags tags={tags} isLoading={isLoading} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<Placeholder height={14} width="100px" />
|
||||||
|
) : (
|
||||||
|
<div className={styles.title}>Важные</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.heroes}>
|
||||||
|
<LabHeroes nodes={heroes} isLoading={isLoading} />
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabStats };
|
17
src/containers/lab/LabStats/styles.module.scss
Normal file
17
src/containers/lab/LabStats/styles.module.scss
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
@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;
|
||||||
|
}
|
|
@ -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,6 @@ 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 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,4 +1,4 @@
|
||||||
import { IError, INode } from '~/redux/types';
|
import { IError, INode, ITag } from '~/redux/types';
|
||||||
|
|
||||||
export type ILabState = Readonly<{
|
export type ILabState = Readonly<{
|
||||||
list: {
|
list: {
|
||||||
|
@ -7,6 +7,12 @@ export type ILabState = Readonly<{
|
||||||
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 = {
|
||||||
|
@ -17,3 +23,8 @@ export type GetLabNodesResult = {
|
||||||
nodes: INode[];
|
nodes: INode[];
|
||||||
count: number;
|
count: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetLabStatsResult = {
|
||||||
|
heroes: INode[];
|
||||||
|
tags: ITag[];
|
||||||
|
};
|
||||||
|
|
|
@ -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%,
|
||||||
|
|
|
@ -27,6 +27,10 @@ body {
|
||||||
background-size: 600px 600px;
|
background-size: 600px 600px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
|
|
|
@ -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