mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
comment form
This commit is contained in:
parent
9531edcd19
commit
1990783fa3
9 changed files with 67 additions and 33 deletions
|
@ -42,6 +42,11 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:global(.disabled) {
|
||||||
|
touch-action: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ import React, {
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
|
HTMLAttributes,
|
||||||
|
TextareaHTMLAttributes,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { getStyle } from '~/utils/dom';
|
import { getStyle } from '~/utils/dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
@ -13,7 +15,7 @@ import classNames from 'classnames';
|
||||||
import * as styles from '~/styles/inputs.scss';
|
import * as styles from '~/styles/inputs.scss';
|
||||||
import { Icon } from '../Icon';
|
import { Icon } from '../Icon';
|
||||||
|
|
||||||
interface IProps {
|
type IProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||||
value: string;
|
value: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
|
@ -24,7 +26,7 @@ interface IProps {
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
status?: 'error' | 'success' | '';
|
status?: 'error' | 'success' | '';
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const Textarea = memo<IProps>(
|
const Textarea = memo<IProps>(
|
||||||
({
|
({
|
||||||
|
@ -37,6 +39,7 @@ const Textarea = memo<IProps>(
|
||||||
required = false,
|
required = false,
|
||||||
title = '',
|
title = '',
|
||||||
status = '',
|
status = '',
|
||||||
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const [rows, setRows] = useState(minRows || 1);
|
const [rows, setRows] = useState(minRows || 1);
|
||||||
const [focused, setFocused] = useState(false);
|
const [focused, setFocused] = useState(false);
|
||||||
|
@ -99,6 +102,7 @@ const Textarea = memo<IProps>(
|
||||||
ref={textarea}
|
ref={textarea}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { FC, useCallback, useEffect } from 'react';
|
import React, { FC, useCallback, KeyboardEventHandler } from 'react';
|
||||||
import { Textarea } from '~/components/input/Textarea';
|
import { Textarea } from '~/components/input/Textarea';
|
||||||
import { CommentWrapper } from '~/components/containers/CommentWrapper';
|
import { CommentWrapper } from '~/components/containers/CommentWrapper';
|
||||||
import * as styles from './styles.scss';
|
import * as styles from './styles.scss';
|
||||||
|
@ -8,8 +8,9 @@ import assocPath from 'ramda/es/assocPath';
|
||||||
import { InputHandler, INode } from '~/redux/types';
|
import { InputHandler, INode } from '~/redux/types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import * as NODE_ACTIONS from '~/redux/node/actions';
|
import * as NODE_ACTIONS from '~/redux/node/actions';
|
||||||
import { store } from '~/redux/store';
|
|
||||||
import { selectNode } from '~/redux/node/selectors';
|
import { selectNode } from '~/redux/node/selectors';
|
||||||
|
import { LoaderCircle } from '~/components/input/LoaderCircle';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
|
||||||
const mapStateToProps = selectNode;
|
const mapStateToProps = selectNode;
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
@ -19,48 +20,63 @@ const mapDispatchToProps = {
|
||||||
|
|
||||||
type IProps = ReturnType<typeof mapStateToProps> &
|
type IProps = ReturnType<typeof mapStateToProps> &
|
||||||
typeof mapDispatchToProps & {
|
typeof mapDispatchToProps & {
|
||||||
id: INode['id'];
|
id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CommentFormUnconnected: FC<IProps> = ({
|
const CommentFormUnconnected: FC<IProps> = ({
|
||||||
nodePostComment,
|
nodePostComment,
|
||||||
nodeSetCommentData,
|
nodeSetCommentData,
|
||||||
comment_data,
|
comment_data,
|
||||||
|
is_sending_comment,
|
||||||
id,
|
id,
|
||||||
}) => {
|
}) => {
|
||||||
// const [data, setData] = useState<IComment>({ ...EMPTY_COMMENT });
|
// const [data, setData] = useState<IComment>({ ...EMPTY_COMMENT });
|
||||||
|
|
||||||
const onInput = useCallback<InputHandler>(
|
const onInput = useCallback<InputHandler>(
|
||||||
text => {
|
text => {
|
||||||
nodeSetCommentData(assocPath(['text'], text, comment_data));
|
nodeSetCommentData(id, assocPath(['text'], text, comment_data[id]));
|
||||||
},
|
},
|
||||||
[nodeSetCommentData, comment_data]
|
[nodeSetCommentData, comment_data, id]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
event => {
|
event => {
|
||||||
event.preventDefault();
|
if (event) event.preventDefault();
|
||||||
nodePostComment();
|
nodePostComment(id);
|
||||||
},
|
},
|
||||||
[nodePostComment]
|
[nodePostComment, id]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
|
||||||
store.subscribe(console.log);
|
({ ctrlKey, key }) => {
|
||||||
}, []);
|
if (!!ctrlKey && key === 'Enter') onSubmit(null);
|
||||||
|
},
|
||||||
|
[onSubmit]
|
||||||
|
);
|
||||||
|
|
||||||
|
const comment = comment_data[id];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommentWrapper>
|
<CommentWrapper>
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<div className={styles.input}>
|
<div className={styles.input}>
|
||||||
<Textarea value={comment_data.text} handler={onInput} />
|
<Textarea
|
||||||
|
value={comment.text}
|
||||||
|
handler={onInput}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
disabled={is_sending_comment}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.buttons}>
|
|
||||||
|
<Group horizontal className={styles.buttons}>
|
||||||
<Filler />
|
<Filler />
|
||||||
<Button size="mini" grey iconRight="enter">
|
|
||||||
|
{is_sending_comment && <LoaderCircle size={20} />}
|
||||||
|
|
||||||
|
<Button size="mini" grey iconRight="enter" disabled={is_sending_comment}>
|
||||||
Сказать
|
Сказать
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Group>
|
||||||
</form>
|
</form>
|
||||||
</CommentWrapper>
|
</CommentWrapper>
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,5 +15,9 @@
|
||||||
padding: $gap / 2;
|
padding: $gap / 2;
|
||||||
border-radius: 0 0 $radius $radius;
|
border-radius: 0 0 $radius $radius;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: transparentize(white, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
@include outer_shadow();
|
@include outer_shadow();
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ const NodeLayoutUnconnected: FC<IProps> = ({
|
||||||
<Padder>
|
<Padder>
|
||||||
<Group horizontal className={styles.content}>
|
<Group horizontal className={styles.content}>
|
||||||
<Group className={styles.comments}>
|
<Group className={styles.comments}>
|
||||||
<CommentForm id={node.id || null} />
|
<CommentForm id={0} />
|
||||||
|
|
||||||
{is_loading_comments || !comments.length ? (
|
{is_loading_comments || !comments.length ? (
|
||||||
<NodeNoComments is_loading={is_loading_comments} />
|
<NodeNoComments is_loading={is_loading_comments} />
|
||||||
|
|
|
@ -33,7 +33,8 @@ export const nodeSetCurrent = (current: INodeState['current']) => ({
|
||||||
type: NODE_ACTIONS.SET_CURRENT,
|
type: NODE_ACTIONS.SET_CURRENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const nodePostComment = () => ({
|
export const nodePostComment = (id: number) => ({
|
||||||
|
id,
|
||||||
type: NODE_ACTIONS.POST_COMMENT,
|
type: NODE_ACTIONS.POST_COMMENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -47,7 +48,8 @@ export const nodeSetComments = (comments: IComment[]) => ({
|
||||||
type: NODE_ACTIONS.SET_COMMENTS,
|
type: NODE_ACTIONS.SET_COMMENTS,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const nodeSetCommentData = (comment_data: IComment) => ({
|
export const nodeSetCommentData = (id: number, comment: IComment) => ({
|
||||||
comment_data,
|
id,
|
||||||
|
comment,
|
||||||
type: NODE_ACTIONS.SET_COMMENT_DATA,
|
type: NODE_ACTIONS.SET_COMMENT_DATA,
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,8 +35,8 @@ const setComments = (state: INodeState, { comments }: ReturnType<typeof nodeSetC
|
||||||
|
|
||||||
const setCommentData = (
|
const setCommentData = (
|
||||||
state: INodeState,
|
state: INodeState,
|
||||||
{ comment_data }: ReturnType<typeof nodeSetCommentData>
|
{ id, comment }: ReturnType<typeof nodeSetCommentData>
|
||||||
) => assocPath(['comment_data'], comment_data, state);
|
) => assocPath(['comment_data', id], comment, state);
|
||||||
|
|
||||||
export const NODE_HANDLERS = {
|
export const NODE_HANDLERS = {
|
||||||
[NODE_ACTIONS.SAVE]: setSaveErrors,
|
[NODE_ACTIONS.SAVE]: setSaveErrors,
|
||||||
|
|
|
@ -7,7 +7,7 @@ export type INodeState = Readonly<{
|
||||||
editor: INode;
|
editor: INode;
|
||||||
current: INode;
|
current: INode;
|
||||||
comments: IComment[];
|
comments: IComment[];
|
||||||
comment_data: IComment;
|
comment_data: Record<number, IComment>;
|
||||||
|
|
||||||
error: string;
|
error: string;
|
||||||
errors: Record<string, string>;
|
errors: Record<string, string>;
|
||||||
|
@ -25,7 +25,7 @@ const INITIAL_STATE: INodeState = {
|
||||||
files: [],
|
files: [],
|
||||||
},
|
},
|
||||||
current: { ...EMPTY_NODE },
|
current: { ...EMPTY_NODE },
|
||||||
comment_data: { ...EMPTY_COMMENT },
|
comment_data: { 0: { ...EMPTY_COMMENT } },
|
||||||
comments: [],
|
comments: [],
|
||||||
|
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
|
|
|
@ -47,6 +47,7 @@ function* onNodeLoad({ id, node_type }: ReturnType<typeof nodeLoadNode>) {
|
||||||
yield put(nodeSetLoading(true));
|
yield put(nodeSetLoading(true));
|
||||||
yield put(nodeSetLoadingComments(true));
|
yield put(nodeSetLoadingComments(true));
|
||||||
yield put(nodeSetSaveErrors({}));
|
yield put(nodeSetSaveErrors({}));
|
||||||
|
yield put(nodeSetCommentData(0, { ...EMPTY_COMMENT }));
|
||||||
|
|
||||||
if (node_type) yield put(nodeSetCurrent({ ...EMPTY_NODE, type: node_type }));
|
if (node_type) yield put(nodeSetCurrent({ ...EMPTY_NODE, type: node_type }));
|
||||||
|
|
||||||
|
@ -77,26 +78,28 @@ function* onNodeLoad({ id, node_type }: ReturnType<typeof nodeLoadNode>) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function* onPostComment() {
|
function* onPostComment({ id }: ReturnType<typeof nodePostComment>) {
|
||||||
const { current, comment_data } = yield select(selectNode);
|
const { current, comment_data } = yield select(selectNode);
|
||||||
|
|
||||||
yield put(nodeSetSendingComment(true));
|
yield put(nodeSetSendingComment(true));
|
||||||
const {
|
const {
|
||||||
data: { comment },
|
data: { comment, id: target_id },
|
||||||
error,
|
error,
|
||||||
} = yield call(reqWrapper, postNodeComment, { data: comment_data, id: current.id });
|
} = yield call(reqWrapper, postNodeComment, { data: comment_data[id], id: current.id });
|
||||||
yield put(nodeSetSendingComment(false));
|
yield put(nodeSetSendingComment(false));
|
||||||
|
|
||||||
if (error || !comment) {
|
if (error || !comment) {
|
||||||
return yield put(nodeSetSaveErrors({ error: error || ERRORS.EMPTY_RESPONSE }));
|
return yield put(nodeSetSaveErrors({ error: error || ERRORS.EMPTY_RESPONSE }));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log({ comment });
|
const { current: current_node } = yield select(selectNode);
|
||||||
|
|
||||||
const { comments } = yield select(selectNode);
|
if (current_node && current_node.id === current.id) {
|
||||||
|
// if user still browsing that node
|
||||||
yield put(nodeSetComments([comment, ...comments]));
|
const { comments } = yield select(selectNode);
|
||||||
yield put(nodeSetCommentData({ ...EMPTY_COMMENT }));
|
yield put(nodeSetComments([comment, ...comments]));
|
||||||
|
yield put(nodeSetCommentData(0, { ...EMPTY_COMMENT }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function* nodeSaga() {
|
export default function* nodeSaga() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue