1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-24 20:36:40 +07:00

refactored lab

This commit is contained in:
Fedor Katurov 2023-11-21 19:26:55 +06:00
parent d0e99adc9f
commit b551fc44ea
41 changed files with 80 additions and 79 deletions

View file

@ -1,13 +0,0 @@
import { FC } from 'react';
import { NodeAudioBlock } from '~/components/node/NodeAudioBlock';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { NodeComponentProps } from '~/constants/node';
const LabAudio: FC<NodeComponentProps> = ({ node, isLoading }) => (
<Placeholder active={isLoading} width="100%" height={100}>
<NodeAudioBlock node={node} isLoading={isLoading} />
</Placeholder>
);
export { LabAudio };

View file

@ -1,27 +0,0 @@
import { FC } from 'react';
import { Group } from '~/components/common/Group';
import { LabSquare } from '~/components/lab/LabSquare';
import styles from './styles.module.scss';
interface IProps {}
const LabBanner: FC<IProps> = () => (
<LabSquare className={styles.wrap}>
<Group>
<div className={styles.title}>Лаборатория!</div>
<Group className={styles.content}>
<p>
<strong>
Всё, что происходит здесь &mdash; всего лишь эксперимент, о котором
не узнает никто за пределами Убежища.
</strong>
</p>
</Group>
</Group>
</LabSquare>
);
export { LabBanner };

View file

@ -1,23 +0,0 @@
@import "src/styles/variables.scss";
.wrap {
@include outer_shadow;
background: url('/images/boris_lab.svg') 50% 50% no-repeat;
background-size: cover;
display: flex;
align-items: flex-start;
justify-content: stretch;
border-radius: $radius;
padding: $gap;
}
.title {
font: $font_24_bold;
text-transform: uppercase;
}
.content {
font: $font_14_regular;
line-height: 19px;
}

View file

@ -1,72 +0,0 @@
import { FC, useCallback } from 'react';
import classNames from 'classnames';
import { Filler } from '~/components/common/Filler';
import { Grid } from '~/components/common/Grid';
import { Group } from '~/components/common/Group';
import { Icon } from '~/components/common/Icon';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { URLS } from '~/constants/urls';
import { useNavigation } from '~/hooks/navigation/useNavigation';
import { INode } from '~/types';
import { getPrettyDate } from '~/utils/dom';
import styles from './styles.module.scss';
type Props = {
node: INode;
isLoading?: boolean;
hasNewComments: boolean;
commentCount: number;
};
const LabBottomPanel: FC<Props> = ({
node,
hasNewComments,
commentCount,
isLoading,
}) => {
const { push } = useNavigation();
const onClick = useCallback(
() => push(URLS.NODE_URL(node.id)),
[push, node.id],
);
return (
<Group horizontal className={styles.wrap} onClick={onClick}>
<div className={styles.timestamp}>
<Placeholder active={isLoading}>
{getPrettyDate(node.created_at)}
</Placeholder>
</div>
<Filler />
<Placeholder active={isLoading} width="48px" height={24}>
{commentCount > 0 && (
<Grid
horizontal
className={classNames(styles.comments, {
[styles.active]: hasNewComments,
})}
>
<Icon icon={hasNewComments ? 'comment_new' : 'comment'} size={24} />
<span>{commentCount}</span>
</Grid>
)}
</Placeholder>
<Placeholder active={isLoading} width="48px" height={24}>
{!!node.like_count && node.like_count > 0 && (
<Grid horizontal className={classNames(styles.like)}>
<Icon icon={node.is_liked ? 'heart_full' : 'heart'} size={24} />
<span>{node.like_count}</span>
</Grid>
)}
</Placeholder>
</Group>
);
};
export { LabBottomPanel };

View file

@ -1,31 +0,0 @@
@import 'src/styles/variables.scss';
.wrap {
padding: $gap $lab_gap $lab_gap;
@include tablet {
padding: $gap $lab_gap_mobile $lab_gap_mobile;
}
}
.timestamp {
font: $font_12_regular;
color: $gray_50;
}
.comments,
.like {
flex: 0;
font: $font_16_semibold;
color: $gray_50;
fill: currentColor;
stroke: none;
column-gap: $gap !important;
align-items: center;
justify-content: center;
padding-left: $gap;
&.active {
color: $color_danger;
}
}

View file

@ -1,29 +0,0 @@
import { FC } from 'react';
import { Markdown } from '~/components/common/Markdown';
import { Paragraph } from '~/components/placeholders/Paragraph';
import { NodeComponentProps } from '~/constants/node';
import { useGotoNode } from '~/hooks/node/useGotoNode';
import { formatText } from '~/utils/dom';
import styles from './styles.module.scss';
const LabDescription: FC<NodeComponentProps> = ({ node, isLoading }) => {
const onClick = useGotoNode(node.id);
if (!node.description) {
return null;
}
return isLoading ? (
<div className={styles.wrap}>
<Paragraph />
</div>
) : (
<Markdown className={styles.wrap} onClick={onClick}>
{formatText(node.description)}
</Markdown>
);
};
export { LabDescription };

View file

@ -1,14 +0,0 @@
@import "src/styles/variables.scss";
.wrap {
padding: $lab_gap * 0.5 $lab_gap 0;
line-height: 1.3em;
@include tablet {
@include clamp(10, 1.3*16px);
line-height: 1.3em;
font: $font_16_regular;
padding: 0 $lab_gap_mobile;
margin: $lab_gap_mobile * 0.5 0;
}
}

View file

@ -1,29 +0,0 @@
import { useRef } from 'react';
import classNames from 'classnames';
import { Group } from '~/components/common/Group';
import styles from './styles.module.scss';
const LabFactoryBanner = () => {
const masked = useRef(Math.random() <= 0.5).current;
return (
<div className={classNames(styles.banner, { [styles.masked]: masked })}>
<Group>
<div className={styles.title}>Лаборатория!</div>
<Group className={styles.content}>
<p>
<strong>
Всё, что происходит здесь &mdash; всего лишь эксперимент, о
котором не узнает никто за пределами Убежища.
</strong>
</p>
</Group>
</Group>
</div>
);
};
export { LabFactoryBanner };

View file

@ -1,26 +0,0 @@
@import 'src/styles/variables';
.banner {
@include outer_shadow;
display: flex;
aspect-ratio: 0.7;
background: url('/images/peoples_lab.svg') 50% 100% / cover;
padding: $gap;
border-radius: $radius;
&.masked {
background-image: url('/images/peoples_lab_masked.svg');
}
}
.title {
font: $font_24_bold;
text-transform: uppercase;
}
.content {
font: $font_14_medium;
line-height: 19px;
opacity: 0.7;
}

View file

@ -1,61 +0,0 @@
import { FC } from 'react';
import { Filler } from '~/components/common/Filler';
import { SearchInput } from '~/components/input/SearchInput';
import { HorizontalMenu } from '~/components/menu/HorizontalMenu';
import { LabNodesSort } from '~/types/lab';
import { useLabContext } from '~/utils/context/LabContextProvider';
import styles from './styles.module.scss';
interface IProps {
isLoading?: boolean;
}
const LabHead: FC<IProps> = ({ isLoading }) => {
const { sort, setSort, search, setSearch } = useLabContext();
return (
<div className={styles.wrap}>
<HorizontalMenu>
<HorizontalMenu.Item
color="green"
icon="recent"
active={sort === LabNodesSort.New}
isLoading={isLoading}
onClick={() => setSort(LabNodesSort.New)}
>
Свежие
</HorizontalMenu.Item>
<HorizontalMenu.Item
color="orange"
icon="hot"
active={sort === LabNodesSort.Hot}
isLoading={isLoading}
onClick={() => setSort(LabNodesSort.Hot)}
>
Популярные
</HorizontalMenu.Item>
<HorizontalMenu.Item
color="yellow"
icon="star_full"
isLoading={isLoading}
active={sort === LabNodesSort.Heroic}
onClick={() => setSort(LabNodesSort.Heroic)}
>
Важные
</HorizontalMenu.Item>
</HorizontalMenu>
<Filler />
<div className={styles.search}>
<SearchInput value={search} handler={setSearch} placeholder="Поиск" />
</div>
</div>
);
};
export { LabHead };

View file

@ -1,17 +0,0 @@
@import "src/styles/variables.scss";
.wrap {
border-radius: $radius;
display: flex;
flex-direction: row;
@include tablet {
flex-direction: column;
}
}
.search {
@include tablet {
margin-top: $gap;
}
}

View file

@ -1,55 +0,0 @@
import { FC, useCallback } from 'react';
import { Group } from '~/components/common/Group';
import { Icon } from '~/components/common/Icon';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { URLS } from '~/constants/urls';
import { useNavigation } from '~/hooks/navigation/useNavigation';
import { INode } from '~/types';
import { getPrettyDate } from '~/utils/dom';
import styles from './styles.module.scss';
interface IProps {
node?: Partial<INode>;
isLoading?: boolean;
}
const LabHero: FC<IProps> = ({ node, isLoading }) => {
const { push } = useNavigation();
const onClick = useCallback(() => {
push(URLS.NODE_URL(node?.id));
}, [push, node]);
if (!node || isLoading) {
return (
<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>
);
};
export { LabHero };

View file

@ -1,33 +0,0 @@
@import 'src/styles/variables.scss';
.wrap {
min-width: 0;
text-decoration: none;
cursor: pointer;
}
.star {
fill: $gray_75;
flex: 0 0 32px;
}
.title {
font: $font_18_semibold;
text-overflow: ellipsis;
line-height: 22px;
word-break: break-all;
color: $gray_50;
@include clamp(2, 22px);
}
.description {
font: $font_10_regular;
color: $gray_50;
padding-top: 4px;
}
.content {
padding: $gap * 0.5 0;
text-decoration: none;
}

View file

@ -1,35 +0,0 @@
import { FC } from 'react';
import { Group } from '~/components/common/Group';
import { LabHero } from '~/components/lab/LabHero';
import styles from '~/containers/lab/LabStats/styles.module.scss';
import { INode } from '~/types';
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 };

View file

@ -1,44 +0,0 @@
import { FC } from 'react';
import Image from 'next/future/image';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { NodeComponentProps } from '~/constants/node';
import { imagePresets } from '~/constants/urls';
import { useGotoNode } from '~/hooks/node/useGotoNode';
import { useNodeImages } from '~/hooks/node/useNodeImages';
import { normalizeBrightColor } from '~/utils/color';
import { getURL } from '~/utils/dom';
import styles from './styles.module.scss';
interface IProps extends NodeComponentProps {}
const LabImage: FC<IProps> = ({ node, isLoading }) => {
const images = useNodeImages(node);
const onClick = useGotoNode(node.id);
if (!images?.length) {
return null;
}
const file = images[0];
return (
<Placeholder active={isLoading} width="100%" height={400}>
<div className={styles.wrapper}>
<Image
src={getURL(file, imagePresets[600])}
width={file.metadata?.width}
height={file.metadata?.height}
onClick={onClick}
alt=""
className={styles.image}
color={normalizeBrightColor(file?.metadata?.dominant_color)}
/>
</div>
</Placeholder>
);
};
export { LabImage };

View file

@ -1,61 +0,0 @@
@import 'src/styles/variables.scss';
.wrapper {
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
border-radius: $radius $radius 0 0;
overflow: hidden;
: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;
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%;
transition: box-shadow 1s;
max-inline-size: 100%;
block-size: auto;
@include tablet {
padding-bottom: 0;
max-height: 100vh;
}
}

