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

Merge branch 'master' into develop

# Conflicts:
#	.env.development
This commit is contained in:
Fedor Katurov 2021-10-06 10:32:14 +07:00
commit 5585d566fd
73 changed files with 1677 additions and 380 deletions

View file

@ -1,2 +1,2 @@
REACT_APP_API_HOST: https://pig.staging.vault48.org/
REACT_APP_API_HOST: http://localhost:3334/
REACT_APP_REMOTE_CURRENT: https://pig.staging.vault48.org/static/

View file

@ -25,6 +25,7 @@
"ramda": "^0.26.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-dropzone": "^11.4.2",
"react-masonry-css": "^1.0.16",
"react-popper": "^2.2.3",
"react-redux": "^7.2.2",
@ -69,6 +70,7 @@
]
},
"devDependencies": {
"@types/throttle-debounce": "^2.1.0",
"@craco/craco": "5.8.0",
"@types/classnames": "^2.2.7",
"@types/marked": "^1.2.2",

View file

@ -19,6 +19,13 @@ const BorisContacts: FC<Props> = () => (
link="https://t.me/boris48bot"
subtitle="телеграм-бот"
/>
<BorisContactItem
icon="github"
title="Github"
link="https://github.com/muerwre?tab=repositories&q=vault"
subtitle="исходники Убежища"
/>
</div>
);

View file

@ -1,102 +1,73 @@
import React, { FC } from 'react';
import { IBorisState } from '~/redux/boris/reducer';
import styles from './styles.module.scss';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { sizeOf } from '~/utils/dom';
import { StatsRow } from '~/components/common/StatsRow';
import { SubTitle } from '~/components/common/SubTitle';
interface IProps {
stats: IBorisState['stats'];
}
const Row: FC<{ isLoading: boolean }> = ({ isLoading, children }) => (
<li>
{isLoading ? (
<>
<Placeholder active={isLoading} loading className={styles.label} />
<Placeholder active={isLoading} loading className={styles.value} width="24px" />
</>
) : (
children
)}
</li>
);
const BorisStatsBackend: FC<IProps> = ({ stats: { is_loading, backend } }) => {
// const is_loading = true;
if (!backend && !is_loading) {
return null;
}
return (
<div className={styles.wrap}>
<div className={styles.title}>
<Placeholder active={is_loading} loading>
Юнитс
</Placeholder>
</div>
<SubTitle isLoading={is_loading} className={styles.title}>
Юнитс
</SubTitle>
<ul>
<Row isLoading={is_loading}>
<span className={styles.label}>В сознании</span>
<span className={styles.value}>{backend.users.alive}</span>
</Row>
<StatsRow isLoading={is_loading} label="В сознании">
{backend.users.alive}
</StatsRow>
<Row isLoading={is_loading}>
<span className={styles.label}>Криокамера</span>
<span className={styles.value}>{backend.users.total - backend.users.alive}</span>
</Row>
<StatsRow isLoading={is_loading} label="Криокамера">
{backend.users.total - backend.users.alive}
</StatsRow>
</ul>
<div className={styles.title}>
<Placeholder active={is_loading} loading>
Контент
</Placeholder>
</div>
<SubTitle isLoading={is_loading} className={styles.title}>
Контент
</SubTitle>
<ul>
<Row isLoading={is_loading}>
<span className={styles.label}>Фотографии</span>
<span className={styles.value}>{backend.nodes.images}</span>
</Row>
<StatsRow isLoading={is_loading} label="Фотографии">
{backend.nodes.images}
</StatsRow>
<Row isLoading={is_loading}>
<span className={styles.label}>Письма</span>
<span className={styles.value}>{backend.nodes.texts}</span>
</Row>
<StatsRow isLoading={is_loading} label="Письма">
{backend.nodes.texts}
</StatsRow>
<Row isLoading={is_loading}>
<span className={styles.label}>Видеозаписи</span>
<span className={styles.value}>{backend.nodes.videos}</span>
</Row>
<StatsRow isLoading={is_loading} label="Видеозаписи">
{backend.nodes.videos}
</StatsRow>
<Row isLoading={is_loading}>
<span className={styles.label}>Аудиозаписи</span>
<span className={styles.value}>{backend.nodes.audios}</span>
</Row>
<StatsRow isLoading={is_loading} label="Аудиозаписи">
{backend.nodes.audios}
</StatsRow>
<Row isLoading={is_loading}>
<span className={styles.label}>Комментарии</span>
<span className={styles.value}>{backend.comments.total}</span>
</Row>
<StatsRow isLoading={is_loading} label="Комментарии">
{backend.comments.total}
</StatsRow>
</ul>
<div className={styles.title}>
<Placeholder active={is_loading} loading>
Сторедж
</Placeholder>
</div>
<SubTitle isLoading={is_loading} className={styles.title}>
Сторедж
</SubTitle>
<ul>
<Row isLoading={is_loading}>
<span className={styles.label}>Файлы</span>
<span className={styles.value}>{backend.files.count}</span>
</Row>
<StatsRow isLoading={is_loading} label="Файлы">
{backend.files.count}
</StatsRow>
<Row isLoading={is_loading}>
<span className={styles.label}>На диске</span>
<span className={styles.value}>{sizeOf(backend.files.size)}</span>
</Row>
<StatsRow isLoading={is_loading} label="На диске">
{sizeOf(backend.files.size)}
</StatsRow>
</ul>
</div>
);

View file

@ -1,34 +1,10 @@
@import "src/styles/variables";
.value {
float: right;
color: white;
font: $font_12_semibold;
line-height: 24px;
.title {
margin: $gap * 2 0 $gap;
}
.wrap {
ul {
font: $font_12_regular;
line-height: 24px;
text-transform: uppercase;
li {
border-bottom: 1px solid #333333;
color: #aaaaaa;
&:last-child {
border-bottom: none;
}
}
}
}
.title {
font: $font_12_semibold;
text-transform: uppercase;
opacity: 0.3;
margin: $gap * 2 0 $gap;
}
.subtitle {

View file

@ -3,7 +3,7 @@ import { useCommentFormFormik } from '~/utils/hooks/useCommentFormFormik';
import { FormikProvider } from 'formik';
import { LocalCommentFormTextarea } from '~/components/comment/LocalCommentFormTextarea';
import { Button } from '~/components/input/Button';
import { FileUploaderProvider, useFileUploader } from '~/utils/hooks/fileUploader';
import { FileUploaderProvider, useFileUploader } from '~/utils/hooks/useFileUploader';
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
import { CommentFormAttachButtons } from '~/components/comment/CommentFormAttachButtons';
import { CommentFormFormatButtons } from '~/components/comment/CommentFormFormatButtons';
@ -11,7 +11,7 @@ import { CommentFormAttaches } from '~/components/comment/CommentFormAttaches';
import { LoaderCircle } from '~/components/input/LoaderCircle';
import { IComment, INode } from '~/redux/types';
import { EMPTY_COMMENT } from '~/redux/node/constants';
import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone';
import { UploadDropzone } from '~/components/upload/UploadDropzone';
import styles from './styles.module.scss';
import { ERROR_LITERAL } from '~/constants/errors';
import { useInputPasteUpload } from '~/utils/hooks/useInputPasteUpload';
@ -50,7 +50,7 @@ const CommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
useInputPasteUpload(textarea, uploader.uploadFiles);
return (
<CommentFormDropzone onUpload={uploader.uploadFiles}>
<UploadDropzone onUpload={uploader.uploadFiles}>
<form onSubmit={formik.handleSubmit} className={styles.wrap}>
<FormikProvider value={formik}>
<FileUploaderProvider value={uploader}>
@ -103,7 +103,7 @@ const CommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
</FileUploaderProvider>
</FormikProvider>
</form>
</CommentFormDropzone>
</UploadDropzone>
);
};

View file

@ -5,9 +5,9 @@ import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
import { IFile } from '~/redux/types';
import { SortEnd } from 'react-sortable-hoc';
import { moveArrItem } from '~/utils/fn';
import { useDropZone } from '~/utils/hooks';
import { useFileDropZone } from '~/utils/hooks';
import { COMMENT_FILE_TYPES, UPLOAD_TYPES } from '~/redux/uploads/constants';
import { useFileUploaderContext } from '~/utils/hooks/fileUploader';
import { useFileUploaderContext } from '~/utils/hooks/useFileUploader';
const CommentFormAttaches: FC = () => {
const uploader = useFileUploaderContext();
@ -29,7 +29,7 @@ const CommentFormAttaches: FC = () => {
pending,
]);
const onDrop = useDropZone(uploadFiles, COMMENT_FILE_TYPES);
const onDrop = useFileDropZone(uploadFiles, COMMENT_FILE_TYPES);
const hasImageAttaches = images.length > 0 || pendingImages.length > 0;
const hasAudioAttaches = audios.length > 0 || pendingAudios.length > 0;

View file

@ -1,14 +0,0 @@
import React, { FC } from 'react';
import { COMMENT_FILE_TYPES } from '~/redux/uploads/constants';
import { useDropZone } from '~/utils/hooks';
interface IProps {
onUpload: (files: File[]) => void;
}
const CommentFormDropzone: FC<IProps> = ({ children, onUpload }) => {
const onDrop = useDropZone(onUpload, COMMENT_FILE_TYPES);
return <div onDropCapture={onDrop}>{children}</div>;
};
export { CommentFormDropzone };

View file

@ -10,11 +10,20 @@ interface Props extends DivProps {
url?: string;
username?: string;
size?: number;
preset?: typeof PRESETS[keyof typeof PRESETS];
innerRef?: React.Ref<any>;
}
const Avatar: FC<Props> = ({ url, username, size, className, innerRef, ...rest }) => {
const backgroundImage = !!url ? `url('${getURLFromString(url, PRESETS.avatar)}')` : undefined;
const Avatar: FC<Props> = ({
url,
username,
size,
className,
innerRef,
preset = PRESETS.avatar,
...rest
}) => {
const backgroundImage = !!url ? `url('${getURLFromString(url, preset)}')` : undefined;
const onOpenProfile = useCallback(() => openUserProfile(username), [username]);
return (

View file

@ -0,0 +1,21 @@
import React, { FC } from 'react';
import { Placeholder } from '~/components/placeholders/Placeholder';
import styles from './styles.module.scss';
const StatsRow: FC<{ isLoading: boolean; label: string }> = ({ isLoading, label, children }) => (
<li className={styles.row}>
{isLoading ? (
<>
<Placeholder active={isLoading} loading className={styles.label} />
<Placeholder active={isLoading} loading className={styles.value} width="24px" />
</>
) : (
<>
<div className={styles.label}>{label}</div>
<div className={styles.value}>{children}</div>
</>
)}
</li>
);
export { StatsRow };

View file

@ -0,0 +1,23 @@
@import '~/styles/variables.scss';
.row {
@include row_shadow;
color: #aaaaaa;
display: flex;
flex-direction: row;
font: $font_12_regular;
line-height: 24px;
text-transform: uppercase;
}
.label {
flex: 1;
margin-right: $gap;
}
.value {
float: right;
color: white;
font: $font_12_semibold;
line-height: 24px;
}

View file

@ -0,0 +1,19 @@
import React, { FC } from 'react';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { DivProps } from '~/utils/types';
import classNames from 'classnames';
import styles from './styles.module.scss';
interface Props extends DivProps {
isLoading?: boolean;
}
const SubTitle: FC<Props> = ({ isLoading, children, ...rest }) => (
<div {...rest} className={classNames(styles.title, rest.className)}>
<Placeholder active={isLoading} loading>
{children}
</Placeholder>
</div>
);
export { SubTitle };

View file

@ -0,0 +1,7 @@
@import "~/styles/variables.scss";
.title {
font: $font_12_semibold;
text-transform: uppercase;
opacity: 0.3;
}

View file

@ -11,13 +11,14 @@ import { NodeEditorProps } from '~/redux/node/types';
import { useNodeImages } from '~/utils/hooks/node/useNodeImages';
import { useNodeAudios } from '~/utils/hooks/node/useNodeAudios';
import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik';
import { useFileUploaderContext } from '~/utils/hooks/fileUploader';
import { useFileUploaderContext } from '~/utils/hooks/useFileUploader';
import { UploadDropzone } from '~/components/upload/UploadDropzone';
type IProps = NodeEditorProps;
const AudioEditor: FC<IProps> = () => {
const { values } = useNodeFormContext();
const { pending, setFiles } = useFileUploaderContext()!;
const { pending, setFiles, uploadFiles } = useFileUploaderContext()!;
const images = useNodeImages(values);
const audios = useNodeAudios(values);
@ -35,10 +36,12 @@ const AudioEditor: FC<IProps> = () => {
const setAudios = useCallback(values => setFiles([...values, ...images]), [setFiles, images]);
return (
<div className={styles.wrap}>
<ImageGrid files={images} setFiles={setImages} locked={pendingImages} />
<AudioGrid files={audios} setFiles={setAudios} locked={pendingAudios} />
</div>
<UploadDropzone onUpload={uploadFiles} helperClassName={styles.dropzone}>
<div className={styles.wrap}>
<ImageGrid files={images} setFiles={setImages} locked={pendingImages} />
<AudioGrid files={audios} setFiles={setAudios} locked={pendingAudios} />
</div>
</UploadDropzone>
);
};

View file

@ -11,9 +11,13 @@
z-index: 10;
display: flex;
flex-direction: row;
pointer-events: none;
touch-action: none;
& > * {
margin: 0 $gap / 2;
pointer-events: all;
touch-action: auto;
&:first-child {
margin-left: 0;

View file

@ -1,9 +1,10 @@
import React, { FC } from 'react';
import { Filler } from '~/components/containers/Filler';
import { IEditorComponentProps } from '~/redux/node/types';
import styles from './styles.module.scss';
type IProps = IEditorComponentProps & {};
const EditorFiller: FC<IProps> = () => <Filler />;
const EditorFiller: FC<IProps> = () => <Filler className={styles.filler} />;
export { EditorFiller };

View file

@ -0,0 +1,4 @@
.filler {
touch-action: none;
pointer-events: none;
}

View file

@ -3,7 +3,7 @@ import styles from './styles.module.scss';
import { Icon } from '~/components/input/Icon';
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
import { IEditorComponentProps } from '~/redux/node/types';
import { useFileUploaderContext } from '~/utils/hooks/fileUploader';
import { useFileUploaderContext } from '~/utils/hooks/useFileUploader';
import { getFileType } from '~/utils/uploader';
import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik';
import { Button } from '~/components/input/Button';

View file

@ -11,7 +11,7 @@ import { getURL } from '~/utils/dom';
import { Icon } from '~/components/input/Icon';
import { PRESETS } from '~/constants/urls';
import { IEditorComponentProps } from '~/redux/node/types';
import { useFileUploader, useFileUploaderContext } from '~/utils/hooks/fileUploader';
import { useFileUploader, useFileUploaderContext } from '~/utils/hooks/useFileUploader';
import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik';
import { getFileType } from '~/utils/uploader';

View file

@ -1,22 +1,21 @@
import React, { FC, useMemo, useCallback } from 'react';
import { connect } from 'react-redux';
import { INode, IFile } from '~/redux/types';
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
import { selectUploads } from '~/redux/uploads/selectors';
import React, { FC } from 'react';
import { ImageGrid } from '~/components/editors/ImageGrid';
import styles from './styles.module.scss';
import { NodeEditorProps } from '~/redux/node/types';
import { useFileUploaderContext } from '~/utils/hooks/fileUploader';
import { useFileUploaderContext } from '~/utils/hooks/useFileUploader';
import { UploadDropzone } from '~/components/upload/UploadDropzone';
type IProps = NodeEditorProps;
const ImageEditor: FC<IProps> = () => {
const { pending, files, setFiles } = useFileUploaderContext()!;
const { pending, files, setFiles, uploadFiles } = useFileUploaderContext()!;
return (
<div className={styles.wrap}>
<ImageGrid files={files} setFiles={setFiles} locked={pending} />
</div>
<UploadDropzone onUpload={uploadFiles} helperClassName={styles.dropzone}>
<div className={styles.wrap}>
<ImageGrid files={files} setFiles={setFiles} locked={pending} />
</div>
</UploadDropzone>
);
};

View file

@ -1,6 +1,9 @@
@import "src/styles/variables";
@import 'src/styles/variables';
.wrap {
min-height: 200px;
padding-bottom: $upload_button_height + $gap;
}
div.dropzone {
}

View file

@ -9,6 +9,7 @@ import { Icon } from '~/components/input/Icon';
import { PRESETS } from '~/constants/urls';
import { NODE_TYPES } from '~/redux/node/constants';
import { Link } from 'react-router-dom';
import { CellShade } from '~/components/flow/CellShade';
const THUMBNAIL_SIZES = {
horizontal: PRESETS.small_hero,
@ -26,7 +27,6 @@ interface IProps {
const Cell: FC<IProps> = ({
node: { id, title, thumbnail, type, flow, description },
can_edit,
onSelect,
onChangeCellView,
}) => {
const ref = useRef(null);
@ -112,6 +112,8 @@ const Cell: FC<IProps> = ({
)}
<Link className={classNames(styles.face)} to={`/post${id}`}>
<CellShade color={flow.dominant_color} />
<div className={styles.face_content}>
{!text && <div className={classNames(styles.title, titleSize)}>{title || '...'}</div>}

View file

@ -195,7 +195,6 @@
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(5deg, transparentize($content_bg, 0), transparentize($content_bg, 1));
z-index: 2;
border-radius: $cell_radius;
padding: $gap / 2;

View file

@ -0,0 +1,26 @@
import React, { FC, useMemo } from 'react';
import styles from './styles.module.scss';
import { DEFAULT_DOMINANT_COLOR } from '~/constants/node';
import { convertHexToRGBA } from '~/utils/color';
import { DivProps } from '~/utils/types';
import classNames from 'classnames';
interface Props extends DivProps {
color?: string;
}
const CellShade: FC<Props> = ({ color, ...rest }) => {
const background = useMemo(() => {
if (!color || color === DEFAULT_DOMINANT_COLOR) {
return undefined;
}
return `linear-gradient(7deg, ${color} 50px, ${convertHexToRGBA(color, 0.3)} 250px)`;
}, [color]);
return (
<div {...rest} className={classNames(rest.className, styles.shade)} style={{ background }} />
);
};
export { CellShade };

View file

@ -0,0 +1,16 @@
@import "~/styles/variables";
.shade {
position: absolute;
bottom: 0;
left: 0;
right: 0;
top: 0;
background: linear-gradient(7deg, transparentize($content_bg, 0.05) 30px, transparentize($content_bg, 1) 250px);
pointer-events: none;
touch-action: none;
@include tablet {
opacity: 0.7;
}
}

View file

@ -0,0 +1,18 @@
import React, { FC } from 'react';
import styles from './styles.module.scss';
import { SVGProps } from '~/utils/types';
interface Props extends SVGProps {}
const DropHereIcon: FC<Props> = ({ ...rest }) => (
<svg viewBox="0 0 24 24" stroke="none" {...rest}>
<path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z" />
<path
d="M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z"
className={styles.arrow}
/>
</svg>
);
export { DropHereIcon };

View file

@ -0,0 +1,8 @@
@keyframes bounce {
0% { transform: translate(0, -5%); }
100% { transform: translate(0, 5%); }
}
.arrow {
animation: bounce alternate infinite 0.25s;
}

View file

@ -7,9 +7,11 @@ import { IFile } from '~/redux/types';
import { LoaderCircle } from '~/components/input/LoaderCircle';
import { Icon } from '~/components/input/Icon';
import { useResizeHandler } from '~/utils/hooks/useResizeHandler';
import { DEFAULT_DOMINANT_COLOR } from '~/constants/node';
interface IProps {
file: IFile;
color?: string;
onLoad?: () => void;
onClick?: MouseEventHandler;
className?: string;
@ -18,7 +20,7 @@ interface IProps {
const DEFAULT_WIDTH = 1920;
const DEFAULT_HEIGHT = 1020;
const ImagePreloader: FC<IProps> = ({ file, onLoad, onClick, className }) => {
const ImagePreloader: FC<IProps> = ({ file, color, onLoad, onClick, className }) => {
const [maxHeight, setMaxHeight] = useState(window.innerHeight - 140);
const [loaded, setLoaded] = useState(false);
const [hasError, setHasError] = useState(false);
@ -48,6 +50,7 @@ const ImagePreloader: FC<IProps> = ({ file, onLoad, onClick, className }) => {
useResizeHandler(onResize);
const estimatedWidth = (width * maxHeight) / height;
const fill = color && color !== DEFAULT_DOMINANT_COLOR ? color : '#222222';
return (
<>
@ -67,7 +70,7 @@ const ImagePreloader: FC<IProps> = ({ file, onLoad, onClick, className }) => {
</defs>
<g filter="url(#f1)">
<rect fill="#222222" width="100%" height="100%" stroke="none" rx="8" ry="8" />
<rect fill={fill} width="100%" height="100%" stroke="none" rx="8" ry="8" />
{!hasError && (
<image

View file

@ -3,24 +3,24 @@ import { INode } from '~/redux/types';
import styles from './styles.module.scss';
import { Avatar } from '~/components/common/Avatar';
import { openUserProfile } from '~/utils/user';
import { useRandomPhrase } from '~/constants/phrases';
import { useUserDescription } from '~/utils/hooks/user/useUserDescription';
interface Props {
node?: INode;
}
const NodeAuthorBlock: FC<Props> = ({ node }) => {
const randomPhrase = useRandomPhrase('USER_DESCRIPTION');
const onOpenProfile = useCallback(() => openUserProfile(node?.user?.username), [
node?.user?.username,
]);
const description = useUserDescription(node?.user);
if (!node?.user) {
return null;
}
const { fullname, username, description, photo } = node.user;
const { fullname, username, photo } = node.user;
return (
<div className={styles.block} onClick={onOpenProfile}>
@ -28,8 +28,7 @@ const NodeAuthorBlock: FC<Props> = ({ node }) => {
<div className={styles.info}>
<div className={styles.username}>{fullname || username}</div>
<div className={styles.description}>{description || randomPhrase}</div>
<div className={styles.description}>{description}</div>
</div>
</div>
);

View file

@ -97,6 +97,7 @@ const NodeImageSwiperBlock: FC<IProps> = ({ node }) => {
onLoad={updateSwiper}
onClick={() => onOpenPhotoSwipe(i)}
className={styles.image}
color={file?.metadata?.dominant_color}
/>
</SwiperSlide>
))}

View file

@ -3,6 +3,7 @@ import styles from './styles.module.scss';
import { Group } from '~/components/containers/Group';
import { INode } from '~/redux/types';
import { NodeRelatedItem } from '~/components/node/NodeRelatedItem';
import { SubTitle } from '~/components/common/SubTitle';
interface IProps {
title: ReactElement | string;
@ -12,9 +13,7 @@ interface IProps {
const NodeRelated: FC<IProps> = ({ title, items }) => {
return (
<Group className={styles.wrap}>
<div className={styles.title}>
<div className={styles.text}>{title}</div>
</div>
<SubTitle className={styles.title}>{title}</SubTitle>
<div className={styles.grid}>
{items.map(item => (

View file

@ -21,7 +21,7 @@
}
.title {
@include title_with_line();
padding-left: 5px;
a {
text-decoration: none;

View file

@ -17,12 +17,10 @@ const Paragraph: FC<Props> = ({ lines = 3, wordsLimit = 12, ...props }) => {
[lines, wordsLimit]
);
console.log({ iters });
return (
<Group>
{iters.map(words => (
<div className={styles.para}>
{iters.map((words, i) => (
<div className={styles.para} key={i}>
{words.map(word => (
<Placeholder key={word} width={`${Math.round(Math.random() * 120) + 60}px`} active />
))}

View file

@ -0,0 +1,48 @@
import React, { FC, useCallback } from 'react';
import Dropzone from 'react-dropzone';
import classnames from 'classnames';
import classNames from 'classnames';
import styles from './styles.module.scss';
import { DivProps } from '~/utils/types';
import { DropHereIcon } from '~/components/input/DropHereIcon';
import { useDragDetector } from '~/utils/hooks/useDragDetector';
interface IProps extends DivProps {
onUpload: (files: File[]) => void;
helperClassName?: string;
}
const UploadDropzone: FC<IProps> = ({ children, onUpload, helperClassName, ...rest }) => {
const { isDragging: isDraggingOnBody, onStopDragging } = useDragDetector();
const onDrop = useCallback(
(files: File[]) => {
onStopDragging();
onUpload(files);
},
[onUpload]
);
return (
<Dropzone onDrop={onDrop}>
{({ getRootProps, isDragActive }) => (
<div
{...getRootProps({
...rest,
className: classnames(styles.zone),
})}
>
{children}
<div
className={classNames(styles.helper, helperClassName, {
[styles.active]: isDragActive || isDraggingOnBody,
})}
>
<DropHereIcon className={styles.icon} />
</div>
</div>
)}
</Dropzone>
);
};
export { UploadDropzone };

View file

@ -0,0 +1,34 @@
@import '~/styles/variables';
.zone {
position: relative;
z-index: 1;
outline: none;
}
.helper {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparentize($wisegreen, 0.7);
border-radius: $radius;
z-index: 10;
box-shadow: inset $wisegreen 0 0 0 2px;
display: none;
align-items: center;
justify-content: center;
pointer-events: none;
touch-action: none;
&.active, :global(.dragging) & {
display: flex;
}
}
svg.icon {
width: auto;
height: 72px;
fill: $wisegreen;
}

1
src/constants/node.ts Normal file
View file

@ -0,0 +1 @@
export const DEFAULT_DOMINANT_COLOR = '#000000';

1
src/constants/user.ts Normal file
View file

@ -0,0 +1 @@
export const INACTIVE_ACCOUNT_DAYS = 40;

View file

@ -10,6 +10,7 @@ import { BlurWrapper } from '~/components/containers/BlurWrapper';
import { PageCover } from '~/components/containers/PageCover';
import { BottomContainer } from '~/containers/main/BottomContainer';
import { MainRouter } from '~/containers/main/MainRouter';
import { DragDetectorProvider } from '~/utils/hooks/useDragDetector';
const mapStateToProps = state => ({
modal: selectModal(state),
@ -21,7 +22,7 @@ type IProps = typeof mapDispatchToProps & ReturnType<typeof mapStateToProps> & {
const Component: FC<IProps> = ({ modal: { is_shown } }) => {
return (
<ConnectedRouter history={history}>
<div>
<DragDetectorProvider>
<BlurWrapper is_blurred={is_shown}>
<PageCover />
@ -32,9 +33,8 @@ const Component: FC<IProps> = ({ modal: { is_shown } }) => {
<MainRouter />
</MainLayout>
</BlurWrapper>
<BottomContainer />
</div>
</DragDetectorProvider>
</ConnectedRouter>
);
};

View file

@ -7,7 +7,7 @@ import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
import { prop } from 'ramda';
import { useNodeFormFormik } from '~/utils/hooks/useNodeFormFormik';
import { EditorButtons } from '~/components/editors/EditorButtons';
import { FileUploaderProvider, useFileUploader } from '~/utils/hooks/fileUploader';
import { FileUploaderProvider, useFileUploader } from '~/utils/hooks/useFileUploader';
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
import { FormikProvider } from 'formik';
import { INode } from '~/redux/types';
@ -15,6 +15,7 @@ import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
import { useTranslatedError } from '~/utils/hooks/useTranslatedError';
import { useCloseOnEscape } from '~/utils/hooks';
import { EditorConfirmClose } from '~/components/editors/EditorConfirmClose';
import { UploadDropzone } from '~/components/upload/UploadDropzone';
interface Props extends IDialogProps {
node: INode;

View file

@ -1,13 +1,15 @@
import React, { FC } from 'react';
import Masonry from 'react-masonry-css';
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
import styles from './styles.module.scss';
import { LabNode } from '~/components/lab/LabNode';
import { selectLabList, selectLabListNodes } from '~/redux/lab/selectors';
import { EMPTY_NODE, NODE_TYPES } from '~/redux/node/constants';
import { values } from 'ramda';
import { ILabNode } from '~/redux/lab/types';
interface IProps {}
interface IProps {
isLoading: boolean;
nodes: ILabNode[];
}
const breakpointCols = {
default: 2,
@ -26,11 +28,8 @@ const LoadingNode = () => (
/>
);
const LabGrid: FC<IProps> = () => {
const nodes = useShallowSelect(selectLabListNodes);
const { is_loading } = useShallowSelect(selectLabList);
if (is_loading) {
const LabGrid: FC<IProps> = ({ isLoading, nodes }) => {
if (isLoading) {
return (
<Masonry
className={styles.wrap}

View file

@ -16,6 +16,7 @@ import {
import { LabTags } from '~/components/lab/LabTags';
import { LabHeroes } from '~/components/lab/LabHeroes';
import { FlowRecentItem } from '~/components/flow/FlowRecentItem';
import { SubTitle } from '~/components/common/SubTitle';
interface IProps {}
@ -31,10 +32,10 @@ const LabStats: FC<IProps> = () => {
<div className={styles.card}>
<Group>
{isLoading ? (
<Placeholder height={14} width="100px" />
) : (
tags.length && <div className={styles.title}>Тэги</div>
{(!!tags.length || isLoading) && (
<SubTitle isLoading={isLoading} className={styles.title}>
Тэги
</SubTitle>
)}
<div className={styles.tags}>
@ -56,10 +57,10 @@ const LabStats: FC<IProps> = () => {
</>
)}
{isLoading ? (
<Placeholder height={14} width="100px" />
) : (
heroes.length > 0 && <div className={styles.title}>Важные</div>
{(!!heroes.length || isLoading) && (
<SubTitle isLoading={isLoading} className={styles.title}>
Важные
</SubTitle>
)}
<div className={styles.heroes}>

View file

@ -1,10 +1,7 @@
@import "~/styles/variables.scss";
.title {
font: $font_14_semibold;
color: darken(white, 50%);
text-transform: uppercase;
padding: 0 $gap / 2;
padding-bottom: $gap;
}
.tags.tags {
@ -26,3 +23,4 @@
.updates {
padding: 0 $gap / 4 $gap * 2;
}

View file

@ -9,6 +9,9 @@ import classNames from 'classnames';
import styles from './styles.module.scss';
import markdown from '~/styles/common/markdown.module.scss';
import { ProfileAvatar } from '~/containers/profile/ProfileAvatar';
import { Avatar } from '~/components/common/Avatar';
import { Markdown } from '~/components/containers/Markdown';
interface IProps {
profile: IAuthState['profile'];
@ -16,49 +19,30 @@ interface IProps {
}
const ProfilePageLeft: FC<IProps> = ({ username, profile }) => {
const thumb = useMemo(() => {
if (!profile || !profile.user || !profile.user.photo) return '';
return getURL(profile.user.photo, PRESETS.small_hero);
}, [profile]);
return (
<div className={styles.wrap}>
<div className={styles.avatar} style={{ backgroundImage: `url('${thumb}')` }} />
<Avatar
username={username}
url={profile.user?.photo?.url}
className={styles.avatar}
preset={PRESETS['600']}
/>
<div className={styles.region_wrap}>
<div className={styles.region}>
<div className={styles.name}>
{profile.is_loading ? <Placeholder /> : profile?.user?.fullname}
</div>
<div className={styles.region}>
<div className={styles.name}>
{profile.is_loading ? <Placeholder /> : profile?.user?.fullname}
</div>
<div className={styles.username}>
{profile.is_loading ? <Placeholder /> : `~${profile?.user?.username}`}
</div>
<div className={styles.menu}>
<Link to={`${URLS.PROFILE_PAGE(username)}/`}>
<Icon icon="profile" size={20} />
Профиль
</Link>
<Link to={`${URLS.PROFILE_PAGE(username)}/settings`}>
<Icon icon="settings" size={20} />
Настройки
</Link>
<Link to={`${URLS.PROFILE_PAGE(username)}/messages`}>
<Icon icon="messages" size={20} />
Сообщения
</Link>
</div>
<div className={styles.username}>
{profile.is_loading ? <Placeholder /> : `~${profile?.user?.username}`}
</div>
</div>
{profile && profile.user && profile.user.description && false && (
<div className={classNames(styles.description, markdown.wrapper)}>
{formatText(profile?.user?.description || '')}
</div>
{profile && profile.user && profile.user.description && (
<Markdown
className={styles.description}
dangerouslySetInnerHTML={{ __html: formatText(profile.user.description) }}
/>
)}
</div>
);

View file

@ -1,92 +1,51 @@
@import "src/styles/variables";
.wrap {
@include outer_shadow;
padding: $gap $gap $gap * 2;
box-sizing: border-box;
display: flex;
align-items: stretch;
justify-content: stretch;
flex-direction: column;
background: $comment_bg;
height: 100%;
border-radius: $radius;
}
.avatar {
width: 100%;
padding-bottom: 75%;
border-radius: 0 $radius 0 0;
background: 50% 50% no-repeat;
background-size: cover;
}
.region_wrap {
width: 100%;
// padding: 0 10px;
position: relative;
margin-top: -$radius;
box-sizing: border-box;
height: 0;
padding-bottom: 100%;
margin-bottom: $gap * 2;
}
.region {
// background: $content_bg;
background: darken($content_bg, 2%);
width: 100%;
border-radius: 0 $radius $radius 0;
text-align: center;
}
.name {
font: $font_24_semibold;
color: white;
padding: $gap $gap 0 $gap;
text-transform: uppercase;
width: 100%;
box-sizing: border-box;
margin-bottom: 4px;
}
.username {
font: $font_14_semibold;
padding: 0 $gap $gap $gap;
font: $font_14_regular;
box-sizing: border-box;
width: 100%;
color: transparentize(white, 0.5);
margin-top: $gap / 2;
}
.menu {
padding: $gap 0 $gap 0;
display: flex;
align-items: stretch;
width: 100%;
flex-direction: column;
box-sizing: border-box;
display: none;
a {
width: 100%;
color: inherit;
text-decoration: none;
text-transform: uppercase;
font: $font_18_semibold;
padding: $gap $gap;
display: flex;
align-items: center;
justify-content: flex-start;
opacity: 0.5;
box-sizing: border-box;
transition: opacity 0.25s;
&:hover {
opacity: 1;
}
}
svg {
margin-right: $gap;
fill: currentColor;
}
}
.description {
padding: $gap;
box-sizing: border-box;
// background: darken($content_bg, 2%);
background: darken($content_bg, 4%);
// margin: 0 $gap;
border-radius: 0 0 $radius $radius;
@include clamp(3, 21px * 3);
line-height: 21px;
font: $font_14_regular;
margin-top: $gap * 3;
display: none;
}

View file

@ -0,0 +1,36 @@
import React, { FC } from 'react';
import styles from './styles.module.scss';
import { StatsRow } from '~/components/common/StatsRow';
import { SubTitle } from '~/components/common/SubTitle';
interface Props {}
const Row: FC<{ count: number; title: string; icon?: string }> = ({ count, title, icon }) => (
<div className={styles.row}>
<div className={styles.title}>{title}</div>
<div className={styles.counter}>{count > 999 ? '999+' : count}</div>
</div>
);
const ProfilePageStats: FC<Props> = () => (
<div className={styles.wrap}>
<SubTitle>Ачивментс</SubTitle>
<ul>
<StatsRow isLoading={false} label="лет в бункере">
9
</StatsRow>
<StatsRow isLoading={false} label="постов">
99
</StatsRow>
<StatsRow isLoading={false} label="комментариев">
999+
</StatsRow>
<StatsRow isLoading={false} label="лайков">
99
</StatsRow>
</ul>
</div>
);
export { ProfilePageStats };

View file

@ -0,0 +1,43 @@
@import "~/styles/variables";
.wrap {
@include outer_shadow;
padding: $gap;
background: $content_bg;
border-radius: $radius;
display: grid;
grid-column-gap: $gap;
grid-auto-flow: row;
grid-row-gap: $gap;
& > .row:not(:last-child) {
margin-bottom: $gap;
}
}
.row {
width: 100%;
border-radius: $radius;
box-sizing: border-box;
height: 100%;
position: relative;
display: flex;
flex-direction: row;
}
.counter {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font: $font_16_semibold;
}
.title {
font: $font_12_semibold;
text-align: left;
word-wrap: break-word;
opacity: 0.5;
flex: 1;
}

View file

@ -55,10 +55,6 @@ const FlowLayout: FC = () => {
labUpdates,
]);
useEffect(() => {
window.scrollTo(0, (window as any).flowScrollPos || 0);
}, []);
return (
<div className={classNames(styles.container, { [styles.fluid]: isFluid })}>
<div className={styles.grid}>

View file

@ -9,66 +9,15 @@
$cols: $content_width / $cell;
@mixin fluid {
@media(min-width: $content_width) {
.fluid & {
@content
}
}
}
.container {
max-width: $content_width;
width: 100%;
&.fluid {
padding: 0 $gap;
box-sizing: border-box;
max-width: none;
}
}
.grid {
width: 100%;
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(auto-fit, minmax($cell - 5, 1fr));
grid-template-rows: 50vh $cell;
grid-auto-rows: $cell;
grid-auto-flow: row dense;
grid-column-gap: $gap;
grid-row-gap: $gap;
@include fluid {
grid-template-columns: repeat(auto-fit, minmax($fluid_cell - 5, 1fr));
grid-template-rows: $fluid_cell;
grid-auto-rows: $fluid_cell;
}
@media (max-width: ($cell + 10) * 3) {
grid-template-columns: repeat(auto-fill, minmax($fluid_cell - 20, 1fr));
grid-auto-rows: $fluid_cell;
grid-template-rows: calc(50vw - 10px) $fluid_cell;
}
@media (max-width: $cell_tablet) {
grid-template-rows: calc(66vw - 10px) auto $fluid_cell;
}
@media (max-width: $cell_mobile) {
// rework stamp, so it will be shown as smaller one on mobiles
grid-template-columns: repeat(auto-fill, minmax(calc(50vw - 20px), 1fr));
grid-template-rows: calc(80vw - 10px) auto 50vw;
grid-auto-rows: 50vw;
}
@media (max-width: ($fluid_cell + 5) * 1.5 + 20) {
grid-template-columns: repeat(auto-fill, minmax(calc(50vw - 20px), 1fr));
grid-template-rows: calc(80vw - 10px) auto 50vw;
grid-auto-rows: 50vw;
}
@include flow_grid;
}
.pad_last {
@ -86,15 +35,6 @@ $cols: $content_width / $cell;
align-items: center;
justify-content: center;
font: $font_24_semibold;
@include fluid {
grid-row-end: span 2;
grid-column-end: span 4;
@media(max-width: $content_width) {
grid-column-end: -1;
}
}
}
.stamp {

View file

@ -15,28 +15,33 @@ import { Superpower } from '~/components/boris/Superpower';
import { Toggle } from '~/components/input/Toggle';
import { usePersistedState } from '~/utils/hooks/usePersistedState';
import classNames from 'classnames';
import { useLabPagination } from '~/utils/hooks/lab/useLabPagination';
interface IProps {}
const LabLayout: FC<IProps> = () => {
const { is_loading } = useShallowSelect(selectLabList);
const { is_loading, nodes } = useShallowSelect(selectLabList);
const dispatch = useDispatch();
useLabPagination({ isLoading: is_loading });
useEffect(() => {
dispatch(labGetList());
dispatch(labGetStats());
}, [dispatch]);
const isInitialLoading = is_loading && !nodes.length;
return (
<Container>
<div className={styles.container}>
<div className={styles.wrap}>
<Group className={styles.content}>
<div className={styles.head}>
<LabHead isLoading={is_loading} />
<LabHead isLoading={isInitialLoading} />
</div>
<LabGrid />
<LabGrid nodes={nodes} isLoading={isInitialLoading} />
</Group>
<div className={styles.panel}>

View file

@ -50,7 +50,7 @@ const NodeLayout: FC<IProps> = memo(
<div className={styles.wrap}>
{head}
<Container>
<Container className={styles.content}>
<Card className={styles.node} seamless>
{block}

View file

@ -9,9 +9,8 @@
}
.content {
align-items: stretch !important;
@include vertical_at_tablet;
position: relative;
z-index: 3;
}
.comments {

View file

@ -1,12 +1,17 @@
import React, { FC, useEffect } from 'react';
import styles from './styles.module.scss';
import { Route, RouteComponentProps, Switch } from 'react-router';
import { RouteComponentProps } from 'react-router';
import { useDispatch } from 'react-redux';
import { authLoadProfile } from '~/redux/auth/actions';
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
import { selectAuthProfile } from '~/redux/auth/selectors';
import { selectAuthProfile, selectUser } from '~/redux/auth/selectors';
import { ProfilePageLeft } from '~/containers/profile/ProfilePageLeft';
import { Container } from '~/containers/main/Container';
import { FlowGrid } from '~/components/flow/FlowGrid';
import { Sticky } from '~/components/containers/Sticky';
import { selectFlow } from '~/redux/flow/selectors';
import { ProfilePageStats } from '~/containers/profile/ProfilePageStats';
import { Card } from '~/components/containers/Card';
type Props = RouteComponentProps<{ username: string }> & {};
@ -15,6 +20,9 @@ const ProfileLayout: FC<Props> = ({
params: { username },
},
}) => {
const { nodes } = useShallowSelect(selectFlow);
const user = useShallowSelect(selectUser);
const dispatch = useDispatch();
useEffect(() => {
@ -25,7 +33,27 @@ const ProfileLayout: FC<Props> = ({
return (
<Container className={styles.wrap}>
<ProfilePageLeft profile={profile} username={username} />
<div className={styles.left}>
<Sticky>
<div className={styles.row}>
<ProfilePageLeft profile={profile} username={username} />
</div>
{!!profile.user?.description && (
<div className={styles.row}>
<Card className={styles.description}>{profile.user.description}</Card>
</div>
)}
<div className={styles.row}>
<ProfilePageStats />
</div>
</Sticky>
</div>
<div className={styles.grid}>
<FlowGrid nodes={nodes} user={user} onChangeCellView={console.log} />
</div>
</Container>
);
};

View file

@ -1,20 +1,23 @@
@import "src/styles/variables";
.wrap {
flex: 1;
display: flex;
align-items: stretch;
justify-content: stretch;
border-radius: $radius;
display: grid;
grid-template-columns: $cell auto;
grid-column-gap: $gap;
}
.grid {
@include flow_grid;
}
.left {
flex: 1;
background: darken($content_bg, 2%);
border-radius: 0 $radius $radius 0;
box-sizing: border-box;
}
.right {
flex: 4;
.row {
margin-bottom: $gap;
}
.description {
font: $font_14_semibold;
text-align: center;
padding: $gap * 2 $gap;
}

View file

@ -34,3 +34,7 @@ export const labSeenNode = (nodeId: INode['id']) => ({
type: LAB_ACTIONS.SEEN_NODE,
nodeId,
});
export const labGetMore = () => ({
type: LAB_ACTIONS.GET_MORE,
});

View file

@ -10,4 +10,5 @@ export const LAB_ACTIONS = {
SET_UPDATES: `${prefix}SET_UPDATES`,
GET_UPDATES: `${prefix}GET_UPDATES`,
SEEN_NODE: `${prefix}SET_UPDATE_SEEN`,
GET_MORE: `${prefix}GET_MORE`,
};

View file

@ -9,7 +9,7 @@ import {
import { LAB_ACTIONS } from '~/redux/lab/constants';
import { Unwrap } from '~/redux/types';
import { getLabNodes, getLabStats, getLabUpdates } from '~/redux/lab/api';
import { selectLabUpdatesNodes } from '~/redux/lab/selectors';
import { selectLabList, selectLabUpdatesNodes } from '~/redux/lab/selectors';
function* getList({ after = '' }: ReturnType<typeof labGetList>) {
try {
@ -53,10 +53,38 @@ function* seenNode({ nodeId }: ReturnType<typeof labSeenNode>) {
yield put(labSetUpdates({ nodes: newNodes }));
}
function* getMore() {
try {
yield put(labSetList({ is_loading: true }));
const list: ReturnType<typeof selectLabList> = yield select(selectLabList);
if (list.nodes.length === list.count) {
return;
}
const last = list.nodes[list.nodes.length - 1];
if (!last) {
return;
}
const after = last.node.commented_at || last.node.created_at;
const { nodes, count }: Unwrap<typeof getLabNodes> = yield call(getLabNodes, { after });
const newNodes = [...list.nodes, ...nodes];
yield put(labSetList({ nodes: newNodes, count }));
} catch (error) {
yield put(labSetList({ error: error.message }));
} finally {
yield put(labSetList({ is_loading: false }));
}
}
export default function* labSaga() {
yield takeLeading(LAB_ACTIONS.GET_LIST, getList);
yield takeLeading(LAB_ACTIONS.GET_STATS, getStats);
yield takeLeading(LAB_ACTIONS.GET_UPDATES, getUpdates);
yield takeLeading(LAB_ACTIONS.SEEN_NODE, seenNode);
yield takeLeading(LAB_ACTIONS.GET_MORE, getMore);
}

View file

@ -83,6 +83,7 @@ export interface IFile {
duration?: number;
width?: number;
height?: number;
dominant_color?: string;
};
createdAt?: string;
@ -133,6 +134,7 @@ export interface INode {
flow: {
display: 'single' | 'vertical' | 'horizontal' | 'quadro';
show_description: boolean;
dominant_color?: string;
};
tags: ITag[];

View file

@ -74,3 +74,5 @@ export const COMMENT_FILE_TYPES = [
...FILE_MIMES[UPLOAD_TYPES.IMAGE],
...FILE_MIMES[UPLOAD_TYPES.AUDIO],
];
export const IMAGE_FILE_TYPES = [...FILE_MIMES[UPLOAD_TYPES.IMAGE]];

View file

@ -325,6 +325,23 @@ const Sprites: FC = () => (
d="M12,2C6.48,2,2,6.48,2,12c0,5.52,4.48,10,10,10c5.52,0,10-4.48,10-10C22,6.48,17.52,2,12,2z M16.64,8.8 c-0.15,1.58-0.8,5.42-1.13,7.19c-0.14,0.75-0.42,1-0.68,1.03c-0.58,0.05-1.02-0.38-1.58-0.75c-0.88-0.58-1.38-0.94-2.23-1.5 c-0.99-0.65-0.35-1.01,0.22-1.59c0.15-0.15,2.71-2.48,2.76-2.69c0.01-0.03,0.01-0.12-0.05-0.18c-0.06-0.05-0.14-0.03-0.21-0.02 c-0.09,0.02-1.49,0.95-4.22,2.79c-0.4,0.27-0.76,0.41-1.08,0.4c-0.36-0.01-1.04-0.2-1.55-0.37c-0.63-0.2-1.12-0.31-1.08-0.66 c0.02-0.18,0.27-0.36,0.74-0.55c2.92-1.27,4.86-2.11,5.83-2.51c2.78-1.16,3.35-1.36,3.73-1.36c0.08,0,0.27,0.02,0.39,0.12 c0.1,0.08,0.13,0.19,0.14,0.27C16.63,8.48,16.65,8.66,16.64,8.8z"
/>
</g>
<g id="github">
<path fill="none" d="M0 0h24v24H0V0z" stroke="none" />
<path
transform="scale(0.05)"
stroke="none"
d="M409.132,114.573c-19.608-33.596-46.205-60.194-79.798-79.8C295.736,15.166,259.057,5.365,219.271,5.365 c-39.781,0-76.472,9.804-110.063,29.408c-33.596,19.605-60.192,46.204-79.8,79.8C9.803,148.168,0,184.854,0,224.63 c0,47.78,13.94,90.745,41.827,128.906c27.884,38.164,63.906,64.572,108.063,79.227c5.14,0.954,8.945,0.283,11.419-1.996 c2.475-2.282,3.711-5.14,3.711-8.562c0-0.571-0.049-5.708-0.144-15.417c-0.098-9.709-0.144-18.179-0.144-25.406l-6.567,1.136 c-4.187,0.767-9.469,1.092-15.846,1c-6.374-0.089-12.991-0.757-19.842-1.999c-6.854-1.231-13.229-4.086-19.13-8.559 c-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559 c-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-0.951-2.568-2.098-3.711-3.429c-1.142-1.331-1.997-2.663-2.568-3.997 c-0.572-1.335-0.098-2.43,1.427-3.289c1.525-0.859,4.281-1.276,8.28-1.276l5.708,0.853c3.807,0.763,8.516,3.042,14.133,6.851 c5.614,3.806,10.229,8.754,13.846,14.842c4.38,7.806,9.657,13.754,15.846,17.847c6.184,4.093,12.419,6.136,18.699,6.136 c6.28,0,11.704-0.476,16.274-1.423c4.565-0.952,8.848-2.383,12.847-4.285c1.713-12.758,6.377-22.559,13.988-29.41 c-10.848-1.14-20.601-2.857-29.264-5.14c-8.658-2.286-17.605-5.996-26.835-11.14c-9.235-5.137-16.896-11.516-22.985-19.126 c-6.09-7.614-11.088-17.61-14.987-29.979c-3.901-12.374-5.852-26.648-5.852-42.826c0-23.035,7.52-42.637,22.557-58.817 c-7.044-17.318-6.379-36.732,1.997-58.24c5.52-1.715,13.706-0.428,24.554,3.853c10.85,4.283,18.794,7.952,23.84,10.994 c5.046,3.041,9.089,5.618,12.135,7.708c17.705-4.947,35.976-7.421,54.818-7.421s37.117,2.474,54.823,7.421l10.849-6.849 c7.419-4.57,16.18-8.758,26.262-12.565c10.088-3.805,17.802-4.853,23.134-3.138c8.562,21.509,9.325,40.922,2.279,58.24 c15.036,16.18,22.559,35.787,22.559,58.817c0,16.178-1.958,30.497-5.853,42.966c-3.9,12.471-8.941,22.457-15.125,29.979 c-6.191,7.521-13.901,13.85-23.131,18.986c-9.232,5.14-18.182,8.85-26.84,11.136c-8.662,2.286-18.415,4.004-29.263,5.146 c9.894,8.562,14.842,22.077,14.842,40.539v60.237c0,3.422,1.19,6.279,3.572,8.562c2.379,2.279,6.136,2.95,11.276,1.995 c44.163-14.653,80.185-41.062,108.068-79.226c27.88-38.161,41.825-81.126,41.825-128.906 C438.536,184.851,428.728,148.168,409.132,114.573z"
/>
</g>
<g id="upload">
<path fill="none" d="M0 0h24v24H0V0z" stroke="none" />
<path
stroke="none"
d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z"
/>
</g>
</svg>
);

883
src/sprites/boris_lab.svg Normal file
View file

@ -0,0 +1,883 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="300"
height="300"
viewBox="0 0 79.374998 79.375002"
version="1.1"
id="svg588"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="boris_lab.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview590"
pagecolor="#000000"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
units="px"
showborder="true"
inkscape:showpageshadow="false"
borderlayer="true"
inkscape:zoom="0.35723204"
inkscape:cx="-284.12905"
inkscape:cy="565.4588"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs585">
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient900"
id="radialGradient4635"
cx="312.92648"
cy="83.708092"
fx="312.92648"
fy="83.708092"
r="44.979172"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.7352932,-0.01764706,0.01387765,1.3646349,-170.32084,-62.042332)" />
<linearGradient
id="linearGradient900"
inkscape:collect="always">
<stop
id="stop896"
offset="0"
style="stop-color:#37483e;stop-opacity:1" />
<stop
id="stop898"
offset="1"
style="stop-color:#15241f;stop-opacity:1" />
</linearGradient>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath4984">
<circle
r="44.979172"
cy="117.83934"
cx="340.17853"
id="circle4986"
style="fill:url(#radialGradient4988);fill-opacity:1;stroke-width:0.171116" />
</clipPath>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter4955"
x="-0.30899032"
width="1.6179806"
y="-0.76629603"
height="2.5325921">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="5.9735542"
id="feGaussianBlur4957" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter3376"
x="-0.20596256"
width="1.4119251"
y="-0.51078719"
height="2.0215744">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="3.9817705"
id="feGaussianBlur3378" />
</filter>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3605"
id="linearGradient3607"
x1="1218.871"
y1="533.89893"
x2="1482.9836"
y2="503.79688"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
id="linearGradient3605">
<stop
style="stop-color:#00ccff;stop-opacity:1;"
offset="0"
id="stop3601" />
<stop
style="stop-color:#000000;stop-opacity:1"
offset="1"
id="stop3603" />
</linearGradient>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter5875"
x="-0.11630558"
width="1.2326112"
y="-0.26428985"
height="1.5324787">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="14.972889"
id="feGaussianBlur5877" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter3156"
x="-0.15793878"
width="1.3158776"
y="-0.063027151"
height="1.1260543">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.99741967"
id="feGaussianBlur3158" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter3775"
x="-0.54122727"
width="2.0824545"
y="-0.51769563"
height="2.0353913">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="5.2506562"
id="feGaussianBlur3777" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter3822"
x="-0.70077273"
width="2.4015455"
y="-0.67030432"
height="2.3406086">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="6.7984687"
id="feGaussianBlur3824" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter3834"
x="-0.67990912"
width="2.3598182"
y="-0.65034783"
height="2.3006957">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="6.5960625"
id="feGaussianBlur3836" />
</filter>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2008"
id="linearGradient2554"
gradientUnits="userSpaceOnUse"
x1="207.37267"
y1="85.704292"
x2="207.37267"
y2="35.489822" />
<linearGradient
inkscape:collect="always"
id="linearGradient2008">
<stop
style="stop-color:#191e1a;stop-opacity:1"
offset="0"
id="stop2004" />
<stop
style="stop-color:#1a2d2a;stop-opacity:1"
offset="1"
id="stop2006" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4677"
id="linearGradient2556"
gradientUnits="userSpaceOnUse"
x1="744.84583"
y1="-147.4702"
x2="689.37933"
y2="-319.99997" />
<linearGradient
inkscape:collect="always"
id="linearGradient4677">
<stop
style="stop-color:#1c241f;stop-opacity:1;"
offset="0"
id="stop4673" />
<stop
style="stop-color:#2b3931;stop-opacity:1"
offset="1"
id="stop4675" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2118"
id="radialGradient2558"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(4.1025138,4.904937e-7,-4.7336306e-7,3.9592323,-527.377,-192.81817)"
cx="168.7189"
cy="57.244839"
fx="168.7189"
fy="57.244839"
r="7.9375" />
<linearGradient
inkscape:collect="always"
id="linearGradient2118">
<stop
style="stop-color:#00ffcc;stop-opacity:1"
offset="0"
id="stop2114" />
<stop
style="stop-color:#237d7f;stop-opacity:0;"
offset="1"
id="stop2116" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5666"
id="linearGradient2560"
gradientUnits="userSpaceOnUse"
x1="169.72586"
y1="66.1511"
x2="191.85043"
y2="66.1511" />
<linearGradient
inkscape:collect="always"
id="linearGradient5666">
<stop
style="stop-color:#242f28;stop-opacity:1;"
offset="0"
id="stop5662" />
<stop
style="stop-color:#1c241f;stop-opacity:1"
offset="1"
id="stop5664" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient5364"
id="radialGradient2562"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.6948297,0.68882556,-1.8240762,7.1361678,-1588.0171,1034.8652)"
cx="672.15662"
cy="-246.05667"
fx="672.15662"
fy="-246.05667"
r="13.702145" />
<linearGradient
inkscape:collect="always"
id="linearGradient5364">
<stop
style="stop-color:#536c5d;stop-opacity:1;"
offset="0"
id="stop5360" />
<stop
style="stop-color:#536c5d;stop-opacity:0;"
offset="1"
id="stop5362" />
</linearGradient>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath5332">
<path
style="fill:url(#linearGradient5326);fill-opacity:1;stroke-width:0.264583"
d="m 175.41875,43.00005 v 0.454753 a 7.8052081,22.886458 0 0 1 0.92604,-0.190169 7.8052081,22.886458 0 0 1 7.80521,22.886457 7.8052081,22.886458 0 0 1 -7.4967,22.854422 h 26.98956 a 7.8052081,22.886458 0 0 0 0.2186,0.03204 7.8052081,22.886458 0 0 0 7.8052,-22.886461 7.8052081,22.886458 0 0 0 -7.8052,-22.886457 7.8052081,22.886458 0 0 0 -0.1323,0.01394 V 43.00005 Z m 0,45.858186 v 0.147277 h 0.70745 a 7.8052081,22.886458 0 0 1 -0.70745,-0.147277 z"
id="path5334"
inkscape:connector-curvature="0" />
</clipPath>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter2516"
x="-0.1389541"
width="1.2779082"
y="-0.54079059"
height="2.0815812">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="1.8796256"
id="feGaussianBlur2518" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter2492"
x="-0.13744183"
width="1.2748837"
y="-0.68956103"
height="2.3791221">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="1.8591692"
id="feGaussianBlur2494" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter2504"
x="-0.095834583"
width="1.1916692"
y="-1.248017"
height="3.4960341">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="1.710971"
id="feGaussianBlur2506" />
</filter>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3040"
id="linearGradient2564"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-1.0583333)"
x1="177.82297"
y1="66.155998"
x2="184.15675"
y2="66.155998" />
<linearGradient
id="linearGradient3040"
inkscape:collect="always">
<stop
id="stop3036"
offset="0"
style="stop-color:#2ca089;stop-opacity:1" />
<stop
id="stop3038"
offset="1"
style="stop-color:#93ac93;stop-opacity:0;" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5548"
id="linearGradient2566"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(3.7795277,0,0,2.6941623,-313.61226,-1006.2008)"
x1="177.82297"
y1="66.155998"
x2="184.15675"
y2="66.155998" />
<linearGradient
inkscape:collect="always"
id="linearGradient5548">
<stop
style="stop-color:#37c8ab;stop-opacity:1"
offset="0"
id="stop5544" />
<stop
style="stop-color:#93ac93;stop-opacity:0;"
offset="1"
id="stop5546" />
</linearGradient>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath962">
<ellipse
style="fill:url(#linearGradient956);fill-opacity:1;stroke-width:0.228428"
id="ellipse964"
cx="176.34479"
cy="66.1511"
rx="6.6189275"
ry="20.116356" />
</clipPath>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1636"
id="linearGradient2568"
gradientUnits="userSpaceOnUse"
x1="180.2618"
y1="79.843285"
x2="177.90164"
y2="58.2136" />
<linearGradient
inkscape:collect="always"
id="linearGradient1636">
<stop
style="stop-color:#6f917c;stop-opacity:1;"
offset="0"
id="stop1632" />
<stop
style="stop-color:#1c2422;stop-opacity:1"
offset="1"
id="stop1634" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1988"
id="linearGradient2570"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0057529,0,0,0.63337186,1.6167144,24.252855)"
x1="180.2618"
y1="79.843285"
x2="177.90164"
y2="58.2136" />
<linearGradient
id="linearGradient1988"
inkscape:collect="always">
<stop
id="stop1984"
offset="0"
style="stop-color:#6f917c;stop-opacity:1;" />
<stop
id="stop1986"
offset="1"
style="stop-color:#b7c8c4;stop-opacity:0.06956521" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2562"
id="linearGradient2572"
gradientUnits="userSpaceOnUse"
x1="164.83987"
y1="43.408844"
x2="203.95857"
y2="43.128212" />
<linearGradient
inkscape:collect="always"
id="linearGradient2562">
<stop
style="stop-color:#00d4aa;stop-opacity:1;"
offset="0"
id="stop2558" />
<stop
style="stop-color:#00d4aa;stop-opacity:0;"
offset="1"
id="stop2560" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3482"
id="linearGradient3484"
x1="356.72446"
y1="182.1709"
x2="344.48761"
y2="129.496"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(34.475201,-21.166667)" />
<linearGradient
inkscape:collect="always"
id="linearGradient3482">
<stop
style="stop-color:#171e1c;stop-opacity:1;"
offset="0"
id="stop3478" />
<stop
id="stop3494"
offset="0.80120975"
style="stop-color:#152320;stop-opacity:1;" />
<stop
style="stop-color:#152521;stop-opacity:1"
offset="1"
id="stop3480" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3490"
id="linearGradient3492"
x1="347.93649"
y1="171.56474"
x2="354.54169"
y2="126.321"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(34.475201,-21.166667)" />
<linearGradient
inkscape:collect="always"
id="linearGradient3490">
<stop
style="stop-color:#374845;stop-opacity:1;"
offset="0"
id="stop3486" />
<stop
style="stop-color:#152a27;stop-opacity:1"
offset="1"
id="stop3488" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3552"
id="linearGradient3554"
x1="345.86197"
y1="127.05593"
x2="350.06058"
y2="140.94655"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(34.475201,-21.166667)" />
<linearGradient
inkscape:collect="always"
id="linearGradient3552">
<stop
style="stop-color:#131915;stop-opacity:1"
offset="0"
id="stop3548" />
<stop
style="stop-color:#0a0c0c;stop-opacity:0;"
offset="1"
id="stop3550" />
</linearGradient>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
transform="matrix(0.45328694,0,0,0.45328694,-150.08426,31.677789)"
id="g3583">
<path
style="fill:#374845;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 384.08136,149.35152 2.85336,-1.98187 17.3047,-17.53781 5.30222,-23.86564 -1.14434,4.90968 -7.12593,20.49392 z"
id="path3476"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:#19201f;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 409.63036,105.6806 -6.42418,23.31826 -19.12481,20.35266 -1.02231,-2.62018 13.36763,-19.08194 -0.12652,-9.90058 z"
id="path3449"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:#374845;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 373.21796,148.73038 -0.80327,-4.14381 -9.92729,-19.61929 2.56866,-5.68006 c 0,0 -2.39194,-2.20708 -2.63096,-1.76366 l -0.23905,0.44342 -1.91789,7.51849 z"
id="path3474"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccc" />
<ellipse
ry="13.768127"
rx="15.163113"
cy="5.6849346"
cx="406.25998"
id="ellipse6063"
style="opacity:0.34;fill:none;fill-opacity:1;stroke:#2affd5;stroke-width:0.259508"
transform="matrix(0.99458815,0.1038962,-0.0109865,0.99993965,0,0)" />
<path
style="fill:#161d1b;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 353.47041,111.22063 -0.49878,15.54703 15.93949,21.91363 4.30684,0.0491 -12.94981,-23.24491 3.13368,-9.68437 z"
id="path3447"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<circle
style="fill:url(#radialGradient4635);fill-opacity:1;stroke-width:0.171116"
id="path4619"
cx="374.65378"
cy="80.797707"
r="44.979172" />
<g
id="g4980"
clip-path="url(#clipPath4984)"
transform="translate(34.475201,-37.041668)">
<ellipse
transform="matrix(1.5757282,0.23146107,-0.20900143,1.7450588,-197.08653,-112.20092)"
style="opacity:0.358;fill:#050e11;fill-opacity:1;stroke-width:0.264583;filter:url(#filter4955)"
id="ellipse5828"
cx="351.59506"
cy="82.067276"
rx="23.198996"
ry="9.3544331" />
<ellipse
ry="9.3544331"
rx="23.198996"
cy="82.067276"
cx="351.59506"
id="path4929"
style="opacity:0.123;fill:#00ccff;fill-opacity:1;stroke-width:0.264583;filter:url(#filter4955)"
transform="matrix(1.2075594,0.12977346,-0.16016825,0.9784035,-67.414432,-42.767194)" />
<ellipse
transform="matrix(0.69536155,0.02706809,-0.01729677,0.58663762,91.984087,21.914596)"
style="opacity:0.217;fill:#37c8ab;fill-opacity:1;stroke-width:0.264583;filter:url(#filter3376)"
id="ellipse4990"
cx="351.59506"
cy="82.067276"
rx="23.198996"
ry="9.3544331" />
<path
style="opacity:0.081;fill:url(#linearGradient3607);fill-opacity:1;stroke:#dbe3de;stroke-width:1.19274;filter:url(#filter5875)"
d="M 1446.4434,435.68945 A 157.04902,125.90364 0 0 1 1290,551.9043 157.04902,125.90364 0 0 1 1133.5566,436.31055 157.04902,125.90364 0 0 0 1132.9512,446 157.04902,125.90364 0 0 0 1290,571.9043 157.04902,125.90364 0 0 0 1447.0488,446 a 157.04902,125.90364 0 0 0 -0.6054,-10.31055 z"
transform="matrix(0.26311638,0,0,0.28631922,2.0539852,-2.9301278)"
id="path5830"
inkscape:connector-curvature="0" />
<path
style="fill:#1c2422;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3156)"
d="m 309.12314,87.292218 c 3.13441,24.914932 -0.21048,39.337292 -5.82115,37.879702 -5.61067,-1.45759 -11.27628,-19.79646 -6.0807,-25.272072 4.54081,-2.856353 11.90185,-12.60763 11.90185,-12.60763 z"
id="path5909"
inkscape:connector-curvature="0"
sodipodi:nodetypes="czcc"
transform="rotate(12.553566,314.41882,114.81993)" />
<ellipse
transform="matrix(0.52125432,0,0,0.52125432,156.27047,108.66382)"
ry="12.170834"
rx="11.641666"
cy="85.862556"
cx="321.73331"
id="ellipse3779"
style="opacity:0.171;fill:#d7f4d7;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter3775)" />
<ellipse
transform="matrix(0.52125432,0,0,0.52125432,186.60865,50.055978)"
ry="12.170834"
rx="11.641666"
cy="85.862556"
cx="321.73331"
id="ellipse3779-7"
style="opacity:0.171;fill:#d7f4d7;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter3822)" />
<ellipse
style="opacity:0.171;fill:#d7f4d7;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter3834)"
id="ellipse3812"
cx="321.73331"
cy="85.862556"
rx="11.641666"
ry="12.170834"
transform="matrix(0.52125432,0,0,0.52125432,208.32803,97.976508)" />
</g>
<g
transform="matrix(0.66411258,0.38342558,-0.38342558,0.66411258,232.97954,-70.367332)"
id="use4648">
<g
style="fill:#171d19;fill-opacity:1"
transform="matrix(0.88505748,0,0,0.88505748,27.142605,7.5883684)"
id="g2504">
<ellipse
ry="22.886457"
rx="7.8052082"
cy="66.1511"
cx="203.86162"
id="ellipse2498"
style="fill:url(#linearGradient2554);fill-opacity:1;stroke-width:0.264583" />
<rect
style="fill:#171d19;fill-opacity:1;stroke-width:0.212422"
id="rect2500"
width="28.310415"
height="46.005398"
x="175.41875"
y="43.000057" />
<ellipse
style="fill:#171d19;fill-opacity:1;stroke-width:0.264583"
id="ellipse2502"
cx="176.34479"
cy="66.1511"
rx="7.8052082"
ry="22.886457" />
</g>
<g
id="g2550">
<path
transform="matrix(0.26458333,0,0,0.26458333,0,130.31255)"
sodipodi:nodetypes="ccccscccc"
inkscape:connector-curvature="0"
id="path2506"
d="M 666.88909,-328.58579 663,-156.12109 h 106.67383 c 0.27518,0.0517 0.5506,0.0921 0.82617,0.12109 16.2924,0 29.5,-38.72737 29.5,-86.5 0,-47.77263 -13.2076,-86.5 -29.5,-86.5 -0.16671,0.0134 -0.33339,0.031 -0.5,0.0527 V -330 Z"
style="fill:url(#linearGradient2556);fill-opacity:1;stroke-width:1" />
<ellipse
ry="22.886457"
rx="7.8052082"
cy="66.1511"
cx="176.34479"
id="ellipse2508"
style="fill:#37483e;fill-opacity:1;stroke:url(#radialGradient2558);stroke-width:0.264583;stroke-opacity:1" />
<ellipse
style="fill:url(#linearGradient2560);fill-opacity:1;stroke:none;stroke-width:0.228428;stroke-opacity:1"
id="ellipse2510"
cx="176.34479"
cy="66.1511"
rx="6.618928"
ry="20.116356" />
<path
inkscape:connector-curvature="0"
id="path2512"
transform="matrix(0.26458333,0,0,0.26458333,0,130.31255)"
d="m 681.98633,-302.10156 a 25.016421,62.241739 0 0 0 -17.875,59.60156 25.016421,62.241739 0 0 0 17.90234,59.63867 25.016421,76.030324 0 0 0 9.50195,-59.63867 25.016421,76.030324 0 0 0 -9.52929,-59.60156 z"
style="fill:url(#radialGradient2562);fill-opacity:1;stroke-width:0.78115" />
<g
style="opacity:1;fill:#dbe3de;fill-opacity:1"
clip-path="url(#clipPath5332)"
id="g2522">
<g
style="opacity:1;fill:#2ad4ff;fill-opacity:1;filter:url(#filter2516)"
transform="matrix(1,0,0,1.1345691,-7.7417057e-7,133.5433)"
id="g2516">
<rect
y="-68.680977"
x="180.44221"
height="8.3416786"
width="32.464687"
id="rect2514"
style="opacity:0.166;fill:#2ad4ff;fill-opacity:1;stroke-width:0.264583" />
</g>
<rect
y="75.459694"
x="180.44945"
height="6.4707923"
width="32.464687"
id="rect2518"
style="opacity:0.464;fill:#000c09;fill-opacity:1;stroke-width:0.233031;filter:url(#filter2492)" />
<rect
y="42.056061"
x="172.21031"
height="3.2902839"
width="42.848106"
id="rect2520"
style="opacity:0.404;fill:#2ad4ff;fill-opacity:1;stroke-width:0.241515;filter:url(#filter2504)" />
</g>
<path
inkscape:connector-curvature="0"
style="fill:url(#linearGradient2564);fill-opacity:1;stroke-width:0.106961"
d="m 179.43157,58.663845 a 3.7324504,7.8216032 0 0 0 -2.66695,7.489825 3.7324504,7.8216032 0 0 0 2.67103,7.494489 3.7324504,9.5543451 0 0 0 1.41769,-7.494489 3.7324504,9.5543451 0 0 0 -1.42177,-7.489825 z"
id="path2524" />
<path
inkscape:connector-curvature="0"
id="path2526"
d="m 186.92813,42.825396 a 7.8052082,23.325705 0 0 1 7.80521,23.325704 7.8052082,23.325705 0 0 1 -7.80521,23.325705"
style="fill:none;fill-opacity:1;stroke:#1f241c;stroke-width:0.26711" />
<path
inkscape:connector-curvature="0"
id="path2528"
d="m 181.63646,42.825396 a 7.8052082,23.325705 0 0 1 7.80521,23.325704 7.8052082,23.325705 0 0 1 -7.80521,23.325705"
style="fill:none;fill-opacity:1;stroke:#1f241c;stroke-width:0.26711" />
<path
inkscape:connector-curvature="0"
style="opacity:0.111;fill:none;fill-opacity:1;stroke:#dbe3de;stroke-width:0.26711"
d="m 187.20875,42.825396 a 7.8052082,23.325705 0 0 1 7.80521,23.325704 7.8052082,23.325705 0 0 1 -7.80521,23.325705"
id="path2530" />
<path
inkscape:connector-curvature="0"
style="opacity:0.111;fill:none;fill-opacity:1;stroke:#dbe3de;stroke-width:0.26711"
d="m 181.91708,42.825396 a 7.8052082,23.325705 0 0 1 7.80521,23.325704 7.8052082,23.325705 0 0 1 -7.80521,23.325705"
id="path2532" />
<path
inkscape:connector-curvature="0"
id="path2534"
transform="matrix(0.26458333,0,0,0.26458333,84.564076,285.22196)"
d="m 368.55469,-848.15039 a 14.1069,21.072668 0 0 0 -10.08008,20.17773 14.1069,21.072668 0 0 0 10.0957,20.19141 14.1069,25.740956 0 0 0 2.85547,-5.56445 25.016421,76.030325 0 0 0 0.47852,-14.63868 25.016421,76.030325 0 0 0 -0.48047,-14.58398 14.1069,25.740956 0 0 0 -2.86914,-5.58203 z"
style="opacity:0.148;fill:url(#linearGradient2566);fill-opacity:1;stroke-width:0.341316" />
<g
clip-path="url(#clipPath962)"
id="g2546">
<path
sodipodi:open="true"
sodipodi:end="4.712389"
sodipodi:start="1.5707963"
sodipodi:ry="16.29439"
sodipodi:rx="6.6350451"
sodipodi:cy="66.1511"
sodipodi:cx="182.26727"
sodipodi:type="arc"
id="path2536"
style="fill:none;fill-opacity:1;stroke:url(#linearGradient2568);stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:arc-type="arc"
d="m 182.26727,82.44549 a 6.6350451,16.29439 0 0 1 -5.74611,-8.147195 6.6350451,16.29439 0 0 1 0,-16.29439 6.6350451,16.29439 0 0 1 5.74611,-8.147195" />
<path
style="fill:none;fill-opacity:1;stroke:#091f1b;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path2538"
sodipodi:type="arc"
sodipodi:cx="177.40311"
sodipodi:cy="66.1511"
sodipodi:rx="6.618928"
sodipodi:ry="20.116356"
sodipodi:start="1.5707963"
sodipodi:end="4.712389"
sodipodi:open="true"
sodipodi:arc-type="arc"
d="m 177.40311,86.267456 a 6.618928,20.116356 0 0 1 -5.73216,-10.058178 6.618928,20.116356 0 0 1 0,-20.116356 6.618928,20.116356 0 0 1 5.73216,-10.058178" />
<path
sodipodi:open="true"
sodipodi:end="4.712389"
sodipodi:start="1.5707963"
sodipodi:ry="18.995991"
sodipodi:rx="6.6234765"
sodipodi:cy="66.1511"
sodipodi:cx="178.9929"
sodipodi:type="arc"
id="path2540"
style="fill:none;fill-opacity:1;stroke:#091f1b;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:arc-type="arc"
d="m 178.9929,85.147091 a 6.6234765,18.995991 0 0 1 -5.73609,-9.497995 6.6234765,18.995991 0 0 1 0,-18.995991 6.6234765,18.995991 0 0 1 5.73609,-9.497996" />
<path
style="fill:none;fill-opacity:1;stroke:#091f1b;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path2542"
sodipodi:type="arc"
sodipodi:cx="180.58263"
sodipodi:cy="66.1511"
sodipodi:rx="6.6278849"
sodipodi:ry="17.941591"
sodipodi:start="1.5707963"
sodipodi:end="4.712389"
sodipodi:open="true"
sodipodi:arc-type="arc"
d="m 180.58263,84.092691 a 6.6278849,17.941591 0 0 1 -5.73992,-8.970795 6.6278849,17.941591 0 0 1 0,-17.941592 6.6278849,17.941591 0 0 1 5.73992,-8.970795" />
<path
style="fill:none;fill-opacity:1;stroke:url(#linearGradient2570);stroke-width:0.211173;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path2544"
sodipodi:type="arc"
sodipodi:cx="184.93251"
sodipodi:cy="66.1511"
sodipodi:rx="6.6732159"
sodipodi:ry="10.320408"
sodipodi:start="1.5707963"
sodipodi:end="4.712389"
sodipodi:open="true"
sodipodi:arc-type="arc"
d="m 184.93251,76.471508 a 6.6732159,10.320408 0 0 1 -5.77917,-5.160204 6.6732159,10.320408 0 0 1 0,-10.320408 6.6732159,10.320408 0 0 1 5.77917,-5.160204" />
</g>
<path
inkscape:connector-curvature="0"
id="path2548"
d="m 176.44064,42.995921 27.51666,0.264584"
style="fill:none;stroke:url(#linearGradient2572);stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
</g>
<ellipse
transform="rotate(-14.215013)"
ry="5.5191154"
rx="2.8998742"
cy="174.85312"
cx="304.87619"
id="ellipse2002"
style="opacity:1;fill:#113f35;fill-opacity:1;stroke:none;stroke-width:0.264583" />
<ellipse
style="opacity:1;fill:#161c1a;fill-opacity:1;stroke:none;stroke-width:0.264583"
id="path6286"
cx="304.74963"
cy="174.27518"
rx="2.8998742"
ry="5.5191154"
transform="rotate(-14.215013)" />
<path
style="fill:#131915;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 370.79736,119.67173 c 1.04003,1.20435 8.7148,-9.18437 13.02347,-22.625767 l 6.40568,2.29773 c 0,0 -10.44064,21.524307 -14.06159,22.372227 z"
id="path3496"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
style="fill:url(#linearGradient3484);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 382.80782,101.31287 4.09249,31.0529 -12.6907,29.28207 -3.62643,-2.08568 8.74484,-28.46283 -3.78779,-15.94331 c -0.0211,-0.0211 1.96246,-3.15061 7.26759,-13.84315 z"
id="path3445"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:url(#linearGradient3492);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 384.13075,102.10662 3.01386,-0.41664 2.53371,30.54079 -13.5599,26.92136 -1.90881,2.49571 12.69072,-29.28207 z"
id="path3472"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="opacity:1;fill:url(#linearGradient3554);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 375.44429,115.21775 2.52847,9.81712 7.73208,-6.39815 c 0,0 3.52257,-0.21254 3.24195,-1.14798 -0.28064,8.77282 -1.34172,-14.89948 -1.34172,-14.89948 l -3.97428,-3.277937 z"
id="path3498"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -268,3 +268,39 @@ $sidebar_border: transparentize(white, 0.95);
$color: mix($wisegreen, $content_bg, 30%);
background: linear-gradient(170deg, lighten($color, 10%), $color);
}
@mixin flow_grid {
width: 100%;
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(auto-fit, minmax($cell - 5, 1fr));
grid-auto-rows: $cell;
grid-auto-flow: row dense;
grid-column-gap: $gap;
grid-row-gap: $gap;
@media (max-width: ($cell + 10) * 3) {
grid-template-columns: repeat(auto-fill, minmax($fluid_cell - 20, 1fr));
grid-auto-rows: $fluid_cell;
grid-template-rows: calc(50vw - 10px) $fluid_cell;
}
@media (max-width: $cell_tablet) {
grid-template-rows: calc(66vw - 10px) auto $fluid_cell;
}
@media (max-width: $cell_mobile) {
// rework stamp, so it will be shown as smaller one on mobiles
grid-template-columns: repeat(auto-fill, minmax(calc(50vw - 20px), 1fr));
grid-template-rows: calc(80vw - 10px) auto 50vw;
grid-auto-rows: 50vw;
}
@media (max-width: ($fluid_cell + 5) * 1.5 + 20) {
grid-template-columns: repeat(auto-fill, minmax(calc(50vw - 20px), 1fr));
grid-template-rows: calc(80vw - 10px) auto 50vw;
grid-auto-rows: 50vw;
}
}

13
src/utils/color.ts Normal file
View file

@ -0,0 +1,13 @@
export const convertHexToRGBA = (hexCode, opacity) => {
let hex = hexCode.replace('#', '');
if (hex.length === 3) {
hex = `${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}`;
}
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r},${g},${b},${opacity})`;
};

View file

@ -1,23 +1,10 @@
import { useCallback, useEffect } from 'react';
import { flowGetMore } from '~/redux/flow/actions';
import { useDispatch } from 'react-redux';
import { useInfiniteLoader } from '~/utils/hooks/useInfiniteLoader';
export const useFlowPagination = ({ isLoading }) => {
const dispatch = useDispatch();
const onLoadMore = useCallback(() => {
(window as any).flowScrollPos = window.scrollY;
const pos = window.scrollY + window.innerHeight - document.body.scrollHeight;
if (isLoading || pos < -600) return;
dispatch(flowGetMore());
}, [dispatch, isLoading]);
useEffect(() => {
window.addEventListener('scroll', onLoadMore);
return () => window.removeEventListener('scroll', onLoadMore);
}, [onLoadMore]);
const loadMore = useCallback(() => dispatch(flowGetMore()), []);
useInfiniteLoader(loadMore, isLoading);
};

View file

@ -39,10 +39,12 @@ export const useDelayedReady = (setReady: (val: boolean) => void, delay: number
* @param onUpload -- upload callback
* @param allowedTypes -- list of allowed types
*/
export const useDropZone = (onUpload: (file: File[]) => void, allowedTypes?: string[]) => {
export const useFileDropZone = (onUpload: (file: File[]) => void, allowedTypes?: string[]) => {
return useCallback(
event => {
event.preventDefault();
event.stopPropagation();
const files: File[] = Array.from((event.dataTransfer?.files as File[]) || []).filter(
(file: File) => file?.type && (!allowedTypes || allowedTypes.includes(file.type))
);

View file

@ -0,0 +1,21 @@
import { useDispatch } from 'react-redux';
import { useCallback } from 'react';
import { useInfiniteLoader } from '~/utils/hooks/useInfiniteLoader';
import { labGetMore } from '~/redux/lab/actions';
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
import { selectLabList } from '~/redux/lab/selectors';
export const useLabPagination = ({ isLoading }) => {
const { nodes, count } = useShallowSelect(selectLabList);
const dispatch = useDispatch();
const loadMore = useCallback(() => {
if (nodes.length >= count) {
return;
}
dispatch(labGetMore());
}, [nodes, count]);
useInfiniteLoader(loadMore, isLoading);
};

View file

@ -2,7 +2,7 @@ import { IComment, INode } from '~/redux/types';
import { useCallback, useEffect, useRef } from 'react';
import { FormikHelpers, useFormik, useFormikContext } from 'formik';
import { array, object, string } from 'yup';
import { FileUploader } from '~/utils/hooks/fileUploader';
import { FileUploader } from '~/utils/hooks/useFileUploader';
import { useDispatch } from 'react-redux';
import { nodePostLocalComment } from '~/redux/node/actions';

View file

@ -0,0 +1,50 @@
import React, { FC, useContext } from 'react';
import { createContext, useCallback, useEffect, useState } from 'react';
const DragContext = createContext({
isDragging: false,
setIsDragging: (val: boolean) => {},
});
export const DragDetectorProvider: FC = ({ children }) => {
const [isDragging, setIsDragging] = useState(false);
return (
<DragContext.Provider value={{ isDragging, setIsDragging }}>{children}</DragContext.Provider>
);
};
export const useDragDetector = () => {
const { isDragging, setIsDragging } = useContext(DragContext);
const onStopDragging = useCallback(() => setIsDragging(false), [setIsDragging]);
useEffect(() => {
const addClass = () => setIsDragging(true);
const removeClass = event => {
// Small hack to ignore intersection with child elements
if (event.pageX !== 0 && event.pageY !== 0) {
return;
}
setIsDragging(false);
};
document.addEventListener('dragenter', addClass);
document.addEventListener('dragover', addClass);
document.addEventListener('dragleave', removeClass);
document.addEventListener('blur', removeClass);
document.addEventListener('drop', onStopDragging);
return () => {
document.removeEventListener('dragenter', addClass);
document.removeEventListener('dragover', addClass);
document.removeEventListener('dragleave', removeClass);
document.removeEventListener('blur', removeClass);
document.removeEventListener('drop', onStopDragging);
};
}, [setIsDragging]);
return { isDragging, onStopDragging };
};

View file

@ -0,0 +1,17 @@
import { useCallback, useEffect } from 'react';
export const useInfiniteLoader = (loader: () => void, isLoading?: boolean) => {
const onLoadMore = useCallback(() => {
const pos = window.scrollY + window.innerHeight - document.body.scrollHeight;
if (isLoading || pos < -600) return;
loader();
}, [loader, isLoading]);
useEffect(() => {
window.addEventListener('scroll', onLoadMore);
return () => window.removeEventListener('scroll', onLoadMore);
}, [onLoadMore]);
};

View file

@ -1,5 +1,5 @@
import { IComment, INode } from '~/redux/types';
import { FileUploader } from '~/utils/hooks/fileUploader';
import { FileUploader } from '~/utils/hooks/useFileUploader';
import { useCallback, useEffect, useRef } from 'react';
import { FormikHelpers, useFormik, useFormikContext } from 'formik';
import { object, string } from 'yup';

View file

@ -0,0 +1,21 @@
import { IUser } from '~/redux/auth/types';
import { useRandomPhrase } from '~/constants/phrases';
import { differenceInDays, parseISO } from 'date-fns';
import { INACTIVE_ACCOUNT_DAYS } from '~/constants/user';
const today = new Date();
export const useUserDescription = (user?: Partial<IUser>) => {
const randomPhrase = useRandomPhrase('USER_DESCRIPTION');
if (!user) {
return '';
}
const lastSeen = user.last_seen ? parseISO(user.last_seen) : undefined;
if (!lastSeen || differenceInDays(today, lastSeen) > INACTIVE_ACCOUNT_DAYS) {
return 'Юнит деактивирован';
}
return user.description || randomPhrase;
};

View file

@ -4,3 +4,5 @@ export type DivProps = React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
export type SVGProps = React.SVGProps<SVGSVGElement>;

View file

@ -1805,6 +1805,11 @@
dependencies:
"@types/jest" "*"
"@types/throttle-debounce@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz#1c3df624bfc4b62f992d3012b84c56d41eab3776"
integrity sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==
"@types/yargs-parser@*":
version "15.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
@ -2410,6 +2415,11 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
attr-accept@^2.2.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
autoprefixer@^9.6.1:
version "9.8.6"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f"
@ -4885,6 +4895,13 @@ file-loader@4.3.0:
loader-utils "^1.2.3"
schema-utils "^2.5.0"
file-selector@^0.2.2:
version "0.2.4"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.2.4.tgz#7b98286f9dbb9925f420130ea5ed0a69238d4d80"
integrity sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==
dependencies:
tslib "^2.0.3"
file-uri-to-path@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@ -9361,6 +9378,15 @@ react-dom@^17.0.1:
object-assign "^4.1.1"
scheduler "^0.20.1"
react-dropzone@^11.4.2:
version "11.4.2"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.4.2.tgz#1eb99e9def4cc7520f4f58e85c853ce52c483d56"
integrity sha512-ocYzYn7Qgp0tFc1gQtUTOaHHSzVTwhWHxxY+r7cj2jJTPfMTZB5GWSJHdIVoxsl+EQENpjJ/6Zvcw0BqKZQ+Eg==
dependencies:
attr-accept "^2.2.1"
file-selector "^0.2.2"
prop-types "^15.7.2"
react-error-overlay@^6.0.7:
version "6.0.8"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.8.tgz#474ed11d04fc6bda3af643447d85e9127ed6b5de"
@ -11137,6 +11163,11 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.3:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
tsutils@^3.17.1:
version "3.17.1"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"