diff --git a/src/app/App.tsx b/src/app/App.tsx index d5ddfc2..e4f5e34 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,15 +1,14 @@ import React from 'react'; import { Helmet } from 'react-helmet'; -import {AppRoutes} from './routes'; +import { AppRoutes } from './routes'; export function App() { return ( <> - + ); } - diff --git a/src/app/routes/AppRoutes.tsx b/src/app/routes/AppRoutes.tsx index 933d0ae..2a7ff46 100644 --- a/src/app/routes/AppRoutes.tsx +++ b/src/app/routes/AppRoutes.tsx @@ -3,7 +3,8 @@ 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'; +import { ResponsePage } from '../../pages/response'; +import { CHAT_PAGE_ROUTE, HOME_PAGE_ROUTE, RESPONSE_PAGE_ROUTE, TEXT_PAGE_ROUTE } from './routes'; export const AppRoutes = () => { return ( @@ -11,6 +12,7 @@ export const AppRoutes = () => { }> } /> } /> + } /> } /> diff --git a/src/app/routes/routes.ts b/src/app/routes/routes.ts index 1818952..89700db 100644 --- a/src/app/routes/routes.ts +++ b/src/app/routes/routes.ts @@ -1,5 +1,8 @@ export const CHAT_PAGE_ROUTE = `/chat`; export const HOME_PAGE_ROUTE = `/`; +export const RESPONSE_PAGE_PARAM = 'processId'; +export const RESPONSE_PAGE_ROUTE = `/response/:${RESPONSE_PAGE_PARAM}`; + export const TEXT_PAGE_PARAM = 'textId'; export const TEXT_PAGE_ROUTE = `/text/:${TEXT_PAGE_PARAM}`; diff --git a/src/app/styles/baseText.scss b/src/app/styles/baseText.scss index f8e15ab..82ba056 100644 --- a/src/app/styles/baseText.scss +++ b/src/app/styles/baseText.scss @@ -103,7 +103,7 @@ $line-height-67: 67px; @mixin text-body-s-regular { font-family: $font-family-inter, $font-family-default; - font-size: $font-size-12; + font-size: $font-size-14; font-weight: $font-weight-400; line-height: $line-height-18; } diff --git a/src/app/styles/index.scss b/src/app/styles/index.scss index cba5e14..0ee8c5e 100644 --- a/src/app/styles/index.scss +++ b/src/app/styles/index.scss @@ -34,3 +34,7 @@ a { text-decoration: none; color: inherit; } + +.scroll-prevented { + overflow: hidden; +} \ No newline at end of file diff --git a/src/components/Modal/Modal.module.scss b/src/components/Modal/Modal.module.scss new file mode 100644 index 0000000..faa683d --- /dev/null +++ b/src/components/Modal/Modal.module.scss @@ -0,0 +1,67 @@ +@import 'src/app/styles/vars'; + +.Modal { + position: fixed; + z-index: $z-index-modal; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + color: $color-text-primary; +} + +.Modal__overlay { + @include transition(opacity); + + position: fixed; + z-index: $z-index-overlay; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + background: #000; +} + +.Modal__content { + @include transition(opacity); + + z-index: $z-index-modal; + border-radius: $radius-large; + background: var(--bg-color); + opacity: 0; + + @include media-down(tablet-small) { + width: 100%; + margin-top: auto; + } +} + +.Modal_entering, +.Modal_entered { + .Modal__overlay { + opacity: 0.5; + } + + .Modal__content { + opacity: 1; + } +} + +.Modal_exiting, +.Modal_exited { + pointer-events: none; + + .Modal__overlay { + opacity: 0; + } + + .Modal__content { + opacity: 0; + } +} diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx new file mode 100644 index 0000000..db4cf2f --- /dev/null +++ b/src/components/Modal/Modal.tsx @@ -0,0 +1,35 @@ +import { ReactNode, useRef } from 'react'; +import { Transition } from 'react-transition-group'; +import clsx from 'clsx'; +import { Portal } from '../Portal'; +import { usePreventWindowScroll } from '../../hooks/usePreventWindowScroll'; +import s from './Modal.module.scss'; + +export interface ModalProps { + className?: string; + children?: ReactNode; + isOpen?: boolean; + onClose?: () => void; + isClosing?: boolean; + preventWindowScroll?: false; +} + +export const Modal = (props: ModalProps) => { + const { className, children, isOpen, onClose, isClosing, preventWindowScroll } = props; + + const nodeRef = useRef(null); + usePreventWindowScroll(preventWindowScroll ?? isOpen); + + return ( + + + {(state) => ( +
+
+
{isOpen && children}
+
+ )} + + + ); +}; diff --git a/src/components/Modal/ModalBody/ModalBody.module.scss b/src/components/Modal/ModalBody/ModalBody.module.scss new file mode 100644 index 0000000..902e0c5 --- /dev/null +++ b/src/components/Modal/ModalBody/ModalBody.module.scss @@ -0,0 +1,11 @@ +@import 'src/app/styles/vars'; + +.ModalBody { + @include scrollbar; + + overflow-y: auto; + + @include media-down(tablet-small) { + border-radius: 0 0 $radius-small $radius-small; + } +} diff --git a/src/components/Modal/ModalBody/ModalBody.tsx b/src/components/Modal/ModalBody/ModalBody.tsx new file mode 100644 index 0000000..f9a78c5 --- /dev/null +++ b/src/components/Modal/ModalBody/ModalBody.tsx @@ -0,0 +1,15 @@ +import { memo } from 'react'; +import clsx from 'clsx'; +import { ReactFCC } from '../../../utils/ReactFCC'; +import s from './ModalBody.module.scss'; + +export interface ModalBodyProps { + className?: string; +} + +export const ModalBody: ReactFCC = memo((props) => { + const { children, className } = props; + + return
{children}
; +}); +ModalBody.displayName = 'ModalBody'; diff --git a/src/components/Modal/ModalBody/index.ts b/src/components/Modal/ModalBody/index.ts new file mode 100644 index 0000000..ad32ff0 --- /dev/null +++ b/src/components/Modal/ModalBody/index.ts @@ -0,0 +1 @@ +export * from './ModalBody'; diff --git a/src/components/Modal/ModalContainer/ModalContainer.module.scss b/src/components/Modal/ModalContainer/ModalContainer.module.scss new file mode 100644 index 0000000..4012526 --- /dev/null +++ b/src/components/Modal/ModalContainer/ModalContainer.module.scss @@ -0,0 +1,32 @@ +@import 'src/app/styles/vars'; + +.ModalContainer { + @include media-down(tablet-small) { + padding: $spacing-small $spacing-small-x; + } +} + +.ModalContainer__wrapper { + display: flex; + width: 600px; + max-height: calc(100vh - $spacing-medium-x * 2); + background: $color-background-primary; + padding: $spacing-small; + + &_borderRadius { + border-radius: $radius-medium; + } + + @include media-down(tablet-small) { + width: 100%; + max-height: calc(100vh - $spacing-small * 2); + margin-top: auto; + background: $color-background-primary; + } +} + +.ModalContainer__content { + display: flex; + flex-direction: column; + width: 100%; +} diff --git a/src/components/Modal/ModalContainer/ModalContainer.tsx b/src/components/Modal/ModalContainer/ModalContainer.tsx new file mode 100644 index 0000000..a63e4fb --- /dev/null +++ b/src/components/Modal/ModalContainer/ModalContainer.tsx @@ -0,0 +1,36 @@ +import { ReactNode } from 'react'; +import clsx from 'clsx'; +import { Modal } from '../Modal'; +import s from './ModalContainer.module.scss'; + +export interface ModalContainerProps { + className?: string; + children?: ReactNode; + onClose?: () => void; + isOpen: boolean; + isClosing?: boolean; + withBorderRadius?: boolean; + preventWindowScroll?: false; +} + +export const ModalContainer = (props: ModalContainerProps) => { + const { isClosing, className, children, isOpen, onClose, withBorderRadius = true, preventWindowScroll } = props; + + return ( + +
+
{children}
+
+
+ ); +}; diff --git a/src/components/Modal/ModalContainer/index.ts b/src/components/Modal/ModalContainer/index.ts new file mode 100644 index 0000000..7b83f34 --- /dev/null +++ b/src/components/Modal/ModalContainer/index.ts @@ -0,0 +1 @@ +export * from './ModalContainer'; diff --git a/src/components/Modal/index.ts b/src/components/Modal/index.ts new file mode 100644 index 0000000..791bbe9 --- /dev/null +++ b/src/components/Modal/index.ts @@ -0,0 +1,5 @@ +export * from './Modal'; +export * from './ModalContainer'; +export * from './ModalBody'; +export * from './useModal'; +// export * from './ModalAlert'; diff --git a/src/components/Modal/useModal.ts b/src/components/Modal/useModal.ts new file mode 100644 index 0000000..f3b4b9a --- /dev/null +++ b/src/components/Modal/useModal.ts @@ -0,0 +1,72 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +export interface UseModalProps { + onClose?: () => void; + isOpen?: boolean; + canClose?: boolean; +} +const ANIMATION_DURATION = 200; + +export function useModal(props: UseModalProps) { + const { isOpen, canClose: canCloseProp = true, onClose } = props; + + const [isClosing, setIsClosing] = useState(false); + const [modalIsVisible, setModalIsVisible] = useState(false); + const [canClose, setCanClose] = useState(false); + + const timerRef = useRef | null>(null); + + const close = useCallback(() => { + if (canClose && canCloseProp && onClose) { + onClose(); + } + }, [canClose, canCloseProp, onClose]); + + const onKeyDown = useCallback( + (evt: KeyboardEvent) => { + if (evt.key === 'Escape') { + close(); + } + }, + [close] + ); + + useEffect(() => { + if (isOpen) { + setModalIsVisible(true); + timerRef.current = setTimeout(() => { + setCanClose(true); + window.addEventListener('keydown', onKeyDown); + }, ANIMATION_DURATION); + + return () => { + window.removeEventListener('keydown', onKeyDown); + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + } + + if (!isOpen) { + setIsClosing(true); + setCanClose(false); + timerRef.current = setTimeout(() => { + setModalIsVisible(false); + setIsClosing(false); + }, ANIMATION_DURATION); + + return () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + } + }, [isOpen, onKeyDown]); + + return { + isClosing, + close, + modalIsVisible, + ANIMATION_DURATION + }; +} diff --git a/src/components/Portal/Portal.tsx b/src/components/Portal/Portal.tsx new file mode 100644 index 0000000..3a5b471 --- /dev/null +++ b/src/components/Portal/Portal.tsx @@ -0,0 +1,16 @@ +import { createPortal } from 'react-dom'; +import { ReactFCC } from '../../utils/ReactFCC'; + +interface PortalProps { + element?: Element | DocumentFragment | null; +} + +export const Portal: ReactFCC = (props) => { + const { children, element = document.body } = props; + + if (!element) { + return <>{children}; + } + + return createPortal(children, element); +}; diff --git a/src/components/Portal/index.ts b/src/components/Portal/index.ts new file mode 100644 index 0000000..73d42c8 --- /dev/null +++ b/src/components/Portal/index.ts @@ -0,0 +1 @@ +export * from './Portal'; diff --git a/src/hooks/usePreventWindowScroll.ts b/src/hooks/usePreventWindowScroll.ts new file mode 100644 index 0000000..d3d493c --- /dev/null +++ b/src/hooks/usePreventWindowScroll.ts @@ -0,0 +1,13 @@ +import { useCallback, useEffect } from 'react'; + +export const usePreventWindowScroll = (isPrevented?: boolean) => { + const preventScroll = useCallback((isPrevented: boolean) => { + document.body.classList.toggle('scroll-prevented', isPrevented); + }, []); + + useEffect(() => { + preventScroll(!!isPrevented); + }, [isPrevented, preventScroll]); + + return preventScroll; +}; diff --git a/src/pages/response/ResponsePage.module.scss b/src/pages/response/ResponsePage.module.scss new file mode 100644 index 0000000..b4bb240 --- /dev/null +++ b/src/pages/response/ResponsePage.module.scss @@ -0,0 +1,78 @@ +@import 'src/app/styles/vars'; + +.ResponsePage { + +} + +.ResponsePage__title { + margin-bottom: $spacing-medium; +} + +.ResponsePage__container { + width: 100%; + + @include mobile-down { + @include flex-col; + row-gap: $spacing-small-2x; + } +} + +.ResponsePage__table { + width: 100%; + border-collapse: collapse; + border-radius: $radius-small; + border-style: hidden; + box-shadow: 0 0 0 1px $color-border-default; + + tbody tr { + @include transition(background-color); + cursor: pointer; + + &:hover { + background-color: $color-background-secondary; + + td { + border-color: $color-border-hover; + + &, > span { + color: $color-text-brand !important; + } + } + + .ResponsePage__tableSummary { + //opacity: 1; + } + } + + &:active { + background-color: $color-button-secondary-hover-fill; + } + } + + th, td { + padding: $spacing-small-3x $spacing-small-2x; + border: 1px solid $color-border-default; + } + + th { + text-align: left; + font-weight: $font-weight-medium; + } + + td { + @include transition(border-color, color); + + &.ResponsePage__tableSummary { + @include transition(opacity); + opacity: 0; + + &:hover { + opacity: 1; + } + } + + > span { + @include transition(color); + } + } +} \ No newline at end of file diff --git a/src/pages/response/ResponsePage.tsx b/src/pages/response/ResponsePage.tsx new file mode 100644 index 0000000..fb95413 --- /dev/null +++ b/src/pages/response/ResponsePage.tsx @@ -0,0 +1,108 @@ +import { FC, useCallback, useState } from 'react'; +import { Heading, HeadingSize } from '../../components/Heading'; +import { Link } from '../../components/Link'; +import { getPercentageColor } from '../../utils/getPercentageColor'; +import { EMDASH } from '../../utils/chars'; +import { ModalBody, useModal, ModalContainer } from '../../components/Modal'; +import { useIsMobile } from '../../hooks/useIsMobile'; +import { TextItem } from './components'; +import s from './ResponsePage.module.scss'; + +export const ResponsePage: FC = () => { + const [isOpen, setIsOpen] = useState(false); + const onClose = useCallback(() => { + setIsOpen(false); + }, []); + const { modalIsVisible, isClosing, close } = useModal({ isOpen, onClose }); + + const isMobile = useIsMobile(); + + return ( +
+ + Результаты по запросу + + +
+ {isMobile ? ( + <> + setIsOpen(true)} /> + + + ) : ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDИмяМ. н-с.М. стат.М. п.Рез.Крат. сод.
1file.txt + AA+ | 0.63 + + AA+ | 0.95 + + AA+ | 0.95 + + AA+ | 0.95 + + Открыть +
1{EMDASH} + AA+ | 0.63 + + AA+ | 0.95 + + AA+ | 0.95 + + AA+ | 0.95 + + { + e.stopPropagation(); + setIsOpen(true); + }}> + Открыть + +
+ )} +
+ + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys + standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to + make a type specimen book. + + +
+ ); +}; diff --git a/src/pages/response/components/TextItem/TextItem.module.scss b/src/pages/response/components/TextItem/TextItem.module.scss new file mode 100644 index 0000000..bdfb00c --- /dev/null +++ b/src/pages/response/components/TextItem/TextItem.module.scss @@ -0,0 +1,33 @@ +@import 'src/app/styles/vars'; + +.TextItem { + background-color: $color-background-dark-100; + border-radius: $radius-medium; + padding: $spacing-small-2x; +} + +.TextItem__row { + margin-bottom: $spacing-small-4x; + + &:not(.TextItem__row_name) { + > span { + font-size: $font-size-14; + } + } +} + +.TextItem__row_name { + margin-bottom: $spacing-small-3x; + +} + +.TextItem__name { + //font-size: $font-size-18; + text-transform: uppercase; + font-weight: $font-weight-600; +} + +.TextItem__row_link { + margin-top: $spacing-small-3x; + margin-bottom: 0; +} \ No newline at end of file diff --git a/src/pages/response/components/TextItem/TextItem.tsx b/src/pages/response/components/TextItem/TextItem.tsx new file mode 100644 index 0000000..eae282e --- /dev/null +++ b/src/pages/response/components/TextItem/TextItem.tsx @@ -0,0 +1,62 @@ +import clsx from 'clsx'; +import { ReactFCC } from '../../../../utils/ReactFCC'; +import { ETextVariants, Text } from '../../../../components/Text'; +import { getPercentageColor } from '../../../../utils/getPercentageColor'; +import { Link } from '../../../../components/Link'; +import s from './TextItem.module.scss'; + +export interface TextItemProps { + /** + * Дополнительный css-класс + */ + className?: string; + + onClickSummary?: () => void; +} + +export const TextItem: ReactFCC = (props) => { + const { className, onClickSummary } = props; + + return ( +
+
+ {/**/} + {/* Имя:*/} + {/*{' '}*/} + + file.txt #1 + + {/*{' | '}*/} + {/**/} + {/* file.txt*/} + {/**/} +
+ +
+ + М. н-с: AA+ | 0.63 + {' '} +
+ +
+ + М. стат: AA+ | 0.63 + {' '} +
+ +
+ + М. п: AA+ | 0.63 + {' '} +
+ +
+ + onClickSummary?.()}> + Краткое содержание + + +
+
+ ); +}; diff --git a/src/pages/response/components/TextItem/index.ts b/src/pages/response/components/TextItem/index.ts new file mode 100644 index 0000000..3045cfb --- /dev/null +++ b/src/pages/response/components/TextItem/index.ts @@ -0,0 +1 @@ +export * from './TextItem'; diff --git a/src/pages/response/components/index.ts b/src/pages/response/components/index.ts new file mode 100644 index 0000000..3045cfb --- /dev/null +++ b/src/pages/response/components/index.ts @@ -0,0 +1 @@ +export * from './TextItem'; diff --git a/src/pages/response/index.ts b/src/pages/response/index.ts new file mode 100644 index 0000000..f9fb08d --- /dev/null +++ b/src/pages/response/index.ts @@ -0,0 +1 @@ +export * from './ResponsePage'; diff --git a/src/pages/text/TextPage.tsx b/src/pages/text/TextPage.tsx index 9b40abf..4cadc3d 100644 --- a/src/pages/text/TextPage.tsx +++ b/src/pages/text/TextPage.tsx @@ -42,7 +42,7 @@ export const TextPage: FC = () => { Результат по методу{' '} похожести - {' '} + : АА+ | Accuracy: 0.63
diff --git a/src/utils/chars.ts b/src/utils/chars.ts new file mode 100644 index 0000000..a97fada --- /dev/null +++ b/src/utils/chars.ts @@ -0,0 +1,21 @@ +// eslint-disable +export const LAQUO = '\u00AB'; // « +export const RAQUO = '\u00BB'; // » +export const EMDASH = '\u2014'; // — +export const THINSP = '\u2009'; // +export const QUARTERSP = '\u2005'; // +export const THIRDSP = '\u2004'; // +export const ENSP = '\u2002'; // +export const EMSP = '\u2001'; // +export const NBSP = '\u00A0'; // +export const HELLIP = '\u2026'; // … +export const LSAQUO = '\u2039'; // ‹ +export const RSAQUO = '\u203A'; // › +export const RUBLE = '\u20BD'; // ₽ +export const LEQ = '\u2264'; // ≤ +export const NEQ = '\u2260'; // ≠ +export const GEQ = '\u2265'; // ≥ +export const ARROW_LEFT = '\u2190'; // ← +export const ARROW_RIGHT = '\u2192'; // → +export const MAC_OPTION = '\u2325'; // ⌥ +// eslint-enable