mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
refactor boris components
This commit is contained in:
parent
8ec77986bf
commit
c53ac831e7
30 changed files with 42 additions and 114 deletions
|
@ -1,37 +0,0 @@
|
|||
import { FC, ReactNode } from 'react';
|
||||
|
||||
import { WithDescription } from '~/components/common/WithDescription';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
|
||||
interface Props {
|
||||
icon: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
link: string;
|
||||
prefix?: ReactNode;
|
||||
suffix?: ReactNode;
|
||||
}
|
||||
|
||||
const BorisContactItem: FC<Props> = ({
|
||||
icon,
|
||||
title,
|
||||
subtitle,
|
||||
link,
|
||||
prefix,
|
||||
suffix,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
{prefix}
|
||||
<WithDescription
|
||||
icon={<Icon icon={icon} size={32} />}
|
||||
title={title}
|
||||
link={link}
|
||||
subtitle={subtitle}
|
||||
/>
|
||||
{suffix}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { BorisContactItem };
|
|
@ -1,46 +0,0 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { BorisContactItem } from '~/components/boris/BorisContactItem';
|
||||
import { Padder } from '~/components/containers/Padder';
|
||||
import { Button } from '~/components/input/Button';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface Props {
|
||||
canConnectTelegram: boolean;
|
||||
connectTelegram: () => void;
|
||||
}
|
||||
|
||||
const BorisContacts: FC<Props> = ({ canConnectTelegram, connectTelegram }) => (
|
||||
<div className={styles.contacts}>
|
||||
<BorisContactItem
|
||||
icon="vk"
|
||||
title="Суицидальные роботы"
|
||||
link="https://vk.com/vault48"
|
||||
subtitle="паблик вконтакте"
|
||||
/>
|
||||
|
||||
<BorisContactItem
|
||||
icon="github"
|
||||
title="Github"
|
||||
link="https://github.com/muerwre?tab=repositories&q=vault"
|
||||
subtitle="исходники Убежища"
|
||||
/>
|
||||
|
||||
<BorisContactItem
|
||||
icon="telegram"
|
||||
title="Телеграм-бот"
|
||||
link="https://t.me/vault48bot"
|
||||
subtitle="@vault48bot"
|
||||
suffix={
|
||||
canConnectTelegram && (
|
||||
<Padder>
|
||||
<Button onClick={connectTelegram}>Получать уведомления</Button>
|
||||
</Padder>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export { BorisContacts };
|
|
@ -1,10 +0,0 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.contacts {
|
||||
@include inner_shadow;
|
||||
border-radius: $radius;
|
||||
|
||||
& > * {
|
||||
@include row_shadow;
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
import React, { VFC } from 'react';
|
||||
|
||||
|
||||
import { StatsGraphCard } from '~/components/charts/StatsGraphCard';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface BorisGraphicStatsProps {
|
||||
totalNodes: number;
|
||||
nodesByMonth: number[];
|
||||
totalComments: number;
|
||||
commentsByMonth: number[];
|
||||
}
|
||||
|
||||
const BorisGraphicStats: VFC<BorisGraphicStatsProps> = ({
|
||||
totalComments,
|
||||
commentsByMonth,
|
||||
totalNodes,
|
||||
nodesByMonth,
|
||||
}) => {
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<div className={styles.group}>
|
||||
<StatsGraphCard
|
||||
title="Посты"
|
||||
total={totalNodes}
|
||||
data={nodesByMonth}
|
||||
className={styles.card}
|
||||
left={year - 1}
|
||||
right={year}
|
||||
/>
|
||||
|
||||
<StatsGraphCard
|
||||
title="Комменты"
|
||||
total={totalComments}
|
||||
data={commentsByMonth}
|
||||
className={styles.card}
|
||||
left={year - 1}
|
||||
right={year}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { BorisGraphicStats };
|
|
@ -1,9 +0,0 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.group {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-column-gap: $gap;
|
||||
grid-row-gap: $gap;
|
||||
grid-auto-rows: 100px;
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { BorisUsageStats } from '~/types/boris';
|
||||
|
||||
import { BorisStatsBackend } from '../BorisStatsBackend';
|
||||
import { BorisStatsGit } from '../BorisStatsGit';
|
||||
|
||||
interface IProps {
|
||||
stats: BorisUsageStats;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const BorisStats: FC<IProps> = ({ stats, isLoading }) => {
|
||||
return (
|
||||
<>
|
||||
<BorisStatsBackend stats={stats.backend} isLoading={isLoading} />
|
||||
<BorisStatsGit issues={stats.issues} isLoading={isLoading} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { BorisStats };
|
|
@ -1,100 +0,0 @@
|
|||
import { FC, useMemo } from 'react';
|
||||
|
||||
import { BorisGraphicStats } from '~/components/boris/BorisGraphicStats';
|
||||
import { StatsRow } from '~/components/common/StatsRow';
|
||||
import { SubTitle } from '~/components/common/SubTitle';
|
||||
import { StatBackend } from '~/types/boris';
|
||||
import { sizeOf } from '~/utils/dom';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface IProps {
|
||||
stats: StatBackend;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const BorisStatsBackend: FC<IProps> = ({ isLoading, stats }) => {
|
||||
const commentsByMonth = useMemo(
|
||||
() => stats.comments.by_month?.slice(0, -1),
|
||||
[stats.comments.by_month],
|
||||
);
|
||||
const nodesByMonth = useMemo(
|
||||
() => stats.nodes.by_month?.slice(0, -1),
|
||||
[stats.nodes.by_month],
|
||||
);
|
||||
|
||||
if (!stats && !isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<SubTitle isLoading={isLoading} className={styles.title}>
|
||||
Юнитс
|
||||
</SubTitle>
|
||||
|
||||
<ul>
|
||||
<StatsRow isLoading={isLoading} label="В сознании">
|
||||
{stats.users.alive}
|
||||
</StatsRow>
|
||||
|
||||
<StatsRow isLoading={isLoading} label="Криокамера">
|
||||
{stats.users.total - stats.users.alive}
|
||||
</StatsRow>
|
||||
</ul>
|
||||
|
||||
<SubTitle isLoading={isLoading} className={styles.title}>
|
||||
Контент
|
||||
</SubTitle>
|
||||
|
||||
<ul>
|
||||
<StatsRow isLoading={isLoading} label="Фотографии">
|
||||
{stats.nodes.images}
|
||||
</StatsRow>
|
||||
|
||||
<StatsRow isLoading={isLoading} label="Письма">
|
||||
{stats.nodes.texts}
|
||||
</StatsRow>
|
||||
|
||||
<StatsRow isLoading={isLoading} label="Видеозаписи">
|
||||
{stats.nodes.videos}
|
||||
</StatsRow>
|
||||
|
||||
<StatsRow isLoading={isLoading} label="Аудиозаписи">
|
||||
{stats.nodes.audios}
|
||||
</StatsRow>
|
||||
|
||||
{/*
|
||||
<StatsRow isLoading={isLoading} label="Комментарии">
|
||||
{stats.comments.total}
|
||||
</StatsRow>
|
||||
*/}
|
||||
</ul>
|
||||
|
||||
<div className={styles.graphs}>
|
||||
<BorisGraphicStats
|
||||
totalComments={stats.comments.total}
|
||||
commentsByMonth={commentsByMonth}
|
||||
totalNodes={stats.nodes.total}
|
||||
nodesByMonth={nodesByMonth}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SubTitle isLoading={isLoading} className={styles.title}>
|
||||
Сторедж
|
||||
</SubTitle>
|
||||
|
||||
<ul>
|
||||
<StatsRow isLoading={isLoading} label="Файлы">
|
||||
{stats.files.count}
|
||||
</StatsRow>
|
||||
|
||||
<StatsRow isLoading={isLoading} label="На диске">
|
||||
{sizeOf(stats.files.size)}
|
||||
</StatsRow>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { BorisStatsBackend };
|
|
@ -1,15 +0,0 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.title {
|
||||
margin: $gap * 2 0 $gap;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-left: $gap;
|
||||
font: $font_12_semibold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.graphs {
|
||||
padding-top: $gap;
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
import React, { FC, useMemo } from 'react';
|
||||
|
||||
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||
import { GithubIssue } from '~/types/boris';
|
||||
|
||||
import { BorisStatsGitCard } from '../BorisStatsGitCard';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface IProps {
|
||||
issues: GithubIssue[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const BorisStatsGit: FC<IProps> = ({ issues, isLoading }) => {
|
||||
const open = useMemo(
|
||||
() => issues.filter(el => !el.pull_request && el.state === 'open').slice(0, 5),
|
||||
[issues]
|
||||
);
|
||||
|
||||
const closed = useMemo(
|
||||
() => issues.filter(el => !el.pull_request && el.state === 'closed').slice(0, 5),
|
||||
[issues]
|
||||
);
|
||||
|
||||
if (!issues.length) return null;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.stats__title}>
|
||||
<Placeholder width="50%" />
|
||||
</div>
|
||||
|
||||
<Placeholder width="50%" />
|
||||
<Placeholder width="100%" />
|
||||
<Placeholder width="50%" />
|
||||
<Placeholder width="70%" />
|
||||
<Placeholder width="60%" />
|
||||
<Placeholder width="100%" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<div className={styles.stats__title}>
|
||||
<span>КОММИТС</span>
|
||||
<img src="https://jenkins.vault48.org/api/badges/muerwre/vault-golang/status.svg" alt="" />
|
||||
</div>
|
||||
|
||||
{open.map(data => (
|
||||
<BorisStatsGitCard data={data} key={data.id} />
|
||||
))}
|
||||
|
||||
{closed.map(data => (
|
||||
<BorisStatsGitCard data={data} key={data.id} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { BorisStatsGit };
|
|
@ -1,17 +0,0 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.stats {
|
||||
&__title {
|
||||
font: $font_12_semibold;
|
||||
text-transform: uppercase;
|
||||
margin: $gap * 2 0 $gap;
|
||||
|
||||
span {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
img {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import React, { FC, useMemo } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { GithubIssue } from '~/types/boris';
|
||||
import { getPrettyDate } from '~/utils/dom';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
|
||||
interface IProps {
|
||||
data: GithubIssue;
|
||||
}
|
||||
|
||||
const stateLabels: Record<GithubIssue['state'], string> = {
|
||||
open: 'Ожидает',
|
||||
closed: 'Сделано',
|
||||
};
|
||||
|
||||
const BorisStatsGitCard: FC<IProps> = ({ data: { created_at, title, html_url, state } }) => {
|
||||
const date = useMemo(() => getPrettyDate(created_at), [created_at]);
|
||||
|
||||
if (!title || !created_at) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<div className={styles.time}>
|
||||
<span className={classNames(styles.icon, styles[state])}>{stateLabels[state]}</span>
|
||||
{date}
|
||||
</div>
|
||||
|
||||
<a className={styles.subject} href={html_url} target="_blank" rel="noreferrer">
|
||||
{title}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { BorisStatsGitCard };
|
|
@ -1,39 +0,0 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.wrap {
|
||||
padding: $gap * 0.5 0;
|
||||
border-bottom: 1px solid #333333;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
font: $font_12_regular;
|
||||
line-height: 17px;
|
||||
color: $gray_75;
|
||||
}
|
||||
|
||||
.subject {
|
||||
font: $font_14_regular;
|
||||
word-break: break-word;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font: $font_10_semibold;
|
||||
margin-right: 5px;
|
||||
border-radius: 2px;
|
||||
padding: 2px 0;
|
||||
text-transform: uppercase;
|
||||
|
||||
&.open {
|
||||
color: $color_offline;
|
||||
}
|
||||
|
||||
&.closed {
|
||||
color: $color_online;
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import { Toggle } from '~/components/input/Toggle';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface IProps {
|
||||
active?: boolean;
|
||||
onChange?: (val: boolean) => void;
|
||||
}
|
||||
|
||||
const BorisSuperpowers: FC<IProps> = ({ active, onChange }) => {
|
||||
const onToggle = useCallback(() => {
|
||||
if (!onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(!active);
|
||||
}, [onChange, active]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<div className={styles.toggle}>
|
||||
<Toggle value={active} handler={onChange} color="primary" />
|
||||
</div>
|
||||
|
||||
<div className={styles.left} onClick={onToggle}>
|
||||
<div className={styles.title}>Суперспособности</div>
|
||||
{active ? (
|
||||
<div className={styles.subtitle}>Ты видишь всё, что скрыто</div>
|
||||
) : (
|
||||
<div className={styles.subtitle}>Включи, чтобы видеть будущее</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { BorisSuperpowers };
|
|
@ -1,20 +0,0 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.wrap {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
column-gap: $gap;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.title {
|
||||
font: $font_14_semibold;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font: $font_12_regular;
|
||||
color: $gray_50;
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
import { FC, useState } from 'react';
|
||||
|
||||
import { Card } from '~/components/containers/Card';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import { InputText } from '~/components/input/InputText';
|
||||
import markdown from '~/styles/common/markdown.module.scss';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface IProps {}
|
||||
|
||||
const BorisUIDemo: FC<IProps> = () => {
|
||||
const [text, setText] = useState('');
|
||||
|
||||
return (
|
||||
<Card className={styles.card}>
|
||||
<div className={markdown.wrapper}>
|
||||
<h1>UI</h1>
|
||||
<p>
|
||||
Простая демонстрация элементов интерфейса. Используется, в основном,
|
||||
как подсказка при разработке
|
||||
</p>
|
||||
|
||||
<h2>Инпуты</h2>
|
||||
|
||||
<form autoComplete="off">
|
||||
<Group>
|
||||
<InputText title="Обычный инпут" handler={setText} value={text} />
|
||||
<InputText
|
||||
title="Инпут с ошибкой"
|
||||
error="Ошибка"
|
||||
handler={setText}
|
||||
value={text}
|
||||
/>
|
||||
<InputText
|
||||
title="Пароль"
|
||||
type="password"
|
||||
handler={setText}
|
||||
value={text}
|
||||
/>
|
||||
</Group>
|
||||
</form>
|
||||
|
||||
<h2>Кнопки</h2>
|
||||
|
||||
<h4>Цвета</h4>
|
||||
|
||||
<Group horizontal className={styles.sample}>
|
||||
<Button>Primary</Button>
|
||||
<Button color="outline">Outline</Button>
|
||||
<Button color="gray">Gray</Button>
|
||||
<Button color="link">Link</Button>
|
||||
</Group>
|
||||
|
||||
<h4>Размеры</h4>
|
||||
|
||||
<Group horizontal className={styles.sample}>
|
||||
<Button size="micro">Micro</Button>
|
||||
<Button size="mini">Mini</Button>
|
||||
<Button size="normal">Normal</Button>
|
||||
<Button size="big">Big</Button>
|
||||
<Button size="giant">Giant</Button>
|
||||
</Group>
|
||||
|
||||
<h4>Варианты</h4>
|
||||
<Group horizontal className={styles.sample}>
|
||||
<Button iconRight="check">iconRight</Button>
|
||||
<Button iconLeft="send">iconLeft</Button>
|
||||
<Button round>Round</Button>
|
||||
</Group>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export { BorisUIDemo };
|
|
@ -1,14 +0,0 @@
|
|||
@import 'src/styles/variables.scss';
|
||||
|
||||
.card {
|
||||
flex: 3;
|
||||
align-self: stretch;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 20px 30px;
|
||||
background-color: $content_bg_lighter;
|
||||
}
|
||||
|
||||
.sample {
|
||||
flex-wrap: wrap;
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
import React, { useMemo, VFC } from 'react';
|
||||
|
||||
import { makeBezierCurve, PathPoint } from '~/utils/dom/makeBezierCurve';
|
||||
import { SVGProps } from '~/utils/types';
|
||||
|
||||
interface BasicCurveChartProps extends SVGProps {
|
||||
items: number[];
|
||||
gap?: number;
|
||||
fullscreen?: boolean;
|
||||
}
|
||||
|
||||
const gap = 5;
|
||||
const BasicCurveChart: VFC<BasicCurveChartProps> = ({
|
||||
stroke = '#007962',
|
||||
items = [],
|
||||
fullscreen = true,
|
||||
...props
|
||||
}) => {
|
||||
const max = Math.max(...items);
|
||||
const height = props.height ? parseFloat(props.height.toString()) : 100;
|
||||
const width = props.width ? parseFloat(props.width.toString()) : 100;
|
||||
const borderGap = fullscreen ? 0 : gap;
|
||||
|
||||
const points = useMemo(
|
||||
() =>
|
||||
items.reduce<PathPoint[]>(
|
||||
(acc, val, index) => [
|
||||
...acc,
|
||||
index === 0
|
||||
? {
|
||||
x: borderGap,
|
||||
y: height - (val / max) * (height - gap * 2) - gap,
|
||||
}
|
||||
: {
|
||||
x: ((width - borderGap) / (items.length - 1)) * index,
|
||||
y: height - (val / max) * (height - gap * 2) - gap,
|
||||
},
|
||||
],
|
||||
[],
|
||||
),
|
||||
[items, borderGap, height, max, width],
|
||||
);
|
||||
|
||||
if (!points.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox={`0 0 ${width} ${height * 1.05}`}
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<defs>
|
||||
<filter id="f1" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="2" />
|
||||
</filter>
|
||||
|
||||
<filter id="brighter">
|
||||
<feComponentTransfer>
|
||||
<feFuncR type="linear" slope="3" />
|
||||
<feFuncG type="linear" slope="3" />
|
||||
<feFuncB type="linear" slope="3" />
|
||||
</feComponentTransfer>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<path
|
||||
d={makeBezierCurve(points)}
|
||||
fill="none"
|
||||
x={0}
|
||||
y={gap / 2}
|
||||
opacity={0.5}
|
||||
stroke={stroke}
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
filter="url(#f1)"
|
||||
/>
|
||||
|
||||
<path
|
||||
d={makeBezierCurve(points)}
|
||||
fill="none"
|
||||
x={0}
|
||||
y={0}
|
||||
opacity={0.3}
|
||||
stroke={stroke}
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
|
||||
<path
|
||||
d={makeBezierCurve(points)}
|
||||
fill="none"
|
||||
x={0}
|
||||
y={0}
|
||||
stroke={stroke}
|
||||
opacity={1}
|
||||
strokeWidth={1}
|
||||
strokeLinecap="round"
|
||||
filter="url(#brighter)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export { BasicCurveChart };
|
|
@ -1,49 +0,0 @@
|
|||
import React, { FC, ReactNode } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { SubTitle } from '~/components/common/SubTitle';
|
||||
import { Card, CardProps } from '~/components/containers/Card';
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface StatsCardProps extends CardProps {
|
||||
title?: string;
|
||||
total?: string | number;
|
||||
background?: ReactNode;
|
||||
}
|
||||
|
||||
const StatsCard: FC<StatsCardProps> = ({
|
||||
children,
|
||||
title,
|
||||
background,
|
||||
total,
|
||||
...props
|
||||
}) => (
|
||||
<Card
|
||||
{...props}
|
||||
className={classNames(styles.card, props.className)}
|
||||
elevation={0}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
{(!!title || !!total) && (
|
||||
<Group className={styles.title} horizontal>
|
||||
{!!title && (
|
||||
<Filler>
|
||||
<SubTitle>{title}</SubTitle>
|
||||
</Filler>
|
||||
)}
|
||||
{!!total && <SubTitle className={styles.total}>{total}</SubTitle>}
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{!!background && <div className={styles.background}>{background}</div>}
|
||||
</Card>
|
||||
);
|
||||
|
||||
export { StatsCard };
|
|
@ -1,28 +0,0 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.background {
|
||||
top: 32px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: $gap;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 0;
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import React, { VFC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { addYears, differenceInMonths, differenceInYears } from 'date-fns';
|
||||
|
||||
import { StatsCard } from '~/components/charts/StatsCard';
|
||||
import { CardProps } from '~/components/containers/Card';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface StatsCountdownCardProps extends CardProps {
|
||||
since: Date;
|
||||
}
|
||||
|
||||
const StatsCountdownCard: VFC<StatsCountdownCardProps> = ({ since, ...props }) => {
|
||||
const years = differenceInYears(new Date(), since);
|
||||
const months = differenceInMonths(new Date(), addYears(since, years));
|
||||
|
||||
return (
|
||||
<StatsCard {...props} title="Нам уже" className={classNames(styles.card, props.className)}>
|
||||
<div className={styles.content}>
|
||||
{years > 0 && (
|
||||
<>
|
||||
<span className={styles.val}>{years}</span>
|
||||
{' лет '}
|
||||
</>
|
||||
)}
|
||||
|
||||
{months > 0 && (
|
||||
<>
|
||||
<span className={styles.val}>{months}</span>
|
||||
{' мес '}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</StatsCard>
|
||||
);
|
||||
};
|
||||
|
||||
export { StatsCountdownCard };
|
|
@ -1,21 +0,0 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font: $font_18_semibold;
|
||||
color: $gray_50;
|
||||
}
|
||||
|
||||
span.val {
|
||||
font: $font_48_bold;
|
||||
color: white;
|
||||
padding: $gap;
|
||||
}
|
||||
|
||||
.card {
|
||||
height: 100%;
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
import React, { VFC } from 'react';
|
||||
|
||||
import { BasicCurveChart } from '~/components/charts/BasicCurveChart';
|
||||
import { StatsCard } from '~/components/charts/StatsCard';
|
||||
import { CardProps } from '~/components/containers/Card';
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface StatsGraphCardProps extends CardProps {
|
||||
title?: string;
|
||||
total?: string | number;
|
||||
data: number[];
|
||||
left?: string | number;
|
||||
right?: string | number;
|
||||
}
|
||||
|
||||
const StatsGraphCard: VFC<StatsGraphCardProps> = ({
|
||||
total,
|
||||
title,
|
||||
data,
|
||||
left,
|
||||
right,
|
||||
}) => (
|
||||
<StatsCard
|
||||
title={title}
|
||||
total={total}
|
||||
background={
|
||||
<BasicCurveChart items={data} stroke={'var(--color_primary)'} />
|
||||
}
|
||||
className={styles.card}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<span className={styles.legend}>{left}</span>
|
||||
<Filler />
|
||||
<span className={styles.legend}>{right}</span>
|
||||
</div>
|
||||
</StatsCard>
|
||||
);
|
||||
|
||||
export { StatsGraphCard };
|
|
@ -1,27 +0,0 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
font: $font_12_medium;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
margin: -2px;
|
||||
}
|
||||
|
||||
.card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.legend {
|
||||
opacity: 0.5;
|
||||
background: $content_bg_light;
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue