1
0
Fork 0
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:
Fedor Katurov 2023-11-19 17:20:55 +06:00
parent 8ec77986bf
commit c53ac831e7
30 changed files with 42 additions and 114 deletions

View file

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

View file

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

View file

@ -1,10 +0,0 @@
@import "src/styles/variables";
.contacts {
@include inner_shadow;
border-radius: $radius;
& > * {
@include row_shadow;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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