mirror of
https://github.com/astuto/astuto.git
synced 2025-12-15 19:27:52 +01:00
Improve UI/UX of Post page (#416)
* Show post content and likes before fetching from backend API * Autofocus reply and edit forms for comments * Autofocus title field in post edit form * More UI/UX improvements
This commit is contained in:
committed by
GitHub
parent
09d3e17c52
commit
20f93736f5
@@ -299,8 +299,8 @@ body {
|
||||
}
|
||||
|
||||
.staffIcon {
|
||||
font-size: 24px;
|
||||
margin: 0 4px;
|
||||
font-size: 22px;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.poweredBy {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.commentsContainer {
|
||||
@extend .my-3;
|
||||
@extend .mt-2;
|
||||
|
||||
.commentForm {
|
||||
@extend
|
||||
@@ -20,7 +20,7 @@
|
||||
@extend
|
||||
.d-flex,
|
||||
.flex-column,
|
||||
.my-3;
|
||||
.mt-4;
|
||||
|
||||
.commentBodyForm {
|
||||
@extend .d-flex;
|
||||
@@ -71,19 +71,27 @@
|
||||
.text-secondary,
|
||||
.text-uppercase,
|
||||
.font-weight-lighter,
|
||||
.my-2;
|
||||
.mt-5,
|
||||
.mb-2;
|
||||
}
|
||||
|
||||
.commentList { @extend .mb-4; }
|
||||
|
||||
.commentList > .commentList {
|
||||
padding-left: 32px;
|
||||
}
|
||||
|
||||
.comment {
|
||||
@extend
|
||||
.my-4;
|
||||
.mb-2;
|
||||
|
||||
.commentHeader {
|
||||
@extend .titleText;
|
||||
@extend
|
||||
.d-flex,
|
||||
.align-items-end,
|
||||
.titleText;
|
||||
|
||||
height: 36px;
|
||||
|
||||
.commentAuthor {
|
||||
@extend .ml-2;
|
||||
|
||||
@@ -59,6 +59,7 @@ class CommentEditForm extends React.Component<Props, State> {
|
||||
value={body}
|
||||
onChange={e => this.handleCommentBodyChange(e.target.value)}
|
||||
rows={3}
|
||||
autoFocus
|
||||
className="commentForm"
|
||||
/>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import I18n from 'i18n-js';
|
||||
import NewComment from './NewComment';
|
||||
import CommentList from './CommentList';
|
||||
import Spinner from '../common/Spinner';
|
||||
import { DangerText } from '../common/CustomTexts';
|
||||
import { DangerText, MutedText } from '../common/CustomTexts';
|
||||
|
||||
import IComment from '../../interfaces/IComment';
|
||||
import { ReplyFormState } from '../../reducers/replyFormReducer';
|
||||
@@ -122,15 +122,16 @@ class CommentsP extends React.Component<Props> {
|
||||
userEmail={userEmail}
|
||||
/>
|
||||
|
||||
{ areLoading ? <Spinner /> : null }
|
||||
{ error ? <DangerText>{error}</DangerText> : null }
|
||||
|
||||
<div className="commentsTitle">
|
||||
{I18n.t('post.comments.title')}
|
||||
<Separator />
|
||||
{I18n.t('common.comments_number', { count: comments.length })}
|
||||
</div>
|
||||
|
||||
{ areLoading ? <Spinner /> : null }
|
||||
{ error ? <DangerText>{error}</DangerText> : null }
|
||||
{ comments.length === 0 && !areLoading && !error && <MutedText>{I18n.t('post.comments.empty')}</MutedText> }
|
||||
|
||||
<CommentList
|
||||
comments={comments}
|
||||
replyForms={replyForms}
|
||||
|
||||
@@ -51,6 +51,7 @@ const NewComment = ({
|
||||
<textarea
|
||||
value={body}
|
||||
onChange={handleChange}
|
||||
autoFocus={parentId != null}
|
||||
placeholder={I18n.t('post.new_comment.body_placeholder')}
|
||||
className="commentForm"
|
||||
/>
|
||||
|
||||
@@ -65,6 +65,7 @@ const PostEditForm = ({
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={e => handleChangeTitle(e.target.value)}
|
||||
autoFocus
|
||||
className="postTitle form-control"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import I18n from 'i18n-js';
|
||||
|
||||
import IPost, { POST_APPROVAL_STATUS_APPROVED, POST_APPROVAL_STATUS_PENDING } from '../../interfaces/IPost';
|
||||
import IPost, { POST_APPROVAL_STATUS_APPROVED, POST_APPROVAL_STATUS_PENDING, postJSON2JS } from '../../interfaces/IPost';
|
||||
import IPostStatus from '../../interfaces/IPostStatus';
|
||||
import IBoard from '../../interfaces/IBoard';
|
||||
import ITenantSetting from '../../interfaces/ITenantSetting';
|
||||
@@ -29,6 +29,7 @@ import HttpStatus from '../../constants/http_status';
|
||||
import ActionLink from '../common/ActionLink';
|
||||
import { EditIcon } from '../common/Icons';
|
||||
import Badge, { BADGE_TYPE_DANGER, BADGE_TYPE_WARNING } from '../common/Badge';
|
||||
import { likeJSON2JS } from '../../interfaces/ILike';
|
||||
|
||||
interface Props {
|
||||
postId: number;
|
||||
@@ -41,6 +42,7 @@ interface Props {
|
||||
postStatusChanges: PostStatusChangesState;
|
||||
boards: Array<IBoard>;
|
||||
postStatuses: Array<IPostStatus>;
|
||||
originPost: any;
|
||||
isLoggedIn: boolean;
|
||||
isPowerUser: boolean;
|
||||
currentUserFullName: string;
|
||||
@@ -48,7 +50,7 @@ interface Props {
|
||||
tenantSetting: ITenantSetting;
|
||||
authenticityToken: string;
|
||||
|
||||
requestPost(postId: number): void;
|
||||
requestPost(postId: number): Promise<any>;
|
||||
updatePost(
|
||||
postId: number,
|
||||
title: string,
|
||||
@@ -58,7 +60,7 @@ interface Props {
|
||||
authenticityToken: string,
|
||||
): Promise<any>;
|
||||
|
||||
requestLikes(postId: number): void;
|
||||
requestLikes(postId: number): Promise<any>;
|
||||
requestFollow(postId: number): void;
|
||||
requestPostStatusChanges(postId: number): void;
|
||||
|
||||
@@ -83,10 +85,20 @@ interface Props {
|
||||
): void;
|
||||
}
|
||||
|
||||
class PostP extends React.Component<Props> {
|
||||
interface State {
|
||||
postLoaded: boolean;
|
||||
likesLoaded: boolean;
|
||||
}
|
||||
|
||||
class PostP extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
postLoaded: false,
|
||||
likesLoaded: false,
|
||||
}
|
||||
|
||||
this._handleUpdatePost = this._handleUpdatePost.bind(this);
|
||||
this._handleDeletePost = this._handleDeletePost.bind(this);
|
||||
}
|
||||
@@ -94,8 +106,8 @@ class PostP extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { postId } = this.props;
|
||||
|
||||
this.props.requestPost(postId);
|
||||
this.props.requestLikes(postId);
|
||||
this.props.requestPost(postId).then(() => this.setState({ postLoaded: true }));
|
||||
this.props.requestLikes(postId).then(() => this.setState({ likesLoaded: true }));
|
||||
this.props.requestFollow(postId);
|
||||
this.props.requestPostStatusChanges(postId);
|
||||
}
|
||||
@@ -137,7 +149,10 @@ class PostP extends React.Component<Props> {
|
||||
this.props.deletePost(
|
||||
this.props.postId,
|
||||
this.props.authenticityToken
|
||||
).then(() => window.location.href = `/boards/${this.props.post.boardId}`);
|
||||
).then(() => {
|
||||
const board = this.props.boards.find(board => board.id === this.props.post.boardId);
|
||||
window.location.href = `/boards/${board.slug || board.id}`;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -151,6 +166,7 @@ class PostP extends React.Component<Props> {
|
||||
postStatusChanges,
|
||||
boards,
|
||||
postStatuses,
|
||||
originPost,
|
||||
|
||||
isLoggedIn,
|
||||
isPowerUser,
|
||||
@@ -166,6 +182,14 @@ class PostP extends React.Component<Props> {
|
||||
handleChangeEditFormPostStatus,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
postLoaded,
|
||||
likesLoaded,
|
||||
} = this.state;
|
||||
|
||||
const postToShow = postLoaded ? post : postJSON2JS(originPost.post);
|
||||
const likesToShow = likesLoaded ? likes : { items: originPost.likes.map(l => likeJSON2JS(l)), areLoading: false, error: null };
|
||||
|
||||
const postUpdates = [
|
||||
...comments.items.filter(comment => comment.isPostUpdate === true),
|
||||
...postStatusChanges.items,
|
||||
@@ -187,15 +211,15 @@ class PostP extends React.Component<Props> {
|
||||
{
|
||||
isPowerUser &&
|
||||
<LikeList
|
||||
likes={likes.items}
|
||||
areLoading={likes.areLoading}
|
||||
error={likes.error}
|
||||
likes={likesToShow.items}
|
||||
areLoading={likesToShow.areLoading}
|
||||
error={likesToShow.error}
|
||||
/>
|
||||
}
|
||||
|
||||
<ActionBox
|
||||
followed={followed}
|
||||
submitFollow={() => submitFollow(post.id, !followed, authenticityToken)}
|
||||
submitFollow={() => submitFollow(postToShow.id, !followed, authenticityToken)}
|
||||
|
||||
isLoggedIn={isLoggedIn}
|
||||
/>
|
||||
@@ -225,24 +249,24 @@ class PostP extends React.Component<Props> {
|
||||
<>
|
||||
<div className="postHeader">
|
||||
<LikeButton
|
||||
postId={post.id}
|
||||
likeCount={likes.items.length}
|
||||
postId={postToShow.id}
|
||||
likeCount={likesToShow.items.length}
|
||||
showLikeCount={isPowerUser || tenantSetting.show_vote_count}
|
||||
liked={likes.items.find(like => like.email === currentUserEmail) ? 1 : 0}
|
||||
liked={likesToShow.items.find(like => like.email === currentUserEmail) ? 1 : 0}
|
||||
size="large"
|
||||
isLoggedIn={isLoggedIn}
|
||||
authenticityToken={authenticityToken}
|
||||
/>
|
||||
|
||||
<h3>{post.title}</h3>
|
||||
<h3>{postToShow.title}</h3>
|
||||
</div>
|
||||
|
||||
<div className="postInfo">
|
||||
<PostBoardLabel
|
||||
{...boards.find(board => board.id === post.boardId)}
|
||||
{...boards.find(board => board.id === postToShow.boardId)}
|
||||
/>
|
||||
<PostStatusLabel
|
||||
{...postStatuses.find(postStatus => postStatus.id === post.postStatusId)}
|
||||
{...postStatuses.find(postStatus => postStatus.id === postToShow.postStatusId)}
|
||||
/>
|
||||
{ isPowerUser &&
|
||||
<ActionLink onClick={toggleEditMode} icon={<EditIcon />} customClass='editAction'>
|
||||
@@ -252,10 +276,10 @@ class PostP extends React.Component<Props> {
|
||||
</div>
|
||||
|
||||
{
|
||||
(isPowerUser && post.approvalStatus !== POST_APPROVAL_STATUS_APPROVED) &&
|
||||
(isPowerUser && postToShow.approvalStatus !== POST_APPROVAL_STATUS_APPROVED) &&
|
||||
<div className="postInfo">
|
||||
<Badge type={post.approvalStatus === POST_APPROVAL_STATUS_PENDING ? BADGE_TYPE_WARNING : BADGE_TYPE_DANGER}>
|
||||
{ I18n.t(`activerecord.attributes.post.approval_status_${post.approvalStatus.toLowerCase()}`) }
|
||||
<Badge type={postToShow.approvalStatus === POST_APPROVAL_STATUS_PENDING ? BADGE_TYPE_WARNING : BADGE_TYPE_DANGER}>
|
||||
{ I18n.t(`activerecord.attributes.post.approval_status_${postToShow.approvalStatus.toLowerCase()}`) }
|
||||
</Badge>
|
||||
</div>
|
||||
}
|
||||
@@ -265,17 +289,17 @@ class PostP extends React.Component<Props> {
|
||||
disallowedTypes={['heading', 'image', 'html']}
|
||||
unwrapDisallowed
|
||||
>
|
||||
{post.description}
|
||||
{postToShow.description}
|
||||
</ReactMarkdown>
|
||||
|
||||
<PostFooter
|
||||
createdAt={post.createdAt}
|
||||
createdAt={postToShow.createdAt}
|
||||
handleDeletePost={this._handleDeletePost}
|
||||
toggleEditMode={toggleEditMode}
|
||||
|
||||
isPowerUser={isPowerUser}
|
||||
authorEmail={post.userEmail}
|
||||
authorFullName={post.userFullName}
|
||||
authorEmail={postToShow.userEmail}
|
||||
authorFullName={postToShow.userFullName}
|
||||
currentUserEmail={currentUserEmail}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -33,7 +33,7 @@ const PostUpdateList = ({
|
||||
|
||||
<div className="postUpdateList">
|
||||
{
|
||||
postUpdates.length === 0 ?
|
||||
postUpdates.length === 0 && !areLoading && !error ?
|
||||
<CenteredMutedText>{I18n.t('post.updates_box.empty')}</CenteredMutedText>
|
||||
:
|
||||
null
|
||||
|
||||
@@ -16,6 +16,7 @@ interface Props {
|
||||
postId: number;
|
||||
boards: Array<IBoard>;
|
||||
postStatuses: Array<IPostStatus>;
|
||||
originPost: any;
|
||||
isLoggedIn: boolean;
|
||||
isPowerUser: boolean;
|
||||
currentUserFullName: string;
|
||||
@@ -38,6 +39,7 @@ class PostRoot extends React.Component<Props> {
|
||||
postId,
|
||||
boards,
|
||||
postStatuses,
|
||||
originPost,
|
||||
isLoggedIn,
|
||||
isPowerUser,
|
||||
currentUserFullName,
|
||||
@@ -52,6 +54,7 @@ class PostRoot extends React.Component<Props> {
|
||||
postId={postId}
|
||||
boards={boards}
|
||||
postStatuses={postStatuses}
|
||||
originPost={originPost}
|
||||
|
||||
isLoggedIn={isLoggedIn}
|
||||
isPowerUser={isPowerUser}
|
||||
|
||||
@@ -34,8 +34,8 @@ const mapStateToProps = (state: State) => ({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
requestPost(postId: number) {
|
||||
dispatch(requestPost(postId));
|
||||
requestPost(postId: number): Promise<any> {
|
||||
return dispatch(requestPost(postId));
|
||||
},
|
||||
|
||||
updatePost(
|
||||
@@ -69,8 +69,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(changePostEditFormPostStatus(postStatusId));
|
||||
},
|
||||
|
||||
requestLikes(postId: number) {
|
||||
dispatch(requestLikes(postId));
|
||||
requestLikes(postId: number): Promise<any> {
|
||||
return dispatch(requestLikes(postId));
|
||||
},
|
||||
|
||||
requestFollow(postId: number) {
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import ILikeJSON from "./json/ILike";
|
||||
|
||||
interface ILike {
|
||||
id: number;
|
||||
fullName: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export default ILike;
|
||||
export default ILike;
|
||||
|
||||
export const likeJSON2JS = (likeJSON: ILikeJSON): ILike => ({
|
||||
id: likeJSON.id,
|
||||
fullName: likeJSON.full_name,
|
||||
email: likeJSON.email,
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import IPostJSON from "./json/IPost";
|
||||
|
||||
// Approval status
|
||||
export const POST_APPROVAL_STATUS_APPROVED = 'approved';
|
||||
export const POST_APPROVAL_STATUS_PENDING = 'pending';
|
||||
@@ -26,4 +28,22 @@ interface IPost {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default IPost;
|
||||
export default IPost;
|
||||
|
||||
export const postJSON2JS = (postJSON: IPostJSON): IPost => ({
|
||||
id: postJSON.id,
|
||||
title: postJSON.title,
|
||||
slug: postJSON.slug,
|
||||
description: postJSON.description,
|
||||
approvalStatus: postJSON.approval_status,
|
||||
boardId: postJSON.board_id,
|
||||
postStatusId: postJSON.post_status_id,
|
||||
likeCount: postJSON.likes_count,
|
||||
liked: postJSON.liked,
|
||||
commentsCount: postJSON.comments_count,
|
||||
hotness: postJSON.hotness,
|
||||
userId: postJSON.user_id,
|
||||
userEmail: postJSON.user_email,
|
||||
userFullName: postJSON.user_full_name,
|
||||
createdAt: postJSON.created_at,
|
||||
});
|
||||
@@ -5,6 +5,10 @@
|
||||
postId: @post.id,
|
||||
boards: @boards,
|
||||
postStatuses: @post_statuses,
|
||||
originPost: {
|
||||
post: @post,
|
||||
likes: @post.likes.select(:id, :full_name, :email).left_outer_joins(:user),
|
||||
},
|
||||
isLoggedIn: user_signed_in?,
|
||||
isPowerUser: user_signed_in? ? current_user.moderator? : false,
|
||||
currentUserFullName: user_signed_in? ? current_user.full_name : nil,
|
||||
|
||||
@@ -158,6 +158,7 @@ en:
|
||||
not_following_description: "you won't receive notifications about this post"
|
||||
comments:
|
||||
title: 'Activity'
|
||||
empty: 'There are no comments yet'
|
||||
post_update_badge: 'Update'
|
||||
reply_button: 'Reply'
|
||||
new_comment:
|
||||
|
||||
Reference in New Issue
Block a user