mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
167c1a8aad
102 changed files with 1560 additions and 385 deletions
|
@ -7,6 +7,8 @@
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
|
"@tippy.js/react": "^3.1.1",
|
||||||
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"autosize": "^4.0.2",
|
"autosize": "^4.0.2",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"body-scroll-lock": "^2.6.4",
|
"body-scroll-lock": "^2.6.4",
|
||||||
|
@ -29,6 +31,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 +74,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"
|
||||||
|
|
40
src/components/boris/BorisComments/index.tsx
Normal file
40
src/components/boris/BorisComments/index.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
import { NodeCommentForm } from '~/components/node/NodeCommentForm';
|
||||||
|
import { NodeNoComments } from '~/components/node/NodeNoComments';
|
||||||
|
import { NodeComments } from '~/components/node/NodeComments';
|
||||||
|
import { Footer } from '~/components/main/Footer';
|
||||||
|
import { Card } from '~/components/containers/Card';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import { selectAuthUser } from '~/redux/auth/selectors';
|
||||||
|
import { IComment, INode } from '~/redux/types';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
isLoadingComments: boolean;
|
||||||
|
commentCount: number;
|
||||||
|
node: INode;
|
||||||
|
comments: IComment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const BorisComments: FC<IProps> = ({ isLoadingComments, node, commentCount, comments }) => {
|
||||||
|
const user = useShallowSelect(selectAuthUser);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Group className={styles.grid}>
|
||||||
|
{user.is_user && <NodeCommentForm isBefore nodeId={node.id} />}
|
||||||
|
|
||||||
|
{isLoadingComments ? (
|
||||||
|
<NodeNoComments is_loading count={7} />
|
||||||
|
) : (
|
||||||
|
<NodeComments comments={comments} count={commentCount} user={user} order="ASC" />
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { BorisComments };
|
18
src/components/boris/BorisComments/styles.module.scss
Normal file
18
src/components/boris/BorisComments/styles.module.scss
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 4;
|
||||||
|
z-index: 2;
|
||||||
|
border-radius: $radius;
|
||||||
|
padding: $gap;
|
||||||
|
background: $node_bg;
|
||||||
|
box-shadow: inset transparentize(mix($wisegreen, white, 60%), 0.6) 0 1px;
|
||||||
|
|
||||||
|
@include desktop {
|
||||||
|
flex: 2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: 1024px) {
|
||||||
|
flex: 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,12 +45,13 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
37
src/components/boris/BorisSuperpowers/index.tsx
Normal file
37
src/components/boris/BorisSuperpowers/index.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { Toggle } from '~/components/input/Toggle';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
active?: boolean;
|
||||||
|
onChange?: (val: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BorisSuperpowers: FC<IProps> = ({ active, onChange }) => {
|
||||||
|
const onToggle = useCallback(() => {
|
||||||
|
if (!onChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(!active);
|
||||||
|
}, [onChange, active]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrap}>
|
||||||
|
<div className={styles.toggle}>
|
||||||
|
<Toggle value={active} handler={onChange} color="primary" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.left} onClick={onToggle}>
|
||||||
|
<div className={styles.title}>Суперспособности</div>
|
||||||
|
{active ? (
|
||||||
|
<div className={styles.subtitle}>Ты видишь всё, что скрыто</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.subtitle}>Включи, чтобы видеть будущее</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { BorisSuperpowers };
|
20
src/components/boris/BorisSuperpowers/styles.module.scss
Normal file
20
src/components/boris/BorisSuperpowers/styles.module.scss
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
@import "~/styles/variables";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
column-gap: $gap;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font: $font_14_semibold;
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font: $font_12_regular;
|
||||||
|
color: transparentize(white, 0.5);
|
||||||
|
}
|
51
src/components/boris/BorisUIDemo/index.tsx
Normal file
51
src/components/boris/BorisUIDemo/index.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Card } from '~/components/containers/Card';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import markdown from '~/styles/common/markdown.module.scss';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
import { Button } from '~/components/input/Button';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const BorisUIDemo: FC<IProps> = () => (
|
||||||
|
<Card className={styles.card}>
|
||||||
|
<div className={markdown.wrapper}>
|
||||||
|
<h1>UI</h1>
|
||||||
|
<p>
|
||||||
|
Простая демонстрация элементов интерфейса. Используется, в основном, как подсказка при
|
||||||
|
разработке
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Кнопки</h2>
|
||||||
|
|
||||||
|
<h4>Цвета</h4>
|
||||||
|
|
||||||
|
<Group horizontal className={styles.sample}>
|
||||||
|
<Button>Primary</Button>
|
||||||
|
<Button color="secondary">Secondary</Button>
|
||||||
|
<Button color="outline">Outline</Button>
|
||||||
|
<Button color="gray">Gray</Button>
|
||||||
|
<Button color="link">Link</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<h4>Размеры</h4>
|
||||||
|
|
||||||
|
<Group horizontal className={styles.sample}>
|
||||||
|
<Button size="micro">Micro</Button>
|
||||||
|
<Button size="mini">Mini</Button>
|
||||||
|
<Button size="normal">Normal</Button>
|
||||||
|
<Button size="big">Big</Button>
|
||||||
|
<Button size="giant">Giant</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<h4>Варианты</h4>
|
||||||
|
<Group horizontal className={styles.sample}>
|
||||||
|
<Button iconRight="check">iconRight</Button>
|
||||||
|
<Button iconLeft="send">iconLeft</Button>
|
||||||
|
<Button round>Round</Button>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { BorisUIDemo };
|
14
src/components/boris/BorisUIDemo/styles.module.scss
Normal file
14
src/components/boris/BorisUIDemo/styles.module.scss
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.card {
|
||||||
|
flex: 3;
|
||||||
|
align-self: stretch;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 20px 30px;
|
||||||
|
background-color: lighten($content_bg, 4%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
16
src/components/boris/Superpower/index.tsx
Normal file
16
src/components/boris/Superpower/index.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import React, { FC, memo } from 'react';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import { selectAuthIsTester, selectUser } from '~/redux/auth/selectors';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const Superpower: FC<IProps> = ({ children }) => {
|
||||||
|
const user = useShallowSelect(selectUser);
|
||||||
|
const is_tester = useShallowSelect(selectAuthIsTester);
|
||||||
|
|
||||||
|
if (!user.is_user || !is_tester) return null;
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Superpower };
|
|
@ -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,34 +66,40 @@ const CommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
|
||||||
|
|
||||||
<CommentFormAttaches />
|
<CommentFormAttaches />
|
||||||
|
|
||||||
<Group horizontal className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
<CommentFormAttachButtons onUpload={uploader.uploadFiles} />
|
<div className={styles.buttons_attach}>
|
||||||
|
<CommentFormAttachButtons onUpload={uploader.uploadFiles} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{!!textarea && (
|
<div className={styles.buttons_format}>
|
||||||
<CommentFormFormatButtons
|
{!!textarea && (
|
||||||
element={textarea}
|
<CommentFormFormatButtons
|
||||||
handler={formik.handleChange('text')}
|
element={textarea}
|
||||||
/>
|
handler={formik.handleChange('text')}
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{isLoading && <LoaderCircle size={20} />}
|
<div className={styles.buttons_submit}>
|
||||||
|
{isLoading && <LoaderCircle size={20} />}
|
||||||
|
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<Button size="small" color="link" type="button" onClick={onCancelEdit}>
|
<Button size="small" color="link" type="button" onClick={onCancelEdit}>
|
||||||
Отмена
|
Отмена
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="small"
|
||||||
|
color="gray"
|
||||||
|
iconRight={!isEditing ? 'enter' : 'check'}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{!isEditing ? 'Сказать' : 'Сохранить'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
|
</div>
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
size="small"
|
|
||||||
color="gray"
|
|
||||||
iconRight={!isEditing ? 'enter' : 'check'}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{!isEditing ? 'Сказать' : 'Сохранить'}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,6 @@
|
||||||
@include tablet {
|
@include tablet {
|
||||||
:global(.comment-author) {
|
:global(.comment-author) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
color: red;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,15 @@
|
||||||
import React, { DetailsHTMLAttributes, FC, useEffect, useRef } from 'react';
|
import React, { DetailsHTMLAttributes, FC } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import StickyBox from 'react-sticky-box/dist/esnext';
|
||||||
import StickySidebar from 'sticky-sidebar';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import ResizeSensor from 'resize-sensor';
|
|
||||||
|
|
||||||
interface IProps extends DetailsHTMLAttributes<HTMLDivElement> {}
|
interface IProps extends DetailsHTMLAttributes<HTMLDivElement> {
|
||||||
|
offsetTop?: number;
|
||||||
(window as any).StickySidebar = StickySidebar;
|
}
|
||||||
(window as any).ResizeSensor = ResizeSensor;
|
|
||||||
|
|
||||||
const Sticky: FC<IProps> = ({ children }) => {
|
|
||||||
const ref = useRef(null);
|
|
||||||
const sb = useRef<StickySidebar>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!ref.current) return;
|
|
||||||
|
|
||||||
sb.current = new StickySidebar(ref.current, {
|
|
||||||
resizeSensor: true,
|
|
||||||
topSpacing: 72,
|
|
||||||
bottomSpacing: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => sb.current?.destroy();
|
|
||||||
}, [ref.current, sb.current, children]);
|
|
||||||
|
|
||||||
if (sb) {
|
|
||||||
sb.current?.updateSticky();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const Sticky: FC<IProps> = ({ children, offsetTop = 65 }) => {
|
||||||
return (
|
return (
|
||||||
<div className={classnames(styles.wrap, 'sidebar_container')}>
|
<StickyBox offsetTop={offsetTop} offsetBottom={10}>
|
||||||
<div className="sidebar" ref={ref}>
|
{children}
|
||||||
<div className={classnames(styles.sticky, 'sidebar__inner')}>{children}</div>
|
</StickyBox>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
@import "src/styles/variables";
|
|
||||||
|
|
||||||
.wrap {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
:global(.sidebar) {
|
|
||||||
will-change: min-height;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.sidebar__inner) {
|
|
||||||
transform: translate(0, 0); /* For browsers don't support translate3d. */
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
will-change: position, transform;
|
|
||||||
}
|
|
||||||
}
|
|
16
src/components/dialogs/Tab/index.tsx
Normal file
16
src/components/dialogs/Tab/index.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import React, { FC, MouseEventHandler } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
active?: boolean;
|
||||||
|
onClick?: MouseEventHandler<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tab: FC<IProps> = ({ active, onClick, children }) => (
|
||||||
|
<div className={classNames(styles.tab, { [styles.active]: active })} onClick={onClick}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { Tab };
|
20
src/components/dialogs/Tab/styles.module.scss
Normal file
20
src/components/dialogs/Tab/styles.module.scss
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
@import "src/styles/variables";
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
@include outer_shadow();
|
||||||
|
|
||||||
|
padding: $gap;
|
||||||
|
margin-right: $gap;
|
||||||
|
border-radius: $radius $radius 0 0;
|
||||||
|
font: $font_14_semibold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: $content_bg;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: lighten($content_bg, 4%);
|
||||||
|
}
|
||||||
|
}
|
12
src/components/dialogs/Tabs/index.tsx
Normal file
12
src/components/dialogs/Tabs/index.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { IAuthState } from '~/redux/auth/types';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const Tabs: FC<IProps> = ({ children }) => {
|
||||||
|
return <div className={styles.wrap}>{children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Tabs };
|
8
src/components/dialogs/Tabs/styles.module.scss
Normal file
8
src/components/dialogs/Tabs/styles.module.scss
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
@import "src/styles/variables";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 0 $gap / 2;
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { EditorUploadButton } from '~/components/editors/EditorUploadButton';
|
import { EditorUploadButton } from '~/components/editors/EditorUploadButton';
|
||||||
import { INode } from '~/redux/types';
|
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
import { IEditorComponentProps } from '~/redux/node/types';
|
import { IEditorComponentProps } from '~/redux/node/types';
|
||||||
|
|
||||||
|
|
|
@ -13,11 +13,12 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
margin: 0 $gap;
|
margin: 0 $gap / 2;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
43
src/components/editors/EditorPublicSwitch/index.tsx
Normal file
43
src/components/editors/EditorPublicSwitch/index.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import { IEditorComponentProps } from '~/redux/node/types';
|
||||||
|
import { Button } from '~/components/input/Button';
|
||||||
|
import { Icon } from '~/components/input/Icon';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { Superpower } from '~/components/boris/Superpower';
|
||||||
|
|
||||||
|
interface IProps extends IEditorComponentProps {}
|
||||||
|
|
||||||
|
const EditorPublicSwitch: FC<IProps> = ({ data, setData }) => {
|
||||||
|
const onChange = useCallback(() => setData({ ...data, is_promoted: !data.is_promoted }), [
|
||||||
|
data,
|
||||||
|
setData,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Superpower>
|
||||||
|
<Button
|
||||||
|
color={data.is_promoted ? 'primary' : 'lab'}
|
||||||
|
type="button"
|
||||||
|
size="giant"
|
||||||
|
label={
|
||||||
|
data.is_promoted
|
||||||
|
? 'Доступно всем на главной странице'
|
||||||
|
: 'Видно только сотрудникам в лаборатории'
|
||||||
|
}
|
||||||
|
onClick={onChange}
|
||||||
|
className={styles.button}
|
||||||
|
round
|
||||||
|
>
|
||||||
|
{data.is_promoted ? (
|
||||||
|
<Icon icon="waves" size={24} />
|
||||||
|
) : (
|
||||||
|
<div className={styles.lab_wrapper}>
|
||||||
|
<Icon icon="lab" size={24} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Superpower>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { EditorPublicSwitch };
|
63
src/components/editors/EditorPublicSwitch/styles.module.scss
Normal file
63
src/components/editors/EditorPublicSwitch/styles.module.scss
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
@import "src/styles/variables";
|
||||||
|
|
||||||
|
@keyframes move_1 {
|
||||||
|
0% {
|
||||||
|
transform: scale(0) translate(0, 0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1) translate(5px, -5px);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1.2) translate(-5px, -10px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes move_2 {
|
||||||
|
0% {
|
||||||
|
transform: scale(0) translate(0, 0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1) translate(-5px, -5px);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1.6) translate(5px, -10px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.lab_wrapper {
|
||||||
|
position: relative;
|
||||||
|
bottom: -2px;
|
||||||
|
|
||||||
|
.button:hover & {
|
||||||
|
&:before,&:after {
|
||||||
|
content: ' ';
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
left: 10px;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
box-shadow: white 0 0 0 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
animation: move_1 0.5s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
animation: move_2 0.5s -0.25s infinite linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,15 @@
|
||||||
import React, { FC, useCallback, useEffect } from 'react';
|
import React, { FC, useCallback, useEffect } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { IFileWithUUID, INode, IFile } from '~/redux/types';
|
import { IFile, IFileWithUUID } from '~/redux/types';
|
||||||
import uuid from 'uuid4';
|
import uuid from 'uuid4';
|
||||||
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
||||||
import { assocPath } from 'ramda';
|
import { append, assocPath } from 'ramda';
|
||||||
import { append } from 'ramda';
|
|
||||||
import { selectUploads } from '~/redux/uploads/selectors';
|
import { selectUploads } from '~/redux/uploads/selectors';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { NODE_SETTINGS } from '~/redux/node/constants';
|
import { NODE_SETTINGS } from '~/redux/node/constants';
|
||||||
|
import { IEditorComponentProps } from '~/redux/node/types';
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const { statuses, files } = selectUploads(state);
|
const { statuses, files } = selectUploads(state);
|
||||||
|
@ -22,12 +22,7 @@ const mapDispatchToProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type IProps = ReturnType<typeof mapStateToProps> &
|
type IProps = ReturnType<typeof mapStateToProps> &
|
||||||
typeof mapDispatchToProps & {
|
typeof mapDispatchToProps & IEditorComponentProps & {
|
||||||
data: INode;
|
|
||||||
setData: (val: INode) => void;
|
|
||||||
temp: string[];
|
|
||||||
setTemp: (val: string[]) => void;
|
|
||||||
|
|
||||||
accept?: string;
|
accept?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
type?: typeof UPLOAD_TYPES[keyof typeof UPLOAD_TYPES];
|
type?: typeof UPLOAD_TYPES[keyof typeof UPLOAD_TYPES];
|
||||||
|
@ -82,18 +77,6 @@ const EditorUploadButtonUnconnected: FC<IProps> = ({
|
||||||
[data, setData]
|
[data, setData]
|
||||||
);
|
);
|
||||||
|
|
||||||
// const onDrop = useCallback(
|
|
||||||
// (event: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
// event.preventDefault();
|
|
||||||
|
|
||||||
// if (!event.dataTransfer || !event.dataTransfer.files || !event.dataTransfer.files.length)
|
|
||||||
// return;
|
|
||||||
|
|
||||||
// onUpload(Array.from(event.dataTransfer.files));
|
|
||||||
// },
|
|
||||||
// [onUpload]
|
|
||||||
// );
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('dragover', eventPreventer, false);
|
window.addEventListener('dragover', eventPreventer, false);
|
||||||
window.addEventListener('drop', eventPreventer, false);
|
window.addEventListener('drop', eventPreventer, false);
|
||||||
|
|
|
@ -2,17 +2,10 @@
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
@include outer_shadow();
|
@include outer_shadow();
|
||||||
|
@include editor_round_button();
|
||||||
|
|
||||||
width: $upload_button_height;
|
|
||||||
height: $upload_button_height;
|
|
||||||
border-radius: ($upload_button_height / 2) !important;
|
|
||||||
position: relative;
|
|
||||||
border-radius: $radius;
|
|
||||||
cursor: pointer;
|
|
||||||
// opacity: 0.7;
|
|
||||||
transition: opacity 0.5s;
|
transition: opacity 0.5s;
|
||||||
background: $red_gradient;
|
background: $red_gradient;
|
||||||
// box-shadow: $content_bg 0 0 5px 10px;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
|
@ -11,7 +11,6 @@ const FlowRecent: FC<IProps> = ({ recent, updated }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{updated && updated.map(node => <FlowRecentItem node={node} key={node.id} has_new />)}
|
{updated && updated.map(node => <FlowRecentItem node={node} key={node.id} has_new />)}
|
||||||
|
|
||||||
{recent && recent.map(node => <FlowRecentItem node={node} key={node.id} />)}
|
{recent && recent.map(node => <FlowRecentItem node={node} key={node.id} />)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,33 +1,24 @@
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import React, {
|
import React, { ButtonHTMLAttributes, DetailedHTMLProps, FC, memo, useMemo } from 'react';
|
||||||
ButtonHTMLAttributes,
|
|
||||||
DetailedHTMLProps,
|
|
||||||
FC,
|
|
||||||
createElement,
|
|
||||||
memo,
|
|
||||||
useRef,
|
|
||||||
} from 'react';
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { IIcon } from '~/redux/types';
|
import { IIcon } from '~/redux/types';
|
||||||
import { usePopper } from 'react-popper';
|
import Tippy from '@tippy.js/react';
|
||||||
|
import 'tippy.js/dist/tippy.css';
|
||||||
|
|
||||||
type IButtonProps = DetailedHTMLProps<
|
type IButtonProps = DetailedHTMLProps<
|
||||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
HTMLButtonElement
|
HTMLButtonElement
|
||||||
> & {
|
> & {
|
||||||
size?: 'mini' | 'normal' | 'big' | 'giant' | 'micro' | 'small';
|
size?: 'mini' | 'normal' | 'big' | 'giant' | 'micro' | 'small';
|
||||||
color?: 'primary' | 'secondary' | 'outline' | 'link' | 'gray';
|
color?: 'primary' | 'secondary' | 'outline' | 'link' | 'gray' | 'lab';
|
||||||
iconLeft?: IIcon;
|
iconLeft?: IIcon;
|
||||||
iconRight?: IIcon;
|
iconRight?: IIcon;
|
||||||
seamless?: boolean;
|
|
||||||
transparent?: boolean;
|
|
||||||
title?: string;
|
title?: string;
|
||||||
non_submitting?: boolean;
|
|
||||||
is_loading?: boolean;
|
|
||||||
stretchy?: boolean;
|
stretchy?: boolean;
|
||||||
iconOnly?: boolean;
|
iconOnly?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
round?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Button: FC<IButtonProps> = memo(
|
const Button: FC<IButtonProps> = memo(
|
||||||
|
@ -38,56 +29,36 @@ const Button: FC<IButtonProps> = memo(
|
||||||
iconLeft,
|
iconLeft,
|
||||||
iconRight,
|
iconRight,
|
||||||
children,
|
children,
|
||||||
seamless = false,
|
|
||||||
transparent = false,
|
|
||||||
non_submitting = false,
|
|
||||||
is_loading,
|
|
||||||
title,
|
title,
|
||||||
stretchy,
|
stretchy,
|
||||||
disabled,
|
disabled,
|
||||||
iconOnly,
|
iconOnly,
|
||||||
label,
|
label,
|
||||||
ref,
|
ref,
|
||||||
|
round,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const tooltip = useRef<HTMLSpanElement | null>(null);
|
const computedClassName = useMemo(
|
||||||
const pop = usePopper(tooltip?.current?.parentElement, tooltip.current, {
|
() =>
|
||||||
placement: 'top',
|
classnames(styles.button, className, styles[size], styles[color], {
|
||||||
modifiers: [
|
|
||||||
{
|
|
||||||
name: 'offset',
|
|
||||||
options: {
|
|
||||||
offset: [0, 5],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
return createElement(
|
|
||||||
seamless || non_submitting ? 'div' : 'button',
|
|
||||||
{
|
|
||||||
className: classnames(styles.button, className, styles[size], styles[color], {
|
|
||||||
seamless,
|
|
||||||
transparent,
|
|
||||||
disabled,
|
disabled,
|
||||||
is_loading,
|
|
||||||
stretchy,
|
stretchy,
|
||||||
icon: ((iconLeft || iconRight) && !title && !children) || iconOnly,
|
icon: ((iconLeft || iconRight) && !title && !children) || iconOnly,
|
||||||
has_icon_left: !!iconLeft,
|
has_icon_left: !!iconLeft,
|
||||||
has_icon_right: !!iconRight,
|
has_icon_right: !!iconRight,
|
||||||
|
round,
|
||||||
}),
|
}),
|
||||||
...props,
|
[round, disabled, className, stretchy, iconLeft, iconRight, size, color]
|
||||||
},
|
);
|
||||||
[
|
|
||||||
iconLeft && <Icon icon={iconLeft} size={20} key={0} className={styles.icon_left} />,
|
return (
|
||||||
title ? <span>{title}</span> : children || null,
|
<Tippy content={label || ''} enabled={!!label}>
|
||||||
iconRight && <Icon icon={iconRight} size={20} key={2} className={styles.icon_right} />,
|
<button className={computedClassName} {...props}>
|
||||||
!!label && (
|
{iconLeft && <Icon icon={iconLeft} size={20} key={0} className={styles.icon_left} />}
|
||||||
<span ref={tooltip} className={styles.tooltip} style={pop.styles.popper} key="tooltip">
|
{!!title ? <span>{title}</span> : children}
|
||||||
{label}
|
{iconRight && <Icon icon={iconRight} size={20} key={2} className={styles.icon_right} />}
|
||||||
</span>
|
</button>
|
||||||
),
|
</Tippy>
|
||||||
]
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -34,17 +34,14 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
filter: grayscale(0);
|
filter: grayscale(0);
|
||||||
|
|
||||||
transition: opacity 0.25s, filter 0.25s, box-shadow 0.25s;
|
transition: opacity 0.25s, filter 0.25s, box-shadow 0.25s, background-color 0.5s;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
|
||||||
@include outer_shadow();
|
@include outer_shadow();
|
||||||
|
|
||||||
input {
|
input {
|
||||||
color: red;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -80,30 +77,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:global(.seamless) {
|
|
||||||
background: transparent;
|
|
||||||
color: black;
|
|
||||||
box-shadow: none;
|
|
||||||
fill: black;
|
|
||||||
stroke: black;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:global(.transparent) {
|
|
||||||
background: transparent;
|
|
||||||
color: white;
|
|
||||||
box-shadow: transparentize(black, 0.5) 0 0 4px;
|
|
||||||
padding: 0;
|
|
||||||
fill: black;
|
|
||||||
stroke: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:global(.red) {
|
|
||||||
fill: $red;
|
|
||||||
stroke: $red;
|
|
||||||
color: $red;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:global(.stretchy) {
|
&:global(.stretchy) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
@ -112,8 +85,6 @@
|
||||||
&:global(.grey) {
|
&:global(.grey) {
|
||||||
background: transparentize(white, 0.9);
|
background: transparentize(white, 0.9);
|
||||||
color: white;
|
color: white;
|
||||||
// background: lighten(white, 0.5);
|
|
||||||
// filter: grayscale(100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:global(.disabled) {
|
&:global(.disabled) {
|
||||||
|
@ -146,14 +117,6 @@
|
||||||
padding-right: $gap;
|
padding-right: $gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.primary {
|
|
||||||
background: $red_gradient;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.secondary {
|
|
||||||
background: $green_gradient;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.outline {
|
&.outline {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow: inset transparentize(white, 0.8) 0 0 0 2px;
|
box-shadow: inset transparentize(white, 0.8) 0 0 0 2px;
|
||||||
|
@ -185,31 +148,60 @@
|
||||||
font: $font_12_semibold;
|
font: $font_12_semibold;
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
border-radius: $radius / 2;
|
border-radius: $radius / 2;
|
||||||
|
|
||||||
|
&:global(.round) {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini {
|
.mini {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border-radius: $radius / 2;
|
border-radius: $radius / 2;
|
||||||
|
|
||||||
|
&:global(.round) {
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.small {
|
.small {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
// border-radius: $radius / 2;
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:global(.round) {
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.normal {
|
.normal {
|
||||||
height: 38px;
|
height: 38px;
|
||||||
|
|
||||||
|
&:global(.round) {
|
||||||
|
border-radius: 19px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.big {
|
.big {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|
||||||
|
&:global(.round) {
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.giant {
|
.giant {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
min-width: 50px;
|
min-width: 50px;
|
||||||
|
|
||||||
|
&:global(.round) {
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled {
|
.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
@ -226,20 +218,14 @@
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
.primary {
|
||||||
padding: 5px 10px;
|
background: $red;
|
||||||
background-color: darken($content_bg, 4%);
|
}
|
||||||
z-index: 2;
|
|
||||||
border-radius: $input_radius;
|
.secondary {
|
||||||
text-transform: none;
|
background: $wisegreen;
|
||||||
opacity: 0;
|
}
|
||||||
pointer-events: none;
|
|
||||||
touch-action: none;
|
.lab {
|
||||||
transition: opacity 0.1s;
|
background: $blue;
|
||||||
border: 1px solid transparentize(white, 0.9);
|
|
||||||
|
|
||||||
.button:hover & {
|
|
||||||
opacity: 1;
|
|
||||||
font: $font_14_semibold;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
31
src/components/input/Toggle/index.tsx
Normal file
31
src/components/input/Toggle/index.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
type ToggleColor = 'primary' | 'secondary' | 'lab' | 'danger';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
value?: boolean;
|
||||||
|
handler?: (val: boolean) => void;
|
||||||
|
color?: ToggleColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Toggle: FC<IProps> = ({ value, handler, color = 'primary' }) => {
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
if (!handler) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(!value);
|
||||||
|
}, [value, handler]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classNames(styles.toggle, { [styles.active]: value }, styles[color])}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Toggle };
|
51
src/components/input/Toggle/styles.module.scss
Normal file
51
src/components/input/Toggle/styles.module.scss
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
height: 24px;
|
||||||
|
width: 48px;
|
||||||
|
flex: 0 0 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: transparentize(white, 0.9);
|
||||||
|
display: flex;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: ' ';
|
||||||
|
position: absolute;
|
||||||
|
left: 3px;
|
||||||
|
top: 3px;
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
border-radius: 11px;
|
||||||
|
background-color: darken(white, 50%);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
transition: transform 0.25s, color 0.25s, background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
transform: translate(24px, 0);
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background-color: $wisegreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
background-color: transparentize(white, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.lab {
|
||||||
|
background-color: $blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
background-color: $red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
src/components/lab/LabBanner/index.tsx
Normal file
22
src/components/lab/LabBanner/index.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { Card } from '~/components/containers/Card';
|
||||||
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const LabBanner: FC<IProps> = () => (
|
||||||
|
<Card className={styles.wrap}>
|
||||||
|
<Group>
|
||||||
|
<Placeholder height={32} />
|
||||||
|
<Placeholder height={18} width="120px" />
|
||||||
|
<Placeholder height={18} width="200px" />
|
||||||
|
<Placeholder height={18} width="60px" />
|
||||||
|
<Placeholder height={18} width="180px" />
|
||||||
|
<Placeholder height={18} width="230px" />
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { LabBanner };
|
5
src/components/lab/LabBanner/styles.module.scss
Normal file
5
src/components/lab/LabBanner/styles.module.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
background: $red_gradient_alt;
|
||||||
|
}
|
32
src/components/lab/LabHead/index.tsx
Normal file
32
src/components/lab/LabHead/index.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
import { Card } from '~/components/containers/Card';
|
||||||
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
|
import { Filler } from '~/components/containers/Filler';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const LabHead: FC<IProps> = () => (
|
||||||
|
<Card>
|
||||||
|
<Group horizontal>
|
||||||
|
<Group horizontal style={{ flex: '0 0 auto' }}>
|
||||||
|
<Placeholder width="32px" height={32} />
|
||||||
|
<Placeholder width="96px" height={18} />
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group horizontal style={{ flex: '0 0 auto' }}>
|
||||||
|
<Placeholder width="32px" height={32} />
|
||||||
|
<Placeholder width="126px" height={18} />
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group horizontal style={{ flex: '0 0 auto' }}>
|
||||||
|
<Placeholder width="32px" height={32} />
|
||||||
|
<Placeholder width="96px" height={18} />
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Filler />
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { LabHead };
|
22
src/components/lab/LabHero/index.tsx
Normal file
22
src/components/lab/LabHero/index.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
import { Icon } from '~/components/input/Icon';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const LabHero: FC<IProps> = () => (
|
||||||
|
<Group horizontal className={styles.wrap1}>
|
||||||
|
<div className={styles.star}>
|
||||||
|
<Icon icon="star_full" size={32} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<Placeholder height={20} />
|
||||||
|
<Placeholder height={12} width="100px" />
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { LabHero };
|
10
src/components/lab/LabHero/styles.module.scss
Normal file
10
src/components/lab/LabHero/styles.module.scss
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
margin-bottom: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star {
|
||||||
|
fill: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
31
src/components/lab/LabNode/index.tsx
Normal file
31
src/components/lab/LabNode/index.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { INode } from '~/redux/types';
|
||||||
|
import { NodePanelInner } from '~/components/node/NodePanelInner';
|
||||||
|
import { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { Card } from '~/components/containers/Card';
|
||||||
|
import { NodePanelLab } from '~/components/node/NodePanelLab';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
node: INode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LabNode: FC<IProps> = ({ node }) => {
|
||||||
|
const { inline, block, head } = useNodeBlocks(node, false);
|
||||||
|
|
||||||
|
console.log(node.id, { inline, block, head });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card seamless className={styles.wrap}>
|
||||||
|
<div className={styles.head}>
|
||||||
|
<NodePanelLab node={node} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{head}
|
||||||
|
{block}
|
||||||
|
{inline}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabNode };
|
11
src/components/lab/LabNode/styles.module.scss
Normal file
11
src/components/lab/LabNode/styles.module.scss
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head {
|
||||||
|
background-color: transparentize(black, 0.9);
|
||||||
|
border-radius: $radius $radius 0 0;
|
||||||
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import * as MODAL_ACTIONS from '~/redux/modal/actions';
|
||||||
import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
||||||
import { IState } from '~/redux/store';
|
import { IState } from '~/redux/store';
|
||||||
import isBefore from 'date-fns/isBefore';
|
import isBefore from 'date-fns/isBefore';
|
||||||
|
import { Superpower } from '~/components/boris/Superpower';
|
||||||
|
|
||||||
const mapStateToProps = (state: IState) => ({
|
const mapStateToProps = (state: IState) => ({
|
||||||
user: pick(['username', 'is_user', 'photo', 'last_seen_boris'])(selectUser(state)),
|
user: pick(['username', 'is_user', 'photo', 'last_seen_boris'])(selectUser(state)),
|
||||||
|
@ -89,6 +90,15 @@ const HeaderUnconnected: FC<IProps> = memo(
|
||||||
ФЛОУ
|
ФЛОУ
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Superpower>
|
||||||
|
<Link
|
||||||
|
className={classNames(styles.item, { [styles.is_active]: pathname === URLS.BASE })}
|
||||||
|
to={URLS.LAB}
|
||||||
|
>
|
||||||
|
ЛАБ
|
||||||
|
</Link>
|
||||||
|
</Superpower>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
className={classNames(styles.item, {
|
className={classNames(styles.item, {
|
||||||
[styles.is_active]: pathname === URLS.BORIS,
|
[styles.is_active]: pathname === URLS.BORIS,
|
||||||
|
@ -122,9 +132,6 @@ const HeaderUnconnected: FC<IProps> = memo(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const Header = connect(
|
const Header = connect(mapStateToProps, mapDispatchToProps)(HeaderUnconnected);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(HeaderUnconnected);
|
|
||||||
|
|
||||||
export { Header };
|
export { Header };
|
||||||
|
|
|
@ -4,14 +4,12 @@ import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { INodeComponentProps } from '~/redux/node/constants';
|
import { INodeComponentProps } from '~/redux/node/constants';
|
||||||
|
import { useNodeAudios } from '~/utils/hooks/node/useNodeAudios';
|
||||||
|
|
||||||
interface IProps extends INodeComponentProps {}
|
interface IProps extends INodeComponentProps {}
|
||||||
|
|
||||||
const NodeAudioBlock: FC<IProps> = ({ node }) => {
|
const NodeAudioBlock: FC<IProps> = ({ node }) => {
|
||||||
const audios = useMemo(
|
const audios = useNodeAudios(node);
|
||||||
() => node.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO),
|
|
||||||
[node.files]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
|
|
|
@ -6,14 +6,12 @@ import { path } from 'ramda';
|
||||||
import { getURL } from '~/utils/dom';
|
import { getURL } from '~/utils/dom';
|
||||||
import { PRESETS } from '~/constants/urls';
|
import { PRESETS } from '~/constants/urls';
|
||||||
import { INodeComponentProps } from '~/redux/node/constants';
|
import { INodeComponentProps } from '~/redux/node/constants';
|
||||||
|
import { useNodeImages } from '~/utils/hooks/node/useNodeImages';
|
||||||
|
|
||||||
interface IProps extends INodeComponentProps {}
|
interface IProps extends INodeComponentProps {}
|
||||||
|
|
||||||
const NodeAudioImageBlock: FC<IProps> = ({ node }) => {
|
const NodeAudioImageBlock: FC<IProps> = ({ node }) => {
|
||||||
const images = useMemo(
|
const images = useNodeImages(node);
|
||||||
() => node.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE),
|
|
||||||
[node.files]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (images.length === 0) return null;
|
if (images.length === 0) return 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;
|
||||||
|
}
|
|
@ -1,12 +1,13 @@
|
||||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||||
import { INodeComponentProps } from '~/redux/node/constants';
|
import { INodeComponentProps } from '~/redux/node/constants';
|
||||||
import SwiperCore, { A11y, Pagination, SwiperOptions } from 'swiper';
|
import SwiperCore, { A11y, Pagination, Navigation, SwiperOptions, Keyboard } from 'swiper';
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
|
|
||||||
import 'swiper/swiper.scss';
|
import 'swiper/swiper.scss';
|
||||||
import 'swiper/components/pagination/pagination.scss';
|
import 'swiper/components/pagination/pagination.scss';
|
||||||
import 'swiper/components/scrollbar/scrollbar.scss';
|
import 'swiper/components/scrollbar/scrollbar.scss';
|
||||||
import 'swiper/components/zoom/zoom.scss';
|
import 'swiper/components/zoom/zoom.scss';
|
||||||
|
import 'swiper/components/navigation/navigation.scss';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { useNodeImages } from '~/utils/hooks/node/useNodeImages';
|
import { useNodeImages } from '~/utils/hooks/node/useNodeImages';
|
||||||
|
@ -16,13 +17,14 @@ import SwiperClass from 'swiper/types/swiper-class';
|
||||||
import { modalShowPhotoswipe } from '~/redux/modal/actions';
|
import { modalShowPhotoswipe } from '~/redux/modal/actions';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
SwiperCore.use([Pagination, A11y]);
|
SwiperCore.use([Navigation, Pagination, A11y]);
|
||||||
|
|
||||||
interface IProps extends INodeComponentProps {}
|
interface IProps extends INodeComponentProps {}
|
||||||
|
|
||||||
const breakpoints: SwiperOptions['breakpoints'] = {
|
const breakpoints: SwiperOptions['breakpoints'] = {
|
||||||
599: {
|
599: {
|
||||||
spaceBetween: 20,
|
spaceBetween: 20,
|
||||||
|
navigation: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -43,6 +45,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,7 +77,12 @@ const NodeImageSwiperBlock: FC<IProps> = ({ node }) => {
|
||||||
observeParents
|
observeParents
|
||||||
resizeObserver
|
resizeObserver
|
||||||
watchOverflow
|
watchOverflow
|
||||||
|
updateOnImagesReady
|
||||||
onInit={resetSwiper}
|
onInit={resetSwiper}
|
||||||
|
keyboard={{
|
||||||
|
enabled: true,
|
||||||
|
onlyInViewport: false,
|
||||||
|
}}
|
||||||
zoom
|
zoom
|
||||||
>
|
>
|
||||||
{images.map(file => (
|
{images.map(file => (
|
||||||
|
|
|
@ -20,6 +20,17 @@
|
||||||
:global(.swiper-container) {
|
:global(.swiper-container) {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.swiper-button-next),
|
||||||
|
:global(.swiper-button-prev) {
|
||||||
|
color: white;
|
||||||
|
font-size: 10px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide {
|
.slide {
|
||||||
|
|
|
@ -31,8 +31,6 @@
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: stretch;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -88,7 +86,7 @@
|
||||||
@include tablet {
|
@include tablet {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
font: $font_20_semibold;
|
font: $font_16_semibold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
19
src/components/node/NodePanelLab/index.tsx
Normal file
19
src/components/node/NodePanelLab/index.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { INode } from '~/redux/types';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { URLS } from '~/constants/urls';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
node: INode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodePanelLab: FC<IProps> = ({ node }) => (
|
||||||
|
<div className={styles.wrap}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
<Link to={URLS.NODE_URL(node.id)}>{node.title || '...'}</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { NodePanelLab };
|
24
src/components/node/NodePanelLab/styles.module.scss
Normal file
24
src/components/node/NodePanelLab/styles.module.scss
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
padding: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font: $font_24_semibold;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-bottom: 0;
|
||||||
|
font: $font_16_semibold;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ type IProps = RouteComponentProps & {
|
||||||
|
|
||||||
type CellSize = 'small' | 'medium' | 'large';
|
type CellSize = 'small' | 'medium' | 'large';
|
||||||
|
|
||||||
const getTitleLetters = (title: string): string => {
|
const getTitleLetters = (title?: string): string => {
|
||||||
const words = (title && title.split(' ')) || [];
|
const words = (title && title.split(' ')) || [];
|
||||||
|
|
||||||
if (!words.length) return '';
|
if (!words.length) return '';
|
||||||
|
|
|
@ -50,4 +50,7 @@ export const API = {
|
||||||
NODES: `/tag/nodes`,
|
NODES: `/tag/nodes`,
|
||||||
AUTOCOMPLETE: `/tag/autocomplete`,
|
AUTOCOMPLETE: `/tag/autocomplete`,
|
||||||
},
|
},
|
||||||
|
LAB: {
|
||||||
|
NODES: `/lab/`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { INode } from '~/redux/types';
|
||||||
|
|
||||||
export const URLS = {
|
export const URLS = {
|
||||||
BASE: '/',
|
BASE: '/',
|
||||||
|
LAB: '/lab',
|
||||||
BORIS: '/boris',
|
BORIS: '/boris',
|
||||||
AUTH: {
|
AUTH: {
|
||||||
LOGIN: '/auth/login',
|
LOGIN: '/auth/login',
|
||||||
|
|
|
@ -95,7 +95,7 @@ const EditorDialogUnconnected: FC<IProps> = ({
|
||||||
maxLength={256}
|
maxLength={256}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button title="Сохранить" iconRight="check" />
|
<Button title="Сохранить" iconRight="check" color={data.is_promoted ? 'primary' : 'lab'} />
|
||||||
</Group>
|
</Group>
|
||||||
</Padder>
|
</Padder>
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { selectAuthRegisterSocial } from '~/redux/auth/selectors';
|
||||||
import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
||||||
import { useCloseOnEscape } from '~/utils/hooks';
|
import { useCloseOnEscape } from '~/utils/hooks';
|
||||||
import { LoginSocialRegisterButtons } from '~/containers/dialogs/LoginSocialRegisterButtons';
|
import { LoginSocialRegisterButtons } from '~/containers/dialogs/LoginSocialRegisterButtons';
|
||||||
|
import { Toggle } from '~/components/input/Toggle';
|
||||||
|
|
||||||
const mapStateToProps = selectAuthRegisterSocial;
|
const mapStateToProps = selectAuthRegisterSocial;
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
@ -21,6 +22,12 @@ const mapDispatchToProps = {
|
||||||
|
|
||||||
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & IDialogProps & {};
|
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & IDialogProps & {};
|
||||||
|
|
||||||
|
const phrase = [
|
||||||
|
'Сушёный кабачок особенно хорош в это время года, знаете ли.',
|
||||||
|
'Бывало, стреляешь по кабачку, или он стреляет в тебя.',
|
||||||
|
'Он всегда рядом, кабачок -- первый сорт! Надежда империи.',
|
||||||
|
];
|
||||||
|
|
||||||
const LoginSocialRegisterDialogUnconnected: FC<Props> = ({
|
const LoginSocialRegisterDialogUnconnected: FC<Props> = ({
|
||||||
onRequestClose,
|
onRequestClose,
|
||||||
errors,
|
errors,
|
||||||
|
@ -32,6 +39,7 @@ const LoginSocialRegisterDialogUnconnected: FC<Props> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
const [isDryingPants, setIsDryingPants] = useState(false);
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(event: FormEvent) => {
|
(event: FormEvent) => {
|
||||||
|
@ -56,7 +64,7 @@ const LoginSocialRegisterDialogUnconnected: FC<Props> = ({
|
||||||
useCloseOnEscape(onRequestClose);
|
useCloseOnEscape(onRequestClose);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit} autoComplete="new-password">
|
||||||
<BetterScrollDialog
|
<BetterScrollDialog
|
||||||
onClose={onRequestClose}
|
onClose={onRequestClose}
|
||||||
width={300}
|
width={300}
|
||||||
|
@ -73,6 +81,7 @@ const LoginSocialRegisterDialogUnconnected: FC<Props> = ({
|
||||||
value={username}
|
value={username}
|
||||||
title="Юзернэйм"
|
title="Юзернэйм"
|
||||||
error={errors.username}
|
error={errors.username}
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputText
|
<InputText
|
||||||
|
@ -81,12 +90,18 @@ const LoginSocialRegisterDialogUnconnected: FC<Props> = ({
|
||||||
title="Пароль"
|
title="Пароль"
|
||||||
type="password"
|
type="password"
|
||||||
error={errors.password}
|
error={errors.password}
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label className={styles.check}>
|
<div className={styles.check} onClick={() => setIsDryingPants(!isDryingPants)}>
|
||||||
<input type="checkbox" />
|
<Toggle value={isDryingPants} color="primary" />
|
||||||
<span>Это не мои штаны сушатся на радиаторе в третьей лаборатории</span>
|
<span>Это не мои штаны сушатся на радиаторе в третьей лаборатории</span>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.check} onClick={() => setIsDryingPants(!isDryingPants)}>
|
||||||
|
<Toggle value={!isDryingPants} color="primary" />
|
||||||
|
<span>{phrase[Math.floor(Math.random() * phrase.length)]}</span>
|
||||||
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
</Padder>
|
</Padder>
|
||||||
|
|
21
src/containers/lab/LabGrid/index.tsx
Normal file
21
src/containers/lab/LabGrid/index.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { LabNode } from '~/components/lab/LabNode';
|
||||||
|
import { selectLabListNodes } from '~/redux/lab/selectors';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const LabGrid: FC<IProps> = () => {
|
||||||
|
const nodes = useShallowSelect(selectLabListNodes);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrap}>
|
||||||
|
{nodes.map(node => (
|
||||||
|
<LabNode node={node} key={node.id} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabGrid };
|
8
src/containers/lab/LabGrid/styles.module.scss
Normal file
8
src/containers/lab/LabGrid/styles.module.scss
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
grid-auto-rows: auto;
|
||||||
|
grid-row-gap: $gap;
|
||||||
|
}
|
112
src/containers/lab/LabLayout/index.tsx
Normal file
112
src/containers/lab/LabLayout/index.tsx
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import React, { FC, useEffect } from 'react';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { Card } from '~/components/containers/Card';
|
||||||
|
import { Sticky } from '~/components/containers/Sticky';
|
||||||
|
import { Container } from '~/containers/main/Container';
|
||||||
|
import { LabGrid } from '~/containers/lab/LabGrid';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { labGetList } from '~/redux/lab/actions';
|
||||||
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
|
import { Grid } from '~/components/containers/Grid';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
import { LabHero } from '~/components/lab/LabHero';
|
||||||
|
import { LabBanner } from '~/components/lab/LabBanner';
|
||||||
|
import { LabHead } from '~/components/lab/LabHead';
|
||||||
|
import { Filler } from '~/components/containers/Filler';
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
const LabLayout: FC<IProps> = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(labGetList());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Container>
|
||||||
|
<div className={styles.wrap}>
|
||||||
|
<Group className={styles.content}>
|
||||||
|
<LabHead />
|
||||||
|
<LabGrid />
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<div className={styles.panel}>
|
||||||
|
<Sticky>
|
||||||
|
<Group>
|
||||||
|
<LabBanner />
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Group>
|
||||||
|
<Placeholder height={36} width="100%" />
|
||||||
|
<Group horizontal>
|
||||||
|
<Filler />
|
||||||
|
<Placeholder height={32} width="120px" />
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
|
||||||
|
<Placeholder height={14} width="100px" />
|
||||||
|
|
||||||
|
<div />
|
||||||
|
|
||||||
|
<div className={styles.tags}>
|
||||||
|
<Placeholder height={20} width="100px" />
|
||||||
|
<Placeholder height={20} width="64px" />
|
||||||
|
<Placeholder height={20} width="100%" />
|
||||||
|
<Placeholder height={20} width="100px" />
|
||||||
|
<Placeholder height={20} width="100px" />
|
||||||
|
<Placeholder height={20} width="64px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
|
||||||
|
<Placeholder height={14} width="180px" />
|
||||||
|
|
||||||
|
<div />
|
||||||
|
|
||||||
|
<Group className={styles.heroes}>
|
||||||
|
<LabHero />
|
||||||
|
<div />
|
||||||
|
<LabHero />
|
||||||
|
<div />
|
||||||
|
<LabHero />
|
||||||
|
<div />
|
||||||
|
<LabHero />
|
||||||
|
<div />
|
||||||
|
<LabHero />
|
||||||
|
<div />
|
||||||
|
<LabHero />
|
||||||
|
<div />
|
||||||
|
<LabHero />
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<Placeholder width="100%" height={100} />
|
||||||
|
<Placeholder width="120px" height={16} />
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<div />
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<Placeholder width="100%" height={100} />
|
||||||
|
<Placeholder width="120px" height={16} />
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
</Group>
|
||||||
|
</Sticky>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LabLayout };
|
20
src/containers/lab/LabLayout/styles.module.scss
Normal file
20
src/containers/lab/LabLayout/styles.module.scss
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 3fr 1fr;
|
||||||
|
column-gap: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
margin-top: -7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
margin: 0 $gap $gap 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,4 +8,8 @@
|
||||||
@include tablet {
|
@include tablet {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: $content_width + $gap * 4) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,14 @@ import { BorisLayout } from '~/containers/node/BorisLayout';
|
||||||
import { ErrorNotFound } from '~/containers/pages/ErrorNotFound';
|
import { ErrorNotFound } from '~/containers/pages/ErrorNotFound';
|
||||||
import { ProfilePage } from '~/containers/profile/ProfilePage';
|
import { ProfilePage } from '~/containers/profile/ProfilePage';
|
||||||
import { Redirect, Route, Switch, useLocation } from 'react-router';
|
import { Redirect, Route, Switch, useLocation } from 'react-router';
|
||||||
|
import { LabLayout } from '~/containers/lab/LabLayout';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import { selectAuthUser } from '~/redux/auth/selectors';
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {}
|
||||||
|
|
||||||
const MainRouter: FC<IProps> = () => {
|
const MainRouter: FC<IProps> = () => {
|
||||||
|
const { is_user } = useShallowSelect(selectAuthUser);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -20,6 +24,12 @@ const MainRouter: FC<IProps> = () => {
|
||||||
<Route path={URLS.ERRORS.NOT_FOUND} component={ErrorNotFound} />
|
<Route path={URLS.ERRORS.NOT_FOUND} component={ErrorNotFound} />
|
||||||
<Route path={URLS.PROFILE_PAGE(':username')} component={ProfilePage} />
|
<Route path={URLS.PROFILE_PAGE(':username')} component={ProfilePage} />
|
||||||
|
|
||||||
|
{is_user && (
|
||||||
|
<>
|
||||||
|
<Route exact path={URLS.LAB} component={LabLayout} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Redirect to="/" />
|
<Redirect to="/" />
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,25 +1,30 @@
|
||||||
import React, { FC, useEffect } from 'react';
|
import React, { FC, useCallback, useEffect } from 'react';
|
||||||
import { selectNode, selectNodeComments } from '~/redux/node/selectors';
|
import { selectNode, selectNodeComments } from '~/redux/node/selectors';
|
||||||
import { selectUser } from '~/redux/auth/selectors';
|
import { selectAuthIsTester, selectUser } from '~/redux/auth/selectors';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { NodeComments } from '~/components/node/NodeComments';
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { Group } from '~/components/containers/Group';
|
||||||
import boris from '~/sprites/boris_robot.svg';
|
import boris from '~/sprites/boris_robot.svg';
|
||||||
import { NodeNoComments } from '~/components/node/NodeNoComments';
|
|
||||||
import { useRandomPhrase } from '~/constants/phrases';
|
import { useRandomPhrase } from '~/constants/phrases';
|
||||||
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 { 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';
|
||||||
import { authSetUser } from '~/redux/auth/actions';
|
import { authSetState, 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';
|
||||||
|
import { BorisComments } from '~/components/boris/BorisComments';
|
||||||
|
import { URLS } from '~/constants/urls';
|
||||||
|
import { Route, Switch, Link } from 'react-router-dom';
|
||||||
|
import { BorisUIDemo } from '~/components/boris/BorisUIDemo';
|
||||||
|
import { BorisSuperpowers } from '~/components/boris/BorisSuperpowers';
|
||||||
|
import { Superpower } from '~/components/boris/Superpower';
|
||||||
|
import { Tabs } from '~/components/dialogs/Tabs';
|
||||||
|
import { Tab } from '~/components/dialogs/Tab';
|
||||||
|
import { useHistory, useLocation } from 'react-router';
|
||||||
|
import { Card } from '~/components/containers/Card';
|
||||||
|
|
||||||
type IProps = {};
|
type IProps = {};
|
||||||
|
|
||||||
|
@ -30,6 +35,7 @@ const BorisLayout: FC<IProps> = () => {
|
||||||
const user = useShallowSelect(selectUser);
|
const user = useShallowSelect(selectUser);
|
||||||
const stats = useShallowSelect(selectBorisStats);
|
const stats = useShallowSelect(selectBorisStats);
|
||||||
const comments = useShallowSelect(selectNodeComments);
|
const comments = useShallowSelect(selectNodeComments);
|
||||||
|
const is_tester = useShallowSelect(selectAuthIsTester);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const last_comment = comments[0];
|
const last_comment = comments[0];
|
||||||
|
@ -55,6 +61,16 @@ const BorisLayout: FC<IProps> = () => {
|
||||||
dispatch(borisLoadStats());
|
dispatch(borisLoadStats());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const setBetaTester = useCallback(
|
||||||
|
(is_tester: boolean) => {
|
||||||
|
dispatch(authSetState({ is_tester }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
|
@ -70,26 +86,40 @@ const BorisLayout: FC<IProps> = () => {
|
||||||
|
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Card className={styles.content}>
|
<Card className={styles.content}>
|
||||||
<Group className={styles.grid}>
|
<Superpower>
|
||||||
{user.is_user && <NodeCommentForm isBefore nodeId={node.current.id} />}
|
<Tabs>
|
||||||
|
<Tab
|
||||||
|
active={location.pathname === URLS.BORIS}
|
||||||
|
onClick={() => history.push(URLS.BORIS)}
|
||||||
|
>
|
||||||
|
Комментарии
|
||||||
|
</Tab>
|
||||||
|
|
||||||
{node.is_loading_comments ? (
|
<Tab
|
||||||
<NodeNoComments is_loading count={7} />
|
active={location.pathname === `${URLS.BORIS}/ui`}
|
||||||
) : (
|
onClick={() => history.push(`${URLS.BORIS}/ui`)}
|
||||||
<NodeComments
|
>
|
||||||
comments={comments}
|
UI Demo
|
||||||
count={node.comment_count}
|
</Tab>
|
||||||
user={user}
|
</Tabs>
|
||||||
order="ASC"
|
</Superpower>
|
||||||
|
|
||||||
|
{
|
||||||
|
<Switch>
|
||||||
|
<Route path={`${URLS.BORIS}/ui`} component={BorisUIDemo} />
|
||||||
|
|
||||||
|
<BorisComments
|
||||||
|
isLoadingComments={node.is_loading_comments}
|
||||||
|
commentCount={node.comment_count}
|
||||||
|
node={node.current}
|
||||||
|
comments={node.comments}
|
||||||
/>
|
/>
|
||||||
)}
|
</Switch>
|
||||||
</Group>
|
}
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</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>
|
||||||
|
@ -102,11 +132,15 @@ const BorisLayout: FC<IProps> = () => {
|
||||||
<p className="grey">// Такова жизнь.</p>
|
<p className="grey">// Такова жизнь.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{user.is_user && <BorisSuperpowers active={is_tester} onChange={setBetaTester} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={styles.stats__wrap}>
|
<div className={styles.stats__wrap}>
|
||||||
<BorisStats stats={stats} />
|
<BorisStats stats={stats} />
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</Sticky>
|
</StickyBox>
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,22 +7,6 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
|
||||||
flex: 4;
|
|
||||||
z-index: 2;
|
|
||||||
border-radius: $radius;
|
|
||||||
padding: 0;
|
|
||||||
background: $node_bg;
|
|
||||||
box-shadow: inset transparentize(mix($wisegreen, white, 60%), 0.6) 0 1px;
|
|
||||||
|
|
||||||
@include desktop {
|
|
||||||
flex: 2.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media(max-width: 1024px) {
|
|
||||||
flex: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
padding: $gap;
|
padding: $gap;
|
||||||
|
@ -36,7 +20,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: 50% 0% no-repeat url('../../../sprites/boris_bg.svg');
|
background: 50% 0 no-repeat url('../../../sprites/boris_bg.svg');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,3 +151,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
flex: 3;
|
||||||
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ const NodeLayout: FC<IProps> = memo(
|
||||||
const { head, block } = useNodeBlocks(current, is_loading);
|
const { head, block } = useNodeBlocks(current, is_loading);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.wrap}>
|
||||||
{head}
|
{head}
|
||||||
|
|
||||||
<Container>
|
<Container>
|
||||||
|
@ -64,7 +64,7 @@ const NodeLayout: FC<IProps> = memo(
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<SidebarRouter prefix="/post:id" />
|
<SidebarRouter prefix="/post:id" />
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
align-items: stretch !important;
|
align-items: stretch !important;
|
||||||
|
|
||||||
@include vertical_at_tablet;
|
@include vertical_at_tablet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,9 @@ const ProfileLayoutUnconnected: FC<IProps> = ({ history, nodeSetCoverImage }) =>
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user && user.id && user.cover) {
|
if (user && user.id && user.cover) {
|
||||||
nodeSetCoverImage(user.cover);
|
nodeSetCoverImage(user.cover);
|
||||||
return () => nodeSetCoverImage(null);
|
return () => {
|
||||||
|
nodeSetCoverImage(undefined);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ import React, { FC, useCallback } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { IAuthState } from '~/redux/auth/types';
|
import { IAuthState } from '~/redux/auth/types';
|
||||||
|
import { Tabs } from '~/components/dialogs/Tabs';
|
||||||
|
import { Tab } from '~/components/dialogs/Tab';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
tab: string;
|
tab: string;
|
||||||
|
@ -20,28 +22,20 @@ const ProfileTabs: FC<IProps> = ({ tab, is_own, setTab }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<div
|
<Tabs>
|
||||||
className={classNames(styles.tab, { [styles.active]: tab === 'profile' })}
|
<Tab active={tab === 'profile'} onClick={changeTab('profile')}>
|
||||||
onClick={changeTab('profile')}
|
Профиль
|
||||||
>
|
</Tab>
|
||||||
Профиль
|
|
||||||
</div>
|
<Tab active={tab === 'messages'} onClick={changeTab('messages')}>
|
||||||
<div
|
Сообщения
|
||||||
className={classNames(styles.tab, { [styles.active]: tab === 'messages' })}
|
</Tab>
|
||||||
onClick={changeTab('messages')}
|
{is_own && (
|
||||||
>
|
<Tab active={tab === 'settings'} onClick={changeTab('settings')}>
|
||||||
Сообщения
|
|
||||||
</div>
|
|
||||||
{is_own && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={classNames(styles.tab, { [styles.active]: tab === 'settings' })}
|
|
||||||
onClick={changeTab('settings')}
|
|
||||||
>
|
|
||||||
Настройки
|
Настройки
|
||||||
</div>
|
</Tab>
|
||||||
</>
|
)}
|
||||||
)}
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,24 +1,6 @@
|
||||||
@import "src/styles/variables";
|
@import "src/styles/variables";
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: flex-start;
|
|
||||||
margin: $gap * 2 0 0 0;
|
margin: $gap * 2 0 0 0;
|
||||||
padding: 0 $gap;
|
padding: 0 $gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
|
||||||
@include outer_shadow();
|
|
||||||
|
|
||||||
padding: $gap;
|
|
||||||
margin-right: $gap;
|
|
||||||
border-radius: $radius $radius 0 0;
|
|
||||||
font: $font_14_semibold;
|
|
||||||
text-transform: uppercase;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: lighten($content_bg, 4%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
1
src/react-app-env.d.ts
vendored
1
src/react-app-env.d.ts
vendored
|
@ -3,5 +3,6 @@ declare namespace NodeJS {
|
||||||
interface ProcessEnv {
|
interface ProcessEnv {
|
||||||
readonly REACT_APP_API_URL: string;
|
readonly REACT_APP_API_URL: string;
|
||||||
readonly REACT_APP_REMOTE_CURRENT: string;
|
readonly REACT_APP_REMOTE_CURRENT: string;
|
||||||
|
readonly REACT_APP_LAB_ENABLED: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,11 @@ export const authSetToken = (token: IAuthState['token']) => ({
|
||||||
token,
|
token,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const authSetState = (payload: Partial<IAuthState>) => ({
|
||||||
|
type: AUTH_USER_ACTIONS.SET_STATE,
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
|
||||||
export const gotAuthPostMessage = ({ token }: { token: string }) => ({
|
export const gotAuthPostMessage = ({ token }: { token: string }) => ({
|
||||||
type: AUTH_USER_ACTIONS.GOT_AUTH_POST_MESSAGE,
|
type: AUTH_USER_ACTIONS.GOT_AUTH_POST_MESSAGE,
|
||||||
token,
|
token,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { api, cleanResult, errorMiddleware, resultMiddleware } from '~/utils/api';
|
import { api, cleanResult } from '~/utils/api';
|
||||||
import { API } from '~/constants/api';
|
import { API } from '~/constants/api';
|
||||||
import { IResultWithStatus } from '~/redux/types';
|
|
||||||
import {
|
import {
|
||||||
ApiAttachSocialRequest,
|
ApiAttachSocialRequest,
|
||||||
ApiAttachSocialResult,
|
ApiAttachSocialResult,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { IToken, IUser } from '~/redux/auth/types';
|
||||||
export const AUTH_USER_ACTIONS = {
|
export const AUTH_USER_ACTIONS = {
|
||||||
SEND_LOGIN_REQUEST: 'SEND_LOGIN_REQUEST',
|
SEND_LOGIN_REQUEST: 'SEND_LOGIN_REQUEST',
|
||||||
SET_LOGIN_ERROR: 'SET_LOGIN_ERROR',
|
SET_LOGIN_ERROR: 'SET_LOGIN_ERROR',
|
||||||
|
SET_STATE: 'SET_STATE',
|
||||||
SET_USER: 'SET_USER',
|
SET_USER: 'SET_USER',
|
||||||
SET_TOKEN: 'SET_TOKEN',
|
SET_TOKEN: 'SET_TOKEN',
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,11 @@ const setUser: ActionHandler<typeof ActionCreators.authSetUser> = (state, { prof
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setState: ActionHandler<typeof ActionCreators.authSetState> = (state, { payload }) => ({
|
||||||
|
...state,
|
||||||
|
...payload,
|
||||||
|
});
|
||||||
|
|
||||||
const setToken: ActionHandler<typeof ActionCreators.authSetToken> = (state, { token }) => ({
|
const setToken: ActionHandler<typeof ActionCreators.authSetToken> = (state, { token }) => ({
|
||||||
...state,
|
...state,
|
||||||
token,
|
token,
|
||||||
|
@ -104,6 +109,7 @@ const setRegisterSocialErrors: ActionHandler<typeof ActionCreators.authSetRegist
|
||||||
export const AUTH_USER_HANDLERS = {
|
export const AUTH_USER_HANDLERS = {
|
||||||
[AUTH_USER_ACTIONS.SET_LOGIN_ERROR]: setLoginError,
|
[AUTH_USER_ACTIONS.SET_LOGIN_ERROR]: setLoginError,
|
||||||
[AUTH_USER_ACTIONS.SET_USER]: setUser,
|
[AUTH_USER_ACTIONS.SET_USER]: setUser,
|
||||||
|
[AUTH_USER_ACTIONS.SET_STATE]: setState,
|
||||||
[AUTH_USER_ACTIONS.SET_TOKEN]: setToken,
|
[AUTH_USER_ACTIONS.SET_TOKEN]: setToken,
|
||||||
[AUTH_USER_ACTIONS.SET_PROFILE]: setProfile,
|
[AUTH_USER_ACTIONS.SET_PROFILE]: setProfile,
|
||||||
[AUTH_USER_ACTIONS.SET_UPDATES]: setUpdates,
|
[AUTH_USER_ACTIONS.SET_UPDATES]: setUpdates,
|
||||||
|
|
|
@ -10,6 +10,7 @@ const HANDLERS = {
|
||||||
const INITIAL_STATE: IAuthState = {
|
const INITIAL_STATE: IAuthState = {
|
||||||
token: '',
|
token: '',
|
||||||
user: { ...EMPTY_USER },
|
user: { ...EMPTY_USER },
|
||||||
|
is_tester: false,
|
||||||
|
|
||||||
updates: {
|
updates: {
|
||||||
last: '',
|
last: '',
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { IState } from '~/redux/store';
|
||||||
|
|
||||||
export const selectAuth = (state: IState) => state.auth;
|
export const selectAuth = (state: IState) => state.auth;
|
||||||
export const selectUser = (state: IState) => state.auth.user;
|
export const selectUser = (state: IState) => state.auth.user;
|
||||||
|
export const selectAuthIsTester = (state: IState) => state.auth.is_tester;
|
||||||
export const selectToken = (state: IState) => state.auth.token;
|
export const selectToken = (state: IState) => state.auth.token;
|
||||||
export const selectAuthLogin = (state: IState) => state.auth.login;
|
export const selectAuthLogin = (state: IState) => state.auth.login;
|
||||||
export const selectAuthProfile = (state: IState) => state.auth.profile;
|
export const selectAuthProfile = (state: IState) => state.auth.profile;
|
||||||
|
|
|
@ -37,6 +37,8 @@ export type IAuthState = Readonly<{
|
||||||
user: IUser;
|
user: IUser;
|
||||||
token: string;
|
token: string;
|
||||||
|
|
||||||
|
is_tester: boolean;
|
||||||
|
|
||||||
updates: {
|
updates: {
|
||||||
last: string;
|
last: string;
|
||||||
notifications: INotification[];
|
notifications: INotification[];
|
||||||
|
|
|
@ -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[];
|
12
src/redux/lab/actions.ts
Normal file
12
src/redux/lab/actions.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
||||||
|
import { ILabState } from '~/redux/lab/types';
|
||||||
|
|
||||||
|
export const labGetList = (after?: string) => ({
|
||||||
|
type: LAB_ACTIONS.GET_LIST,
|
||||||
|
after,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const labSetList = (list: Partial<ILabState['list']>) => ({
|
||||||
|
type: LAB_ACTIONS.SET_LIST,
|
||||||
|
list,
|
||||||
|
});
|
8
src/redux/lab/api.ts
Normal file
8
src/redux/lab/api.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { api, cleanResult } from '~/utils/api';
|
||||||
|
import { API } from '~/constants/api';
|
||||||
|
import { GetLabNodesRequest, GetLabNodesResult } from '~/redux/lab/types';
|
||||||
|
|
||||||
|
export const getLabNodes = ({ after }: GetLabNodesRequest) =>
|
||||||
|
api
|
||||||
|
.get<GetLabNodesResult>(API.LAB.NODES, { params: { after } })
|
||||||
|
.then(cleanResult);
|
6
src/redux/lab/constants.ts
Normal file
6
src/redux/lab/constants.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
const prefix = 'LAB.';
|
||||||
|
|
||||||
|
export const LAB_ACTIONS = {
|
||||||
|
GET_LIST: `${prefix}GET_LIST`,
|
||||||
|
SET_LIST: `${prefix}SET_LIST`,
|
||||||
|
};
|
20
src/redux/lab/handlers.ts
Normal file
20
src/redux/lab/handlers.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
||||||
|
import { labSetList } from '~/redux/lab/actions';
|
||||||
|
import { ILabState } from '~/redux/lab/types';
|
||||||
|
|
||||||
|
type LabHandler<T extends (...args: any) => any> = (
|
||||||
|
state: Readonly<ILabState>,
|
||||||
|
payload: ReturnType<T>
|
||||||
|
) => Readonly<ILabState>;
|
||||||
|
|
||||||
|
const setList: LabHandler<typeof labSetList> = (state, { list }) => ({
|
||||||
|
...state,
|
||||||
|
list: {
|
||||||
|
...state.list,
|
||||||
|
...list,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LAB_HANDLERS = {
|
||||||
|
[LAB_ACTIONS.SET_LIST]: setList,
|
||||||
|
};
|
14
src/redux/lab/index.ts
Normal file
14
src/redux/lab/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { createReducer } from '~/utils/reducer';
|
||||||
|
import { LAB_HANDLERS } from '~/redux/lab/handlers';
|
||||||
|
import { ILabState } from '~/redux/lab/types';
|
||||||
|
|
||||||
|
const INITIAL_STATE: ILabState = {
|
||||||
|
list: {
|
||||||
|
is_loading: false,
|
||||||
|
nodes: [],
|
||||||
|
count: 0,
|
||||||
|
error: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createReducer(INITIAL_STATE, LAB_HANDLERS);
|
21
src/redux/lab/sagas.ts
Normal file
21
src/redux/lab/sagas.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { takeLeading, call, put } from 'redux-saga/effects';
|
||||||
|
import { labGetList, labSetList } from '~/redux/lab/actions';
|
||||||
|
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
||||||
|
import { Unwrap } from '~/redux/types';
|
||||||
|
import { getLabNodes } from '~/redux/lab/api';
|
||||||
|
|
||||||
|
function* getList({ after = '' }: ReturnType<typeof labGetList>) {
|
||||||
|
try {
|
||||||
|
yield put(labSetList({ is_loading: true }));
|
||||||
|
const { nodes, count }: Unwrap<typeof getLabNodes> = yield call(getLabNodes, { after });
|
||||||
|
yield put(labSetList({ nodes, 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);
|
||||||
|
}
|
4
src/redux/lab/selectors.ts
Normal file
4
src/redux/lab/selectors.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { IState } from '~/redux/store';
|
||||||
|
|
||||||
|
export const selectLab = (state: IState) => state.lab;
|
||||||
|
export const selectLabListNodes = (state: IState) => state.lab.list.nodes;
|
19
src/redux/lab/types.ts
Normal file
19
src/redux/lab/types.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { IError, INode } from '~/redux/types';
|
||||||
|
|
||||||
|
export type ILabState = Readonly<{
|
||||||
|
list: {
|
||||||
|
is_loading: boolean;
|
||||||
|
nodes: INode[];
|
||||||
|
count: number;
|
||||||
|
error: IError;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type GetLabNodesRequest = {
|
||||||
|
after?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetLabNodesResult = {
|
||||||
|
nodes: INode[];
|
||||||
|
count: number;
|
||||||
|
};
|
|
@ -10,13 +10,23 @@ function* onPathChange({
|
||||||
},
|
},
|
||||||
}: LocationChangeAction) {
|
}: LocationChangeAction) {
|
||||||
if (pathname.match(/^\/~([\wа-яА-Я]+)/)) {
|
if (pathname.match(/^\/~([\wа-яА-Я]+)/)) {
|
||||||
const [, username] = pathname.match(/^\/~([\wа-яА-Я]+)/);
|
const match = pathname.match(/^\/~([\wа-яА-Я]+)/);
|
||||||
return yield put(authOpenProfile(username));
|
|
||||||
|
if (!match || !match.length || !match[1]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return yield put(authOpenProfile(match[1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.match(/^\/restore\/([\w\-]+)/)) {
|
if (pathname.match(/^\/restore\/([\w\-]+)/)) {
|
||||||
const [, code] = pathname.match(/^\/restore\/([\w\-]+)/);
|
const match = pathname.match(/^\/restore\/([\w\-]+)/);
|
||||||
return yield put(authShowRestoreModal(code));
|
|
||||||
|
if (!match || !match.length || !match[1]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return yield put(authShowRestoreModal(match[1]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { EditorAudioUploadButton } from '~/components/editors/EditorAudioUploadB
|
||||||
import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton';
|
import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton';
|
||||||
import { IEditorComponentProps, NodeEditorProps } from '~/redux/node/types';
|
import { IEditorComponentProps, NodeEditorProps } from '~/redux/node/types';
|
||||||
import { EditorFiller } from '~/components/editors/EditorFiller';
|
import { EditorFiller } from '~/components/editors/EditorFiller';
|
||||||
|
import { EditorPublicSwitch } from '~/components/editors/EditorPublicSwitch';
|
||||||
import { NodeImageSwiperBlock } from '~/components/node/NodeImageSwiperBlock';
|
import { NodeImageSwiperBlock } from '~/components/node/NodeImageSwiperBlock';
|
||||||
|
|
||||||
const prefix = 'NODE.';
|
const prefix = 'NODE.';
|
||||||
|
@ -59,6 +60,8 @@ export const EMPTY_NODE: INode = {
|
||||||
|
|
||||||
blocks: [],
|
blocks: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
|
is_public: true,
|
||||||
|
is_promoted: true,
|
||||||
|
|
||||||
flow: {
|
flow: {
|
||||||
display: 'single',
|
display: 'single',
|
||||||
|
@ -112,14 +115,20 @@ export const NODE_EDITORS: Record<
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NODE_PANEL_COMPONENTS: Record<string, FC<IEditorComponentProps>[]> = {
|
export const NODE_PANEL_COMPONENTS: Record<string, FC<IEditorComponentProps>[]> = {
|
||||||
[NODE_TYPES.TEXT]: [EditorFiller, EditorUploadCoverButton],
|
[NODE_TYPES.TEXT]: [EditorFiller, EditorUploadCoverButton, EditorPublicSwitch],
|
||||||
[NODE_TYPES.VIDEO]: [EditorFiller, EditorUploadCoverButton],
|
[NODE_TYPES.VIDEO]: [EditorFiller, EditorUploadCoverButton, EditorPublicSwitch],
|
||||||
[NODE_TYPES.IMAGE]: [EditorImageUploadButton, EditorFiller, EditorUploadCoverButton],
|
[NODE_TYPES.IMAGE]: [
|
||||||
|
EditorImageUploadButton,
|
||||||
|
EditorFiller,
|
||||||
|
EditorUploadCoverButton,
|
||||||
|
EditorPublicSwitch,
|
||||||
|
],
|
||||||
[NODE_TYPES.AUDIO]: [
|
[NODE_TYPES.AUDIO]: [
|
||||||
EditorAudioUploadButton,
|
EditorAudioUploadButton,
|
||||||
EditorImageUploadButton,
|
EditorImageUploadButton,
|
||||||
EditorFiller,
|
EditorFiller,
|
||||||
EditorUploadCoverButton,
|
EditorUploadCoverButton,
|
||||||
|
EditorPublicSwitch,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,10 @@ import nodeSaga from '~/redux/node/sagas';
|
||||||
import flow, { IFlowState } from '~/redux/flow/reducer';
|
import flow, { IFlowState } from '~/redux/flow/reducer';
|
||||||
import flowSaga from '~/redux/flow/sagas';
|
import flowSaga from '~/redux/flow/sagas';
|
||||||
|
|
||||||
|
import lab from '~/redux/lab';
|
||||||
|
import labSaga from '~/redux/lab/sagas';
|
||||||
|
import { ILabState } from '~/redux/lab/types';
|
||||||
|
|
||||||
import uploads, { IUploadState } from '~/redux/uploads/reducer';
|
import uploads, { IUploadState } from '~/redux/uploads/reducer';
|
||||||
import uploadSaga from '~/redux/uploads/sagas';
|
import uploadSaga from '~/redux/uploads/sagas';
|
||||||
|
|
||||||
|
@ -42,7 +46,7 @@ import { assocPath } from 'ramda';
|
||||||
|
|
||||||
const authPersistConfig: PersistConfig = {
|
const authPersistConfig: PersistConfig = {
|
||||||
key: 'auth',
|
key: 'auth',
|
||||||
whitelist: ['token', 'user', 'updates'],
|
whitelist: ['token', 'user', 'updates', 'is_tester'],
|
||||||
storage,
|
storage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -69,13 +73,16 @@ export interface IState {
|
||||||
boris: IBorisState;
|
boris: IBorisState;
|
||||||
messages: IMessagesState;
|
messages: IMessagesState;
|
||||||
tag: ITagState;
|
tag: ITagState;
|
||||||
|
lab: ILabState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sagaMiddleware = createSagaMiddleware();
|
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;
|
||||||
|
|
||||||
|
@ -91,6 +98,7 @@ export const store = createStore(
|
||||||
player: persistReducer(playerPersistConfig, player),
|
player: persistReducer(playerPersistConfig, player),
|
||||||
messages,
|
messages,
|
||||||
tag: tag,
|
tag: tag,
|
||||||
|
lab: lab,
|
||||||
}),
|
}),
|
||||||
composeEnhancers(applyMiddleware(routerMiddleware(history), sagaMiddleware))
|
composeEnhancers(applyMiddleware(routerMiddleware(history), sagaMiddleware))
|
||||||
);
|
);
|
||||||
|
@ -108,6 +116,7 @@ export function configureStore(): {
|
||||||
sagaMiddleware.run(borisSaga);
|
sagaMiddleware.run(borisSaga);
|
||||||
sagaMiddleware.run(messagesSaga);
|
sagaMiddleware.run(messagesSaga);
|
||||||
sagaMiddleware.run(tagSaga);
|
sagaMiddleware.run(tagSaga);
|
||||||
|
sagaMiddleware.run(labSaga);
|
||||||
|
|
||||||
window.addEventListener('message', message => {
|
window.addEventListener('message', message => {
|
||||||
if (message && message.data && message.data.type === 'oauth_login' && message.data.token)
|
if (message && message.data && message.data.type === 'oauth_login' && message.data.token)
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -124,6 +124,8 @@ export interface INode {
|
||||||
description?: string;
|
description?: string;
|
||||||
is_liked?: boolean;
|
is_liked?: boolean;
|
||||||
is_heroic?: boolean;
|
is_heroic?: boolean;
|
||||||
|
is_promoted?: boolean;
|
||||||
|
is_public?: boolean;
|
||||||
like_count?: number;
|
like_count?: number;
|
||||||
|
|
||||||
flow: {
|
flow: {
|
||||||
|
|
|
@ -255,6 +255,16 @@ const Sprites: FC<{}> = () => (
|
||||||
<path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z" />
|
<path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z" />
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
|
<g id="waves" stroke="none">
|
||||||
|
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||||
|
<path d="M17 16.99c-1.35 0-2.2.42-2.95.8-.65.33-1.18.6-2.05.6-.9 0-1.4-.25-2.05-.6-.75-.38-1.57-.8-2.95-.8s-2.2.42-2.95.8c-.65.33-1.17.6-2.05.6v1.95c1.35 0 2.2-.42 2.95-.8.65-.33 1.17-.6 2.05-.6s1.4.25 2.05.6c.75.38 1.57.8 2.95.8s2.2-.42 2.95-.8c.65-.33 1.18-.6 2.05-.6.9 0 1.4.25 2.05.6.75.38 1.58.8 2.95.8v-1.95c-.9 0-1.4-.25-2.05-.6-.75-.38-1.6-.8-2.95-.8zm0-4.45c-1.35 0-2.2.43-2.95.8-.65.32-1.18.6-2.05.6-.9 0-1.4-.25-2.05-.6-.75-.38-1.57-.8-2.95-.8s-2.2.43-2.95.8c-.65.32-1.17.6-2.05.6v1.95c1.35 0 2.2-.43 2.95-.8.65-.35 1.15-.6 2.05-.6s1.4.25 2.05.6c.75.38 1.57.8 2.95.8s2.2-.43 2.95-.8c.65-.35 1.15-.6 2.05-.6s1.4.25 2.05.6c.75.38 1.58.8 2.95.8v-1.95c-.9 0-1.4-.25-2.05-.6-.75-.38-1.6-.8-2.95-.8zm2.95-8.08c-.75-.38-1.58-.8-2.95-.8s-2.2.42-2.95.8c-.65.32-1.18.6-2.05.6-.9 0-1.4-.25-2.05-.6-.75-.37-1.57-.8-2.95-.8s-2.2.42-2.95.8c-.65.33-1.17.6-2.05.6v1.93c1.35 0 2.2-.43 2.95-.8.65-.33 1.17-.6 2.05-.6s1.4.25 2.05.6c.75.38 1.57.8 2.95.8s2.2-.43 2.95-.8c.65-.32 1.18-.6 2.05-.6.9 0 1.4.25 2.05.6.75.38 1.58.8 2.95.8V5.04c-.9 0-1.4-.25-2.05-.58zM17 8.09c-1.35 0-2.2.43-2.95.8-.65.35-1.15.6-2.05.6s-1.4-.25-2.05-.6c-.75-.38-1.57-.8-2.95-.8s-2.2.43-2.95.8c-.65.35-1.15.6-2.05.6v1.95c1.35 0 2.2-.43 2.95-.8.65-.32 1.18-.6 2.05-.6s1.4.25 2.05.6c.75.38 1.57.8 2.95.8s2.2-.43 2.95-.8c.65-.32 1.18-.6 2.05-.6.9 0 1.4.25 2.05.6.75.38 1.58.8 2.95.8V9.49c-.9 0-1.4-.25-2.05-.6-.75-.38-1.6-.8-2.95-.8z" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="lab" stroke="none">
|
||||||
|
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||||
|
<path d="M13,11.33L18,18H6l5-6.67V6h2 M15.96,4H8.04C7.62,4,7.39,4.48,7.65,4.81L9,6.5v4.17L3.2,18.4C2.71,19.06,3.18,20,4,20h16 c0.82,0,1.29-0.94,0.8-1.6L15,10.67V6.5l1.35-1.69C16.61,4.48,16.38,4,15.96,4L15.96,4z" />
|
||||||
|
</g>
|
||||||
|
|
||||||
<g id="search">
|
<g id="search">
|
||||||
<path fill="none" d="M0 0h24v24H0V0z" stroke="none" />
|
<path fill="none" d="M0 0h24v24H0V0z" stroke="none" />
|
||||||
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
|
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
$red: #ff3344;
|
$red: #ff3344;
|
||||||
$yellow: #ffd60f;
|
$yellow: #ffd60f;
|
||||||
$dark_blue: #3c75ff;
|
$dark_blue: #3c75ff;
|
||||||
$blue: #3ca1ff;
|
$blue: #582cd0;
|
||||||
$green: #00d2b9;
|
$green: #00d2b9;
|
||||||
//$green: #00503c;
|
//$green: #00503c;
|
||||||
$olive: #8bc12a;
|
$olive: #8bc12a;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -209,3 +209,13 @@ $sidebar_border: transparentize(white, 0.95);
|
||||||
background: transparentize($content_bg, 0.4);
|
background: transparentize($content_bg, 0.4);
|
||||||
box-shadow: transparentize(white, 0.95) -1px 0;
|
box-shadow: transparentize(white, 0.95) -1px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin editor_round_button {
|
||||||
|
width: $upload_button_height;
|
||||||
|
height: $upload_button_height;
|
||||||
|
border-radius: ($upload_button_height / 2) !important;
|
||||||
|
flex: 0 0 $upload_button_height;
|
||||||
|
position: relative;
|
||||||
|
border-radius: $radius;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,10 @@ import { useMemo } from 'react';
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
|
|
||||||
export const useNodeAudios = (node: INode) => {
|
export const useNodeAudios = (node: INode) => {
|
||||||
|
if (!node?.files) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), [
|
return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), [
|
||||||
node.files,
|
node.files,
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -3,6 +3,10 @@ import { useMemo } from 'react';
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||||
|
|
||||||
export const useNodeImages = (node: INode) => {
|
export const useNodeImages = (node: INode) => {
|
||||||
|
if (!node?.files) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [
|
return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [
|
||||||
node.files,
|
node.files,
|
||||||
]);
|
]);
|
||||||
|
|
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]);
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue