1
0
Fork 0
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:
muerwre 2019-08-27 21:33:46 +07:00
parent 9531edcd19
commit 1990783fa3
9 changed files with 67 additions and 33 deletions

View file

@ -42,6 +42,11 @@
flex: 1; flex: 1;
} }
&:global(.disabled) {
touch-action: none;
pointer-events: none;
}
&:hover { &:hover {
opacity: 1; opacity: 1;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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