mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
Merge remote-tracking branch 'origin/master'
# Conflicts: # src/redux/uploads/sagas.ts
This commit is contained in:
commit
3d4f60c2b4
24 changed files with 292 additions and 156 deletions
|
@ -1,6 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ['airbnb', 'airbnb-base', 'prettier/@typescript-eslint', 'plugin:@typescript-eslint/recommended'],
|
extends: ['plugin:@typescript-eslint/recommended', 'prettier/@typescript-eslint', 'airbnb', 'airbnb-base'],
|
||||||
// "parser": "babel-eslint",
|
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
|
@ -11,15 +10,12 @@ module.exports = {
|
||||||
plugins: ['@typescript-eslint', 'react', 'jsx-a11y', 'import', 'react-hooks'],
|
plugins: ['@typescript-eslint', 'react', 'jsx-a11y', 'import', 'react-hooks'],
|
||||||
settings: {
|
settings: {
|
||||||
'import/resolver': {
|
'import/resolver': {
|
||||||
// node: {
|
|
||||||
// extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
|
||||||
// },
|
|
||||||
typescript: {},
|
typescript: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/explicit-function-return-type': 0,
|
'@typescript-eslint/explicit-function-return-type': 0,
|
||||||
'@typescript-eslint/indent': ['warn', 2],
|
// '@typescript-eslint/indent': ['warn', 2],
|
||||||
'comma-dangle': 0,
|
'comma-dangle': 0,
|
||||||
'no-restricted-syntax': 1,
|
'no-restricted-syntax': 1,
|
||||||
'react/prop-types': 0,
|
'react/prop-types': 0,
|
||||||
|
@ -63,5 +59,6 @@ module.exports = {
|
||||||
window: false,
|
window: false,
|
||||||
HTMLInputElement: false,
|
HTMLInputElement: false,
|
||||||
HTMLDivElement: false,
|
HTMLDivElement: false,
|
||||||
|
FormData: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import React, {
|
import React, { FC, ChangeEventHandler, DragEventHandler } from 'react';
|
||||||
FC,
|
|
||||||
ChangeEventHandler,
|
|
||||||
DragEventHandler
|
|
||||||
} from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
||||||
|
@ -12,25 +8,26 @@ import { IUploadStatus } from '~/redux/uploads/reducer';
|
||||||
|
|
||||||
const mapStateToProps = selectUploads;
|
const mapStateToProps = selectUploads;
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles
|
uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
|
||||||
};
|
};
|
||||||
|
|
||||||
type IProps = ReturnType<typeof mapStateToProps> &
|
type IProps = ReturnType<typeof mapStateToProps> &
|
||||||
typeof mapDispatchToProps & {
|
typeof mapDispatchToProps & {
|
||||||
data: INode;
|
data: INode;
|
||||||
pending_files: IUploadStatus[];
|
pending_files: IUploadStatus[];
|
||||||
setData: (val: INode) => void;
|
|
||||||
onFileMove: (o: number, n: number) => void;
|
setData: (val: INode) => void;
|
||||||
onInputChange: ChangeEventHandler<HTMLInputElement>;
|
onFileMove: (o: number, n: number) => void;
|
||||||
onDrop: DragEventHandler<HTMLFormElement>;
|
onInputChange: ChangeEventHandler<HTMLInputElement>;
|
||||||
};
|
onDrop: DragEventHandler<HTMLFormElement>;
|
||||||
|
};
|
||||||
|
|
||||||
const ImageEditorUnconnected: FC<IProps> = ({
|
const ImageEditorUnconnected: FC<IProps> = ({
|
||||||
data,
|
data,
|
||||||
onFileMove,
|
onFileMove,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
onDrop,
|
onDrop,
|
||||||
pending_files
|
pending_files,
|
||||||
}) => (
|
}) => (
|
||||||
<ImageGrid
|
<ImageGrid
|
||||||
onFileMove={onFileMove}
|
onFileMove={onFileMove}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import * as styles from './styles.scss';
|
import * as styles from './styles.scss';
|
||||||
|
import { TEXTS } from '~/constants/texts';
|
||||||
|
|
||||||
import classNames = require('classnames');
|
import classNames = require('classnames');
|
||||||
|
|
||||||
|
@ -9,6 +10,7 @@ interface IProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
is_hero?: boolean;
|
is_hero?: boolean;
|
||||||
is_stamp?: boolean;
|
is_stamp?: boolean;
|
||||||
|
is_text?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Cell: FC<IProps> = ({
|
const Cell: FC<IProps> = ({
|
||||||
|
@ -16,14 +18,22 @@ const Cell: FC<IProps> = ({
|
||||||
height = 1,
|
height = 1,
|
||||||
title,
|
title,
|
||||||
is_hero,
|
is_hero,
|
||||||
|
is_text = (Math.random() > 0.8),
|
||||||
}) => (
|
}) => (
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.cell, `vert-${height}`, `hor-${width}`)}
|
className={
|
||||||
|
classNames(
|
||||||
|
styles.cell,
|
||||||
|
`vert-${height}`,
|
||||||
|
`hor-${width}`,
|
||||||
|
{ is_text },
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
// gridRowEnd: `span ${height}`,
|
// gridRowEnd: `span ${height}`,
|
||||||
// gridColumnEnd: `span ${width}`,
|
// gridColumnEnd: `span ${width}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{is_text && <div className={styles.text}>{TEXTS.LOREM_IPSUM}</div>}
|
||||||
{ title && <div className={styles.title}>{title}</div> }
|
{ title && <div className={styles.title}>{title}</div> }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
background: $cell_bg;
|
background: $cell_bg;
|
||||||
border-radius: $cell_radius;
|
border-radius: $cell_radius;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
&:global(.is_hero) {
|
&:global(.is_hero) {
|
||||||
.title {
|
.title {
|
||||||
|
@ -16,6 +17,16 @@
|
||||||
@include outer_shadow();
|
@include outer_shadow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
line-height: 1.6em;
|
||||||
|
font-size: 18px;
|
||||||
|
font: $font_18_regular;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font: $font_cell_title;
|
font: $font_cell_title;
|
||||||
|
|
||||||
|
@ -49,4 +60,10 @@
|
||||||
.hor-2 {
|
.hor-2 {
|
||||||
grid-column-end: span 2;
|
grid-column-end: span 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is_text {
|
||||||
|
background: none;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: inset #444 0 0 0 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,31 +6,10 @@ import * as styles from './styles.scss';
|
||||||
|
|
||||||
export const TestGrid = () => (
|
export const TestGrid = () => (
|
||||||
<div>
|
<div>
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
// gridRow: "1 / 2",
|
|
||||||
// gridColumn: "1 / -1",
|
|
||||||
background: '#222222',
|
|
||||||
borderRadius: 6,
|
|
||||||
height: 300,
|
|
||||||
marginBottom: 4,
|
|
||||||
display: 'flex',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
HERO
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.grid_test}>
|
<div className={styles.grid_test}>
|
||||||
<div
|
<div className={styles.hero}>HERO</div>
|
||||||
style={{
|
|
||||||
gridRow: '1 / 3',
|
<div className={styles.stamp}>STAMP</div>
|
||||||
gridColumn: '-2 / -1',
|
|
||||||
background: '#090909',
|
|
||||||
borderRadius: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
STAMP
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{range(1, 20).map(el => (
|
{range(1, 20).map(el => (
|
||||||
<Cell
|
<Cell
|
||||||
|
|
|
@ -17,3 +17,32 @@ $cols: $content_width / $cell;
|
||||||
.pad_last {
|
.pad_last {
|
||||||
grid-column-end: $cols + 1;
|
grid-column-end: $cols + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
grid-row-start: 0;
|
||||||
|
grid-row-end: span 2;
|
||||||
|
grid-column-start: 0;
|
||||||
|
grid-column-end: span 4;
|
||||||
|
// gridRow: "1 / 2",
|
||||||
|
// gridColumn: "1 / -1",
|
||||||
|
background: #222222;
|
||||||
|
border-radius: $radius;
|
||||||
|
// height: 33vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font: $font_24_semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stamp {
|
||||||
|
// grid-row: -1 / 3;
|
||||||
|
grid-row-end: span 3;
|
||||||
|
grid-column: -2 / -1;
|
||||||
|
background: #090909;
|
||||||
|
border-radius: $radius;
|
||||||
|
padding: $gap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font: $font_24_semibold;
|
||||||
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ const HeaderUnconnected: FC<IProps> = ({ username, is_user, showDialog }) => {
|
||||||
const onOpenEditor = useCallback(() => showDialog(DIALOGS.EDITOR), [showDialog]);
|
const onOpenEditor = useCallback(() => showDialog(DIALOGS.EDITOR), [showDialog]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="default_container head_container">
|
<div>
|
||||||
<div className={style.container}>
|
<div className={style.container}>
|
||||||
<Logo />
|
<Logo />
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
height: 96px;
|
height: 96px;
|
||||||
|
margin-top: -20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
|
|
|
@ -12,15 +12,21 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageUpload: FC<IProps> = ({
|
const ImageUpload: FC<IProps> = ({
|
||||||
thumb,
|
thumb, id, progress, is_uploading,
|
||||||
id,
|
|
||||||
progress,
|
|
||||||
is_uploading,
|
|
||||||
}) => (
|
}) => (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<div className={classNames(styles.thumb_wrap, { is_uploading })}>
|
<div className={classNames(styles.thumb_wrap, { is_uploading })}>
|
||||||
{thumb && <div className={styles.thumb} style={{ background: `url("${thumb}")` }}>{id}</div>}
|
{thumb && (
|
||||||
{is_uploading && <div className={styles.progress}><ArcProgress size={72} progress={progress} /></div>}
|
<div
|
||||||
|
className={styles.thumb}
|
||||||
|
style={{ backgroundImage: `url("${process.env.API_HOST}${thumb}")` }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{is_uploading && (
|
||||||
|
<div className={styles.progress}>
|
||||||
|
<ArcProgress size={72} progress={progress} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
background: no-repeat 50% 50%;
|
background: no-repeat 50% 50%;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
filter: saturate(0);
|
// filter: saturate(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
|
|
|
@ -3,5 +3,6 @@ export const API = {
|
||||||
USER: {
|
USER: {
|
||||||
LOGIN: '/auth/login',
|
LOGIN: '/auth/login',
|
||||||
ME: '/auth/me', //
|
ME: '/auth/me', //
|
||||||
}
|
UPLOAD: (target, type) => `/upload/${target}/${type}`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
4
src/constants/texts.ts
Normal file
4
src/constants/texts.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export const TEXTS = {
|
||||||
|
LOREM_IPSUM:
|
||||||
|
'Многие думают, что Lorem Ipsum - взятый с потолка псевдо-латинский набор слов, но это не совсем так. Его корни уходят в один фрагмент классической латыни 45 года н.э., то есть более двух тысячелетий назад. Ричард МакКлинток, профессор латыни из колледжа Hampden-Sydney, штат Вирджиния, взял одно из самых странных слов в Lorem Ipsum, "consectetur", и занялся его поисками в классической латинской литературе. В результате он нашёл неоспоримый первоисточник Lorem Ipsum в разделах 1.10.32 и 1.10.33 книги "de Finibus Bonorum et Malorum" ("О пределах добра и зла"), написанной Цицероном в 45 году н.э. Этот трактат по теории этики был очень популярен в эпоху Возрождения. Первая строка Lorem Ipsum, "Lorem ipsum dolor sit amet..", происходит от одной из строк в разделе 1.10.32',
|
||||||
|
};
|
|
@ -1,11 +1,8 @@
|
||||||
import * as React from 'react';
|
import React, { FC } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import { ConnectedRouter } from 'connected-react-router';
|
import { ConnectedRouter } from 'connected-react-router';
|
||||||
import {
|
import { Switch, Route, Redirect } from 'react-router-dom';
|
||||||
NavLink, Switch, Route, Redirect
|
|
||||||
} from 'react-router-dom';
|
|
||||||
import { history } from '~/redux/store';
|
import { history } from '~/redux/store';
|
||||||
import { FlowLayout } from '~/containers/flow/FlowLayout';
|
import { FlowLayout } from '~/containers/flow/FlowLayout';
|
||||||
import { MainLayout } from '~/containers/main/MainLayout';
|
import { MainLayout } from '~/containers/main/MainLayout';
|
||||||
|
@ -23,29 +20,28 @@ const mapDispatchToProps = {};
|
||||||
|
|
||||||
type IProps = typeof mapDispatchToProps & ReturnType<typeof mapStateToProps> & {};
|
type IProps = typeof mapDispatchToProps & ReturnType<typeof mapStateToProps> & {};
|
||||||
|
|
||||||
class Component extends React.Component<IProps, {}> {
|
const Component: FC<IProps> = ({ is_shown }) => (
|
||||||
render() {
|
<ConnectedRouter history={history}>
|
||||||
return (
|
<BlurWrapper is_blurred={is_shown}>
|
||||||
<ConnectedRouter history={history}>
|
<Modal />
|
||||||
<BlurWrapper is_blurred={this.props.is_shown}>
|
<Sprites />
|
||||||
<MainLayout>
|
|
||||||
<Modal />
|
|
||||||
<Sprites />
|
|
||||||
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={URLS.EXAMPLES.IMAGE} component={ImageExample} />
|
<Route exact path={URLS.BASE} component={FlowLayout} />
|
||||||
<Route path={URLS.EXAMPLES.EDITOR} component={EditorExample} />
|
|
||||||
<Route path="/examples/horizontal" component={HorizontalExample} />
|
|
||||||
<Route exact path={URLS.BASE} component={FlowLayout} />
|
|
||||||
|
|
||||||
<Redirect to="/" />
|
<MainLayout>
|
||||||
</Switch>
|
<Switch>
|
||||||
</MainLayout>
|
<Route path={URLS.EXAMPLES.IMAGE} component={ImageExample} />
|
||||||
</BlurWrapper>
|
<Route path={URLS.EXAMPLES.EDITOR} component={EditorExample} />
|
||||||
</ConnectedRouter>
|
<Route path="/examples/horizontal" component={HorizontalExample} />
|
||||||
);
|
|
||||||
}
|
<Redirect to="/" />
|
||||||
}
|
</Switch>
|
||||||
|
</MainLayout>
|
||||||
|
</Switch>
|
||||||
|
</BlurWrapper>
|
||||||
|
</ConnectedRouter>
|
||||||
|
);
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, {
|
import React, {
|
||||||
FC, useState, useCallback, useEffect
|
FC, useState, useCallback, useEffect,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import assocPath from 'ramda/es/assocPath';
|
import assocPath from 'ramda/es/assocPath';
|
||||||
|
@ -20,6 +20,7 @@ import { moveArrItem } from '~/utils/fn';
|
||||||
import { IFile, IFileWithUUID } from '~/redux/types';
|
import { IFile, IFileWithUUID } from '~/redux/types';
|
||||||
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
||||||
import { selectUploads } from '~/redux/uploads/selectors';
|
import { selectUploads } from '~/redux/uploads/selectors';
|
||||||
|
import { UPLOAD_TARGETS, UPLOAD_TYPES, UPLOAD_SUBJECTS } from '~/redux/uploads/constants';
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
const { editor } = selectNode(state);
|
const { editor } = selectNode(state);
|
||||||
|
@ -35,19 +36,29 @@ const mapDispatchToProps = {
|
||||||
type IProps = IDialogProps & ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
type IProps = IDialogProps & ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||||
|
|
||||||
const EditorDialogUnconnected: FC<IProps> = ({
|
const EditorDialogUnconnected: FC<IProps> = ({
|
||||||
onRequestClose, editor, uploadUploadFiles, files, statuses
|
onRequestClose,
|
||||||
|
editor,
|
||||||
|
uploadUploadFiles,
|
||||||
|
files,
|
||||||
|
statuses,
|
||||||
}) => {
|
}) => {
|
||||||
const [data, setData] = useState(editor);
|
const [data, setData] = useState(editor);
|
||||||
const eventPreventer = useCallback(event => event.preventDefault(), []);
|
const eventPreventer = useCallback(event => event.preventDefault(), []);
|
||||||
const [temp, setTemp] = useState([]);
|
const [temp, setTemp] = useState([]);
|
||||||
|
|
||||||
const onFileMove = useCallback((old_index: number, new_index: number) => {
|
const onFileMove = useCallback(
|
||||||
setData(assocPath(['files'], moveArrItem(old_index, new_index, data.files), data));
|
(old_index: number, new_index: number) => {
|
||||||
}, [data, setData]);
|
setData(assocPath(['files'], moveArrItem(old_index, new_index, data.files), data));
|
||||||
|
},
|
||||||
|
[data, setData]
|
||||||
|
);
|
||||||
|
|
||||||
const onFileAdd = useCallback((file: IFile) => {
|
const onFileAdd = useCallback(
|
||||||
setData(assocPath(['files'], append(file, data.files), data));
|
(file: IFile) => {
|
||||||
}, [data, setData]);
|
setData(assocPath(['files'], append(file, data.files), data));
|
||||||
|
},
|
||||||
|
[data, setData]
|
||||||
|
);
|
||||||
|
|
||||||
const onDrop = useCallback(
|
const onDrop = useCallback(
|
||||||
(event: React.DragEvent<HTMLFormElement>) => {
|
(event: React.DragEvent<HTMLFormElement>) => {
|
||||||
|
@ -59,7 +70,9 @@ const EditorDialogUnconnected: FC<IProps> = ({
|
||||||
(file: File): IFileWithUUID => ({
|
(file: File): IFileWithUUID => ({
|
||||||
file,
|
file,
|
||||||
temp_id: uuid(),
|
temp_id: uuid(),
|
||||||
subject: 'editor'
|
subject: UPLOAD_SUBJECTS.EDITOR,
|
||||||
|
target: UPLOAD_TARGETS.NODES,
|
||||||
|
type: UPLOAD_TYPES.IMAGE,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -81,7 +94,9 @@ const EditorDialogUnconnected: FC<IProps> = ({
|
||||||
(file: File): IFileWithUUID => ({
|
(file: File): IFileWithUUID => ({
|
||||||
file,
|
file,
|
||||||
temp_id: uuid(),
|
temp_id: uuid(),
|
||||||
subject: 'editor'
|
subject: UPLOAD_SUBJECTS.EDITOR,
|
||||||
|
target: UPLOAD_TARGETS.NODES,
|
||||||
|
type: UPLOAD_TYPES.IMAGE,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -124,11 +139,7 @@ const EditorDialogUnconnected: FC<IProps> = ({
|
||||||
|
|
||||||
const buttons = (
|
const buttons = (
|
||||||
<Padder style={{ position: 'relative' }}>
|
<Padder style={{ position: 'relative' }}>
|
||||||
<EditorPanel
|
<EditorPanel data={data} setData={setData} onUpload={onInputChange} />
|
||||||
data={data}
|
|
||||||
setData={setData}
|
|
||||||
onUpload={onInputChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Group horizontal>
|
<Group horizontal>
|
||||||
<InputText title="Название" value={data.title} handler={setTitle} />
|
<InputText title="Название" value={data.title} handler={setTitle} />
|
||||||
|
|
|
@ -10,17 +10,16 @@ import { NodeRelated } from '~/components/node/NodeRelated';
|
||||||
import { Tags } from '~/components/node/Tags';
|
import { Tags } from '~/components/node/Tags';
|
||||||
import { MenuButton } from '~/components/node/MenuButton';
|
import { MenuButton } from '~/components/node/MenuButton';
|
||||||
import { NodeNoComments } from '~/components/node/NodeNoComments';
|
import { NodeNoComments } from '~/components/node/NodeNoComments';
|
||||||
import { InputText } from '~/components/input/InputText';
|
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {}
|
||||||
|
|
||||||
const ImageExample: FC<IProps> = () => (
|
const ImageExample: FC<IProps> = () => (
|
||||||
<Card className={styles.node} seamless>
|
<Card className={styles.node} seamless>
|
||||||
<InputText />
|
|
||||||
<div className={styles.image_container}>
|
<div className={styles.image_container}>
|
||||||
<img
|
<img
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
src="http://37.192.131.144/full/attached/2017/11/f01fdaaea789915284757634baf7cd11.jpg"
|
src="http://37.192.131.144/full/attached/2017/11/f01fdaaea789915284757634baf7cd11.jpg"
|
||||||
|
alt=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -41,11 +40,7 @@ const ImageExample: FC<IProps> = () => (
|
||||||
<Group style={{ flex: 1 }}>
|
<Group style={{ flex: 1 }}>
|
||||||
<Padder className={styles.buttons}>
|
<Padder className={styles.buttons}>
|
||||||
<Group>
|
<Group>
|
||||||
<MenuButton
|
<MenuButton title="На главной" description="плывет по течению" icon="star" />
|
||||||
title="На главной"
|
|
||||||
description="плывет по течению"
|
|
||||||
icon="star"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MenuButton title="Видно всем" icon="star" />
|
<MenuButton title="Видно всем" icon="star" />
|
||||||
|
|
||||||
|
@ -59,7 +54,7 @@ const ImageExample: FC<IProps> = () => (
|
||||||
{ title: 'Плейлист', feature: 'green' },
|
{ title: 'Плейлист', feature: 'green' },
|
||||||
{ title: 'Просто' },
|
{ title: 'Просто' },
|
||||||
{ title: '+ фото', feature: 'black' },
|
{ title: '+ фото', feature: 'black' },
|
||||||
{ title: '+ с музыкой', feature: 'black' }
|
{ title: '+ с музыкой', feature: 'black' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { TestGrid } from '~/components/flow/TestGrid';
|
import { TestGrid } from '~/components/flow/TestGrid';
|
||||||
|
import * as styles from './styles.scss';
|
||||||
|
import { Header } from '~/components/main/Header';
|
||||||
|
|
||||||
export const FlowLayout = () => (
|
export const FlowLayout = () => (
|
||||||
<div className="default_container content_container">
|
<div className={styles.wrap}>
|
||||||
|
<Header />
|
||||||
<TestGrid />
|
<TestGrid />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
6
src/containers/flow/FlowLayout/styles.scss
Normal file
6
src/containers/flow/FlowLayout/styles.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
.wrap {
|
||||||
|
max-width: 2000px;
|
||||||
|
padding: 0 40px 40px 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import {
|
import {
|
||||||
call, put, takeLatest, select
|
call, put, takeLatest, select,
|
||||||
} from 'redux-saga/effects';
|
} from 'redux-saga/effects';
|
||||||
import { SagaIterator } from 'redux-saga';
|
import { SagaIterator } from 'redux-saga';
|
||||||
import { push } from 'connected-react-router';
|
import { push } from 'connected-react-router';
|
||||||
|
@ -15,10 +15,14 @@ import { IResultWithStatus } from '../types';
|
||||||
import { IUser } from './types';
|
import { IUser } from './types';
|
||||||
|
|
||||||
export function* reqWrapper(requestAction, props = {}): ReturnType<typeof requestAction> {
|
export function* reqWrapper(requestAction, props = {}): ReturnType<typeof requestAction> {
|
||||||
const { access } = yield select(selectToken);
|
const access = yield select(selectToken);
|
||||||
|
|
||||||
|
console.log('firing reqWrapper');
|
||||||
|
|
||||||
const result = yield call(requestAction, { access, ...props });
|
const result = yield call(requestAction, { access, ...props });
|
||||||
|
|
||||||
|
console.log('at reqWrapper', { result });
|
||||||
|
|
||||||
if (result && result.status === 401) {
|
if (result && result.status === 401) {
|
||||||
yield put(push(URLS.BASE));
|
yield put(push(URLS.BASE));
|
||||||
yield put(modalShowDialog(DIALOGS.LOGIN));
|
yield put(modalShowDialog(DIALOGS.LOGIN));
|
||||||
|
@ -26,15 +30,29 @@ export function* reqWrapper(requestAction, props = {}): ReturnType<typeof reques
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('reqWrapper will return');
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function* sendLoginRequestSaga({ username, password }: ReturnType<typeof ActionCreators.userSendLoginRequest>) {
|
function* sendLoginRequestSaga({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}: ReturnType<typeof ActionCreators.userSendLoginRequest>) {
|
||||||
if (!username || !password) return;
|
if (!username || !password) return;
|
||||||
|
|
||||||
const { error, data: { token, user } }: IResultWithStatus<{ token: string; user: IUser }> = yield call(apiUserLogin, { username, password });
|
const {
|
||||||
|
error,
|
||||||
|
data: { token, user },
|
||||||
|
}: IResultWithStatus<{ token: string; user: IUser }> = yield call(apiUserLogin, {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
if (error) { yield put(userSetLoginError(error)); return; }
|
if (error) {
|
||||||
|
yield put(userSetLoginError(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
yield put(authSetToken(token));
|
yield put(authSetToken(token));
|
||||||
yield put(authSetUser({ ...user, is_user: true }));
|
yield put(authSetUser({ ...user, is_user: true }));
|
||||||
|
|
|
@ -16,22 +16,8 @@ const INITIAL_STATE: INodeState = {
|
||||||
editor: {
|
editor: {
|
||||||
...EMPTY_NODE,
|
...EMPTY_NODE,
|
||||||
type: 'image',
|
type: 'image',
|
||||||
blocks: [
|
blocks: [{ ...EMPTY_BLOCK, type: 'image' }],
|
||||||
{ ...EMPTY_BLOCK, type: 'image' },
|
files: [],
|
||||||
],
|
|
||||||
files: [
|
|
||||||
{ ...EMPTY_FILE, id: uuid() },
|
|
||||||
{ ...EMPTY_FILE, id: uuid() },
|
|
||||||
{ ...EMPTY_FILE, id: uuid() },
|
|
||||||
{ ...EMPTY_FILE, id: uuid() },
|
|
||||||
{ ...EMPTY_FILE, id: uuid() },
|
|
||||||
{ ...EMPTY_FILE, id: uuid() },
|
|
||||||
{ ...EMPTY_FILE, id: uuid() },
|
|
||||||
{ ...EMPTY_FILE, id: uuid() },
|
|
||||||
{ ...EMPTY_FILE, id: uuid() },
|
|
||||||
{ ...EMPTY_FILE, id: uuid() },
|
|
||||||
{ ...EMPTY_FILE, id: uuid() },
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
|
|
@ -7,8 +7,8 @@ export interface ITag {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IInputTextProps = DetailedHTMLProps<
|
export type IInputTextProps = DetailedHTMLProps<
|
||||||
InputHTMLAttributes<HTMLInputElement>,
|
InputHTMLAttributes<HTMLInputElement>,
|
||||||
HTMLInputElement
|
HTMLInputElement
|
||||||
> & {
|
> & {
|
||||||
wrapperClassName?: string;
|
wrapperClassName?: string;
|
||||||
handler?: (value: string) => void;
|
handler?: (value: string) => void;
|
||||||
|
@ -46,6 +46,8 @@ export interface IResultWithStatus<T> {
|
||||||
|
|
||||||
export type UUID = string;
|
export type UUID = string;
|
||||||
|
|
||||||
|
export type IUploadType = 'image' | 'text' | 'audio' | 'video' | 'other';
|
||||||
|
|
||||||
export interface IFile {
|
export interface IFile {
|
||||||
id?: UUID;
|
id?: UUID;
|
||||||
temp_id?: UUID;
|
temp_id?: UUID;
|
||||||
|
@ -58,7 +60,7 @@ export interface IFile {
|
||||||
url: string;
|
url: string;
|
||||||
size: number;
|
size: number;
|
||||||
|
|
||||||
type: 'image' | 'text' | 'audio' | 'video';
|
type: IUploadType;
|
||||||
mime: string;
|
mime: string;
|
||||||
|
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
|
@ -68,7 +70,9 @@ export interface IFile {
|
||||||
export interface IFileWithUUID {
|
export interface IFileWithUUID {
|
||||||
temp_id?: UUID;
|
temp_id?: UUID;
|
||||||
file: File;
|
file: File;
|
||||||
subject: string;
|
subject?: string;
|
||||||
|
target: string;
|
||||||
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBlock {
|
export interface IBlock {
|
||||||
|
@ -107,3 +111,5 @@ export interface INode {
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IUploadProgressHandler = (current: number, total: number) => void;
|
||||||
|
|
|
@ -4,27 +4,27 @@ import { IUploadStatus } from './reducer';
|
||||||
|
|
||||||
export const uploadUploadFiles = (files: IFileWithUUID[]) => ({
|
export const uploadUploadFiles = (files: IFileWithUUID[]) => ({
|
||||||
files,
|
files,
|
||||||
type: UPLOAD_ACTIONS.UPLOAD_FILES
|
type: UPLOAD_ACTIONS.UPLOAD_FILES,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const uploadAddStatus = (temp_id: UUID, status?: Partial<IUploadStatus>) => ({
|
export const uploadAddStatus = (temp_id: UUID, status?: Partial<IUploadStatus>) => ({
|
||||||
temp_id,
|
temp_id,
|
||||||
status,
|
status,
|
||||||
type: UPLOAD_ACTIONS.ADD_STATUS
|
type: UPLOAD_ACTIONS.ADD_STATUS,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const uploadAddFile = (file: IFile) => ({
|
export const uploadAddFile = (file: IFile) => ({
|
||||||
file,
|
file,
|
||||||
type: UPLOAD_ACTIONS.ADD_FILE
|
type: UPLOAD_ACTIONS.ADD_FILE,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const uploadSetStatus = (temp_id: UUID, status?: Partial<IUploadStatus>) => ({
|
export const uploadSetStatus = (temp_id: UUID, status?: Partial<IUploadStatus>) => ({
|
||||||
temp_id,
|
temp_id,
|
||||||
status,
|
status,
|
||||||
type: UPLOAD_ACTIONS.SET_STATUS
|
type: UPLOAD_ACTIONS.SET_STATUS,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const uploadDropStatus = (temp_id: UUID) => ({
|
export const uploadDropStatus = (temp_id: UUID) => ({
|
||||||
temp_id,
|
temp_id,
|
||||||
type: UPLOAD_ACTIONS.DROP_STATUS
|
type: UPLOAD_ACTIONS.DROP_STATUS,
|
||||||
});
|
});
|
||||||
|
|
20
src/redux/uploads/api.ts
Normal file
20
src/redux/uploads/api.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import {
|
||||||
|
IResultWithStatus, IFile, IUploadProgressHandler, IFileWithUUID,
|
||||||
|
} from '~/redux/types';
|
||||||
|
import { api, configWithToken } from '~/utils/api';
|
||||||
|
import { API } from '~/constants/api';
|
||||||
|
|
||||||
|
export const postUploadFile = ({
|
||||||
|
access,
|
||||||
|
file,
|
||||||
|
target = 'others',
|
||||||
|
type = 'image',
|
||||||
|
}: IFileWithUUID & {
|
||||||
|
access: string;
|
||||||
|
onProgress: IUploadProgressHandler;
|
||||||
|
}): Promise<IResultWithStatus<IFile>> => {
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('file', file);
|
||||||
|
|
||||||
|
return api.post(API.USER.UPLOAD(target, type), data, configWithToken(access));
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import { IFile } from '~/redux/types';
|
import { IFile, IUploadType } from '~/redux/types';
|
||||||
import { IUploadState, IUploadStatus } from './reducer';
|
import { IUploadState, IUploadStatus } from './reducer';
|
||||||
|
|
||||||
const prefix = 'UPLOAD.';
|
const prefix = 'UPLOAD.';
|
||||||
|
@ -11,7 +11,7 @@ export const UPLOAD_ACTIONS = {
|
||||||
DROP_STATUS: `${prefix}DROP_STATUS`,
|
DROP_STATUS: `${prefix}DROP_STATUS`,
|
||||||
SET_STATUS: `${prefix}SET_STATUS`,
|
SET_STATUS: `${prefix}SET_STATUS`,
|
||||||
|
|
||||||
ADD_FILE: `${prefix}ADD_FILE`
|
ADD_FILE: `${prefix}ADD_FILE`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EMPTY_FILE: IFile = {
|
export const EMPTY_FILE: IFile = {
|
||||||
|
@ -25,7 +25,7 @@ export const EMPTY_FILE: IFile = {
|
||||||
url: 'https://cdn.arstechnica.net/wp-content/uploads/2017/09/mario-collage-800x450.jpg',
|
url: 'https://cdn.arstechnica.net/wp-content/uploads/2017/09/mario-collage-800x450.jpg',
|
||||||
size: 2400000,
|
size: 2400000,
|
||||||
type: 'image',
|
type: 'image',
|
||||||
mime: 'image/jpeg'
|
mime: 'image/jpeg',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EMPTY_UPLOAD_STATUS: IUploadStatus = {
|
export const EMPTY_UPLOAD_STATUS: IUploadStatus = {
|
||||||
|
@ -37,5 +37,26 @@ export const EMPTY_UPLOAD_STATUS: IUploadStatus = {
|
||||||
progress: 0,
|
progress: 0,
|
||||||
thumbnail_url: null,
|
thumbnail_url: null,
|
||||||
type: null,
|
type: null,
|
||||||
temp_id: null
|
temp_id: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// for targeted cancellation
|
||||||
|
export const UPLOAD_SUBJECTS = {
|
||||||
|
EDITOR: 'editor',
|
||||||
|
COMMENT: 'comment',
|
||||||
|
AVATAR: 'avatar',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UPLOAD_TARGETS = {
|
||||||
|
NODES: 'nodes',
|
||||||
|
COMMENTS: 'comments',
|
||||||
|
PROFILES: 'profiles',
|
||||||
|
OTHER: 'other',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UPLOAD_TYPES: Record<string, IUploadType> = {
|
||||||
|
IMAGE: 'image',
|
||||||
|
AUDIO: 'audio',
|
||||||
|
VIDEO: 'video',
|
||||||
|
OTHER: 'other',
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,16 +1,35 @@
|
||||||
import { takeEvery, all, spawn, call, put, take, fork, race } from 'redux-saga/effects';
|
import {
|
||||||
|
takeEvery, all, spawn, call, put, take, fork, race,
|
||||||
|
} from 'redux-saga/effects';
|
||||||
|
import { postUploadFile } from './api';
|
||||||
import { UPLOAD_ACTIONS } from '~/redux/uploads/constants';
|
import { UPLOAD_ACTIONS } from '~/redux/uploads/constants';
|
||||||
import {
|
import {
|
||||||
uploadUploadFiles, uploadSetStatus, uploadAddStatus, uploadDropStatus, uploadAddFile
|
uploadUploadFiles,
|
||||||
|
uploadSetStatus,
|
||||||
|
uploadAddStatus,
|
||||||
|
uploadDropStatus,
|
||||||
|
uploadAddFile,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import { reqWrapper } from '../auth/sagas';
|
import { reqWrapper } from '../auth/sagas';
|
||||||
import { createUploader, uploadGetThumb, fakeUploader } from '~/utils/uploader';
|
import { createUploader, uploadGetThumb } from '~/utils/uploader';
|
||||||
import { HTTP_RESPONSES } from '~/utils/api';
|
import { HTTP_RESPONSES } from '~/utils/api';
|
||||||
import { VALIDATORS } from '~/utils/validators';
|
import { VALIDATORS } from '~/utils/validators';
|
||||||
import { UUID, IFileWithUUID, IFile } from '../types';
|
import { IFileWithUUID, IFile, IUploadProgressHandler } from '../types';
|
||||||
|
|
||||||
function* uploadCall({ temp_id, onProgress, file }) {
|
function* uploadCall({
|
||||||
return yield call(reqWrapper, fakeUploader, { file: { url: 'some', error: 'cant do this boss' }, onProgress, mustSucceed: true });
|
file,
|
||||||
|
temp_id,
|
||||||
|
target,
|
||||||
|
type,
|
||||||
|
onProgress,
|
||||||
|
}: IFileWithUUID & { onProgress: IUploadProgressHandler }) {
|
||||||
|
return yield call(reqWrapper, postUploadFile, {
|
||||||
|
file,
|
||||||
|
temp_id,
|
||||||
|
type,
|
||||||
|
target,
|
||||||
|
onProgress,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function* onUploadProgress(chan) {
|
function* onUploadProgress(chan) {
|
||||||
|
@ -30,14 +49,27 @@ function* uploadCancelWorker(id) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function* uploadWorker(file: File, temp_id: UUID) {
|
function* uploadWorker({
|
||||||
const [promise, chan] = createUploader<{ temp_id; file }, { temp_id }>(uploadCall, { temp_id });
|
file, temp_id, target, type,
|
||||||
yield fork(onUploadProgress, chan);
|
}: IFileWithUUID) {
|
||||||
|
const [promise, chan] = createUploader<Partial<IFileWithUUID>, Partial<IFileWithUUID>>(
|
||||||
|
uploadCall,
|
||||||
|
{ temp_id, target, type }
|
||||||
|
);
|
||||||
|
|
||||||
return yield call(promise, { temp_id, file });
|
fork(onUploadProgress, chan);
|
||||||
|
|
||||||
|
return yield call(promise, {
|
||||||
|
temp_id,
|
||||||
|
file,
|
||||||
|
target,
|
||||||
|
type,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function* uploadFile({ file, temp_id }: IFileWithUUID) {
|
function* uploadFile({
|
||||||
|
file, temp_id, type, target, subject,
|
||||||
|
}: IFileWithUUID) {
|
||||||
if (!file.type || !VALIDATORS.IS_IMAGE_MIME(file.type)) {
|
if (!file.type || !VALIDATORS.IS_IMAGE_MIME(file.type)) {
|
||||||
return { error: 'File_Not_Image', status: HTTP_RESPONSES.BAD_REQUEST, data: {} };
|
return { error: 'File_Not_Image', status: HTTP_RESPONSES.BAD_REQUEST, data: {} };
|
||||||
}
|
}
|
||||||
|
@ -57,10 +89,13 @@ function* uploadFile({ file, temp_id }: IFileWithUUID) {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const { result, cancel, cancel_editing } = yield race({
|
||||||
result, cancel, cancel_editing, save_inventory,
|
result: call(uploadWorker, {
|
||||||
} = yield race({
|
file,
|
||||||
result: call(uploadWorker, file, temp_id),
|
temp_id,
|
||||||
|
target,
|
||||||
|
type,
|
||||||
|
}),
|
||||||
cancel: call(uploadCancelWorker, temp_id),
|
cancel: call(uploadCancelWorker, temp_id),
|
||||||
// subject_cancel: call(uploadSubjectCancelWorker, subject)
|
// subject_cancel: call(uploadSubjectCancelWorker, subject)
|
||||||
// add here CANCEL_UPLOADS worker, that will watch for subject
|
// add here CANCEL_UPLOADS worker, that will watch for subject
|
||||||
|
@ -68,7 +103,7 @@ function* uploadFile({ file, temp_id }: IFileWithUUID) {
|
||||||
// save_inventory: take(INVENTORY_ACTIONS.SAVE_INVENTORY),
|
// save_inventory: take(INVENTORY_ACTIONS.SAVE_INVENTORY),
|
||||||
}) as any;
|
}) as any;
|
||||||
|
|
||||||
if (cancel || cancel_editing || save_inventory) {
|
if (cancel || cancel_editing) {
|
||||||
return yield put(uploadDropStatus(temp_id));
|
return yield put(uploadDropStatus(temp_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,8 +115,6 @@ function* uploadFile({ file, temp_id }: IFileWithUUID) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('upload', data);
|
|
||||||
|
|
||||||
yield put(
|
yield put(
|
||||||
uploadSetStatus(temp_id, {
|
uploadSetStatus(temp_id, {
|
||||||
is_uploading: false,
|
is_uploading: false,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue