mirror of
https://github.com/magnum-opus-nn-cp/frontend.git
synced 2024-11-21 17:16:36 +03:00
feat(textpage): add text page layout
This commit is contained in:
parent
89d6b794bb
commit
3815bb7b5e
|
@ -3,6 +3,7 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@react-pdf/renderer": "^3.1.12",
|
||||
"@seznam/compose-react-refs": "^1.0.6",
|
||||
"@tanstack/react-query": "^4.33.0",
|
||||
|
@ -13,6 +14,7 @@
|
|||
"@types/node": "^16.7.13",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@types/react-transition-group": "^4.4.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||
"@typescript-eslint/parser": "^6.6.0",
|
||||
"axios": "^1.4.0",
|
||||
|
@ -35,8 +37,10 @@
|
|||
"react-dropzone": "^14.2.3",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hook-form": "^7.45.4",
|
||||
"react-popper": "^2.3.0",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"sanitize.css": "^13.0.0",
|
||||
"sass": "^1.66.1",
|
||||
"ts-key-enum": "^2.0.12",
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
import {Route, Routes} from 'react-router-dom';
|
||||
import {ChatPage} from '../../pages/chat';
|
||||
import {CHAT_PAGE_ROUTE, HOME_PAGE_ROUTE} from './routes';
|
||||
import {HomePage} from '../../pages/home';
|
||||
import {DefaultLayout} from '../../pages/_layouts/DefaultLayout';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { ChatPage } from '../../pages/chat';
|
||||
import { HomePage } from '../../pages/home';
|
||||
import { DefaultLayout } from '../../pages/_layouts/DefaultLayout';
|
||||
import { TextPage } from '../../pages/text';
|
||||
import { CHAT_PAGE_ROUTE, HOME_PAGE_ROUTE, TEXT_PAGE_ROUTE } from './routes';
|
||||
|
||||
export const AppRoutes = () => {
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route element={<DefaultLayout />}>
|
||||
<Route path={CHAT_PAGE_ROUTE} element={<ChatPage />} />
|
||||
<Route path={HOME_PAGE_ROUTE} element={<HomePage />} />
|
||||
<Route path={TEXT_PAGE_ROUTE} element={<TextPage />} />
|
||||
</Route>
|
||||
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
5
src/app/routes/PathBuilder.ts
Normal file
5
src/app/routes/PathBuilder.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { TEXT_PAGE_PARAM, TEXT_PAGE_ROUTE } from './routes';
|
||||
|
||||
export class PathBuilder {
|
||||
static getTextPath = (id: number) => TEXT_PAGE_ROUTE.replace(`:${TEXT_PAGE_PARAM}`, String(id));
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
export * from './AppRoutes';
|
||||
export * from './routes';
|
||||
export * from './routes';
|
||||
export * from './PathBuilder';
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
export const CHAT_PAGE_ROUTE = `/chat`;
|
||||
export const HOME_PAGE_ROUTE = `/`;
|
||||
|
||||
export const TEXT_PAGE_PARAM = 'textId';
|
||||
export const TEXT_PAGE_ROUTE = `/text/:${TEXT_PAGE_PARAM}`;
|
||||
|
|
51
src/components/Link/Link.module.scss
Normal file
51
src/components/Link/Link.module.scss
Normal file
|
@ -0,0 +1,51 @@
|
|||
@import 'src/app/styles/vars';
|
||||
|
||||
.Link {
|
||||
@include reset-button;
|
||||
@include transition(border-color, color);
|
||||
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.Link_disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.Link_standalone {
|
||||
&.Link_size_large {
|
||||
@include text-body-l-regular;
|
||||
}
|
||||
|
||||
&.Link_size_medium {
|
||||
@include text-body-m-regular;
|
||||
}
|
||||
|
||||
&.Link_size_small {
|
||||
@include text-body-s-regular;
|
||||
}
|
||||
}
|
||||
|
||||
.Link_variant_primary {
|
||||
color: $color-brand-primary;
|
||||
|
||||
&.Link_hover:not(.Link_disabled),
|
||||
&:hover:not(.Link_disabled, :active) {
|
||||
color: $color-brand-hover;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: $color-brand-active;
|
||||
}
|
||||
}
|
||||
|
||||
.Link_underlined {
|
||||
border-bottom: 1px solid currentcolor;
|
||||
}
|
119
src/components/Link/Link.tsx
Normal file
119
src/components/Link/Link.tsx
Normal file
|
@ -0,0 +1,119 @@
|
|||
import React, { ElementType, JSX, MouseEvent } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PolyExtends } from '../../utils/types';
|
||||
import s from './Link.module.scss';
|
||||
|
||||
export const LinkDefaultComponent = 'a';
|
||||
export type LinkDefaultComponentType = typeof LinkDefaultComponent;
|
||||
|
||||
export enum LinkVariant {
|
||||
PRIMARY = 'primary'
|
||||
// SECONDARY = 'secondary'
|
||||
}
|
||||
|
||||
export enum LinkSize {
|
||||
LARGE = 'large',
|
||||
MEDIUM = 'medium',
|
||||
SMALL = 'small'
|
||||
}
|
||||
|
||||
export interface LinkPropsSelf<ComponentType extends React.ElementType> {
|
||||
/**
|
||||
* Дополнительный css-класс корневого элемента
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Основной слот
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
/**
|
||||
* Вариант оформления ссылки ("primary", "secondary")
|
||||
*/
|
||||
variant?: LinkVariant;
|
||||
/**
|
||||
* Вариант размера ссылки ("large", "medium", "small")
|
||||
*/
|
||||
size?: LinkSize;
|
||||
/**
|
||||
* hover состояние
|
||||
*/
|
||||
hover?: boolean;
|
||||
/**
|
||||
* Состояние неактивной ссылки. Компонент недоступен для действий пользователя
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Применять ли фиксированные свойства шрифта.
|
||||
* Если не передать standalone, то свойства шрифта наследуются от родителя.
|
||||
* Полезно в случае, если Link используется самостоятельно вне текстового блока.
|
||||
*/
|
||||
standalone?: boolean;
|
||||
/**
|
||||
* Подчеркивание ссылки
|
||||
*/
|
||||
underlined?: boolean;
|
||||
/**
|
||||
* Реф на корневой элемент
|
||||
*/
|
||||
innerRef?: React.ComponentProps<ComponentType>['ref'];
|
||||
/**
|
||||
* Событие клика
|
||||
* @param e - объект события
|
||||
*/
|
||||
onClick?: (e: MouseEvent) => void;
|
||||
}
|
||||
|
||||
export interface LinkComponentProps {
|
||||
className: string;
|
||||
onClick: (e: MouseEvent) => void;
|
||||
}
|
||||
|
||||
export type LinkProps<ComponentType extends React.ElementType<LinkComponentProps> = LinkDefaultComponentType> =
|
||||
PolyExtends<ComponentType, LinkPropsSelf<ComponentType>, React.ComponentProps<ComponentType>>;
|
||||
|
||||
export function Link(props: LinkProps<'button'>): JSX.Element;
|
||||
export function Link<ComponentType extends ElementType>(props: LinkProps<ComponentType>): JSX.Element;
|
||||
export function Link<ComponentType extends React.ElementType<LinkComponentProps> = LinkDefaultComponentType>({
|
||||
className,
|
||||
children,
|
||||
variant = LinkVariant.PRIMARY,
|
||||
size = LinkSize.MEDIUM,
|
||||
disabled,
|
||||
standalone,
|
||||
underlined,
|
||||
onClick,
|
||||
component,
|
||||
innerRef,
|
||||
hover,
|
||||
...props
|
||||
}: LinkProps<ComponentType>) {
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (disabled) e.preventDefault();
|
||||
onClick?.(e);
|
||||
};
|
||||
|
||||
const Component = component || LinkDefaultComponent;
|
||||
|
||||
// Тайпскрипт жалуется на сложный union тип, поэтому используем createElement
|
||||
return React.createElement(
|
||||
Component,
|
||||
{
|
||||
ref: innerRef,
|
||||
className: clsx(
|
||||
s.Link,
|
||||
s[`Link_variant_${variant}`],
|
||||
s[`Link_size_${size}`],
|
||||
{
|
||||
[s.Link_underlined]: underlined,
|
||||
[s.Link_disabled]: disabled,
|
||||
[s.Link_standalone]: standalone,
|
||||
[s.Link_hover]: hover
|
||||
},
|
||||
className
|
||||
),
|
||||
onClick: handleClick,
|
||||
...props
|
||||
},
|
||||
<>{children}</>
|
||||
);
|
||||
}
|
1
src/components/Link/index.ts
Normal file
1
src/components/Link/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Link';
|
66
src/components/Tooltip/Tooltip.module.scss
Normal file
66
src/components/Tooltip/Tooltip.module.scss
Normal file
|
@ -0,0 +1,66 @@
|
|||
@import 'src/app/styles/vars';
|
||||
|
||||
.Tooltip {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.Tooltip__trigger {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Tooltip__contentWrapper {
|
||||
z-index: 1;
|
||||
transition: opacity 0.15s ease-out;
|
||||
}
|
||||
|
||||
.Tooltip__contentWrapper_entering,
|
||||
.Tooltip__contentWrapper_entered {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.Tooltip__contentWrapper_exiting,
|
||||
.Tooltip__contentWrapper_exited {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.Tooltip__title {
|
||||
@include text-header-h4;
|
||||
|
||||
margin-bottom: $spacing-small-3x;
|
||||
}
|
||||
|
||||
.Tooltip__content {
|
||||
@include text-body-s-regular;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 12px;
|
||||
color: $color-text-quaternary;
|
||||
border-radius: $radius-medium;
|
||||
background-color: $color-text-primary;
|
||||
}
|
||||
|
||||
.Tooltip__arrow {
|
||||
color: $color-text-primary;
|
||||
border: 7px solid transparent;
|
||||
}
|
||||
|
||||
.Tooltip__arrow_top {
|
||||
bottom: -13px;
|
||||
border-top-color: currentcolor;
|
||||
}
|
||||
|
||||
.Tooltip__arrow_right {
|
||||
left: -13px;
|
||||
border-right-color: currentcolor;
|
||||
}
|
||||
|
||||
.Tooltip__arrow_bottom {
|
||||
top: -13px;
|
||||
border-bottom-color: currentcolor;
|
||||
}
|
||||
|
||||
.Tooltip__arrow_left {
|
||||
right: -13px;
|
||||
border-left-color: currentcolor;
|
||||
}
|
146
src/components/Tooltip/Tooltip.tsx
Normal file
146
src/components/Tooltip/Tooltip.tsx
Normal file
|
@ -0,0 +1,146 @@
|
|||
import React, { memo, ReactNode, useCallback, useRef, useState } from 'react';
|
||||
import { usePopper } from 'react-popper';
|
||||
import { Transition } from 'react-transition-group';
|
||||
import clsx from 'clsx';
|
||||
import { Placement } from '@popperjs/core';
|
||||
import { useSingleTimeout } from '../../hooks/useSingleTimeout';
|
||||
import { useToggle } from '../../hooks/useToggle';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile';
|
||||
import s from './Tooltip.module.scss';
|
||||
|
||||
export interface TooltipProps {
|
||||
className?: string;
|
||||
children?: React.ReactElement;
|
||||
title?: ReactNode;
|
||||
content?: ReactNode;
|
||||
placement?: Placement;
|
||||
disableOnMobile?: boolean;
|
||||
offset?: [number, number];
|
||||
classes?: {
|
||||
content?: string;
|
||||
trigger?: string;
|
||||
};
|
||||
bgColor?: string;
|
||||
}
|
||||
|
||||
const placementMapper: Partial<Record<Placement, string>> = {
|
||||
bottom: 'bottom',
|
||||
'bottom-start': 'bottom',
|
||||
'bottom-end': 'bottom',
|
||||
|
||||
left: 'left',
|
||||
'left-start': 'left',
|
||||
'left-end': 'left',
|
||||
|
||||
right: 'right',
|
||||
'right-start': 'right',
|
||||
'right-end': 'right',
|
||||
|
||||
top: 'top',
|
||||
'top-start': 'top',
|
||||
'top-end': 'top'
|
||||
};
|
||||
|
||||
export const Tooltip = memo((props: TooltipProps) => {
|
||||
const {
|
||||
title,
|
||||
content,
|
||||
children,
|
||||
className,
|
||||
placement = 'auto',
|
||||
disableOnMobile,
|
||||
classes,
|
||||
offset = [0, 11],
|
||||
bgColor
|
||||
} = props;
|
||||
const [isVisible, { set: setVisible, unset: unsetVisible }] = useToggle();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const nodeRef = useRef(null);
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLSpanElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
strategy: 'fixed',
|
||||
placement: placement,
|
||||
modifiers: [
|
||||
{ name: 'offset', options: { offset } },
|
||||
{ name: 'arrow', options: { element: arrowElement } },
|
||||
{ name: 'flip', options: { fallbackPlacements: ['bottom-start', 'top-start', 'right', 'left'] } },
|
||||
{ name: 'preventOverflow', options: { rootBoundary: 'viewport', tether: false, altAxis: true } }
|
||||
]
|
||||
});
|
||||
|
||||
const timeout = useSingleTimeout();
|
||||
|
||||
const set = useCallback(() => {
|
||||
timeout.clear();
|
||||
setVisible();
|
||||
}, [setVisible, timeout]);
|
||||
|
||||
const unset = useCallback(() => {
|
||||
timeout.set(unsetVisible, 50);
|
||||
}, [unsetVisible, timeout]);
|
||||
|
||||
if (isMobile && disableOnMobile) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx(s.Tooltip, className)}>
|
||||
<span
|
||||
className={clsx(s.Tooltip__trigger, classes?.trigger)}
|
||||
onMouseEnter={set}
|
||||
onMouseLeave={unset}
|
||||
ref={setReferenceElement}>
|
||||
{children}
|
||||
</span>
|
||||
|
||||
{content && (
|
||||
<Transition unmountOnExit nodeRef={nodeRef} timeout={150} in={isVisible}>
|
||||
{(state) => (
|
||||
<div
|
||||
ref={nodeRef}
|
||||
className={clsx(s.Tooltip__contentWrapper, s[`Tooltip__contentWrapper_${state}`])}
|
||||
onMouseEnter={set}
|
||||
onMouseLeave={unset}>
|
||||
<div
|
||||
className={clsx(s.Tooltip__content, classes?.content)}
|
||||
ref={setPopperElement}
|
||||
style={{
|
||||
...styles.popper,
|
||||
backgroundColor: bgColor
|
||||
}}
|
||||
{...attributes.popper}>
|
||||
{title && <div className={s.Tooltip__title}>{title}</div>}
|
||||
{content}
|
||||
<div
|
||||
className={clsx(
|
||||
s.Tooltip__arrow,
|
||||
s[
|
||||
`Tooltip__arrow_${
|
||||
attributes.popper?.['data-popper-placement']
|
||||
? placementMapper[attributes.popper?.['data-popper-placement'] as Placement]
|
||||
: ''
|
||||
}`
|
||||
]
|
||||
)}
|
||||
ref={setArrowElement}
|
||||
style={{
|
||||
...styles.arrow,
|
||||
color: bgColor
|
||||
}}
|
||||
{...attributes.arrow}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Transition>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Tooltip.displayName = 'Tooltip';
|
1
src/components/Tooltip/index.ts
Normal file
1
src/components/Tooltip/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Tooltip';
|
|
@ -1,6 +1,7 @@
|
|||
import { Outlet } from 'react-router-dom';
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import clsx from 'clsx';
|
||||
import { ReactFCC } from '../../../utils/ReactFCC';
|
||||
import { HOME_PAGE_ROUTE } from '../../../app/routes';
|
||||
import cbrLogoSrc from './assets/cbr-logo.svg';
|
||||
import cpLogoSrc from './assets/cp-logo.svg';
|
||||
import mopusLogoSrc from './assets/mopus-logo.svg';
|
||||
|
@ -11,9 +12,9 @@ export const DefaultLayout: ReactFCC = () => {
|
|||
<div className={s.DefaultLayout}>
|
||||
<div className={s.DefaultLayout__container}>
|
||||
<header className={s.DefaultLayout__header}>
|
||||
<div className={s.DefaultLayout__logoMopusContainer}>
|
||||
<Link to={HOME_PAGE_ROUTE} className={s.DefaultLayout__logoMopusContainer} reloadDocument>
|
||||
<img className={s.DefaultLayout__logo} src={mopusLogoSrc} alt={''} />
|
||||
</div>
|
||||
</Link>
|
||||
<img className={clsx(s.DefaultLayout__logo, s.DefaultLayout__logo_cp)} src={cpLogoSrc} alt={''} />
|
||||
<img className={clsx(s.DefaultLayout__logo, s.DefaultLayout__logo_cbr)} src={cbrLogoSrc} alt={''} />
|
||||
</header>
|
||||
|
|
80
src/pages/text/TextPage.module.scss
Normal file
80
src/pages/text/TextPage.module.scss
Normal file
|
@ -0,0 +1,80 @@
|
|||
@import 'src/app/styles/vars';
|
||||
|
||||
.TextPage {
|
||||
padding-bottom: $spacing-medium-x;
|
||||
}
|
||||
|
||||
.TextPage__title {
|
||||
margin-bottom: $spacing-small-x;
|
||||
}
|
||||
|
||||
.TextPage__props {
|
||||
@include flex-col;
|
||||
row-gap: $spacing-small-3x;
|
||||
margin-bottom: $spacing-small;
|
||||
}
|
||||
|
||||
.TextPage__prop {
|
||||
font-size: $font-size-14;
|
||||
line-height: $line-height-20;
|
||||
}
|
||||
|
||||
.TextPage__tooltip {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.TextPage__underline {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.TextPage__summary {
|
||||
margin-bottom: $spacing-small;
|
||||
}
|
||||
|
||||
.TextPage__summaryHeading {
|
||||
margin-bottom: $spacing-small-x;
|
||||
}
|
||||
|
||||
.TextPage__summaryText {
|
||||
}
|
||||
|
||||
.TextPage__summaryLink {
|
||||
display: block;
|
||||
margin-bottom: $spacing-small-4x;
|
||||
width: fit-content;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: $spacing-small-x;
|
||||
}
|
||||
}
|
||||
|
||||
.TextPage__full {
|
||||
}
|
||||
|
||||
.TextPage__fullHeading {
|
||||
margin-bottom: $spacing-small-x;
|
||||
}
|
||||
|
||||
.TextPage__fullText {
|
||||
line-height: $line-height-24;
|
||||
|
||||
@include mobile-up {
|
||||
border: 1px solid $color-border-default;
|
||||
padding: $spacing-medium $spacing-small;
|
||||
border-radius: $radius-small;
|
||||
}
|
||||
|
||||
@include mobile-down {
|
||||
border-top: 1px solid $color-border-default;
|
||||
border-bottom: 1px solid $color-border-default;
|
||||
padding: $spacing-small-x 0;
|
||||
}
|
||||
}
|
||||
|
||||
.TextPage__tag {
|
||||
display: inline-block;
|
||||
background-color: #efe1ae;
|
||||
padding: 0 2px;
|
||||
margin: 0 -2px;
|
||||
}
|
119
src/pages/text/TextPage.tsx
Normal file
119
src/pages/text/TextPage.tsx
Normal file
|
@ -0,0 +1,119 @@
|
|||
import { FC } from 'react';
|
||||
import { Heading, HeadingSize } from '../../components/Heading';
|
||||
import { useUrlParam } from '../../hooks/useUrlParam';
|
||||
import { TEXT_PAGE_PARAM } from '../../app/routes';
|
||||
import { ETextVariants, Text } from '../../components/Text';
|
||||
import { getPercentageColor } from '../../utils/getPercentageColor';
|
||||
import { Tooltip } from '../../components/Tooltip';
|
||||
import { Link } from '../../components/Link';
|
||||
import s from './TextPage.module.scss';
|
||||
|
||||
export const TextPage: FC = () => {
|
||||
const textId = useUrlParam(TEXT_PAGE_PARAM, { parser: parseInt });
|
||||
|
||||
return (
|
||||
<div className={s.TextPage}>
|
||||
<Heading size={HeadingSize.H2} className={s.TextPage__title}>
|
||||
Результат обработки запроса №{textId}
|
||||
</Heading>
|
||||
|
||||
<div className={s.TextPage__props}>
|
||||
<Text className={s.TextPage__prop} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
|
||||
Имя файла: file.txt
|
||||
</Text>
|
||||
|
||||
<Text component={'div'} className={s.TextPage__prop} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
|
||||
Результат по{' '}
|
||||
<Tooltip className={s.TextPage__tooltip} content={'Языковая модель (Berd)'}>
|
||||
<span className={s.TextPage__underline}>нейросетевому</span>
|
||||
</Tooltip>{' '}
|
||||
методу: АА+ | Accuracy: <span style={{ color: getPercentageColor(0.95) }}>0.95</span>
|
||||
</Text>
|
||||
|
||||
<Text component={'div'} className={s.TextPage__prop} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
|
||||
Результат по{' '}
|
||||
<Tooltip className={s.TextPage__tooltip} content={'Лемматизация + TF/IDF + RandomForest'}>
|
||||
<span className={s.TextPage__underline}>статистическому</span>
|
||||
</Tooltip>{' '}
|
||||
методу: АА+ | Accuracy: <span style={{ color: getPercentageColor(0.71) }}>0.71</span>
|
||||
</Text>
|
||||
|
||||
<Text component={'div'} className={s.TextPage__prop} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
|
||||
Результат по методу{' '}
|
||||
<Tooltip className={s.TextPage__tooltip} content={'Berd + Annoy'}>
|
||||
<span className={s.TextPage__underline}>похожести</span>
|
||||
</Tooltip>{' '}
|
||||
: АА+ | Accuracy: <span style={{ color: getPercentageColor(0.63) }}>0.63</span>
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className={s.TextPage__summary}>
|
||||
<Heading size={HeadingSize.H4} className={s.TextPage__summaryHeading}>
|
||||
Summary
|
||||
</Heading>
|
||||
|
||||
<Text className={s.TextPage__summaryText} variant={ETextVariants.BODY_M_REGULAR}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||
consequat.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className={s.TextPage__full}>
|
||||
<Heading size={HeadingSize.H4} className={s.TextPage__fullHeading}>
|
||||
Полный текст
|
||||
</Heading>
|
||||
|
||||
<Link className={s.TextPage__summaryLink}>Скачать DOCX</Link>
|
||||
<Link className={s.TextPage__summaryLink}>Скачать PDF</Link>
|
||||
|
||||
<Text className={s.TextPage__fullText} variant={ETextVariants.BODY_M_REGULAR}>
|
||||
Повышение кредитного рейтинга Акционерного общества «Уральская сталь» (далее — «Уральская сталь», Компания)
|
||||
вызвано улучшением качественной оценки ликвидности в связи с рефинансированием краткосрочного банковского
|
||||
кредита посредством выпуска облигационного займа с погашением в 2025 году. Также{' '}
|
||||
<span className={s.TextPage__tag}>пересмотр стратегических планов</span> по реализации ряда инвестиционных
|
||||
проектов способствовал улучшению показателя «капитальные затраты к выручке». Улучшение ценовой конъюнктуры на
|
||||
мировом рынке чугуна обеспечило запуск доменной печи №3, находившейся ранее в резерве, что окажет
|
||||
дополнительное положительное влияние на денежный поток Компании в 2023 году. Кредитный рейтинг Компании
|
||||
определяется средними рыночной позицией, бизнес-профилем и уровнем корпоративного управления, а также средней
|
||||
оценкой за размер бизнеса. Показатели рентабельности, ликвидности, долговой нагрузки, обслуживания долга и
|
||||
денежного потока получили высокие оценки. «Уральская сталь» — один из крупнейших в России производителей
|
||||
товарного чугуна, мостостали и стали для производства труб большого диаметра (ТБД). В начале 2022 года
|
||||
Акционерное общество «Загорский трубный завод» ( рейтинг АКРА — rating, прогноз «Стабильный» ; далее — ЗТЗ)
|
||||
<span className={s.TextPage__tag}>приобрело 100% уставного капитала</span> Компании у АО «ХК «МЕТАЛЛОИНВЕСТ» (
|
||||
рейтинг АКРА — rating, прогноз «Стабильный» ). Ключевые факторы оценки Средняя оценка рыночной позиции
|
||||
обусловлена оценкой рыночных позиций «Уральской стали» по основным видам продукции (мостосталь, штрипс и
|
||||
чугун), взвешенных с учетом их доли в консолидированной выручке Компании. Средняя оценка бизнес-профиля
|
||||
Компании определяется: низкой оценкой степени вертикальной интеграции, которая отсутствует в Компании,
|
||||
поскольку она не обеспечена собственными углем и железорудным сырьем; средней оценкой за долю продукции с
|
||||
высокой добавленной стоимостью, которая учитывает сталь для ТБД и мостосталь как высокотехнологичные виды
|
||||
продукции; средней оценкой за характеристику и диверсификацию рынков сбыта, так как рынки сбыта основной
|
||||
продукции «Уральской стали» характеризуются умеренной цикличностью и насыщенностью, а продуктовый портфель
|
||||
Компании умеренно диверсифицирован. Средняя оценка географической диверсификации является следствием наличия
|
||||
экспорта чугуна, толстолистового проката и заготовки, доля которого формирует до{' '}
|
||||
<span className={s.TextPage__tag}>50% консолидированной выручки</span>
|
||||
Компании. С одной стороны, это обуславливает высокую оценку субфактора «доступность и диверсификация рынков
|
||||
сбыта», а с другой — очень низкую оценку субфактора «концентрация на одном заводе». Средний уровень
|
||||
корпоративного управления обусловлен прозрачной структурой бизнеса и успешной реализацией Компанией стратегии
|
||||
роста и расширения продуктового портфеля. Топ-менеджмент Компании представлен экспертами с большим опытом
|
||||
работы в отрасли. «Уральская сталь» применяет отдельные элементы системы риск-менеджмента (например,
|
||||
хеджирование валютного риска в определенных случаях), однако единые документы по стратегии и управлению
|
||||
рисками, а также по дивидендной политике пока не утверждены. Совет директоров и ключевые комитеты пока не
|
||||
сформированы. Структура бизнеса проста. Компания готовит отчетность по МСФО. Высокая оценка финансового
|
||||
риск-профиля Компании обусловлена: высокой оценкой за рентабельность (рентабельность по FFO до процентов и
|
||||
налогов за 2022 год составила 12% и ожидается АКРА на уровне{' '}
|
||||
<span className={s.TextPage__tag}>около 18%</span> в 2023-м); высокой оценкой за обслуживание долга (отношение
|
||||
FFO до чистых процентных платежей к процентным платежам составило 24,7х по результатам 2022 года и
|
||||
прогнозируется АКРА на уровне около 11,7х в 2023-м); высокой оценкой за долговую нагрузку (отношение общего
|
||||
долга с учетом поручительства по долгу ЗТЗ к FFO до чистых процентных платежей ожидается АКРА на уровне 2,5х
|
||||
(0,8х без учета поручительств) по результатам 2023 года); средней оценкой размера бизнеса (абсолютное значение
|
||||
годового FFO до чистых процентных платежей и налогов — менее 30 млрд руб.). Высокая оценка уровня ликвидности.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/*<div className={s.TextPage__downloads}>*/}
|
||||
{/* <a href="#">Скачать DOCX</a>*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
);
|
||||
};
|
1
src/pages/text/index.ts
Normal file
1
src/pages/text/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './TextPage';
|
7
src/utils/getPercentageColor.ts
Normal file
7
src/utils/getPercentageColor.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* @param value from 0 to 1
|
||||
*/
|
||||
export const getPercentageColor = (value: number) => {
|
||||
const hue = (value * 120).toString(10);
|
||||
return ['hsl(', hue, ', 60%, 50%)'].join('');
|
||||
};
|
56
yarn.lock
56
yarn.lock
|
@ -1113,6 +1113,13 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7":
|
||||
version "7.22.15"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8"
|
||||
integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/template@^7.22.5", "@babel/template@^7.3.3":
|
||||
version "7.22.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec"
|
||||
|
@ -1709,6 +1716,11 @@
|
|||
schema-utils "^3.0.0"
|
||||
source-map "^0.7.3"
|
||||
|
||||
"@popperjs/core@^2.11.8":
|
||||
version "2.11.8"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
|
||||
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
|
||||
|
||||
"@react-pdf/fns@2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-pdf/fns/-/fns-2.0.1.tgz#8948464044fc8a69975d9d07b1a12673377b72e2"
|
||||
|
@ -2381,6 +2393,13 @@
|
|||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-transition-group@^4.4.6":
|
||||
version "4.4.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.6.tgz#18187bcda5281f8e10dfc48f0943e2fdf4f75e2e"
|
||||
integrity sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^18.0.0":
|
||||
version "18.2.21"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.21.tgz#774c37fd01b522d0b91aed04811b58e4e0514ed9"
|
||||
|
@ -4262,6 +4281,14 @@ dom-converter@^0.2.0:
|
|||
dependencies:
|
||||
utila "~0.4"
|
||||
|
||||
dom-helpers@^5.0.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
|
||||
integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.8.7"
|
||||
csstype "^3.0.2"
|
||||
|
||||
dom-serializer@0:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
|
||||
|
@ -6926,7 +6953,7 @@ lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
|
|||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
|
@ -8357,7 +8384,7 @@ react-error-overlay@^6.0.11:
|
|||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
|
||||
integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
|
||||
|
||||
react-fast-compare@^3.1.1:
|
||||
react-fast-compare@^3.0.1, react-fast-compare@^3.1.1:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49"
|
||||
integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
|
||||
|
@ -8392,6 +8419,14 @@ react-is@^18.0.0:
|
|||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
||||
|
||||
react-popper@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.3.0.tgz#17891c620e1320dce318bad9fede46a5f71c70ba"
|
||||
integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==
|
||||
dependencies:
|
||||
react-fast-compare "^3.0.1"
|
||||
warning "^4.0.2"
|
||||
|
||||
react-refresh@^0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
|
||||
|
@ -8472,6 +8507,16 @@ react-side-effect@^2.1.0:
|
|||
resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.2.tgz#dc6345b9e8f9906dc2eeb68700b615e0b4fe752a"
|
||||
integrity sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==
|
||||
|
||||
react-transition-group@^4.4.5:
|
||||
version "4.4.5"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
|
||||
integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
dom-helpers "^5.0.1"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
|
@ -9876,6 +9921,13 @@ walker@^1.0.7:
|
|||
dependencies:
|
||||
makeerror "1.0.12"
|
||||
|
||||
warning@^4.0.2:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
||||
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
||||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
watchpack@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
|
||||
|
|
Loading…
Reference in New Issue
Block a user