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:
Riccardo Graziosi
2024-09-26 19:45:48 +02:00
committed by GitHub
parent 09d3e17c52
commit 20f93736f5
14 changed files with 114 additions and 42 deletions

View File

@@ -299,8 +299,8 @@ body {
}
.staffIcon {
font-size: 24px;
margin: 0 4px;
font-size: 22px;
margin: 0 0.5rem;
}
.poweredBy {

View File

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

View File

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

View File

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

View File

@@ -51,6 +51,7 @@ const NewComment = ({
<textarea
value={body}
onChange={handleChange}
autoFocus={parentId != null}
placeholder={I18n.t('post.new_comment.body_placeholder')}
className="commentForm"
/>

View File

@@ -65,6 +65,7 @@ const PostEditForm = ({
type="text"
value={title}
onChange={e => handleChangeTitle(e.target.value)}
autoFocus
className="postTitle form-control"
/>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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