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

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/redux/uploads/sagas.ts
This commit is contained in:
muerwre 2019-08-20 20:37:50 +07:00
commit 3d4f60c2b4
24 changed files with 292 additions and 156 deletions

View file

@ -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,
}, },
}; };

View file

@ -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}

View file

@ -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>
); );

View file

@ -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;
}
} }

View file

@ -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

View file

@ -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;
}

View file

@ -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 />

View file

@ -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 {

View file

@ -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>
); );

View file

@ -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 {

View file

@ -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
View 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',
};

View file

@ -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,

View file

@ -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} />

View file

@ -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' },
]} ]}
/> />

View file

@ -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>
); );

View file

@ -0,0 +1,6 @@
.wrap {
max-width: 2000px;
padding: 0 40px 40px 40px;
display: flex;
flex-direction: column;
}

View file

@ -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 }));

View file

@ -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,

View file

@ -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;

View file

@ -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
View 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));
};

View file

@ -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',
}; };

View file

@ -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,