View file

@ -1,16 +0,0 @@
import { FC } from 'react';
import { NodeComponentProps } from '~/constants/node';
import { useColorGradientFromString } from '~/hooks/color/useColorGradientFromString';
import styles from './styles.module.scss';
interface Props extends NodeComponentProps {}
const LabLine: FC<Props> = ({ node: { title } }) => {
const background = useColorGradientFromString(title, 5, 3, 270);
return <div className={styles.line} style={{ background }} />;
};
export { LabLine };

View file

@ -1,7 +0,0 @@
@import "src/styles/variables";
.line {
height: 4px;
border-radius: $radius $radius 0 0;
width: 100%;
}

View file

@ -1,19 +0,0 @@
import { VFC } from 'react';
import { Card } from '~/components/common/Card';
import { Button } from '~/components/input/Button';
import styles from './styles.module.scss';
interface LabNoResultsProps {
resetSearch: () => void;
}
const LabNoResults: VFC<LabNoResultsProps> = ({ resetSearch }) => (
<Card className={styles.wrap}>
<div className={styles.title}> Здесь ничего нет</div>
<Button onClick={resetSearch}>Сбросить поиск</Button>
</Card>
);
export { LabNoResults };

View file

@ -1,16 +0,0 @@
@import "src/styles/variables";
.wrap {
padding: $gap * 2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 33vh;
}
.title {
text-transform: uppercase;
font: $font_20_semibold;
margin-bottom: $gap * 2;
}

View file

@ -1,46 +0,0 @@
import { FC, useMemo } from 'react';
import classNames from 'classnames';
import { isAfter, parseISO } from 'date-fns';
import { LabBottomPanel } from '~/components/lab/LabBottomPanel';
import { useColorGradientFromString } from '~/hooks/color/useColorGradientFromString';
import { useNodeBlocks } from '~/hooks/node/useNodeBlocks';
import { INode } from '~/types';
import styles from './styles.module.scss';
interface IProps {
node: INode;
lastSeen: string | null | undefined;
isLoading?: boolean;
commentCount: number;
}
const LabNode: FC<IProps> = ({ node, isLoading, lastSeen, commentCount }) => {
const { lab } = useNodeBlocks(node, !!isLoading);
const hasNewComments = useMemo(
() =>
!!node.commented_at &&
!!lastSeen &&
isAfter(parseISO(node.commented_at), parseISO(lastSeen)),
[node.commented_at, lastSeen],
);
const background = useColorGradientFromString(node.title, 3, 2);
return (
<div className={classNames(styles.wrap)} style={{ background }}>
{lab}
<LabBottomPanel
node={node}
isLoading={isLoading}
hasNewComments={hasNewComments}
commentCount={commentCount}
/>
</div>
);
};
export { LabNode };

View file

@ -1,12 +0,0 @@
@import 'src/styles/variables.scss';
.wrap {
@include outer_shadow;
position: relative;
background-color: $content_bg;
cursor: pointer;
min-width: 0;
border-radius: $radius;
}

View file

@ -1,37 +0,0 @@
import { FC } from 'react';
import Tippy from '@tippyjs/react';
import { Group } from '~/components/common/Group';
import { Icon } from '~/components/common/Icon';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { NodeComponentProps } from '~/constants/node';
import { useGotoNode } from '~/hooks/node/useGotoNode';
import styles from './styles.module.scss';
const LabNodeTitle: FC<NodeComponentProps> = ({ node, isLoading }) => {
const onClick = useGotoNode(node.id);
if (!node.title) return null;
return (
<Group horizontal className={styles.wrap} onClick={onClick}>
<div className={styles.title}>
<Placeholder active={isLoading}>{node.title || '...'}</Placeholder>
</div>
{(node.is_heroic || isLoading) && (
<Placeholder active={isLoading} width="24px" height={24}>
<Tippy content="Важный пост">
<div className={styles.star}>
<Icon icon="star_full" size={24} />
</div>
</Tippy>
</Placeholder>
)}
</Group>
);
};
export { LabNodeTitle };

View file

@ -1,35 +0,0 @@
@import 'src/styles/variables.scss';
.wrap {
padding: $lab_gap - $gap $lab_gap 0;
@include tablet {
padding: $gap $lab_gap_mobile 0;
}
}
.title {
text-transform: uppercase;
font: $font_24_semibold;
overflow: hidden;
flex: 1;
text-overflow: ellipsis;
word-break: break-word;
@include clamp(2, 1.2em);
a {
text-decoration: none;
color: inherit;
}
@include tablet {
padding-bottom: 0;
font: $font_20_semibold;
}
}
.star {
fill: $gray_50;
flex: 0 0 24px;
}

View file

@ -1,13 +0,0 @@
import { FC } from 'react';
import { NodeComponentProps } from '~/constants/node';
import { useGotoNode } from '~/hooks/node/useGotoNode';
import styles from './styles.module.scss';
const LabPad: FC<NodeComponentProps> = ({ node }) => {
const onClick = useGotoNode(node.id);
return <div className={styles.pad} onClick={onClick} />;
};
export { LabPad };

View file

@ -1,5 +0,0 @@
@import "src/styles/variables.scss";
.pad {
height: $gap;
}

View file

@ -1,19 +0,0 @@
import { FC } from 'react';
import classNames from 'classnames';
import { DivProps } from '~/utils/types';
import styles from './styles.module.scss';
interface IProps extends DivProps {}
const LabSquare: FC<IProps> = ({ children, ...rest }) => (
<div className={styles.square}>
<div {...rest} className={classNames(styles.content, rest.className)}>
{children}
</div>
</div>
);
export { LabSquare };

View file

@ -1,12 +0,0 @@
.square {
position: relative;
padding-bottom: 100%;
}
.content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}

View file

@ -1,37 +0,0 @@
import { FC } from 'react';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { Tag } from '~/components/tags/Tag';
import { ITag } from '~/types';
import styles from './/styles.module.scss';
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

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

View file

@ -1,31 +0,0 @@
import { FC, useMemo } from 'react';
import { Markdown } from '~/components/common/Markdown';
import { Paragraph } from '~/components/placeholders/Paragraph';
import { NodeComponentProps } from '~/constants/node';
import { useGotoNode } from '~/hooks/node/useGotoNode';
import { formatTextParagraphs } from '~/utils/dom';
import { path } from '~/utils/ramda';
import styles from './styles.module.scss';
const LabText: FC<NodeComponentProps> = ({ node, isLoading }) => {
const content = useMemo(
() => formatTextParagraphs(path(['blocks', 0, 'text'], node) || ''),
[node],
);
const onClick = useGotoNode(node.id);
return isLoading ? (
<div className={styles.wrap}>
<Paragraph lines={5} />
</div>
) : (
<Markdown className={styles.wrap} onClick={onClick}>
{content}
</Markdown>
);
};
export { LabText };

View file

@ -1,13 +0,0 @@
@import "src/styles/variables.scss";
.wrap {
padding: 0 $lab_gap;
line-height: 1.3em;
@include tablet {
@include clamp(20, 1.3 * 16px);
padding: 0 $lab_gap_mobile;
font: $font_16_regular;
}
}

View file

@ -1,13 +0,0 @@
import { FC } from 'react';
import { NodeVideoBlock } from '~/components/node/NodeVideoBlock';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { NodeComponentProps } from '~/constants/node';
const LabVideo: FC<NodeComponentProps> = ({ node, isLoading }) => (
<Placeholder active={isLoading} width="100%" height={400}>
<NodeVideoBlock node={node} isLoading={isLoading} />
</Placeholder>
);
export { LabVideo };