1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 12:56:41 +07:00
This commit is contained in:
Fedor Katurov 2021-03-22 15:56:35 +07:00
parent 9745b895f1
commit 11fd582453
23 changed files with 328 additions and 101 deletions

View file

@ -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" /> Всё, что происходит здесь &mdash; всего лишь эксперимент, о котором не узнает никто за
пределами Убежища.
</strong>
</p>
<p>
Ловим радиоактивных жуков, приручаем утконосов-вампиров, катаемся на младшем научном
сотруднике Егоре Порсифоровиче (у него как раз сейчас линька).
</p>
</Group>
</Group> </Group>
</Card> </Card>
); );

View file

@ -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;
}
} }

View file

@ -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 };

View file

@ -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;
} }

View 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 };

View file

@ -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;

View 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 };

View file

@ -0,0 +1,10 @@
@import "~/styles/variables.scss";
.tags {
display: flex;
flex-wrap: wrap;
& > * {
margin: $gap / 2;
}
}

View file

@ -52,5 +52,6 @@ export const API = {
}, },
LAB: { LAB: {
NODES: `/lab/`, NODES: `/lab/`,
STATS: '/lab/stats',
}, },
}; };

View file

@ -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>

View 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 };

View 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;
}

View file

@ -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,
});

View file

@ -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);

View file

@ -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`,
}; };

View file

@ -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,
}; };

View file

@ -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);

View file

@ -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);
} }

View file

@ -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;

View file

@ -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[];
};

View file

@ -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%,

View file

@ -27,6 +27,10 @@ body {
background-size: 600px 600px; background-size: 600px 600px;
pointer-events: none; pointer-events: none;
} }
* {
min-width: 0;
}
} }
#app { #app {

View file

@ -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;