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

Merge branch 'master' into develop

This commit is contained in:
Fedor Katurov 2022-01-17 16:38:49 +07:00
commit 2b519a07b7
85 changed files with 948 additions and 566 deletions

4
.env.local Normal file
View file

@ -0,0 +1,4 @@
#REACT_APP_API_HOST=http://localhost:3334/
NEXT_PUBLIC_API_HOST=https://pig.staging.vault48.org/
#REACT_APP_API_HOST=https://pig.vault48.org/
NEXT_PUBLIC_REMOTE_CURRENT=https://pig.staging.vault48.org/static/

13
next.config.js Normal file
View file

@ -0,0 +1,13 @@
const withTM = require('next-transpile-modules')(['ramda']);
module.exports = withTM({
/* Your Next.js config */
async rewrites() {
return [
{
source: '/post:id',
destination: '/node/:id',
},
];
},
});

View file

@ -40,6 +40,7 @@
"react-scripts": "^5.0.0", "react-scripts": "^5.0.0",
"react-sortable-hoc": "^2.0.0", "react-sortable-hoc": "^2.0.0",
"react-sticky-box": "^0.9.3", "react-sticky-box": "^0.9.3",
"react-stickynode": "^4.0.0",
"sticky-sidebar": "^3.3.1", "sticky-sidebar": "^3.3.1",
"swiper": "^6.8.4", "swiper": "^6.8.4",
"swr": "^1.0.1", "swr": "^1.0.1",
@ -86,6 +87,7 @@
"craco-alias": "^2.3.1", "craco-alias": "^2.3.1",
"husky": "^7.0.4", "husky": "^7.0.4",
"lint-staged": "^12.1.6", "lint-staged": "^12.1.6",
"next-transpile-modules": "^9.0.0",
"prettier": "^1.18.2" "prettier": "^1.18.2"
}, },
"lint-staged": { "lint-staged": {

View file

@ -5,11 +5,14 @@ import markdown from '~/styles/common/markdown.module.scss';
import { Group } from '~/components/containers/Group'; import { Group } from '~/components/containers/Group';
import { Button } from '~/components/input/Button'; import { Button } from '~/components/input/Button';
import { InputText } from '~/components/input/InputText'; import { InputText } from '~/components/input/InputText';
import { useShowModal } from '~/hooks/modal/useShowModal';
import { Dialog } from '~/constants/modal';
interface IProps {} interface IProps {}
const BorisUIDemo: FC<IProps> = () => { const BorisUIDemo: FC<IProps> = () => {
const [text, setText] = useState(''); const [text, setText] = useState('');
const openProfileSidebar = useShowModal(Dialog.ProfileSidebar);
return ( return (
<Card className={styles.card}> <Card className={styles.card}>
@ -20,6 +23,9 @@ const BorisUIDemo: FC<IProps> = () => {
разработке разработке
</p> </p>
<h2>Тестовые фичи</h2>
<Button onClick={() => openProfileSidebar({})}>Профиль в сайдбаре</Button>
<h2>Инпуты</h2> <h2>Инпуты</h2>
<form autoComplete="off"> <form autoComplete="off">

View file

@ -36,9 +36,9 @@ const CommentAvatar: FC<Props> = ({ user, withDetails, className }) => {
url={path(['photo', 'url'], user)} url={path(['photo', 'url'], user)}
username={user.username} username={user.username}
className={className} className={className}
innerRef={ref}
onMouseOver={onMouseOver} onMouseOver={onMouseOver}
onMouseOut={onMouseOut} onMouseOut={onMouseOut}
ref={ref}
/> />
)} )}
</Reference> </Reference>

View file

@ -1,4 +1,9 @@
import React, { forwardRef, KeyboardEventHandler, TextareaHTMLAttributes, useCallback } from 'react'; import React, {
forwardRef,
KeyboardEventHandler,
TextareaHTMLAttributes,
useCallback,
} from 'react';
import { Textarea } from '~/components/input/Textarea'; import { Textarea } from '~/components/input/Textarea';
import { useCommentFormContext } from '~/hooks/comments/useCommentFormFormik'; import { useCommentFormContext } from '~/hooks/comments/useCommentFormFormik';
import { useRandomPhrase } from '~/constants/phrases'; import { useRandomPhrase } from '~/constants/phrases';
@ -11,8 +16,8 @@ const LocalCommentFormTextarea = forwardRef<HTMLTextAreaElement, IProps>(({ ...r
const { values, handleChange, handleSubmit, isSubmitting } = useCommentFormContext(); const { values, handleChange, handleSubmit, isSubmitting } = useCommentFormContext();
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>( const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
({ ctrlKey, key }) => { ({ ctrlKey, key, metaKey }) => {
if (ctrlKey && key === 'Enter') handleSubmit(undefined); if ((ctrlKey || metaKey) && key === 'Enter') handleSubmit(undefined);
}, },
[handleSubmit] [handleSubmit]
); );

View file

@ -0,0 +1,18 @@
import React, { VFC } from 'react';
import { LinkProps } from '~/utils/types';
import { CONFIG } from '~/utils/config';
import NextLink from 'next/link';
import { Link } from 'react-router-dom';
interface AnchorProps extends LinkProps {}
const Anchor: VFC<AnchorProps> = ({ ref, href, ...rest }) =>
CONFIG.isNextEnvironment ? (
<NextLink href={href ?? ''} passHref>
<a {...rest} />
</NextLink>
) : (
<Link {...rest} to={href ?? ''} />
);
export { Anchor };

View file

@ -1,40 +1,34 @@
import React, { FC, useCallback } from 'react'; import React, { FC, forwardRef, useCallback } from 'react';
import { getURLFromString } from '~/utils/dom'; import { getURLFromString } from '~/utils/dom';
import { PRESETS } from '~/constants/urls'; import { PRESETS } from '~/constants/urls';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import classNames from 'classnames'; import classNames from 'classnames';
import { openUserProfile } from '~/utils/user'; import { openUserProfile } from '~/utils/user';
import { DivProps } from '~/utils/types'; import { DivProps } from '~/utils/types';
import { Square } from '~/components/common/Square';
interface Props extends DivProps { interface Props extends DivProps {
url?: string; url?: string;
username?: string; username?: string;
size?: number; size?: number;
preset?: typeof PRESETS[keyof typeof PRESETS]; preset?: typeof PRESETS[keyof typeof PRESETS];
innerRef?: React.Ref<any>;
} }
const Avatar: FC<Props> = ({ const Avatar = forwardRef<HTMLDivElement, Props>(
url, ({ url, username, size, className, preset = PRESETS.avatar, ...rest }, ref) => {
username, const onOpenProfile = useCallback(() => openUserProfile(username), [username]);
size,
className,
innerRef,
preset = PRESETS.avatar,
...rest
}) => {
const backgroundImage = !!url ? `url('${getURLFromString(url, preset)}')` : undefined;
const onOpenProfile = useCallback(() => openUserProfile(username), [username]);
return ( return (
<div <Square
{...rest} {...rest}
className={classNames(styles.avatar, className)} image={getURLFromString(url, preset)}
style={{ backgroundImage }} className={classNames(styles.avatar, className)}
onClick={onOpenProfile} onClick={onOpenProfile}
ref={innerRef} size={size}
/> ref={ref}
); />
}; );
}
);
export { Avatar }; export { Avatar };

View file

@ -0,0 +1,27 @@
import React, { VFC } from 'react';
import styles from './styles.module.scss';
import { useScrollTop } from '~/hooks/dom/useScrollTop';
import { useScrollHeight } from '~/hooks/dom/useScrollHeight';
import classNames from 'classnames';
import { useScrollToBottom } from '~/hooks/dom/useScrollToBottom';
interface ScrollHelperBottomProps {}
const ScrollHelperBottom: VFC<ScrollHelperBottomProps> = () => {
const top = useScrollTop();
const scrollHeight = useScrollHeight();
const scrollToBottom = useScrollToBottom();
const isVisible = scrollHeight > 2000 && top < scrollHeight * 0.25;
return (
<div
className={classNames(styles.helper, { [styles.visible]: isVisible })}
onClick={scrollToBottom}
>
Вниз
</div>
);
};
export { ScrollHelperBottom };

View file

@ -0,0 +1,26 @@
@import "src/styles/variables";
.helper {
position: fixed;
bottom: 0;
background: radial-gradient($red, transparent) 50% 24px no-repeat;
background-size: 100% 48px;
display: none;
width: calc(100% - 20px);
z-index: 4;
text-align: center;
max-width: 500px;
height: 64px;
align-items: flex-end;
justify-content: center;
padding: $gap;
text-transform: uppercase;
font: $font_16_medium;
border-radius: $radius $radius 0 0;
user-select: none;
cursor: pointer;
&.visible {
display: flex;
}
}

View file

@ -0,0 +1,29 @@
import React, { forwardRef } from 'react';
import styles from './styles.module.scss';
import { DivProps } from '~/utils/types';
import classNames from 'classnames';
interface SquareProps extends DivProps {
image?: string;
size?: number;
}
const Square = forwardRef<HTMLDivElement, SquareProps>(
({ image, size, children, ...rest }, ref) => {
const backgroundImage = image ? `url('${image}')` : undefined;
return (
<div
{...rest}
className={classNames(styles.wrapper, rest.className)}
style={{ backgroundImage, width: size }}
ref={ref}
>
<svg className={styles.svg} viewBox="0 0 1 1" />
{!!children && <div className={styles.content}>{children}</div>}
</div>
);
}
);
export { Square };

View file

@ -0,0 +1,21 @@
@import "src/styles/variables";
.svg {
width: 100%;
}
.wrapper {
width: 100%;
border-radius: $radius;
background-size: cover;
background-repeat: no-repeat;
position: relative;
}
.content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}

View file

@ -5,12 +5,10 @@ interface IProps extends DetailsHTMLAttributes<HTMLDivElement> {
offsetTop?: number; offsetTop?: number;
} }
const Sticky: FC<IProps> = ({ children, offsetTop = 65 }) => { const Sticky: FC<IProps> = ({ children, offsetTop = 65 }) => (
return ( <StickyBox offsetTop={offsetTop} offsetBottom={10}>
<StickyBox offsetTop={offsetTop} offsetBottom={10}> {children}
{children} </StickyBox>
</StickyBox> );
);
};
export { Sticky }; export { Sticky };

View file

@ -22,7 +22,6 @@
flex-direction: column; flex-direction: column;
min-width: $cell; min-width: $cell;
max-width: 400px; max-width: 400px;
max-height: 100%;
max-height: calc(100vh - 75px); max-height: calc(100vh - 75px);
width: 100%; width: 100%;
position: relative; position: relative;
@ -43,7 +42,6 @@
.footer { .footer {
@include outer_shadow(); @include outer_shadow();
// padding: 10px;
background: darken($content_bg, 2%); background: darken($content_bg, 2%);
} }
@ -66,13 +64,15 @@
} }
.close { .close {
background: darken($content_bg, 2%); @include outer_shadow;
width: 48px;
height: 48px; background: lighten($content_bg, 4%);
width: 36px;
height: 36px;
position: absolute; position: absolute;
top: -58px; top: -14px;
right: 50%; right: 4px;
transform: translate(50%, 0); transform: translate(50%, 0) scale(1);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -80,15 +80,21 @@
cursor: pointer; cursor: pointer;
transition: transform 0.25s, background-color 0.25s; transition: transform 0.25s, background-color 0.25s;
animation: appear 0.5s forwards; animation: appear 0.5s forwards;
z-index: 10;
@include tablet {
top: -16px;
right: 16px;
}
&:hover { &:hover {
background-color: $red; background-color: $red;
transform: translate(50%, -5px); transform: translate(50%, 0) scale(1.25);
} }
svg { svg {
width: 24px; width: 20px;
height: 24px; height: 20px;
} }
} }

View file

@ -10,6 +10,7 @@ import { useFlowCellControls } from '~/hooks/flow/useFlowCellControls';
import { useClickOutsideFocus } from '~/hooks/dom/useClickOutsideFocus'; import { useClickOutsideFocus } from '~/hooks/dom/useClickOutsideFocus';
import { MenuDots } from '~/components/common/MenuDots'; import { MenuDots } from '~/components/common/MenuDots';
import { FlowCellImage } from '~/components/flow/FlowCellImage'; import { FlowCellImage } from '~/components/flow/FlowCellImage';
import { Anchor } from '~/components/common/Anchor';
interface Props { interface Props {
id: INode['id']; id: INode['id'];
@ -71,7 +72,7 @@ const FlowCell: FC<Props> = ({
</div> </div>
)} )}
<NavLink className={styles.link} to={to}> <Anchor className={styles.link} href={to}>
{withText && ( {withText && (
<FlowCellText className={styles.text} heading={<h4 className={styles.title}>{title}</h4>}> <FlowCellText className={styles.text} heading={<h4 className={styles.title}>{title}</h4>}>
{text!} {text!}
@ -94,7 +95,7 @@ const FlowCell: FC<Props> = ({
<h4 className={styles.title}>{title}</h4> <h4 className={styles.title}>{title}</h4>
</div> </div>
)} )}
</NavLink> </Anchor>
</div> </div>
); );
}; };

View file

@ -1,21 +1,22 @@
import React, { FC } from 'react'; import React, { FC, MouseEventHandler } from 'react';
import { INode } from '~/types'; import { INode } from '~/types';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { URLS } from '~/constants/urls'; import { URLS } from '~/constants/urls';
import { NodeRelatedItem } from '~/components/node/NodeRelatedItem'; import { NodeRelatedItem } from '~/components/node/NodeRelatedItem';
import { getPrettyDate } from '~/utils/dom'; import { getPrettyDate } from '~/utils/dom';
import { Link } from 'react-router-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import { Icon } from '~/components/input/Icon'; import { Icon } from '~/components/input/Icon';
import { Anchor } from '~/components/common/Anchor';
interface IProps { interface IProps {
node: Partial<INode>; node: Partial<INode>;
has_new?: boolean; has_new?: boolean;
onClick?: MouseEventHandler;
} }
const FlowRecentItem: FC<IProps> = ({ node, has_new }) => { const FlowRecentItem: FC<IProps> = ({ node, has_new, onClick }) => {
return ( return (
<Link key={node.id} className={styles.item} to={URLS.NODE_URL(node.id)}> <Anchor key={node.id} className={styles.item} href={URLS.NODE_URL(node.id)} onClick={onClick}>
<div <div
className={classNames(styles.thumb, { className={classNames(styles.thumb, {
[styles.new]: has_new, [styles.new]: has_new,
@ -33,7 +34,7 @@ const FlowRecentItem: FC<IProps> = ({ node, has_new }) => {
<span>{getPrettyDate(node.created_at)}</span> <span>{getPrettyDate(node.created_at)}</span>
</div> </div>
</div> </div>
</Link> </Anchor>
); );
}; };

View file

@ -12,6 +12,7 @@ import { useHistory } from 'react-router';
import classNames from 'classnames'; import classNames from 'classnames';
import { IFlowNode } from '~/types'; import { IFlowNode } from '~/types';
import { useWindowSize } from '~/hooks/dom/useWindowSize'; import { useWindowSize } from '~/hooks/dom/useWindowSize';
import { useNavigation } from '~/hooks/navigation/useNavigation';
SwiperCore.use([EffectFade, Lazy, Autoplay, Navigation]); SwiperCore.use([EffectFade, Lazy, Autoplay, Navigation]);
@ -21,13 +22,13 @@ interface Props {
export const FlowSwiperHero: FC<Props> = ({ heroes }) => { export const FlowSwiperHero: FC<Props> = ({ heroes }) => {
const { innerWidth } = useWindowSize(); const { innerWidth } = useWindowSize();
const { push } = useNavigation();
const [controlledSwiper, setControlledSwiper] = useState<SwiperClass | undefined>(undefined); const [controlledSwiper, setControlledSwiper] = useState<SwiperClass | undefined>(undefined);
const [currentIndex, setCurrentIndex] = useState(heroes.length); const [currentIndex, setCurrentIndex] = useState(heroes.length);
const preset = useMemo(() => (innerWidth <= 768 ? PRESETS.cover : PRESETS.small_hero), [ const preset = useMemo(() => (innerWidth <= 768 ? PRESETS.cover : PRESETS.small_hero), [
innerWidth, innerWidth,
]); ]);
const history = useHistory();
const onNext = useCallback(() => { const onNext = useCallback(() => {
controlledSwiper?.slideNext(1); controlledSwiper?.slideNext(1);
@ -63,9 +64,9 @@ export const FlowSwiperHero: FC<Props> = ({ heroes }) => {
const onClick = useCallback( const onClick = useCallback(
(sw: SwiperClass) => { (sw: SwiperClass) => {
history.push(URLS.NODE_URL(heroes[sw.realIndex]?.id)); push(URLS.NODE_URL(heroes[sw.realIndex]?.id));
}, },
[history, heroes] [push, heroes]
); );
if (!heroes.length) { if (!heroes.length) {

View file

@ -10,6 +10,8 @@
} }
.button { .button {
@include outer_shadow();
position: relative; position: relative;
height: $input_height; height: $input_height;
border: none; border: none;
@ -34,12 +36,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
filter: grayscale(0);
transition: opacity 0.25s, filter 0.25s, box-shadow 0.25s, background-color 0.5s; transition: opacity 0.25s, filter 0.25s, box-shadow 0.25s, background-color 0.5s;
opacity: 0.8;
@include outer_shadow();
input { input {
position: absolute; position: absolute;

View file

@ -1,12 +1,12 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { Group } from '~/components/containers/Group'; import { Group } from '~/components/containers/Group';
import { Square } from '~/components/containers/Square'; import { LabSquare } from '~/components/lab/LabSquare';
interface IProps {} interface IProps {}
const LabBanner: FC<IProps> = () => ( const LabBanner: FC<IProps> = () => (
<Square className={styles.wrap}> <LabSquare className={styles.wrap}>
<Group> <Group>
<div className={styles.title}>Лаборатория!</div> <div className={styles.title}>Лаборатория!</div>
@ -19,7 +19,7 @@ const LabBanner: FC<IProps> = () => (
</p> </p>
</Group> </Group>
</Group> </Group>
</Square> </LabSquare>
); );
export { LabBanner }; export { LabBanner };

View file

@ -10,6 +10,7 @@ import { Grid } from '~/components/containers/Grid';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { URLS } from '~/constants/urls'; import { URLS } from '~/constants/urls';
import { Placeholder } from '~/components/placeholders/Placeholder'; import { Placeholder } from '~/components/placeholders/Placeholder';
import { useNavigation } from '~/hooks/navigation/useNavigation';
type Props = { type Props = {
node: INode; node: INode;
@ -19,8 +20,8 @@ type Props = {
}; };
const LabBottomPanel: FC<Props> = ({ node, hasNewComments, commentCount, isLoading }) => { const LabBottomPanel: FC<Props> = ({ node, hasNewComments, commentCount, isLoading }) => {
const history = useHistory(); const { push } = useNavigation();
const onClick = useCallback(() => history.push(URLS.NODE_URL(node.id)), [history, node.id]); const onClick = useCallback(() => push(URLS.NODE_URL(node.id)), [push, node.id]);
return ( return (
<Group horizontal className={styles.wrap} onClick={onClick}> <Group horizontal className={styles.wrap} onClick={onClick}>

View file

@ -7,6 +7,7 @@ import { INode } from '~/types';
import { getPrettyDate } from '~/utils/dom'; import { getPrettyDate } from '~/utils/dom';
import { URLS } from '~/constants/urls'; import { URLS } from '~/constants/urls';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useNavigation } from '~/hooks/navigation/useNavigation';
interface IProps { interface IProps {
node?: Partial<INode>; node?: Partial<INode>;
@ -14,10 +15,10 @@ interface IProps {
} }
const LabHero: FC<IProps> = ({ node, isLoading }) => { const LabHero: FC<IProps> = ({ node, isLoading }) => {
const history = useHistory(); const { push } = useNavigation();
const onClick = useCallback(() => { const onClick = useCallback(() => {
history.push(URLS.NODE_URL(node?.id)); push(URLS.NODE_URL(node?.id));
}, [history, node]); }, [push, node]);
if (!node || isLoading) { if (!node || isLoading) {
return ( return (

View file

@ -5,7 +5,7 @@ import classNames from 'classnames';
interface IProps extends DivProps {} interface IProps extends DivProps {}
const Square: FC<IProps> = ({ children, ...rest }) => ( const LabSquare: FC<IProps> = ({ children, ...rest }) => (
<div className={styles.square}> <div className={styles.square}>
<div {...rest} className={classNames(styles.content, rest.className)}> <div {...rest} className={classNames(styles.content, rest.className)}>
{children} {children}
@ -13,4 +13,4 @@ const Square: FC<IProps> = ({ children, ...rest }) => (
</div> </div>
); );
export { Square }; export { LabSquare };

View file

@ -1,9 +1,10 @@
import React from 'react'; import React from 'react';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { Link } from 'react-router-dom'; import { Anchor } from '~/components/common/Anchor';
import { URLS } from '~/constants/urls';
export const Logo = () => ( export const Logo = () => (
<Link className={styles.logo} to="/"> <Anchor className={styles.logo} href={URLS.BASE}>
VAULT VAULT
</Link> </Anchor>
); );

View file

@ -25,6 +25,8 @@
:global(.swiper-button-next), :global(.swiper-button-next),
:global(.swiper-button-prev) { :global(.swiper-button-prev) {
@include outer_shadow;
color: white; color: white;
font-size: 10px; font-size: 10px;
width: 40px; width: 40px;
@ -79,7 +81,7 @@
} }
.slide { .slide.slide {
text-align: center; text-align: center;
text-transform: uppercase; text-transform: uppercase;
font: $font_32_bold; font: $font_32_bold;
@ -90,7 +92,8 @@
width: auto; width: auto;
max-width: 100vw; max-width: 100vw;
opacity: 1; opacity: 1;
transform: translate(0, 10px); //transform: translate(0, 10px);
transform: scale(0.99);
filter: brightness(50%) saturate(0.5); filter: brightness(50%) saturate(0.5);
transition: opacity 0.5s, filter 0.5s, transform 0.5s; transition: opacity 0.5s, filter 0.5s, transform 0.5s;
padding-bottom: $gap * 1.5; padding-bottom: $gap * 1.5;
@ -99,7 +102,8 @@
&:global(.swiper-slide-active) { &:global(.swiper-slide-active) {
opacity: 1; opacity: 1;
filter: brightness(100%); filter: brightness(100%);
transform: translate(0, 0); //transform: translate(0, 0);
transform: scale(1);
} }
@include tablet { @include tablet {

View file

@ -4,7 +4,7 @@ import { NodeRelated } from '~/components/node/NodeRelated';
import { URLS } from '~/constants/urls'; import { URLS } from '~/constants/urls';
import { INode } from '~/types'; import { INode } from '~/types';
import { INodeRelated } from '~/types/node'; import { INodeRelated } from '~/types/node';
import { Link } from 'react-router-dom'; import { Anchor } from '~/components/common/Anchor';
interface IProps { interface IProps {
isLoading: boolean; isLoading: boolean;
@ -27,7 +27,9 @@ const NodeRelatedBlock: FC<IProps> = ({ isLoading, node, related }) => {
.map(album => ( .map(album => (
<NodeRelated <NodeRelated
title={ title={
<Link to={URLS.NODE_TAG_URL(node.id!, encodeURIComponent(album))}>{album}</Link> <Anchor href={URLS.NODE_TAG_URL(node.id!, encodeURIComponent(album))}>
{album}
</Anchor>
} }
items={related.albums[album]} items={related.albums[album]}
key={album} key={album}

View file

@ -3,12 +3,13 @@ import styles from './styles.module.scss';
import classNames from 'classnames'; import classNames from 'classnames';
import { INode } from '~/types'; import { INode } from '~/types';
import { PRESETS, URLS } from '~/constants/urls'; import { PRESETS, URLS } from '~/constants/urls';
import { RouteComponentProps, withRouter } from 'react-router'; import { RouteComponentProps } from 'react-router';
import { getURL } from '~/utils/dom'; import { getURL, getURLFromString } from '~/utils/dom';
import { Avatar } from '~/components/common/Avatar';
import { useColorGradientFromString } from '~/hooks/color/useColorGradientFromString'; import { useColorGradientFromString } from '~/hooks/color/useColorGradientFromString';
import { Square } from '~/components/common/Square';
import { useGotoNode } from '~/hooks/node/useGotoNode';
type IProps = RouteComponentProps & { type IProps = {
item: Partial<INode>; item: Partial<INode>;
}; };
@ -28,11 +29,11 @@ const getTitleLetters = (title?: string): string => {
: words[0].substr(0, 2).toUpperCase(); : words[0].substr(0, 2).toUpperCase();
}; };
const NodeRelatedItemUnconnected: FC<IProps> = memo(({ item, history }) => { const NodeRelatedItem: FC<IProps> = memo(({ item }) => {
const onClick = useGotoNode(item.id);
const [is_loaded, setIsLoaded] = useState(false); const [is_loaded, setIsLoaded] = useState(false);
const [width, setWidth] = useState(0); const [width, setWidth] = useState(0);
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const onClick = useCallback(() => history.push(URLS.NODE_URL(item.id)), [item, history]);
const thumb = useMemo( const thumb = useMemo(
() => (item.thumbnail ? getURL({ url: item.thumbnail }, PRESETS.avatar) : ''), () => (item.thumbnail ? getURL({ url: item.thumbnail }, PRESETS.avatar) : ''),
@ -68,11 +69,13 @@ const NodeRelatedItemUnconnected: FC<IProps> = memo(({ item, history }) => {
onClick={onClick} onClick={onClick}
ref={ref} ref={ref}
> >
<Avatar {item.thumbnail && (
username={item.title} <Square
url={item.thumbnail} image={getURLFromString(item.thumbnail, 'avatar')}
className={classNames(styles.thumb, { [styles.is_loaded]: is_loaded })} onClick={onClick}
/> className={classNames(styles.thumb, { [styles.is_loaded]: is_loaded })}
/>
)}
{!item.thumbnail && size === 'small' && ( {!item.thumbnail && size === 'small' && (
<div className={styles.letters} style={{ background }}> <div className={styles.letters} style={{ background }}>
@ -91,6 +94,4 @@ const NodeRelatedItemUnconnected: FC<IProps> = memo(({ item, history }) => {
); );
}); });
const NodeRelatedItem = withRouter(NodeRelatedItemUnconnected);
export { NodeRelatedItem }; export { NodeRelatedItem };

View file

@ -2,16 +2,17 @@ import React, { ChangeEvent, FC, useCallback } from 'react';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { getURL } from '~/utils/dom'; import { getURL } from '~/utils/dom';
import { PRESETS } from '~/constants/urls'; import { PRESETS } from '~/constants/urls';
import { Icon } from '~/components/input/Icon';
import { IFile } from '~/types'; import { IFile } from '~/types';
import { Button } from '~/components/input/Button';
export interface ProfileAvatarProps { export interface ProfileAvatarProps {
size?: number;
canEdit: boolean; canEdit: boolean;
photo?: IFile; photo?: IFile;
onChangePhoto: (file: File) => void; onChangePhoto: (file: File) => void;
} }
const ProfileAvatar: FC<ProfileAvatarProps> = ({ photo, onChangePhoto, canEdit }) => { const ProfileAvatar: FC<ProfileAvatarProps> = ({ photo, onChangePhoto, canEdit, size }) => {
const onInputChange = useCallback( const onInputChange = useCallback(
async (event: ChangeEvent<HTMLInputElement>) => { async (event: ChangeEvent<HTMLInputElement>) => {
if (!event.target.files?.length) { if (!event.target.files?.length) {
@ -30,14 +31,12 @@ const ProfileAvatar: FC<ProfileAvatarProps> = ({ photo, onChangePhoto, canEdit }
className={styles.avatar} className={styles.avatar}
style={{ style={{
backgroundImage, backgroundImage,
width: size,
height: size,
}} }}
> >
{canEdit && <input type="file" onInput={onInputChange} />} {canEdit && <input type="file" onInput={onInputChange} />}
{canEdit && ( {canEdit && <Button iconLeft="photo_add" round iconOnly className={styles.can_edit} />}
<div className={styles.can_edit}>
<Icon icon="photo_add" />
</div>
)}
</div> </div>
); );
}; };

View file

@ -8,10 +8,8 @@
height: 100px; height: 100px;
background: $content_bg 50% 50% no-repeat; background: $content_bg 50% 50% no-repeat;
background-size: cover; background-size: cover;
position: absolute;
bottom: 0;
left: $gap;
cursor: pointer; cursor: pointer;
position: relative;
input { input {
position: absolute; position: absolute;
@ -21,35 +19,12 @@
height: 100%; height: 100%;
opacity: 0; opacity: 0;
} }
&:hover {
svg {
fill: $red;
}
}
} }
.can_edit { .can_edit {
position: absolute; position: absolute;
top: 0; right: -4px;
left: 0; bottom: -4px;
width: 100%;
height: 100%;
pointer-events: none;
touch-action: none; touch-action: none;
// background: red; pointer-events: none;
display: flex;
align-items: flex-end;
justify-content: flex-end;
padding: $gap / 2;
box-sizing: border-box;
background: linear-gradient(330deg, $content_bg, transparentize($content_bg, 1));
border-radius: $radius;
svg {
width: 32px;
height: 32px;
fill: white;
transition: fill 0.25s;
}
} }

View file

@ -1,28 +0,0 @@
import React, { FC } from 'react';
import styles from './styles.module.scss';
import { Icon } from '~/components/input/Icon';
import { Link } from 'react-router-dom';
interface IProps {
path: string;
}
const ProfileSidebarMenu: FC<IProps> = ({ path }) => {
const cleaned = path.replace(/\/$/, '');
return (
<div className={styles.wrap}>
<Link className={styles.row} to={`${cleaned}/settings`}>
<Icon icon="settings" />
<span>Настройки</span>
</Link>
<div className={styles.row}>
<Icon icon="messages" />
<span>Сообщения</span>
</div>
</div>
);
};
export { ProfileSidebarMenu };

View file

@ -1,47 +0,0 @@
@import "src/styles/variables";
.wrap {
display: flex;
align-items: stretch;
justify-content: center;
flex-direction: column;
margin: 0 $gap;
box-sizing: border-box;
box-shadow: $sidebar_border 0 0 0 1px;
border-radius: $radius;
background-color: transparentize(black, 0.9)
}
.row {
padding: $gap;
font: $font_18_semibold;
box-shadow: $sidebar_border 0 -1px;
cursor: pointer;
background-color: transparentize(black, 1);
transition: all 250ms;
display: flex;
align-items: center;
justify-content: flex-start;
height: 30px;
opacity: 0.5;
text-decoration: none;
fill: white;
color: white;
&:hover {
background-color: transparentize($wisegreen, 0.5);
opacity: 1;
}
&:first-child {
border-radius: $radius $radius 0 0;
}
&:last-child {
border-radius: 0 0 $radius $radius;
}
svg {
margin-right: $gap * 1.2;
}
}

View file

@ -11,6 +11,7 @@ const ProfileSidebarSettings: FC<IProps> = () => (
<div className={styles.scroller}> <div className={styles.scroller}>
<ProfileSettings /> <ProfileSettings />
</div> </div>
<div className={styles.buttons}> <div className={styles.buttons}>
<Filler /> <Filler />
<Button color="outline">Отмена</Button> <Button color="outline">Отмена</Button>

View file

@ -1,7 +1,6 @@
@import "src/styles/variables"; @import "src/styles/variables";
.wrap { .wrap {
@include sidebar_content(600px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -18,7 +17,6 @@
.buttons { .buttons {
width: 100%; width: 100%;
padding: $gap; padding: $gap;
box-shadow: $sidebar_border 0 -1px;
box-sizing: border-box; box-sizing: border-box;
display: grid; display: grid;
grid-template-columns: 1fr auto auto; grid-template-columns: 1fr auto auto;

View file

@ -0,0 +1,28 @@
import React, { FC, useMemo } from 'react';
import styles from './styles.module.scss';
import { Button } from '~/components/input/Button';
import { Filler } from '~/components/containers/Filler';
interface SidebarStackProps {}
const SidebarStack: FC<SidebarStackProps> = ({ children }) => {
const nonEmptyChildren = useMemo(() => {
if (!children) {
return [];
}
return Array.isArray(children) ? children.filter(it => it) : [children];
}, [children]);
return (
<div className={styles.stack}>
{nonEmptyChildren.map((child, i) => (
<div className={styles.card} key={i}>
{child}
</div>
))}
</div>
);
};
export { SidebarStack };

View file

@ -0,0 +1,29 @@
@import "src/styles/variables";
.stack {
background: transparentize($content_bg, 0.1);
display: flex;
flex-direction: row-reverse;
width: auto;
border-radius: $radius 0 0 $radius;
}
.card {
box-shadow: transparentize(white, 0.9) -1px 0, transparentize(black, 0.7) -5px 0 15px;
border-radius: $radius 0 0 $radius;
display: flex;
flex-direction: column;
}
.content {
border-radius: $radius;
height: 100%;
box-sizing: border-box;
overflow: auto;
display: flex;
min-height: 0;
flex-direction: column;
width: 100%;
max-width: 400px;
padding: $gap;
}

View file

@ -0,0 +1,31 @@
import React, { FC, useMemo } from 'react';
import styles from './styles.module.scss';
import { Filler } from '~/components/containers/Filler';
import { Button } from '~/components/input/Button';
interface SidebarStackCardProps {
width?: number;
headerFeature?: 'back' | 'close';
title?: string;
}
const SidebarStackCard: FC<SidebarStackCardProps> = ({ children, title, width, headerFeature }) => {
const style = useMemo(() => ({ maxWidth: width, flexBasis: width }), [width]);
return (
<div style={style} className={styles.card}>
{(headerFeature || title) && (
<div className={styles.head}>
<Filler>{!!title && <h5>{title}</h5>}</Filler>
{headerFeature === 'back' && <Button color="link" iconRight="right" />}
{headerFeature === 'close' && <Button color="link" iconRight="close" />}
</div>
)}
<div className={styles.content}>{children}</div>
</div>
);
};
export { SidebarStackCard };

View file

@ -0,0 +1,26 @@
@import "src/styles/variables";
.card {
width: 100vw;
max-width: 400px;
flex: 1 1 400px;
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.head {
@include row_shadow;
display: flex;
flex-direction: row;
align-items: center;
padding: $gap $gap $gap $gap * 2;
}
.content {
flex: 1;
display: flex;
flex-direction: column;
}

View file

@ -1,16 +1,17 @@
import React, { FC } from 'react'; import React, { FC, MouseEventHandler } from 'react';
import { INode } from '~/types'; import { INode } from '~/types';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { FlowRecentItem } from '~/components/flow/FlowRecentItem'; import { FlowRecentItem } from '~/components/flow/FlowRecentItem';
interface IProps { interface IProps {
nodes: INode[]; nodes: INode[];
onClick?: MouseEventHandler;
} }
const TagSidebarList: FC<IProps> = ({ nodes }) => ( const TagSidebarList: FC<IProps> = ({ nodes, onClick }) => (
<div className={styles.list}> <div className={styles.list}>
{nodes.map(node => ( {nodes.map(node => (
<FlowRecentItem node={node} key={node.id} /> <FlowRecentItem node={node} key={node.id} onClick={onClick} />
))} ))}
</div> </div>
); );

View file

@ -35,6 +35,7 @@ const TagAutocomplete: VFC<TagAutocompleteProps> = ({
const pop = usePopper(wrapper?.current?.parentElement, wrapper.current, { const pop = usePopper(wrapper?.current?.parentElement, wrapper.current, {
placement: 'bottom-end', placement: 'bottom-end',
strategy: 'fixed',
modifiers: [ modifiers: [
{ {
name: 'offset', name: 'offset',

View file

@ -1,12 +1,12 @@
import { IComment, INode, ITag } from '~/types'; import { IComment, INode, ITag } from '~/types';
import { OAuthProvider } from '~/types/auth'; import { OAuthProvider } from '~/types/auth';
import { CONFIG } from '~/utils/config';
export const API = { export const API = {
BASE: process.env.REACT_APP_API_HOST, BASE: CONFIG.apiHost,
USER: { USER: {
LOGIN: '/user/login', LOGIN: '/user/login',
OAUTH_WINDOW: (provider: OAuthProvider) => OAUTH_WINDOW: (provider: OAuthProvider) => `${CONFIG.apiHost}oauth/${provider}/redirect`,
`${process.env.REACT_APP_API_HOST}oauth/${provider}/redirect`,
ME: '/user/', ME: '/user/',
PROFILE: (username: string) => `/user/user/${username}/profile`, PROFILE: (username: string) => `/user/user/${username}/profile`,
MESSAGES: (username: string) => `/user/user/${username}/messages`, MESSAGES: (username: string) => `/user/user/${username}/messages`,

View file

@ -9,6 +9,7 @@ import { PhotoSwipe } from '~/containers/dialogs/PhotoSwipe';
import { EditorCreateDialog } from '~/containers/dialogs/EditorCreateDialog'; import { EditorCreateDialog } from '~/containers/dialogs/EditorCreateDialog';
import { EditorEditDialog } from '~/containers/dialogs/EditorEditDialog'; import { EditorEditDialog } from '~/containers/dialogs/EditorEditDialog';
import { TagSidebar } from '~/containers/sidebars/TagSidebar'; import { TagSidebar } from '~/containers/sidebars/TagSidebar';
import { ProfileSidebar } from '~/containers/sidebars/ProfileSidebar';
export enum Dialog { export enum Dialog {
Login = 'Login', Login = 'Login',
@ -22,6 +23,7 @@ export enum Dialog {
CreateNode = 'CreateNode', CreateNode = 'CreateNode',
EditNode = 'EditNode', EditNode = 'EditNode',
TagSidebar = 'TagNodes', TagSidebar = 'TagNodes',
ProfileSidebar = 'ProfileSidebar',
} }
export const DIALOG_CONTENT = { export const DIALOG_CONTENT = {
@ -36,4 +38,5 @@ export const DIALOG_CONTENT = {
[Dialog.CreateNode]: EditorCreateDialog, [Dialog.CreateNode]: EditorCreateDialog,
[Dialog.EditNode]: EditorEditDialog, [Dialog.EditNode]: EditorEditDialog,
[Dialog.TagSidebar]: TagSidebar, [Dialog.TagSidebar]: TagSidebar,
[Dialog.ProfileSidebar]: ProfileSidebar,
} as const; } as const;

View file

@ -1,5 +1,4 @@
import React, { FC, useCallback, useMemo } from 'react'; import React, { FC, useCallback, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Logo } from '~/components/main/Logo'; import { Logo } from '~/components/main/Logo';
import { Filler } from '~/components/containers/Filler'; import { Filler } from '~/components/containers/Filler';
@ -19,6 +18,7 @@ import { useModal } from '~/hooks/modal/useModal';
import { useScrollTop } from '~/hooks/dom/useScrollTop'; import { useScrollTop } from '~/hooks/dom/useScrollTop';
import { useFlow } from '~/hooks/flow/useFlow'; import { useFlow } from '~/hooks/flow/useFlow';
import { useUpdates } from '~/hooks/updates/useUpdates'; import { useUpdates } from '~/hooks/updates/useUpdates';
import { Anchor } from '~/components/common/Anchor';
type HeaderProps = {}; type HeaderProps = {};
@ -62,32 +62,32 @@ const Header: FC<HeaderProps> = observer(() => {
<div className={styles.plugs}> <div className={styles.plugs}>
<Authorized> <Authorized>
<Link <Anchor
className={classNames(styles.item, { className={classNames(styles.item, {
[styles.has_dot]: hasFlowUpdates, [styles.has_dot]: hasFlowUpdates,
})} })}
to={URLS.BASE} href={URLS.BASE}
> >
ФЛОУ ФЛОУ
</Link> </Anchor>
<Link <Anchor
className={classNames(styles.item, styles.lab, { className={classNames(styles.item, styles.lab, {
[styles.has_dot]: hasLabUpdates, [styles.has_dot]: hasLabUpdates,
})} })}
to={URLS.LAB} href={URLS.LAB}
> >
ЛАБ ЛАБ
</Link> </Anchor>
<Link <Anchor
className={classNames(styles.item, styles.boris, { className={classNames(styles.item, styles.boris, {
[styles.has_dot]: hasBorisUpdates, [styles.has_dot]: hasBorisUpdates,
})} })}
to={URLS.BORIS} href={URLS.BORIS}
> >
БОРИС БОРИС
</Link> </Anchor>
</Authorized> </Authorized>
</div> </div>

View file

@ -1,12 +1,14 @@
import * as React from 'react'; import * as React from 'react';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { Header } from '~/containers/main/Header'; import { Header } from '~/containers/main/Header';
import { SidebarRouter } from '~/containers/main/SidebarRouter';
export const MainLayout = ({ children }) => ( export const MainLayout = ({ children }) => (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className={styles.content}> <div className={styles.content}>
<Header /> <Header />
{children} {children}
<SidebarRouter />
</div> </div>
</div> </div>
); );

View file

@ -1,5 +1,4 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { createPortal } from 'react-dom';
import { Authorized } from '~/components/containers/Authorized'; import { Authorized } from '~/components/containers/Authorized';
import { SubmitBar } from '~/components/bars/SubmitBar'; import { SubmitBar } from '~/components/bars/SubmitBar';
@ -8,13 +7,10 @@ interface IProps {
isLab?: boolean; isLab?: boolean;
} }
const SidebarRouter: FC<IProps> = ({ prefix = '', isLab }) => { const SidebarRouter: FC<IProps> = ({ isLab }) => (
return createPortal( <Authorized>
<Authorized> <SubmitBar isLab={isLab} />
<SubmitBar isLab={isLab} /> </Authorized>
</Authorized>, );
document.body
);
};
export { SidebarRouter }; export { SidebarRouter };

View file

@ -16,6 +16,7 @@ import { NodeComments } from '~/containers/node/NodeComments';
import { useUserContext } from '~/utils/context/UserContextProvider'; import { useUserContext } from '~/utils/context/UserContextProvider';
import { useNodeRelatedContext } from '~/utils/context/NodeRelatedContextProvider'; import { useNodeRelatedContext } from '~/utils/context/NodeRelatedContextProvider';
import { useAuthProvider } from '~/utils/providers/AuthProvider'; import { useAuthProvider } from '~/utils/providers/AuthProvider';
import { Sticky } from '~/components/containers/Sticky';
interface IProps { interface IProps {
commentsOrder: 'ASC' | 'DESC'; commentsOrder: 'ASC' | 'DESC';
@ -52,8 +53,8 @@ const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => {
</Group> </Group>
<div className={styles.panel}> <div className={styles.panel}>
<StickyBox className={styles.sticky} offsetTop={72}> <div className={styles.left}>
<div className={styles.left}> <Sticky>
<div className={styles.left_item}> <div className={styles.left_item}>
<NodeAuthorBlock user={node?.user} /> <NodeAuthorBlock user={node?.user} />
</div> </div>
@ -64,8 +65,8 @@ const NodeBottomBlock: FC<IProps> = ({ commentsOrder }) => {
<div className={styles.left_item}> <div className={styles.left_item}>
<NodeRelatedBlock isLoading={isLoadingRelated} node={node} related={related} /> <NodeRelatedBlock isLoading={isLoadingRelated} node={node} related={related} />
</div> </div>
</div> </Sticky>
</StickyBox> </div>
</div> </div>
</Group> </Group>
</Padder> </Padder>

View file

@ -34,7 +34,6 @@
@media (max-width: 1024px) { @media (max-width: 1024px) {
padding-left: 0; padding-left: 0;
padding-top: $comment_height / 2;
flex: 1 2; flex: 1 2;
} }
} }
@ -49,6 +48,7 @@
.left { .left {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
height: 100%;
} }
.left_item { .left_item {

View file

@ -27,7 +27,9 @@ const ProfileInfo: FC<IProps> = ({ isOwn }) => {
return ( return (
<div> <div>
<Group className={styles.wrap} horizontal> <Group className={styles.wrap} horizontal>
<ProfileAvatar canEdit={isOwn} onChangePhoto={updatePhoto} photo={photo} /> <div className={styles.avatar}>
<ProfileAvatar canEdit={isOwn} onChangePhoto={updatePhoto} photo={photo} />
</div>
<div className={styles.field}> <div className={styles.field}>
<div className={styles.name}> <div className={styles.name}>

View file

@ -8,13 +8,6 @@
} }
.avatar { .avatar {
@include outer_shadow();
border-radius: $radius;
width: 100px;
height: 100px;
background: $content_bg 50% 50% no-repeat;
background-size: cover;
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: $gap; left: $gap;

View file

@ -0,0 +1,25 @@
import React, { VFC } from 'react';
import { Group } from '~/components/containers/Group';
import { Filler } from '~/components/containers/Filler';
import { ProfileAvatar } from '~/components/profile/ProfileAvatar';
import { usePatchUser } from '~/hooks/auth/usePatchUser';
import { useUser } from '~/hooks/auth/useUser';
interface ProfileSidebarHeadProps {}
const ProfileSidebarHead: VFC<ProfileSidebarHeadProps> = () => {
const { user } = useUser();
const { updatePhoto } = usePatchUser();
return (
<Group horizontal>
<ProfileAvatar canEdit onChangePhoto={updatePhoto} photo={user.photo} size={72} />
<Filler>
<h2>{user.fullname || user.username}</h2>
</Filler>
</Group>
);
};
export { ProfileSidebarHead };

View file

@ -0,0 +1,67 @@
import React, { VFC } from 'react';
import styles from './styles.module.scss';
import { ProfileSidebarHead } from '~/containers/profile/ProfileSidebarHead';
import { Filler } from '~/components/containers/Filler';
import classNames from 'classnames';
import markdown from '~/styles/common/markdown.module.scss';
import { Group } from '~/components/containers/Group';
import { Grid } from '~/components/containers/Grid';
import { Card } from '~/components/containers/Card';
import { Square } from '~/components/common/Square';
import { Button } from '~/components/input/Button';
interface ProfileSidebarMenuProps {
onClose: () => void;
}
const ProfileSidebarMenu: VFC<ProfileSidebarMenuProps> = ({ onClose }) => (
<div className={styles.wrap}>
<div>
<ProfileSidebarHead />
</div>
<Filler className={classNames(markdown.wrapper, styles.text)}>
<Group>
<ul className={styles.menu}>
<li>Настройки</li>
<li>Заметки</li>
<li>Удалённые посты</li>
</ul>
<Grid columns="2fr 1fr">
<Card>
<h4>1 год 2 месяца</h4>
<small>в убежище</small>
</Card>
<Card>
<Square>
<h4>24 поста</h4>
<small>Создано</small>
</Square>
</Card>
</Grid>
<Grid columns="1fr 2fr">
<Card>
<Square>
<h4>16545 лайка</h4>
<small>получено</small>
</Square>
</Card>
<Card>
<h4>123123 комментария</h4>
<small>под постами</small>
</Card>
</Grid>
</Group>
</Filler>
<Button round onClick={onClose} color="secondary">
Закрыть
</Button>
</div>
);
export { ProfileSidebarMenu };

View file

@ -0,0 +1,42 @@
@import "../../../styles/variables";
.wrap {
padding: $gap;
box-sizing: border-box;
}
.text {
margin-top: $gap * 2;
}
.menu {
@include outer_shadow;
list-style: none;
border-radius: $radius;
padding: 0 !important;
& > li {
@include row_shadow;
list-style: none;
margin: 0 !important;
padding: $gap;
font: $font_16_semibold;
cursor: pointer;
background-color: transparentize($secondary, 1);
transition: background-color 0.25s;
&:hover {
background-color: transparentize($secondary, 0.5);
}
&:first-child {
border-radius: $radius $radius 0 0;
}
&:last-child {
border-radius: 0 0 $radius $radius;
}
}
}

View file

@ -0,0 +1,27 @@
import React, { VFC } from 'react';
import { SidebarWrapper } from '~/containers/sidebars/SidebarWrapper';
import { DialogComponentProps } from '~/types/modal';
import { ProfileSidebarMenu } from '~/containers/profile/ProfileSidebarMenu';
import { SidebarStack } from '~/components/sidebar/SidebarStack';
import { ProfileSidebarSettings } from '~/components/profile/ProfileSidebarSettings';
import { SidebarStackCard } from '~/components/sidebar/SidebarStackCard';
interface ProfileSidebarProps extends DialogComponentProps {}
const ProfileSidebar: VFC<ProfileSidebarProps> = ({ onRequestClose }) => {
return (
<SidebarWrapper onClose={onRequestClose}>
<SidebarStack>
<SidebarStackCard headerFeature="close" title="Профиль">
<ProfileSidebarMenu onClose={onRequestClose} />
</SidebarStackCard>
<SidebarStackCard width={600} headerFeature="back" title="Настройки">
<ProfileSidebarSettings />
</SidebarStackCard>
</SidebarStack>
</SidebarWrapper>
);
};
export { ProfileSidebar };

View file

@ -8,6 +8,7 @@ import { InfiniteScroll } from '~/components/containers/InfiniteScroll';
import { Tag } from '~/components/tags/Tag'; import { Tag } from '~/components/tags/Tag';
import { useTagNodes } from '~/hooks/tag/useTagNodes'; import { useTagNodes } from '~/hooks/tag/useTagNodes';
import { DialogComponentProps } from '~/types/modal'; import { DialogComponentProps } from '~/types/modal';
import { SidebarStack } from '~/components/sidebar/SidebarStack';
interface TagSidebarProps extends DialogComponentProps { interface TagSidebarProps extends DialogComponentProps {
tag: string; tag: string;
@ -19,43 +20,45 @@ const TagSidebar: VFC<TagSidebarProps> = ({ tag, onRequestClose }) => {
return ( return (
<SidebarWrapper onClose={onRequestClose}> <SidebarWrapper onClose={onRequestClose}>
<div className={styles.wrap}> <SidebarStack>
<div className={styles.content}> <div className={styles.wrap}>
<div className={styles.head}> <div className={styles.content}>
<div className={styles.tag}> <div className={styles.head}>
<Tag tag={{ title }} size="big" /> <div className={styles.tag}>
<Tag tag={{ title }} size="big" />
</div>
{isLoading && (
<div className={styles.sync}>
<LoaderCircle size={20} />
</div>
)}
<div className={styles.close}>
<button onClick={onRequestClose}>
<Icon icon="close" size={32} />
</button>
</div>
</div> </div>
{isLoading && ( {!nodes.length && !isLoading ? (
<div className={styles.sync}> <div className={styles.none}>
<LoaderCircle size={20} /> <Icon icon="sad" size={120} />
<div>
У этого тэга нет постов
<br />
<br />
Такие дела
</div>
</div> </div>
) : (
<InfiniteScroll hasMore={hasMore} loadMore={loadMore} className={styles.list}>
<TagSidebarList nodes={nodes} onClick={onRequestClose} />
</InfiniteScroll>
)} )}
<div className={styles.close}>
<button onClick={onRequestClose}>
<Icon icon="close" size={32} />
</button>
</div>
</div> </div>
{!nodes.length && !isLoading ? (
<div className={styles.none}>
<Icon icon="sad" size={120} />
<div>
У этого тэга нет постов
<br />
<br />
Такие дела
</div>
</div>
) : (
<InfiniteScroll hasMore={hasMore} loadMore={loadMore} className={styles.list}>
<TagSidebarList nodes={nodes} />
</InfiniteScroll>
)}
</div> </div>
</div> </SidebarStack>
</SidebarWrapper> </SidebarWrapper>
); );
}; };

View file

@ -2,6 +2,8 @@
.wrap { .wrap {
@include sidebar_content(400px); @include sidebar_content(400px);
width: 100vw;
max-width: 400px;
} }
.content { .content {
@ -17,11 +19,12 @@
} }
.head { .head {
@include row_shadow;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: $gap; padding: $gap;
box-shadow: transparentize(white, 0.95) 0 1px;
} }
.tag { .tag {

View file

@ -101,8 +101,8 @@ const TagInput: FC<IProps> = ({ exclude, onAppend, onClearTag, onSubmit }) => {
const onAutocompleteSelect = useCallback( const onAutocompleteSelect = useCallback(
(val: string) => { (val: string) => {
onAppend([val]);
setInput(''); setInput('');
onAppend([val]);
}, },
[onAppend, setInput] [onAppend, setInput]
); );

View file

@ -24,7 +24,6 @@ export const useLoginForm = (
try { try {
await fetcher(values.username, values.password); await fetcher(values.username, values.password);
onSuccess(); onSuccess();
showToastSuccess(getRandomPhrase('WELCOME'));
} catch (error) { } catch (error) {
showErrorToast(error); showErrorToast(error);

View file

@ -1,16 +1,23 @@
import { useAuthStore } from '~/store/auth/useAuthStore'; import { useAuthStore } from '~/store/auth/useAuthStore';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { apiUserLogin } from '~/api/auth'; import { apiUserLogin } from '~/api/auth';
import { showErrorToast } from '~/utils/errors/showToast';
import { getRandomPhrase } from '~/constants/phrases';
import { showToastInfo, showToastSuccess } from '~/utils/toast';
export const useLoginLogoutRestore = () => { export const useLoginLogoutRestore = () => {
const auth = useAuthStore(); const auth = useAuthStore();
const logout = useCallback(() => auth.logout(), [auth]); const logout = useCallback(() => {
auth.logout();
showToastInfo(getRandomPhrase('GOODBYE'));
}, [auth]);
const login = useCallback( const login = useCallback(
async (username: string, password: string) => { async (username: string, password: string) => {
const result = await apiUserLogin({ username, password }); const result = await apiUserLogin({ username, password });
auth.setToken(result.token); auth.setToken(result.token);
showToastInfo(getRandomPhrase('WELCOME'));
return result.user; return result.user;
}, },
[auth] [auth]

View file

@ -20,7 +20,7 @@ export const useMessageEventReactions = () => {
return; return;
} }
console.log('caught event:', type); console.log('caught event:', type, event.data);
switch (type) { switch (type) {
case EventMessageType.OAuthLogin: case EventMessageType.OAuthLogin:

View file

@ -1,11 +1,11 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useHistory } from 'react-router';
import { useModal } from '~/hooks/modal/useModal'; import { useModal } from '~/hooks/modal/useModal';
import { Dialog } from '~/constants/modal'; import { Dialog } from '~/constants/modal';
import { useNavigation } from '~/hooks/navigation/useNavigation';
/** redirects to the password redirect modal */ /** redirects to the password redirect modal */
export const useRestorePasswordRedirect = () => { export const useRestorePasswordRedirect = () => {
const history = useHistory(); const { push } = useNavigation();
const { showModal } = useModal(); const { showModal } = useModal();
useEffect(() => { useEffect(() => {
@ -15,7 +15,7 @@ export const useRestorePasswordRedirect = () => {
return; return;
} }
history.push('/'); push('/');
showModal(Dialog.RestorePassword, { code: match[1] }); showModal(Dialog.RestorePassword, { code: match[1] });
}, [showModal, history]); }, [showModal, push]);
}; };

View file

@ -0,0 +1,17 @@
const getHeight = () => {
if (typeof document === 'undefined') {
return 0;
}
const body = document.body;
const html = document.documentElement;
return Math.max(
body.scrollHeight,
body.offsetHeight,
html.clientHeight,
html.scrollHeight,
html.offsetHeight
);
};
export const useScrollHeight = () => getHeight();

View file

@ -0,0 +1,10 @@
import { useCallback } from 'react';
import { useScrollHeight } from '~/hooks/dom/useScrollHeight';
export const useScrollToBottom = () => {
const top = useScrollHeight();
return useCallback(() => {
window.scrollTo({ top });
}, [top]);
};

View file

@ -14,7 +14,6 @@ export const useScrollToTop = (deps?: any[]) => {
const bounds = targetElement.getBoundingClientRect(); const bounds = targetElement.getBoundingClientRect();
window.scrollTo({ window.scrollTo({
top: bounds.top - 100, top: bounds.top - 100,
behavior: 'smooth',
}); });
}, },
deps && Array.isArray(deps) ? deps : [] deps && Array.isArray(deps) ? deps : []

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
export const useScrollTop = () => { export const useScrollTop = () => {
const [top, setTop] = useState(0); const [top, setTop] = useState(typeof window !== 'undefined' ? window.scrollY : 0);
useEffect(() => { useEffect(() => {
const onScroll = () => { const onScroll = () => {

View file

@ -2,7 +2,7 @@ import { useCallback, useState } from 'react';
import { getNodeDiff } from '~/api/node'; import { getNodeDiff } from '~/api/node';
import { uniq } from 'ramda'; import { uniq } from 'ramda';
import { useFlowStore } from '~/store/flow/useFlowStore'; import { useFlowStore } from '~/store/flow/useFlowStore';
import { runInAction } from 'mobx'; import { runInAction, toJS } from 'mobx';
import { showErrorToast } from '~/utils/errors/showToast'; import { showErrorToast } from '~/utils/errors/showToast';
export const useFlowLoader = () => { export const useFlowLoader = () => {
@ -81,5 +81,7 @@ export const useFlowLoader = () => {
} }
}, [flow]); }, [flow]);
console.log(toJS(flow.nodes));
return { getInitialNodes, isSyncing, loadMore }; return { getInitialNodes, isSyncing, loadMore };
}; };

View file

@ -0,0 +1,22 @@
import { useCallback } from 'react';
import { CONFIG } from '~/utils/config';
import { useRouter } from 'next/router';
import { useHistory } from 'react-router';
export const useNavigation = () => {
const nextRouter = useRouter();
const craHistory = useHistory();
const push = useCallback(
(url: string) => {
if (CONFIG.isNextEnvironment) {
nextRouter.push(url);
} else {
craHistory.push(url);
}
},
[craHistory, nextRouter]
);
return { push };
};

View file

@ -1,10 +1,10 @@
import { INode } from '~/types'; import { INode } from '~/types';
import { useHistory } from 'react-router';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { URLS } from '~/constants/urls'; import { URLS } from '~/constants/urls';
import { useNavigation } from '~/hooks/navigation/useNavigation';
// useGotoNode returns fn, that navigates to node // useGotoNode returns fn, that navigates to node
export const useGotoNode = (id: INode['id']) => { export const useGotoNode = (id: INode['id']) => {
const history = useHistory(); const { push } = useNavigation();
return useCallback(() => history.push(URLS.NODE_URL(id)), [history, id]); return useCallback(() => push(URLS.NODE_URL(id)), [push, id]);
}; };

View file

@ -0,0 +1,9 @@
import { useRouteMatch } from 'react-router';
import { useRouter } from 'next/router';
import { CONFIG } from '~/utils/config';
export const useNodePageParams = () => {
return CONFIG.isNextEnvironment
? (useRouter().query.id as string)
: useRouteMatch<{ id: string }>().params.id;
};

View file

@ -2,7 +2,6 @@ import React, { FC } from 'react';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { Group } from '~/components/containers/Group'; import { Group } from '~/components/containers/Group';
import { Container } from '~/containers/main/Container'; import { Container } from '~/containers/main/Container';
import StickyBox from 'react-sticky-box';
import { BorisComments } from '~/containers/boris/BorisComments'; import { BorisComments } from '~/containers/boris/BorisComments';
import { Card } from '~/components/containers/Card'; import { Card } from '~/components/containers/Card';
import { SidebarRouter } from '~/containers/main/SidebarRouter'; import { SidebarRouter } from '~/containers/main/SidebarRouter';
@ -14,6 +13,7 @@ import { BorisUIDemo } from '~/components/boris/BorisUIDemo';
import boris from '~/sprites/boris_robot.svg'; import boris from '~/sprites/boris_robot.svg';
import { useAuthProvider } from '~/utils/providers/AuthProvider'; import { useAuthProvider } from '~/utils/providers/AuthProvider';
import { Sticky } from '~/components/containers/Sticky';
type IProps = { type IProps = {
title: string; title: string;
@ -56,7 +56,7 @@ const BorisLayout: FC<IProps> = ({ title, setIsBetaTester, isTester, stats, isLo
</Card> </Card>
<Group className={styles.stats}> <Group className={styles.stats}>
<StickyBox className={styles.sticky} offsetTop={72} offsetBottom={10}> <Sticky>
<BorisSidebar <BorisSidebar
isTester={isTester} isTester={isTester}
stats={stats} stats={stats}
@ -64,7 +64,7 @@ const BorisLayout: FC<IProps> = ({ title, setIsBetaTester, isTester, stats, isLo
isUser={isUser} isUser={isUser}
isLoading={isLoadingStats} isLoading={isLoadingStats}
/> />
</StickyBox> </Sticky>
</Group> </Group>
</div> </div>
</div> </div>

View file

@ -17,6 +17,8 @@ import { useNodeContext } from '~/utils/context/NodeContextProvider';
import { useNodePermissions } from '~/hooks/node/useNodePermissions'; import { useNodePermissions } from '~/hooks/node/useNodePermissions';
import { useNodeActions } from '~/hooks/node/useNodeActions'; import { useNodeActions } from '~/hooks/node/useNodeActions';
import { NodeTitle } from '~/components/node/NodeTitle'; import { NodeTitle } from '~/components/node/NodeTitle';
import { ScrollHelperBottom } from '~/components/common/ScrollHelperBottom';
import { Superpower } from '~/components/boris/Superpower';
type IProps = {}; type IProps = {};
@ -65,7 +67,9 @@ const NodeLayout: FC<IProps> = () => {
<SidebarRouter prefix="/post:id" /> <SidebarRouter prefix="/post:id" />
<Route path={URLS.NODE_EDIT_URL(':id')} component={EditorEditDialog} /> <Superpower>
<ScrollHelperBottom />
</Superpower>
</div> </div>
); );
}; };

50
src/pages/_app.tsx Normal file
View file

@ -0,0 +1,50 @@
import { StoreContextProvider } from '~/utils/context/StoreContextProvider';
import { getMOBXStore } from '~/store';
import '~/styles/main.scss';
import { ToastProvider } from '~/utils/providers/ToastProvider';
import { Modal } from '~/containers/dialogs/Modal';
import { Sprites } from '~/sprites/Sprites';
import React from 'react';
import { BottomContainer } from '~/containers/main/BottomContainer';
import { SWRConfigProvider } from '~/utils/providers/SWRConfigProvider';
import { UserContextProvider } from '~/utils/context/UserContextProvider';
import { DragDetectorProvider } from '~/hooks/dom/useDragDetector';
import { PageCoverProvider } from '~/components/containers/PageCoverProvider';
import { SearchProvider } from '~/utils/providers/SearchProvider';
import { AudioPlayerProvider } from '~/utils/providers/AudioPlayerProvider';
import { MetadataProvider } from '~/utils/providers/MetadataProvider';
import { AuthProvider } from '~/utils/providers/AuthProvider';
import { MainLayout } from '~/containers/main/MainLayout';
import { useGlobalLoader } from '~/hooks/dom/useGlobalLoader';
const mobxStore = getMOBXStore();
export default function MyApp({ Component, pageProps }) {
return (
<StoreContextProvider store={mobxStore}>
<SWRConfigProvider>
<UserContextProvider>
<DragDetectorProvider>
<PageCoverProvider>
<SearchProvider>
<AudioPlayerProvider>
<MetadataProvider>
<AuthProvider>
<MainLayout>
<ToastProvider />
<Modal />
<Sprites />
<Component {...pageProps} />
</MainLayout>
<BottomContainer />
</AuthProvider>
</MetadataProvider>
</AudioPlayerProvider>
</SearchProvider>
</PageCoverProvider>
</DragDetectorProvider>
</UserContextProvider>
</SWRConfigProvider>
</StoreContextProvider>
);
}

View file

@ -3,10 +3,13 @@ import { FlowLayout } from '~/layouts/FlowLayout';
import { useFlow } from '~/hooks/flow/useFlow'; import { useFlow } from '~/hooks/flow/useFlow';
import { FlowContextProvider } from '~/utils/context/FlowContextProvider'; import { FlowContextProvider } from '~/utils/context/FlowContextProvider';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useGlobalLoader } from '~/hooks/dom/useGlobalLoader';
interface Props {} interface Props {}
const FlowPage: FC<Props> = observer(() => { const FlowPage: FC<Props> = observer(() => {
useGlobalLoader();
const { const {
updates, updates,
nodes, nodes,

View file

@ -12,67 +12,63 @@ import { useNodePermissions } from '~/hooks/node/useNodePermissions';
import { NodeRelatedProvider } from '~/utils/providers/NodeRelatedProvider'; import { NodeRelatedProvider } from '~/utils/providers/NodeRelatedProvider';
import { useLoadNode } from '~/hooks/node/useLoadNode'; import { useLoadNode } from '~/hooks/node/useLoadNode';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useNodePageParams } from '~/hooks/node/useNodePageParams';
type Props = RouteComponentProps<{ id: string }> & {}; type Props = RouteComponentProps<{ id: string }> & {};
const NodePage: FC<Props> = observer( const NodePage: FC<Props> = observer(() => {
({ const id = useNodePageParams();
match: { const { node, isLoading, update, lastSeen } = useLoadNode(parseInt(id, 10));
params: { id },
},
}) => {
const { node, isLoading, update, lastSeen } = useLoadNode(parseInt(id, 10));
const onShowImageModal = useImageModal(); const onShowImageModal = useImageModal();
const { const {
onLoadMoreComments, onLoadMoreComments,
onDelete: onDeleteComment, onDelete: onDeleteComment,
onEdit: onSaveComment, onEdit: onSaveComment,
comments, comments,
hasMore, hasMore,
isLoading: isLoadingComments, isLoading: isLoadingComments,
} = useNodeComments(parseInt(id, 10)); } = useNodeComments(parseInt(id, 10));
const { onDelete: onTagDelete, onChange: onTagsChange, onClick: onTagClick } = useNodeTags( const { onDelete: onTagDelete, onChange: onTagsChange, onClick: onTagClick } = useNodeTags(
parseInt(id, 10) parseInt(id, 10)
); );
const [canEdit] = useNodePermissions(node); const [canEdit] = useNodePermissions(node);
useScrollToTop([id, isLoadingComments]); useScrollToTop([id, isLoadingComments]);
if (!node) { if (!node) {
// TODO: do something here // TODO: do something here
return null; return null;
}
return (
<NodeContextProvider node={node} isLoading={isLoading} update={update}>
<NodeRelatedProvider id={parseInt(id, 10)} tags={node.tags}>
<CommentContextProvider
onSaveComment={onSaveComment}
comments={comments}
hasMore={hasMore}
lastSeenCurrent={lastSeen}
isLoading={isLoadingComments}
onShowImageModal={onShowImageModal}
onLoadMoreComments={onLoadMoreComments}
onDeleteComment={onDeleteComment}
>
<TagsContextProvider
tags={node.tags}
canAppend={canEdit}
canDelete={canEdit}
isLoading={isLoading}
onChange={onTagsChange}
onTagClick={onTagClick}
onTagDelete={onTagDelete}
>
<NodeLayout />
</TagsContextProvider>
</CommentContextProvider>
</NodeRelatedProvider>
</NodeContextProvider>
);
} }
);
return (
<NodeContextProvider node={node} isLoading={isLoading} update={update}>
<NodeRelatedProvider id={parseInt(id, 10)} tags={node.tags}>
<CommentContextProvider
onSaveComment={onSaveComment}
comments={comments}
hasMore={hasMore}
lastSeenCurrent={lastSeen}
isLoading={isLoadingComments}
onShowImageModal={onShowImageModal}
onLoadMoreComments={onLoadMoreComments}
onDeleteComment={onDeleteComment}
>
<TagsContextProvider
tags={node.tags}
canAppend={canEdit}
canDelete={canEdit}
isLoading={isLoading}
onChange={onTagsChange}
onTagClick={onTagClick}
onTagDelete={onTagDelete}
>
<NodeLayout />
</TagsContextProvider>
</CommentContextProvider>
</NodeRelatedProvider>
</NodeContextProvider>
);
});
export default NodePage; export default NodePage;

View file

@ -2,6 +2,7 @@ import { IUser } from '~/types/auth';
import { EMPTY_USER } from '~/constants/auth'; import { EMPTY_USER } from '~/constants/auth';
import { makeAutoObservable } from 'mobx'; import { makeAutoObservable } from 'mobx';
import { isHydrated, makePersistable } from 'mobx-persist-store'; import { isHydrated, makePersistable } from 'mobx-persist-store';
import { CONFIG } from '~/utils/config';
export class AuthStore { export class AuthStore {
token: string = ''; token: string = '';
@ -12,7 +13,7 @@ export class AuthStore {
makeAutoObservable(this); makeAutoObservable(this);
void makePersistable(this, { void makePersistable(this, {
name: `vault48_auth_${process.env.REACT_APP_API_URL}`, name: `vault48_auth_${CONFIG.apiHost}`,
properties: ['token', 'user', 'isTesterInternal'], properties: ['token', 'user', 'isTesterInternal'],
storage: typeof window !== 'undefined' ? window.localStorage : undefined, storage: typeof window !== 'undefined' ? window.localStorage : undefined,
}); });

View file

@ -3,6 +3,7 @@ import { FlowStore } from '~/store/flow/FlowStore';
import { ModalStore } from '~/store/modal/ModalStore'; import { ModalStore } from '~/store/modal/ModalStore';
import { LabStore } from '~/store/lab/LabStore'; import { LabStore } from '~/store/lab/LabStore';
import { AuthStore } from '~/store/auth/AuthStore'; import { AuthStore } from '~/store/auth/AuthStore';
import { useStaticRendering } from 'mobx-react-lite';
export class Store { export class Store {
flow = new FlowStore(); flow = new FlowStore();
@ -22,3 +23,6 @@ export class Store {
const defaultStore = new Store(); const defaultStore = new Store();
export const getMOBXStore = () => defaultStore; export const getMOBXStore = () => defaultStore;
// eslint-disable-next-line react-hooks/rules-of-hooks
useStaticRendering(typeof window === 'undefined');

View file

@ -128,3 +128,36 @@ button {
background-color: transparent; background-color: transparent;
outline: none; outline: none;
} }
h5, h4, h3, h2, h1 {
color: white;
font-weight: 800;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.8em;
}
h3 {
font-size: 1.6em;
}
h4 {
font-size: 1.4em;
}
h5 {
font-size: 1.2em;
}
p {
margin-bottom: $gap;
&:last-child {
margin-bottom: 0;
}
}

View file

@ -164,8 +164,6 @@
flex: 0 1 $width; flex: 0 1 $width;
max-width: 100vw; max-width: 100vw;
position: relative; position: relative;
background: transparentize($content_bg, 0.1);
box-shadow: transparentize(white, 0.95) -1px 0;
border-radius: $radius 0 0 $radius; border-radius: $radius 0 0 $radius;
} }

View file

@ -53,17 +53,7 @@ $margin: 1em;
margin-bottom: $margin; margin-bottom: $margin;
} }
p {
margin-bottom: $margin;
&:last-child {
margin-bottom: 0;
}
}
h5, h4, h3, h2, h1 { h5, h4, h3, h2, h1 {
color: white;
font-weight: 800;
line-height: 1.2em; line-height: 1.2em;
margin: $margin * 1.5 0 $margin / 2; margin: $margin * 1.5 0 $margin / 2;
@ -72,26 +62,6 @@ $margin: 1em;
} }
} }
h1 {
font-size: 2em;
}
h2 {
font-size: 1.8em;
}
h3 {
font-size: 1.6em;
}
h4 {
font-size: 1.4em;
}
h5 {
font-size: 1.2em;
}
ul { ul {
list-style: disc; list-style: disc;
padding-left: 20px; padding-left: 20px;

View file

@ -0,0 +1,7 @@
export const CONFIG = {
apiHost: process.env.REACT_APP_API_HOST || process.env.NEXT_PUBLIC_API_HOST || '',
remoteCurrent:
process.env.REACT_APP_REMOTE_CURRENT || process.env.NEXT_PUBLIC_REMOTE_CURRENT || '',
// transitional prop, marks migration to nextjs
isNextEnvironment: !!process.env.NEXT_PUBLIC_REMOTE_CURRENT || typeof window === 'undefined',
};

View file

@ -19,6 +19,7 @@ import {
formatTextTodos, formatTextTodos,
} from '~/utils/formatText'; } from '~/utils/formatText';
import { splitTextByYoutube, splitTextOmitEmpty } from '~/utils/splitText'; import { splitTextByYoutube, splitTextOmitEmpty } from '~/utils/splitText';
import { CONFIG } from '~/utils/config';
function polarToCartesian(centerX, centerY, radius, angleInDegrees) { function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0; const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
@ -67,13 +68,10 @@ export const getURLFromString = (
size?: typeof PRESETS[keyof typeof PRESETS] size?: typeof PRESETS[keyof typeof PRESETS]
): string => { ): string => {
if (size) { if (size) {
return (url || '').replace( return (url || '').replace('REMOTE_CURRENT://', `${CONFIG.remoteCurrent}cache/${size}/`);
'REMOTE_CURRENT://',
`${process.env.REACT_APP_REMOTE_CURRENT}cache/${size}/`
);
} }
return (url || '').replace('REMOTE_CURRENT://', process.env.REACT_APP_REMOTE_CURRENT); return (url || '').replace('REMOTE_CURRENT://', CONFIG.remoteCurrent);
}; };
export const getURL = ( export const getURL = (

View file

@ -1,4 +1,12 @@
import React, { createContext, FC, useCallback, useContext, useEffect, useState } from 'react'; import React, {
createContext,
FC,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { IFile } from '~/types'; import { IFile } from '~/types';
import { getURL } from '~/utils/dom'; import { getURL } from '~/utils/dom';
import { path } from 'ramda'; import { path } from 'ramda';
@ -33,41 +41,47 @@ const PlayerContext = createContext<AudioPlayerProps>({
toPercent: () => {}, toPercent: () => {},
}); });
const audio = new Audio();
export const AudioPlayerProvider: FC = ({ children }) => { export const AudioPlayerProvider: FC = ({ children }) => {
const audio = useRef(typeof Audio !== 'undefined' ? new Audio() : undefined).current;
const [status, setStatus] = useState(PlayerState.UNSET); const [status, setStatus] = useState(PlayerState.UNSET);
const [file, setFile] = useState<IFile | undefined>(); const [file, setFile] = useState<IFile | undefined>();
const [progress, setProgress] = useState<PlayerProgress>({ progress: 0, current: 0, total: 0 }); const [progress, setProgress] = useState<PlayerProgress>({ progress: 0, current: 0, total: 0 });
/** controls */ /** controls */
const play = audio.play.bind(audio); const play = async () => audio?.play();
const pause = audio.pause.bind(audio); const pause = () => audio?.pause();
const stop = useCallback(() => { const stop = useCallback(() => {
audio.pause(); audio?.pause();
audio.dispatchEvent(new CustomEvent('stop')); audio?.dispatchEvent(new CustomEvent('stop'));
setFile(undefined); setFile(undefined);
setStatus(PlayerState.UNSET); setStatus(PlayerState.UNSET);
}, [setFile]); }, [audio, setFile]);
const toTime = useCallback((time: number) => { const toTime = useCallback(
audio.currentTime = time; (time: number) => {
}, []); if (!audio) {
return;
}
audio.currentTime = time;
},
[audio]
);
const toPercent = useCallback( const toPercent = useCallback(
(percent: number) => { (percent: number) => {
audio.currentTime = (progress.total * percent) / 100; toTime((progress.total * percent) / 100);
}, },
[progress] [progress, toTime]
); );
/** handles progress update */ /** handles progress update */
useEffect(() => { useEffect(() => {
const onProgress = () => { const onProgress = () => {
setProgress({ setProgress({
total: audio.duration, total: audio?.duration ?? 0,
current: audio.currentTime, current: audio?.currentTime ?? 0,
progress: (audio.currentTime / audio.duration) * 100, progress: audio ? (audio.currentTime / audio.duration) * 100 : 0,
}); });
}; };
@ -79,23 +93,25 @@ export const AudioPlayerProvider: FC = ({ children }) => {
setStatus(PlayerState.PLAYING); setStatus(PlayerState.PLAYING);
}; };
audio.addEventListener('playprogress', onProgress); audio?.addEventListener('playprogress', onProgress);
audio.addEventListener('timeupdate', onProgress); audio?.addEventListener('timeupdate', onProgress);
audio.addEventListener('pause', onPause); audio?.addEventListener('pause', onPause);
audio.addEventListener('playing', onPlay); audio?.addEventListener('playing', onPlay);
return () => { return () => {
audio.removeEventListener('playprogress', onProgress); audio?.removeEventListener('playprogress', onProgress);
audio.removeEventListener('timeupdate', onProgress); audio?.removeEventListener('timeupdate', onProgress);
audio.removeEventListener('pause', onPause); audio?.removeEventListener('pause', onPause);
audio.removeEventListener('playing', onPlay); audio?.removeEventListener('playing', onPlay);
}; };
}, []); }, [audio]);
/** update audio src */ /** update audio src */
useEffect(() => { useEffect(() => {
if (!audio) return;
audio.src = file ? getURL(file) : ''; audio.src = file ? getURL(file) : '';
}, [file]); }, [file, audio]);
const metadata: IFile['metadata'] = path(['metadata'], file); const metadata: IFile['metadata'] = path(['metadata'], file);
const title = const title =

View file

@ -2,6 +2,7 @@
.toast { .toast {
@include outer_shadow; @include outer_shadow;
cursor: pointer; cursor: pointer;
font: $font_14_semibold; font: $font_14_semibold;
user-select: none; user-select: none;

View file

@ -16,3 +16,8 @@ export type ButtonProps = React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>, React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement HTMLButtonElement
>; >;
export type LinkProps = React.DetailedHTMLProps<
React.AnchorHTMLAttributes<HTMLAnchorElement>,
HTMLAnchorElement
>;

View file

@ -14,7 +14,7 @@
"strict": true, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"module": "esnext", "module": "commonjs",
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,

1
tsconfig.tsbuildinfo Normal file

File diff suppressed because one or more lines are too long

201
yarn.lock
View file

@ -1036,7 +1036,7 @@
dependencies: dependencies:
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.1.5", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": "@babel/runtime@^7.1.2", "@babel/runtime@^7.1.5", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.16.7" version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa"
integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==
@ -1496,50 +1496,6 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9"
integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA== integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==
"@redux-saga/core@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@redux-saga/core/-/core-1.1.3.tgz#3085097b57a4ea8db5528d58673f20ce0950f6a4"
integrity sha512-8tInBftak8TPzE6X13ABmEtRJGjtK17w7VUs7qV17S8hCO5S3+aUTWZ/DBsBJPdE8Z5jOPwYALyvofgq1Ws+kg==
dependencies:
"@babel/runtime" "^7.6.3"
"@redux-saga/deferred" "^1.1.2"
"@redux-saga/delay-p" "^1.1.2"
"@redux-saga/is" "^1.1.2"
"@redux-saga/symbols" "^1.1.2"
"@redux-saga/types" "^1.1.0"
redux "^4.0.4"
typescript-tuple "^2.2.1"
"@redux-saga/deferred@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@redux-saga/deferred/-/deferred-1.1.2.tgz#59937a0eba71fff289f1310233bc518117a71888"
integrity sha512-908rDLHFN2UUzt2jb4uOzj6afpjgJe3MjICaUNO3bvkV/kN/cNeI9PMr8BsFXB/MR8WTAZQq/PlTq8Kww3TBSQ==
"@redux-saga/delay-p@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@redux-saga/delay-p/-/delay-p-1.1.2.tgz#8f515f4b009b05b02a37a7c3d0ca9ddc157bb355"
integrity sha512-ojc+1IoC6OP65Ts5+ZHbEYdrohmIw1j9P7HS9MOJezqMYtCDgpkoqB5enAAZrNtnbSL6gVCWPHaoaTY5KeO0/g==
dependencies:
"@redux-saga/symbols" "^1.1.2"
"@redux-saga/is@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@redux-saga/is/-/is-1.1.2.tgz#ae6c8421f58fcba80faf7cadb7d65b303b97e58e"
integrity sha512-OLbunKVsCVNTKEf2cH4TYyNbbPgvmZ52iaxBD4I1fTif4+MTXMa4/Z07L83zW/hTCXwpSZvXogqMqLfex2Tg6w==
dependencies:
"@redux-saga/symbols" "^1.1.2"
"@redux-saga/types" "^1.1.0"
"@redux-saga/symbols@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@redux-saga/symbols/-/symbols-1.1.2.tgz#216a672a487fc256872b8034835afc22a2d0595d"
integrity sha512-EfdGnF423glv3uMwLsGAtE6bg+R9MdqlHEzExnfagXPrIiuxwr3bdiAwz3gi+PsrQ3yBlaBpfGLtDG8rf3LgQQ==
"@redux-saga/types@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@redux-saga/types/-/types-1.1.0.tgz#0e81ce56b4883b4b2a3001ebe1ab298b84237204"
integrity sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg==
"@rollup/plugin-babel@^5.2.0": "@rollup/plugin-babel@^5.2.0":
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879" resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879"
@ -1931,14 +1887,6 @@
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.9.tgz#1cfb6d60ef3822c589f18e70f8b12f9a28ce8724" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.9.tgz#1cfb6d60ef3822c589f18e70f8b12f9a28ce8724"
integrity sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ== integrity sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==
"@types/hoist-non-react-statics@^3.3.0":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/html-minifier-terser@^6.0.0": "@types/html-minifier-terser@^6.0.0":
version "6.1.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35"
@ -2050,16 +1998,6 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-redux@^7.1.11", "@types/react-redux@^7.1.20":
version "7.1.21"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.21.tgz#f32bbaf7cbc4b93f51e10d340aa54035c084b186"
integrity sha512-bLdglUiBSQNzWVVbmNPKGYYjrzp3/YDPwfOH3nLEz99I4awLlaRAPWjo6bZ2POpxztFWtDDXIPxBLVykXqBt+w==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-router-dom@^5.1.7": "@types/react-router-dom@^5.1.7":
version "5.3.2" version "5.3.2"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.2.tgz#ebd8e145cf056db5c66eb1dac63c72f52e8542ee" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.2.tgz#ebd8e145cf056db5c66eb1dac63c72f52e8542ee"
@ -3449,7 +3387,7 @@ cjs-module-lexer@^1.0.0:
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40"
integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==
classnames@*, classnames@^2.2.6: classnames@*, classnames@^2.0.0, classnames@^2.2.6:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
@ -3652,17 +3590,6 @@ connect-history-api-fallback@^1.6.0:
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
connected-react-router@^6.5.2:
version "6.9.2"
resolved "https://registry.yarnpkg.com/connected-react-router/-/connected-react-router-6.9.2.tgz#f89fa87f0e977fcabf17475fb4552e170cc7e48e"
integrity sha512-bE8kNBiZv9Mivp7pYn9JvLH5ItTjLl45kk1/Vha0rmAK9I/ETb5JPJrAm0h2KCG9qLfv7vqU3Jo4UUDo0oJnQg==
dependencies:
lodash.isequalwith "^4.4.0"
prop-types "^15.7.2"
optionalDependencies:
immutable "^3.8.1 || ^4.0.0"
seamless-immutable "^7.1.3"
console-control-strings@^1.0.0, console-control-strings@~1.1.0: console-control-strings@^1.0.0, console-control-strings@~1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
@ -3722,7 +3649,7 @@ core-js-pure@^3.19.0, core-js-pure@^3.8.1:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.20.2.tgz#5d263565f0e34ceeeccdc4422fae3e84ca6b8c0f" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.20.2.tgz#5d263565f0e34ceeeccdc4422fae3e84ca6b8c0f"
integrity sha512-CmWHvSKn2vNL6p6StNp1EmMIfVY/pqn3JLAjfZQ8WZGPOlGoO92EkX9/Mk81i6GxvoPXjUqEQnpM3rJ5QxxIOg== integrity sha512-CmWHvSKn2vNL6p6StNp1EmMIfVY/pqn3JLAjfZQ8WZGPOlGoO92EkX9/Mk81i6GxvoPXjUqEQnpM3rJ5QxxIOg==
core-js@^3.19.2: core-js@^3.19.2, core-js@^3.6.5:
version "3.20.2" version "3.20.2"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.20.2.tgz#46468d8601eafc8b266bd2dd6bf9dee622779581" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.20.2.tgz#46468d8601eafc8b266bd2dd6bf9dee622779581"
integrity sha512-nuqhq11DcOAbFBV4zCbKeGbKQsUDRqTX0oqx7AttUBuqe3h20ixsE039QHelbL6P4h+9kytVqyEtyZ6gsiwEYw== integrity sha512-nuqhq11DcOAbFBV4zCbKeGbKQsUDRqTX0oqx7AttUBuqe3h20ixsE039QHelbL6P4h+9kytVqyEtyZ6gsiwEYw==
@ -4521,7 +4448,7 @@ encoding@0.1.13:
dependencies: dependencies:
iconv-lite "^0.6.2" iconv-lite "^0.6.2"
enhanced-resolve@^5.8.3: enhanced-resolve@^5.7.0, enhanced-resolve@^5.8.3:
version "5.8.3" version "5.8.3"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0"
integrity sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA== integrity sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==
@ -4898,6 +4825,11 @@ etag@1.8.1, etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
eventemitter3@^3.0.0:
version "3.1.2"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==
eventemitter3@^4.0.0: eventemitter3@^4.0.0:
version "4.0.7" version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
@ -5614,7 +5546,7 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0" minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1" minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@ -5853,11 +5785,6 @@ immer@^9.0.7:
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.7.tgz#b6156bd7db55db7abc73fd2fdadf4e579a701075" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.7.tgz#b6156bd7db55db7abc73fd2fdadf4e579a701075"
integrity sha512-KGllzpbamZDvOIxnmJ0jI840g7Oikx58lBPWV0hUh7dtAyZpFqqrBZdKka5GlTwMTZ1Tjc/bKKW4VSFAt6BqMA== integrity sha512-KGllzpbamZDvOIxnmJ0jI840g7Oikx58lBPWV0hUh7dtAyZpFqqrBZdKka5GlTwMTZ1Tjc/bKKW4VSFAt6BqMA==
"immutable@^3.8.1 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@ -7040,11 +6967,6 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
lodash.isequalwith@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.isequalwith/-/lodash.isequalwith-4.4.0.tgz#266726ddd528f854f21f4ea98a065606e0fbc6b0"
integrity sha1-Jmcm3dUo+FTyH06pigZWBuD7xrA=
lodash.memoize@^4.1.2: lodash.memoize@^4.1.2:
version "4.1.2" version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@ -7317,18 +7239,11 @@ mobx-persist-store@^1.0.4:
resolved "https://registry.yarnpkg.com/mobx-persist-store/-/mobx-persist-store-1.0.4.tgz#233f0c03a708c0f58948c5ff223aa59610a2711a" resolved "https://registry.yarnpkg.com/mobx-persist-store/-/mobx-persist-store-1.0.4.tgz#233f0c03a708c0f58948c5ff223aa59610a2711a"
integrity sha512-zxYvRSGKIJG/2brJC8+u6rJOnhe6HvvIOSaPSoOKdRFw4R91EV+e8PsraiScH3STNkGQlArbjnAUpd7cga1VCA== integrity sha512-zxYvRSGKIJG/2brJC8+u6rJOnhe6HvvIOSaPSoOKdRFw4R91EV+e8PsraiScH3STNkGQlArbjnAUpd7cga1VCA==
mobx-react-lite@^3.2.0: mobx-react-lite@^3.2.3:
version "3.2.3" version "3.2.3"
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.2.3.tgz#83d2b32ebf4383cd0dc0d397acbf53a8e9c66765" resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.2.3.tgz#83d2b32ebf4383cd0dc0d397acbf53a8e9c66765"
integrity sha512-7exWp1FV0M9dP08H9PIeHlJqDw4IdkQVRMfLYaZFMmlbzSS6ZU6p/kx392KN+rVf81hH3IQYewvRGQ70oiwmbw== integrity sha512-7exWp1FV0M9dP08H9PIeHlJqDw4IdkQVRMfLYaZFMmlbzSS6ZU6p/kx392KN+rVf81hH3IQYewvRGQ70oiwmbw==
mobx-react@^7.2.1:
version "7.2.1"
resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-7.2.1.tgz#e9d4c04dc63d05e1139ce773f5fee7a5b4cb7c78"
integrity sha512-LZS99KFLn75VWDXPdRJhILzVQ7qLcRjQbzkK+wVs0Qg4kWw5hOI2USp7tmu+9zP9KYsVBmKyx2k/8cTTBfsymw==
dependencies:
mobx-react-lite "^3.2.0"
mobx@^6.3.10: mobx@^6.3.10:
version "6.3.12" version "6.3.12"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.12.tgz#2ceb4f632081a8bf8757ba7e5e01b5810800022f" resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.12.tgz#2ceb4f632081a8bf8757ba7e5e01b5810800022f"
@ -7392,6 +7307,14 @@ neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
next-transpile-modules@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/next-transpile-modules/-/next-transpile-modules-9.0.0.tgz#133b1742af082e61cc76b02a0f12ffd40ce2bf90"
integrity sha512-VCNFOazIAnXn1hvgYYSTYMnoWgKgwlYh4lm1pKbSfiB3kj5ZYLcKVhfh3jkPOg1cnd9DP+pte9yCUocdPEUBTQ==
dependencies:
enhanced-resolve "^5.7.0"
escalade "^3.1.1"
next@^12.0.7: next@^12.0.7:
version "12.0.7" version "12.0.7"
resolved "https://registry.yarnpkg.com/next/-/next-12.0.7.tgz#33ebf229b81b06e583ab5ae7613cffe1ca2103fc" resolved "https://registry.yarnpkg.com/next/-/next-12.0.7.tgz#33ebf229b81b06e583ab5ae7613cffe1ca2103fc"
@ -8681,7 +8604,7 @@ prompts@^2.0.1, prompts@^2.4.2:
kleur "^3.0.3" kleur "^3.0.3"
sisteransi "^1.0.5" sisteransi "^1.0.5"
prop-types@^15.5.7, prop-types@^15.6.2, prop-types@^15.7.2: prop-types@^15.5.7, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.8.1" version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -8767,7 +8690,7 @@ quick-lru@^5.1.1:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
raf@^3.4.1: raf@^3.0.0, raf@^3.4.1:
version "3.4.1" version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
@ -8906,7 +8829,7 @@ react-hot-toast@^2.1.1:
dependencies: dependencies:
goober "^2.1.1" goober "^2.1.1"
react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2: react-is@17.0.2, react-is@^17.0.1:
version "17.0.2" version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
@ -8934,18 +8857,6 @@ react-popper@^2.2.3:
react-fast-compare "^3.0.1" react-fast-compare "^3.0.1"
warning "^4.0.2" warning "^4.0.2"
react-redux@^7.2.2:
version "7.2.6"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.6.tgz#49633a24fe552b5f9caf58feb8a138936ddfe9aa"
integrity sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==
dependencies:
"@babel/runtime" "^7.15.4"
"@types/react-redux" "^7.1.20"
hoist-non-react-statics "^3.3.2"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^17.0.2"
react-refresh@0.8.3: react-refresh@0.8.3:
version "0.8.3" version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
@ -9057,6 +8968,17 @@ react-sticky-box@^0.9.3:
"@babel/runtime" "^7.1.5" "@babel/runtime" "^7.1.5"
resize-observer-polyfill "^1.5.1" resize-observer-polyfill "^1.5.1"
react-stickynode@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/react-stickynode/-/react-stickynode-4.0.0.tgz#ca1deeda866aeace3d522d01eb868f286cdb49d1"
integrity sha512-H6Ae6l8soAc188eFAmE4CGJz3oidQa88jNO/fnJWjpFw4DwGRS6boL9gHNE4DCvbMPgek1AAP85pUPIEKUMgtw==
dependencies:
classnames "^2.0.0"
core-js "^3.6.5"
prop-types "^15.6.0"
shallowequal "^1.0.0"
subscribe-ui-event "^2.0.6"
react@^17.0.1: react@^17.0.1:
version "17.0.2" version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
@ -9141,25 +9063,6 @@ redent@^3.0.0:
indent-string "^4.0.0" indent-string "^4.0.0"
strip-indent "^3.0.0" strip-indent "^3.0.0"
redux-persist@^5.10.0:
version "5.10.0"
resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-5.10.0.tgz#5d8d802c5571e55924efc1c3a9b23575283be62b"
integrity sha512-sSJAzNq7zka3qVHKce1hbvqf0Vf5DuTVm7dr4GtsqQVOexnrvbV47RWFiPxQ8fscnyiuWyD2O92DOxPl0tGCRg==
redux-saga@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-1.1.3.tgz#9f3e6aebd3c994bbc0f6901a625f9a42b51d1112"
integrity sha512-RkSn/z0mwaSa5/xH/hQLo8gNf4tlvT18qXDNvedihLcfzh+jMchDgaariQoehCpgRltEm4zHKJyINEz6aqswTw==
dependencies:
"@redux-saga/core" "^1.1.3"
redux@^4.0.0, redux@^4.0.4, redux@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104"
integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==
dependencies:
"@babel/runtime" "^7.9.2"
regenerate-unicode-properties@^9.0.0: regenerate-unicode-properties@^9.0.0:
version "9.0.0" version "9.0.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326"
@ -9539,11 +9442,6 @@ scss-tokenizer@^0.2.3:
js-base64 "^2.1.8" js-base64 "^2.1.8"
source-map "^0.4.2" source-map "^0.4.2"
seamless-immutable@^7.1.3:
version "7.1.4"
resolved "https://registry.yarnpkg.com/seamless-immutable/-/seamless-immutable-7.1.4.tgz#6e9536def083ddc4dea0207d722e0e80d0f372f8"
integrity sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A==
select-hose@^2.0.0: select-hose@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@ -9672,6 +9570,11 @@ sha.js@^2.4.0, sha.js@^2.4.8:
inherits "^2.0.1" inherits "^2.0.1"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
shallowequal@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
shebang-command@^2.0.0: shebang-command@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@ -10198,6 +10101,15 @@ stylis@3.5.4:
resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe" resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe"
integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q== integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==
subscribe-ui-event@^2.0.6:
version "2.0.7"
resolved "https://registry.yarnpkg.com/subscribe-ui-event/-/subscribe-ui-event-2.0.7.tgz#8d18b6339c35b25246a5335775573f0e5dc461f8"
integrity sha512-Acrtf9XXl6lpyHAWYeRD1xTPUQHDERfL4GHeNuYAtZMc4Z8Us2iDBP0Fn3xiRvkQ1FO+hx+qRLmPEwiZxp7FDQ==
dependencies:
eventemitter3 "^3.0.0"
lodash "^4.17.15"
raf "^3.0.0"
supports-color@^2.0.0: supports-color@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@ -10649,25 +10561,6 @@ typedarray-to-buffer@^3.1.5:
dependencies: dependencies:
is-typedarray "^1.0.0" is-typedarray "^1.0.0"
typescript-compare@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/typescript-compare/-/typescript-compare-0.0.2.tgz#7ee40a400a406c2ea0a7e551efd3309021d5f425"
integrity sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==
dependencies:
typescript-logic "^0.0.0"
typescript-logic@^0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/typescript-logic/-/typescript-logic-0.0.0.tgz#66ebd82a2548f2b444a43667bec120b496890196"
integrity sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==
typescript-tuple@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/typescript-tuple/-/typescript-tuple-2.2.1.tgz#7d9813fb4b355f69ac55032e0363e8bb0f04dad2"
integrity sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==
dependencies:
typescript-compare "^0.0.2"
typescript@^4.0.5: typescript@^4.0.5:
version "4.5.4" version "4.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8"