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