mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +07:00
commit
7f62ebcc39
24 changed files with 311 additions and 70 deletions
|
@ -29,6 +29,7 @@
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scripts": "3.4.4",
|
"react-scripts": "3.4.4",
|
||||||
"react-sortable-hoc": "^1.11",
|
"react-sortable-hoc": "^1.11",
|
||||||
|
"react-sticky-box": "^0.9.3",
|
||||||
"redux": "^4.0.1",
|
"redux": "^4.0.1",
|
||||||
"redux-persist": "^5.10.0",
|
"redux-persist": "^5.10.0",
|
||||||
"redux-saga": "^1.1.1",
|
"redux-saga": "^1.1.1",
|
||||||
|
@ -71,8 +72,8 @@
|
||||||
"@types/node": "^11.13.22",
|
"@types/node": "^11.13.22",
|
||||||
"@types/ramda": "^0.26.33",
|
"@types/ramda": "^0.26.33",
|
||||||
"@types/react-redux": "^7.1.11",
|
"@types/react-redux": "^7.1.11",
|
||||||
"@types/yup": "^0.29.11",
|
|
||||||
"@types/swiper": "^5.4.2",
|
"@types/swiper": "^5.4.2",
|
||||||
|
"@types/yup": "^0.29.11",
|
||||||
"craco-alias": "^2.1.1",
|
"craco-alias": "^2.1.1",
|
||||||
"craco-fast-refresh": "^1.0.2",
|
"craco-fast-refresh": "^1.0.2",
|
||||||
"prettier": "^1.18.2"
|
"prettier": "^1.18.2"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC, useMemo } from 'react';
|
||||||
import { IBorisState } from '~/redux/boris/reducer';
|
import { IBorisState } from '~/redux/boris/reducer';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { Placeholder } from '~/components/placeholders/Placeholder';
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
|
@ -9,7 +9,17 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const BorisStatsGit: FC<IProps> = ({ stats }) => {
|
const BorisStatsGit: FC<IProps> = ({ stats }) => {
|
||||||
if (!stats.git.length) return null;
|
if (!stats.issues.length) return null;
|
||||||
|
|
||||||
|
const open = useMemo(
|
||||||
|
() => stats.issues.filter(el => !el.pull_request && el.state === 'open').slice(0, 5),
|
||||||
|
[stats.issues]
|
||||||
|
);
|
||||||
|
|
||||||
|
const closed = useMemo(
|
||||||
|
() => stats.issues.filter(el => !el.pull_request && el.state === 'closed').slice(0, 5),
|
||||||
|
[stats.issues]
|
||||||
|
);
|
||||||
|
|
||||||
if (stats.is_loading) {
|
if (stats.is_loading) {
|
||||||
return (
|
return (
|
||||||
|
@ -35,11 +45,12 @@ const BorisStatsGit: FC<IProps> = ({ stats }) => {
|
||||||
<img src="https://jenkins.vault48.org/api/badges/muerwre/vault-golang/status.svg" />
|
<img src="https://jenkins.vault48.org/api/badges/muerwre/vault-golang/status.svg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{stats.git
|
{open.map(data => (
|
||||||
.filter(data => data.commit && data.timestamp && data.subject)
|
<BorisStatsGitCard data={data} key={data.id} />
|
||||||
.slice(0, 5)
|
))}
|
||||||
.map(data => (
|
|
||||||
<BorisStatsGitCard data={data} key={data.commit} />
|
{closed.map(data => (
|
||||||
|
<BorisStatsGitCard data={data} key={data.id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,22 +1,33 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC, useMemo } from 'react';
|
||||||
import { IStatGitRow } from '~/redux/boris/reducer';
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { getPrettyDate } from '~/utils/dom';
|
import { getPrettyDate } from '~/utils/dom';
|
||||||
|
import { IGithubIssue } from '~/redux/boris/types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
data: Partial<IStatGitRow>;
|
data: IGithubIssue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BorisStatsGitCard: FC<IProps> = ({ data: { timestamp, subject } }) => {
|
const stateLabels: Record<IGithubIssue['state'], string> = {
|
||||||
if (!subject || !timestamp) return null;
|
open: 'Ожидает',
|
||||||
|
closed: 'Сделано',
|
||||||
|
};
|
||||||
|
|
||||||
|
const BorisStatsGitCard: FC<IProps> = ({ data: { created_at, title, html_url, state } }) => {
|
||||||
|
if (!title || !created_at) return null;
|
||||||
|
|
||||||
|
const date = useMemo(() => getPrettyDate(created_at), [created_at]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<div className={styles.time}>
|
<div className={styles.time}>
|
||||||
{getPrettyDate(new Date(parseInt(`${timestamp}000`)).toISOString())}
|
<span className={classNames(styles.icon, styles[state])}>{stateLabels[state]}</span>
|
||||||
|
{date}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.subject}>{subject}</div>
|
<a className={styles.subject} href={html_url} target="_blank">
|
||||||
|
{title}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,10 +12,28 @@
|
||||||
.time {
|
.time {
|
||||||
font: $font_12_regular;
|
font: $font_12_regular;
|
||||||
line-height: 17px;
|
line-height: 17px;
|
||||||
opacity: 0.3;
|
color: transparentize(white, 0.7)
|
||||||
}
|
}
|
||||||
|
|
||||||
.subject {
|
.subject {
|
||||||
font: $font_14_regular;
|
font: $font_14_regular;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font: $font_10_semibold;
|
||||||
|
margin-right: 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 2px 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
color: $red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.closed {
|
||||||
|
color: $green;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,8 @@ const CommentEmbedBlockUnconnected: FC<Props> = memo(
|
||||||
return (match && match[1]) || '';
|
return (match && match[1]) || '';
|
||||||
}, [block.content]);
|
}, [block.content]);
|
||||||
|
|
||||||
|
const url = useMemo(() => `https://youtube.com/watch?v=${id}`, [id]);
|
||||||
|
|
||||||
const preview = useMemo(() => getYoutubeThumb(block.content), [block.content]);
|
const preview = useMemo(() => getYoutubeThumb(block.content), [block.content]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -47,7 +49,7 @@ const CommentEmbedBlockUnconnected: FC<Props> = memo(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.embed}>
|
<div className={styles.embed}>
|
||||||
<a href={id[0]} target="_blank" />
|
<a href={url} target="_blank" />
|
||||||
|
|
||||||
<div className={styles.preview}>
|
<div className={styles.preview}>
|
||||||
<div style={{ backgroundImage: `url("${preview}")` }}>
|
<div style={{ backgroundImage: `url("${preview}")` }}>
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { EMPTY_COMMENT } from '~/redux/node/constants';
|
||||||
import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone';
|
import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { ERROR_LITERAL } from '~/constants/errors';
|
import { ERROR_LITERAL } from '~/constants/errors';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { useInputPasteUpload } from '~/utils/hooks/useInputPasteUpload';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
comment?: IComment;
|
comment?: IComment;
|
||||||
|
@ -47,6 +47,7 @@ const CommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
|
||||||
}, [formik]);
|
}, [formik]);
|
||||||
|
|
||||||
const error = formik.status || formik.errors.text;
|
const error = formik.status || formik.errors.text;
|
||||||
|
useInputPasteUpload(textarea, uploader.uploadFiles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommentFormDropzone onUpload={uploader.uploadFiles}>
|
<CommentFormDropzone onUpload={uploader.uploadFiles}>
|
||||||
|
@ -65,16 +66,21 @@ const CommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
|
||||||
|
|
||||||
<CommentFormAttaches />
|
<CommentFormAttaches />
|
||||||
|
|
||||||
<Group horizontal className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
|
<div className={styles.buttons_attach}>
|
||||||
<CommentFormAttachButtons onUpload={uploader.uploadFiles} />
|
<CommentFormAttachButtons onUpload={uploader.uploadFiles} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttons_format}>
|
||||||
{!!textarea && (
|
{!!textarea && (
|
||||||
<CommentFormFormatButtons
|
<CommentFormFormatButtons
|
||||||
element={textarea}
|
element={textarea}
|
||||||
handler={formik.handleChange('text')}
|
handler={formik.handleChange('text')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttons_submit}>
|
||||||
{isLoading && <LoaderCircle size={20} />}
|
{isLoading && <LoaderCircle size={20} />}
|
||||||
|
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
|
@ -92,7 +98,8 @@ const CommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
|
||||||
>
|
>
|
||||||
{!isEditing ? 'Сказать' : 'Сохранить'}
|
{!isEditing ? 'Сказать' : 'Сохранить'}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</div>
|
||||||
|
</div>
|
||||||
</FileUploaderProvider>
|
</FileUploaderProvider>
|
||||||
</FormikProvider>
|
</FormikProvider>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -21,13 +21,42 @@
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: row;
|
|
||||||
background: transparentize(black, 0.8);
|
background: transparentize(black, 0.8);
|
||||||
padding: $gap / 2;
|
padding: $gap / 2;
|
||||||
border-radius: 0 0 $radius $radius;
|
border-radius: 0 0 $radius $radius;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
column-gap: $gap;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
grid-template-areas: "attach format submit";
|
||||||
|
|
||||||
|
@media(max-width: 470px) {
|
||||||
|
padding: $gap;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"attach format"
|
||||||
|
"submit submit";
|
||||||
|
row-gap: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_attach {
|
||||||
|
grid-area: attach;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_format {
|
||||||
|
grid-area: format;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_submit {
|
||||||
|
grid-area: submit;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
column-gap: $gap / 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploads {
|
.uploads {
|
||||||
|
|
|
@ -2,11 +2,8 @@
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
@media(max-width: 480px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import React, { DetailsHTMLAttributes, FC, useEffect, useRef } from 'react';
|
import React, { DetailsHTMLAttributes, FC, useEffect, useRef } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import StickySidebar from 'sticky-sidebar';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import ResizeSensor from 'resize-sensor';
|
import ResizeSensor from 'resize-sensor';
|
||||||
|
(window as any).ResizeSensor = ResizeSensor;
|
||||||
|
|
||||||
|
import StickySidebar from 'sticky-sidebar';
|
||||||
|
(window as any).StickySidebar = StickySidebar;
|
||||||
|
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
interface IProps extends DetailsHTMLAttributes<HTMLDivElement> {}
|
interface IProps extends DetailsHTMLAttributes<HTMLDivElement> {}
|
||||||
|
|
||||||
(window as any).StickySidebar = StickySidebar;
|
|
||||||
(window as any).ResizeSensor = ResizeSensor;
|
|
||||||
|
|
||||||
const Sticky: FC<IProps> = ({ children }) => {
|
const Sticky: FC<IProps> = ({ children }) => {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const sb = useRef<StickySidebar>(null);
|
const sb = useRef<StickySidebar>(null);
|
||||||
|
|
|
@ -2,16 +2,16 @@ import React, { FC } from 'react';
|
||||||
import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge';
|
import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import { Padder } from '~/components/containers/Padder';
|
import { Padder } from '~/components/containers/Padder';
|
||||||
import styles from '~/containers/node/NodeLayout/styles.module.scss';
|
|
||||||
import { NodeCommentsBlock } from '~/components/node/NodeCommentsBlock';
|
import { NodeCommentsBlock } from '~/components/node/NodeCommentsBlock';
|
||||||
import { NodeCommentForm } from '~/components/node/NodeCommentForm';
|
import { NodeCommentForm } from '~/components/node/NodeCommentForm';
|
||||||
import { Sticky } from '~/components/containers/Sticky';
|
|
||||||
import { NodeRelatedBlock } from '~/components/node/NodeRelatedBlock';
|
import { NodeRelatedBlock } from '~/components/node/NodeRelatedBlock';
|
||||||
import { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks';
|
import { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks';
|
||||||
import { IComment, INode } from '~/redux/types';
|
import { IComment, INode } from '~/redux/types';
|
||||||
import { useUser } from '~/utils/hooks/user/userUser';
|
import { useUser } from '~/utils/hooks/user/userUser';
|
||||||
import { NodeTagsBlock } from '~/components/node/NodeTagsBlock';
|
import { NodeTagsBlock } from '~/components/node/NodeTagsBlock';
|
||||||
import { INodeRelated } from '~/redux/node/types';
|
import { INodeRelated } from '~/redux/node/types';
|
||||||
|
import StickyBox from 'react-sticky-box/dist/esnext';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
node: INode;
|
node: INode;
|
||||||
|
@ -59,12 +59,12 @@ const NodeBottomBlock: FC<IProps> = ({
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<div className={styles.panel}>
|
<div className={styles.panel}>
|
||||||
<Sticky>
|
<StickyBox className={styles.sticky} offsetTop={72}>
|
||||||
<Group style={{ flex: 1, minWidth: 0 }}>
|
<Group style={{ flex: 1, minWidth: 0 }}>
|
||||||
<NodeTagsBlock node={node} isLoading={isLoading} />
|
<NodeTagsBlock node={node} isLoading={isLoading} />
|
||||||
<NodeRelatedBlock isLoading={isLoading} node={node} related={related} />
|
<NodeRelatedBlock isLoading={isLoading} node={node} related={related} />
|
||||||
</Group>
|
</Group>
|
||||||
</Sticky>
|
</StickyBox>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</Padder>
|
</Padder>
|
||||||
|
|
48
src/components/node/NodeBottomBlock/styles.module.scss
Normal file
48
src/components/node/NodeBottomBlock/styles.module.scss
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.sticky {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
align-items: stretch !important;
|
||||||
|
@include vertical_at_tablet;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments {
|
||||||
|
flex: 3 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
flex: 2 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
flex: 1 3;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-left: $gap / 2;
|
||||||
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-top: $comment_height / 2;
|
||||||
|
flex: 1 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
background: $node_buttons_bg;
|
||||||
|
flex: 1;
|
||||||
|
border-radius: $panel_radius;
|
||||||
|
box-shadow: $comment_shadow;
|
||||||
|
}
|
|
@ -43,6 +43,7 @@ const NodeImageSwiperBlock: FC<IProps> = ({ node }) => {
|
||||||
const resetSwiper = useCallback(() => {
|
const resetSwiper = useCallback(() => {
|
||||||
if (!controlledSwiper) return;
|
if (!controlledSwiper) return;
|
||||||
controlledSwiper.slideTo(0, 0);
|
controlledSwiper.slideTo(0, 0);
|
||||||
|
setTimeout(() => controlledSwiper.slideTo(0, 0), 300);
|
||||||
}, [controlledSwiper]);
|
}, [controlledSwiper]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -74,6 +75,7 @@ const NodeImageSwiperBlock: FC<IProps> = ({ node }) => {
|
||||||
observeParents
|
observeParents
|
||||||
resizeObserver
|
resizeObserver
|
||||||
watchOverflow
|
watchOverflow
|
||||||
|
updateOnImagesReady
|
||||||
onInit={resetSwiper}
|
onInit={resetSwiper}
|
||||||
zoom
|
zoom
|
||||||
>
|
>
|
||||||
|
|
|
@ -9,4 +9,8 @@
|
||||||
@include tablet {
|
@include tablet {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: $content_width + $gap * 4) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { NodeCommentForm } from '~/components/node/NodeCommentForm';
|
||||||
import isBefore from 'date-fns/isBefore';
|
import isBefore from 'date-fns/isBefore';
|
||||||
import { Card } from '~/components/containers/Card';
|
import { Card } from '~/components/containers/Card';
|
||||||
import { Footer } from '~/components/main/Footer';
|
import { Footer } from '~/components/main/Footer';
|
||||||
import { Sticky } from '~/components/containers/Sticky';
|
|
||||||
import { BorisStats } from '~/components/boris/BorisStats';
|
import { BorisStats } from '~/components/boris/BorisStats';
|
||||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
import { selectBorisStats } from '~/redux/boris/selectors';
|
import { selectBorisStats } from '~/redux/boris/selectors';
|
||||||
|
@ -20,6 +19,7 @@ import { authSetUser } from '~/redux/auth/actions';
|
||||||
import { nodeLoadNode } from '~/redux/node/actions';
|
import { nodeLoadNode } from '~/redux/node/actions';
|
||||||
import { borisLoadStats } from '~/redux/boris/actions';
|
import { borisLoadStats } from '~/redux/boris/actions';
|
||||||
import { Container } from '~/containers/main/Container';
|
import { Container } from '~/containers/main/Container';
|
||||||
|
import StickyBox from 'react-sticky-box/dist/esnext';
|
||||||
|
|
||||||
type IProps = {};
|
type IProps = {};
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ const BorisLayout: FC<IProps> = () => {
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Group className={styles.stats}>
|
<Group className={styles.stats}>
|
||||||
<Sticky>
|
<StickyBox className={styles.sticky} offsetTop={72} offsetBottom={10}>
|
||||||
<Group className={styles.stats__container}>
|
<Group className={styles.stats__container}>
|
||||||
<div className={styles.stats__about}>
|
<div className={styles.stats__about}>
|
||||||
<h4>Господи-боженьки, где это я?</h4>
|
<h4>Господи-боженьки, где это я?</h4>
|
||||||
|
@ -106,7 +106,7 @@ const BorisLayout: FC<IProps> = () => {
|
||||||
<BorisStats stats={stats} />
|
<BorisStats stats={stats} />
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</Sticky>
|
</StickyBox>
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,20 @@
|
||||||
import git from '~/stats/git.json';
|
import git from '~/stats/git.json';
|
||||||
import { API } from '~/constants/api';
|
import { API } from '~/constants/api';
|
||||||
import { api, resultMiddleware, errorMiddleware, cleanResult } from '~/utils/api';
|
import { api, cleanResult } from '~/utils/api';
|
||||||
import { IBorisState, IStatBackend } from './reducer';
|
import { IBorisState, IStatBackend } from './reducer';
|
||||||
import { IResultWithStatus } from '../types';
|
import axios from 'axios';
|
||||||
|
import { IGetGithubIssuesResult } from '~/redux/boris/types';
|
||||||
|
|
||||||
export const getBorisGitStats = () => Promise.resolve<IBorisState['stats']['git']>(git);
|
export const getBorisGitStats = () => Promise.resolve<IBorisState['stats']['git']>(git);
|
||||||
|
|
||||||
export const getBorisBackendStats = () =>
|
export const getBorisBackendStats = () =>
|
||||||
api.get<IStatBackend>(API.BORIS.GET_BACKEND_STATS).then(cleanResult);
|
api.get<IStatBackend>(API.BORIS.GET_BACKEND_STATS).then(cleanResult);
|
||||||
|
|
||||||
|
export const getGithubIssues = () => {
|
||||||
|
return axios
|
||||||
|
.get<IGetGithubIssuesResult>('https://api.github.com/repos/muerwre/vault-frontend/issues', {
|
||||||
|
params: { state: 'all', sort: 'created' },
|
||||||
|
})
|
||||||
|
.then(result => result.data)
|
||||||
|
.catch(() => []);
|
||||||
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { createReducer } from '~/utils/reducer';
|
import { createReducer } from '~/utils/reducer';
|
||||||
import { BORIS_HANDLERS } from './handlers';
|
import { BORIS_HANDLERS } from './handlers';
|
||||||
|
import { IGithubIssue } from '~/redux/boris/types';
|
||||||
|
|
||||||
export type IStatGitRow = {
|
export type IStatGitRow = {
|
||||||
commit: string;
|
commit: string;
|
||||||
|
@ -31,6 +32,7 @@ export type IStatBackend = {
|
||||||
export type IBorisState = Readonly<{
|
export type IBorisState = Readonly<{
|
||||||
stats: {
|
stats: {
|
||||||
git: Partial<IStatGitRow>[];
|
git: Partial<IStatGitRow>[];
|
||||||
|
issues: IGithubIssue[];
|
||||||
backend?: IStatBackend;
|
backend?: IStatBackend;
|
||||||
is_loading: boolean;
|
is_loading: boolean;
|
||||||
};
|
};
|
||||||
|
@ -39,6 +41,7 @@ export type IBorisState = Readonly<{
|
||||||
const BORIS_INITIAL_STATE: IBorisState = {
|
const BORIS_INITIAL_STATE: IBorisState = {
|
||||||
stats: {
|
stats: {
|
||||||
git: [],
|
git: [],
|
||||||
|
issues: [],
|
||||||
backend: undefined,
|
backend: undefined,
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { takeLatest, put, call } from 'redux-saga/effects';
|
import { call, put, takeLatest } from 'redux-saga/effects';
|
||||||
import { BORIS_ACTIONS } from './constants';
|
import { BORIS_ACTIONS } from './constants';
|
||||||
import { borisSetStats } from './actions';
|
import { borisSetStats } from './actions';
|
||||||
import { getBorisGitStats, getBorisBackendStats } from './api';
|
import { getBorisBackendStats, getGithubIssues } from './api';
|
||||||
import { Unwrap } from '../types';
|
import { Unwrap } from '../types';
|
||||||
|
|
||||||
function* loadStats() {
|
function* loadStats() {
|
||||||
try {
|
try {
|
||||||
yield put(borisSetStats({ is_loading: true }));
|
yield put(borisSetStats({ is_loading: true }));
|
||||||
|
|
||||||
const git: Unwrap<typeof getBorisGitStats> = yield call(getBorisGitStats);
|
|
||||||
const backend: Unwrap<typeof getBorisBackendStats> = yield call(getBorisBackendStats);
|
const backend: Unwrap<typeof getBorisBackendStats> = yield call(getBorisBackendStats);
|
||||||
|
const issues: Unwrap<typeof getGithubIssues> = yield call(getGithubIssues);
|
||||||
|
|
||||||
yield put(borisSetStats({ git, backend }));
|
yield put(borisSetStats({ issues, backend }));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
yield put(borisSetStats({ git: [], backend: undefined }));
|
yield put(borisSetStats({ git: [], backend: undefined }));
|
||||||
} finally {
|
} finally {
|
||||||
|
|
12
src/redux/boris/types.ts
Normal file
12
src/redux/boris/types.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export interface IGithubIssue {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
html_url: string;
|
||||||
|
body: string;
|
||||||
|
title: string;
|
||||||
|
state: 'open' | 'closed';
|
||||||
|
created_at: string;
|
||||||
|
pull_request?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IGetGithubIssuesResult = IGithubIssue[];
|
|
@ -75,7 +75,9 @@ export const sagaMiddleware = createSagaMiddleware();
|
||||||
export const history = createBrowserHistory();
|
export const history = createBrowserHistory();
|
||||||
|
|
||||||
const composeEnhancers =
|
const composeEnhancers =
|
||||||
typeof window === 'object' && (<any>window).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
typeof window === 'object' &&
|
||||||
|
(<any>window).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ &&
|
||||||
|
process.env.NODE_ENV === 'development'
|
||||||
? (<any>window).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
|
? (<any>window).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
|
||||||
: compose;
|
: compose;
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { apiGetTagSuggestions, apiGetNodesOfTag } from '~/redux/tag/api';
|
||||||
import { Unwrap } from '~/redux/types';
|
import { Unwrap } from '~/redux/types';
|
||||||
|
|
||||||
function* loadTagNodes({ tag }: ReturnType<typeof tagLoadNodes>) {
|
function* loadTagNodes({ tag }: ReturnType<typeof tagLoadNodes>) {
|
||||||
yield put(tagSetNodes({ isLoading: true, list: [] }));
|
yield put(tagSetNodes({ isLoading: true }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { list }: ReturnType<typeof selectTagNodes> = yield select(selectTagNodes);
|
const { list }: ReturnType<typeof selectTagNodes> = yield select(selectTagNodes);
|
||||||
|
|
|
@ -55,6 +55,10 @@ $margin: 1em;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: $margin;
|
margin-bottom: $margin;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h5, h4, h3, h2, h1 {
|
h5, h4, h3, h2, h1 {
|
||||||
|
|
24
src/utils/hooks/useInputPasteUpload.ts
Normal file
24
src/utils/hooks/useInputPasteUpload.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { getImageFromPaste } from '~/utils/uploader';
|
||||||
|
|
||||||
|
// useInputPasteUpload attaches event listener to input, that calls onUpload if user pasted any image
|
||||||
|
export const useInputPasteUpload = (
|
||||||
|
input: HTMLTextAreaElement | HTMLInputElement | undefined,
|
||||||
|
onUpload: (files: File[]) => void
|
||||||
|
) => {
|
||||||
|
const onPaste = useCallback(async event => {
|
||||||
|
const image = await getImageFromPaste(event);
|
||||||
|
|
||||||
|
if (!image) return;
|
||||||
|
|
||||||
|
onUpload([image]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
input.addEventListener('paste', onPaste);
|
||||||
|
|
||||||
|
return () => input.removeEventListener('paste', onPaste);
|
||||||
|
}, [input, onPaste]);
|
||||||
|
};
|
|
@ -74,3 +74,37 @@ export const fakeUploader = ({
|
||||||
export const getFileType = (file: File): keyof typeof UPLOAD_TYPES | undefined =>
|
export const getFileType = (file: File): keyof typeof UPLOAD_TYPES | undefined =>
|
||||||
(file.type && Object.keys(FILE_MIMES).find(mime => FILE_MIMES[mime].includes(file.type))) ||
|
(file.type && Object.keys(FILE_MIMES).find(mime => FILE_MIMES[mime].includes(file.type))) ||
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
|
// getImageFromPaste returns any images from paste event
|
||||||
|
export const getImageFromPaste = (event: ClipboardEvent): Promise<File | undefined> => {
|
||||||
|
const items = event.clipboardData?.items;
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
for (let index in items) {
|
||||||
|
const item = items[index];
|
||||||
|
|
||||||
|
if (item.kind === 'file' && item.type.match(/^image\//)) {
|
||||||
|
const blob = item.getAsFile();
|
||||||
|
const reader = new FileReader();
|
||||||
|
const type = item.type;
|
||||||
|
|
||||||
|
reader.onload = function(e) {
|
||||||
|
if (!e.target?.result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
new File([e.target?.result], 'paste.png', {
|
||||||
|
type,
|
||||||
|
lastModified: new Date().getTime(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsArrayBuffer(blob);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve(undefined);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
20
yarn.lock
20
yarn.lock
|
@ -1109,6 +1109,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.1.5":
|
||||||
|
version "7.13.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
|
||||||
|
integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/runtime@^7.10.5":
|
"@babel/runtime@^7.10.5":
|
||||||
version "7.13.7"
|
version "7.13.7"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.7.tgz#d494e39d198ee9ca04f4dcb76d25d9d7a1dc961a"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.7.tgz#d494e39d198ee9ca04f4dcb76d25d9d7a1dc961a"
|
||||||
|
@ -9454,6 +9461,14 @@ react-sortable-hoc@^1.11:
|
||||||
invariant "^2.2.4"
|
invariant "^2.2.4"
|
||||||
prop-types "^15.5.7"
|
prop-types "^15.5.7"
|
||||||
|
|
||||||
|
react-sticky-box@^0.9.3:
|
||||||
|
version "0.9.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-sticky-box/-/react-sticky-box-0.9.3.tgz#8450d4cef8e4fdd7b0351520365bc98c97da11af"
|
||||||
|
integrity sha512-Y/qO7vTqAvXuRR6G6ZCW4fX2Bz0GZRwiiLTVeZN5CVz9wzs37ev0Xj3KSKF/PzF0jifwATivI4t24qXG8rSz4Q==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.1.5"
|
||||||
|
resize-observer-polyfill "^1.5.1"
|
||||||
|
|
||||||
react@^17.0.1:
|
react@^17.0.1:
|
||||||
version "17.0.1"
|
version "17.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
|
resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
|
||||||
|
@ -9780,6 +9795,11 @@ requires-port@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||||
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
||||||
|
|
||||||
|
resize-observer-polyfill@^1.5.1:
|
||||||
|
version "1.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||||
|
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
|
||||||
|
|
||||||
resize-sensor@^0.0.6:
|
resize-sensor@^0.0.6:
|
||||||
version "0.0.6"
|
version "0.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/resize-sensor/-/resize-sensor-0.0.6.tgz#75147dcb273de6832760e461d2e28de6dcf88c45"
|
resolved "https://registry.yarnpkg.com/resize-sensor/-/resize-sensor-0.0.6.tgz#75147dcb273de6832760e461d2e28de6dcf88c45"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue