fix: Bring UI closer to mockups

This makes many tweaks to get the UI closer to
the mockup and using Paragon components as much as possible.

Related tickets:
* [TNL-8427](https://openedx.atlassian.net/browse/TNL-8427)
* [BB-4417](https://tasks.opencraft.com/browse/BB-4417)
This commit is contained in:
João Cabrita
2021-07-20 16:39:49 +01:00
committed by Mehak Nasir
parent 80ad9f1dc2
commit 7138e63cf0
15 changed files with 313 additions and 293 deletions

View File

@@ -31,9 +31,13 @@ function CommentsView({ intl }) {
<div className="mb-2">
<div className="list-group list-group-flush">
<Post post={thread} />
{
comments.map(reply => <Reply reply={reply} key={reply.id} />)
}
<div className="list-group">
{comments.map(reply => (
<div key={reply.id} className="list-group-item list-group-item-action">
<Reply reply={reply} />
</div>
))}
</div>
</div>
</div>
<div className="actions d-flex">

View File

@@ -1,48 +1,65 @@
import React from 'react';
import PropTypes from 'prop-types';
import { faFlag, faStar as faEmptyStar } from '@fortawesome/free-regular-svg-icons';
import { faEllipsisV, faStar as faSolidStar } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, Icon } from '@edx/paragon';
import {
Flag,
StarFilled,
StarOutline,
} from '@edx/paragon/icons';
import LikeButton from '../../posts/post/LikeButton';
import { Button } from '@edx/paragon';
function CommentIcons({ abuseFlagged, following }) {
function CommentIcons(
{
abuseFlagged,
count,
following,
onLike,
voted,
},
) {
return (
<div className="d-flex flex-column icons">
<LikeButton
count={count}
onClick={() => onLike && onLike()}
voted={voted}
/>
{/* Only show the star if the comment has a following attribute, indicating it can be followed */}
{ following !== undefined && (
following
? (
<Button variant="link" className="p-0" size="xs">
<FontAwesomeIcon icon={faSolidStar} />
<Icon src={StarFilled} />
</Button>
) : (
<Button variant="link" className="p-0" size="xs">
<FontAwesomeIcon icon={faEmptyStar} />
<Icon src={StarOutline} />
</Button>
)
)}
{ abuseFlagged && (
{abuseFlagged && (
<Button variant="link" className="p-0" size="xs">
<FontAwesomeIcon icon={faFlag} />
<Icon src={Flag} />
</Button>
) }
<Button variant="link" className="p-0" size="xs">
<FontAwesomeIcon icon={faEllipsisV} />
</Button>
)}
</div>
);
}
CommentIcons.propTypes = {
abuseFlagged: PropTypes.bool,
count: PropTypes.number.isRequired,
following: PropTypes.bool,
onLike: PropTypes.func,
voted: PropTypes.bool,
};
CommentIcons.defaultProps = {
abuseFlagged: undefined,
following: undefined,
onLike: undefined,
voted: false,
};
export default CommentIcons;

View File

@@ -24,7 +24,7 @@ const messages = defineMessages({
},
replyTime: {
id: 'discussions.comments.comment.repliedTime',
defaultMessage: 'posted {relativeTime}',
defaultMessage: 'Posted {relativeTime}',
description: 'Message about hwo long ago a reply was posted. Appears as "username posted 7 minutes ago"',
},
});

View File

@@ -4,13 +4,51 @@ import { useDispatch, useSelector } from 'react-redux';
import * as timeago from 'timeago.js';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Avatar, Icon, IconButton } from '@edx/paragon';
import { MoreVert } from '@edx/paragon/icons';
import { commentShape } from '../comment/Comment';
import CommentIcons from '../comment-icons/CommentIcons';
import { selectCommentResponses } from '../data/selectors';
import { fetchCommentResponses } from '../data/thunks';
import { editComment, fetchCommentResponses } from '../data/thunks';
import messages from '../messages';
function ReplyHeader({ reply, intl }) {
return (
<div className="d-flex flex-row justify-content-between">
<div className="align-items-center d-flex flex-row">
<Avatar className="m-2" alt={reply.author} src={reply.users[reply.author].profile.image.image_url_small} />
<div className="status small">
<a href="#nowhere">
<h1 className="font-weight-normal text-info-300 mr-1 small">
{reply.author}
</h1>
</a>
<h2 className="font-weight-normal small text-gray-500" title={reply.createdAt}>
{intl.formatMessage(messages.replyTime, {
relativeTime: timeago.format(reply.createdAt, intl.locale),
})}
</h2>
</div>
</div>
<IconButton
alt="Options"
iconAs={Icon}
// TODO: Implement overlay
onClick={() => { }}
size="sm"
src={MoreVert}
variant="primary"
/>
</div>
);
}
ReplyHeader.propTypes = {
reply: commentShape.isRequired,
intl: intlShape.isRequired,
};
function Reply({
reply,
intl,
@@ -26,22 +64,18 @@ function Reply({
}, [reply.id]);
return (
<div className="discussion-reply d-flex flex-column mt-2">
<div className="d-flex flex-row">
<div className="d-flex flex-column flex-fill">
<div className="status small">
<span className="font-weight-bold text-info-300 mr-1">
{reply.author}
</span>
<span title={reply.createdAt}>
{intl.formatMessage(messages.replyTime, {
relativeTime: timeago.format(reply.createdAt, intl.locale),
})}
</span>
</div>
<div className="reply-body d-flex" dangerouslySetInnerHTML={{ __html: reply.renderedBody }} />
</div>
<CommentIcons abuseFlagged={reply.abuseFlagged} following={reply.following} />
<div className="bt-1 discussion-reply d-flex flex-column my-2">
<div>
<ReplyHeader reply={reply} intl={intl} />
<div className="reply-body d-flex px-2" dangerouslySetInnerHTML={{ __html: reply.renderedBody }} />
<CommentIcons
abuseFlagged={reply.abuseFlagged}
count={reply.voteCount}
following={reply.following}
// FIXME: this API call fails
onLike={() => dispatch(editComment(reply.id, { voted: !reply.voted }))}
voted={reply.voted}
/>
</div>
<div className="ml-4">
{/* Pass along intl since the `Reply` component used here is the one before it's injected with `injectIntl` */}

View File

@@ -12,7 +12,7 @@ import { TopicsView } from '../topics';
export default function DiscussionsHome() {
return (
<main className="container my-4 d-flex flex-row">
<div className="d-flex flex-column w-50 mr-1 card">
<div className="d-flex flex-column w-50 mr-1">
<Route path={Routes.DISCUSSIONS.PATH} component={NavigationBar} />
<Route
path={[
@@ -21,14 +21,16 @@ export default function DiscussionsHome() {
]}
component={BreadcrumbMenu}
/>
<Switch>
<Route path={Routes.POSTS.MY_POSTS}>
<PostsView showOwnPosts />
</Route>
<Route path={Routes.POSTS.ALL_POSTS} component={PostsView} />
<Route path={Routes.POSTS.PATH} component={PostsView} />
<Route path={Routes.TOPICS.PATH} component={TopicsView} />
</Switch>
<div className="card">
<Switch>
<Route path={Routes.POSTS.MY_POSTS}>
<PostsView showOwnPosts />
</Route>
<Route path={Routes.POSTS.ALL_POSTS} component={PostsView} />
<Route path={Routes.POSTS.PATH} component={PostsView} />
<Route path={Routes.TOPICS.PATH} component={TopicsView} />
</Switch>
</div>
</div>
<div className="d-flex w-50 pl-1 flex-column">
<PostActionsBar />

View File

@@ -28,7 +28,7 @@ function NavigationBar({ intl }) {
];
return (
<Nav variant="pills" className="border-bottom py-2">
<Nav variant="pills" className="py-2">
{navLinks.map(link => (
<Nav.Item key={link.label}>
<Nav.Link as={NavLink} to={link.url}>

View File

@@ -1,11 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { faSort } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useDispatch, useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Icon } from '@edx/paragon';
import { ArrowDropUpDown } from '@edx/paragon/icons';
import { SelectableDropdown } from '../../../components';
import {
@@ -71,7 +71,7 @@ function PostFilterBar({
options={postOrderingOptions}
onChange={(sortBy) => dispatch(setSortedBy(sortBy.value))}
label={
<FontAwesomeIcon icon={faSort} aria-label="Sort" title="Sort" />
<Icon src={ArrowDropUpDown} aria-label="Sort" title="Sort" />
}
/>
</div>

View File

@@ -0,0 +1,61 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Icon, IconButton, OverlayTrigger, Tooltip,
} from '@edx/paragon';
import { ThumbUpFilled, ThumbUpOutline } from '@edx/paragon/icons';
import messages from './messages';
function LikeButton(
{
count,
intl,
onClick,
voted,
},
) {
const handleClick = (e) => {
e.preventDefault();
if (onClick) {
onClick();
}
return false;
};
return (
<div className="d-flex align-items-center mx-2.5">
<OverlayTrigger
overlay={(
<Tooltip>
{intl.formatMessage(voted ? messages.removeLike : messages.like)}
</Tooltip>
)}
>
<IconButton
onClick={handleClick}
alt="Like"
iconAs={Icon}
size="inline"
src={voted ? ThumbUpFilled : ThumbUpOutline}
/>
</OverlayTrigger>
{count}
</div>
);
}
LikeButton.propTypes = {
count: PropTypes.number.isRequired,
intl: intlShape.isRequired,
onClick: PropTypes.func,
voted: PropTypes.bool,
};
LikeButton.defaultProps = {
voted: false,
onClick: undefined,
};
export default injectIntl(LikeButton);

View File

@@ -1,28 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
faQuestionCircle,
faStar as faEmptyStar,
faThumbsUp as faEmptyThumb,
} from '@fortawesome/free-regular-svg-icons';
import {
faComments,
faStar as faSolidStar,
faThumbsUp as faSolidThumb,
faThumbtack,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useDispatch } from 'react-redux';
import * as timeago from 'timeago.js';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Avatar, IconButton, OverlayTrigger, Tooltip,
Avatar, Icon, IconButton, OverlayTrigger, Tooltip,
} from '@edx/paragon';
import {
Pin,
QuestionAnswer,
StarFilled,
StarOutline,
} from '@edx/paragon/icons';
import { updateExistingThread } from '../data/thunks';
import messages from './messages';
import LikeButton from './LikeButton';
export const postShape = PropTypes.shape({
abuseFlagged: PropTypes.bool,
@@ -43,18 +38,11 @@ export const postShape = PropTypes.shape({
function PostTypeIcon(props) {
return (
<div className="m-1">
{props.type === 'question' && <FontAwesomeIcon icon={faQuestionCircle} size="lg" />}
{props.type === 'discussion' && <FontAwesomeIcon icon={faComments} size="lg" />}
{props.type === 'question' && <Icon src={QuestionAnswer} size="lg" />}
{props.type === 'discussion' && <Icon src={QuestionAnswer} size="lg" />}
{props.pinned && (
<FontAwesomeIcon
icon={faThumbtack}
size="sm"
className="position-relative bg-white rounded"
style={{
left: '-0.25rem',
bottom: '-0.75rem',
padding: '0.125rem',
}}
<Icon
src={Pin}
/>
)}
</div>
@@ -71,27 +59,32 @@ function PostHeader({
post,
}) {
return (
<div className="d-flex flex-fill">
<Avatar className="my-1" alt={post.author} src={post.authorAvatars.imageUrlSmall} />
<div className="d-flex flex-row flex-fill">
<PostTypeIcon type={post.type} pinned={post.pinned} />
<div className="d-flex flex-fill justify-content-between">
<Avatar className="m-2" alt={post.author} src={post.authorAvatars.imageUrlSmall} />
<PostTypeIcon type={post.type} pinned={post.pinned} />
<div className="align-items-center d-flex flex-row flex-fill">
<div className="d-flex flex-column flex-fill">
<span className="d-flex font-weight-bold text-gray">
<span className="d-flex font-weight-bold">
{post.title}
</span>
<span className="d-flex small text-gray-300">
{post.author}{' | '}
<span className="d-flex text-gray-500 x-small">
<span title={post.createdAt}>
{intl.formatMessage(messages.postedOn, { time: timeago.format(post.createdAt, intl.locale) })}
{intl.formatMessage(
messages.postedOn,
{
author: post.author,
time: timeago.format(post.createdAt, intl.locale),
},
)}
</span>
</span>
</div>
<div className="d-flex align-items-center mr-3">
<FontAwesomeIcon icon={faComments} className="mr-2" />
<span style={{ minWidth: '2rem' }}>
{post.commentCount}
</span>
</div>
</div>
<div className="d-flex mr-3">
<Icon src={QuestionAnswer} />
<span style={{ minWidth: '2rem' }}>
{post.commentCount}
</span>
</div>
</div>
);
@@ -109,35 +102,22 @@ function Post({
const dispatch = useDispatch();
return (
<div className="d-flex flex-column mt-2">
<div className="d-flex flex-column p-2.5 w-100">
<PostHeader post={post} intl={intl} />
<div className="d-flex mt-2 mb-0 p-0" dangerouslySetInnerHTML={{ __html: post.renderedBody }} />
<div className="d-flex align-items-center">
<div className="d-flex align-items-center">
<OverlayTrigger overlay={(
<LikeButton
count={post.voteCount}
onClick={() => dispatch(updateExistingThread(post.id, { voted: !post.voted }))}
voted={post.voted}
/>
<OverlayTrigger
className="mx-2.5"
overlay={(
<Tooltip>
{intl.formatMessage(post.voted ? messages.removeLike : messages.like)}
{intl.formatMessage(post.following ? messages.unfollow : messages.follow)}
</Tooltip>
)}
>
<IconButton
onClick={(e) => {
e.preventDefault();
dispatch(updateExistingThread(post.id, { voted: !post.voted }));
return false;
}}
alt="Like"
size="inline"
icon={post.voted ? faSolidThumb : faEmptyThumb}
/>
</OverlayTrigger>
{post.voteCount}
</div>
<OverlayTrigger overlay={(
<Tooltip>
{intl.formatMessage(post.following ? messages.unfollow : messages.follow)}
</Tooltip>
)}
>
<IconButton
onClick={() => {
@@ -145,8 +125,9 @@ function Post({
return true;
}}
alt="Follow"
iconAs={Icon}
size="inline"
icon={post.following ? faSolidStar : faEmptyStar}
src={post.following ? StarFilled : StarOutline}
/>
</OverlayTrigger>
</div>

View File

@@ -1,10 +1,10 @@
import React from 'react';
import { faCircle, faFlag } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Icon } from '@edx/paragon';
import { Flag, Unread } from '@edx/paragon/icons';
import { Routes } from '../../../data/constants';
import messages from './messages';
@@ -24,13 +24,13 @@ function PostLink({
}
>
{post.abuseFlagged && (
<div className="bg-gray-100 flex-fill">
<FontAwesomeIcon icon={faFlag} className="mx-1" />
{intl.formatMessage(messages.contentReported)}
<div className="align-items-center bg-danger-100 d-flex flex-fill p-1">
<Icon className="text-danger-700" src={Flag} />
<span className="text-gray-700 x-small">{intl.formatMessage(messages.contentReported)}</span>
</div>
)}
<div className="d-flex flex-row mb-2">
<FontAwesomeIcon icon={faCircle} className={`my-1 text-accent-a ${post.read && 'invisible'}`} size="xs" />
<div className="d-flex flex-row p-2">
{!post.read && <Icon className="text-brand-500" src={Unread} />}
<Post post={post} />
</div>
</Link>

View File

@@ -7,7 +7,7 @@ const messages = defineMessages({
},
postedOn: {
id: 'discussions.post.posted-on',
defaultMessage: 'Posted {time}',
defaultMessage: 'Posted {time} by {author}',
},
contentReported: {
id: 'discussions.post.content-reported',

View File

@@ -2,9 +2,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { faQuestionCircle } from '@fortawesome/free-regular-svg-icons';
import { faComments, faFlag } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Icon } from '@edx/paragon';
import { Flag, Help, QuestionAnswer } from '@edx/paragon/icons';
import { useParams } from 'react-router';
import { Link } from 'react-router-dom';
@@ -33,16 +32,16 @@ function Topic({
</div>
<div className="d-flex text-gray-300 h4 mt-1 mb-0">
<div className="badge mr-4">
<FontAwesomeIcon className="mr-2" icon={faQuestionCircle} />
<Icon className="mr-2" src={Help} />
{questions}
</div>
<div className="badge mr-4">
<FontAwesomeIcon className="mr-2" icon={faComments} />
<Icon className="mr-2" src={QuestionAnswer} />
{discussions}
</div>
{flags !== null && (
<div className="badge">
<FontAwesomeIcon className="mr-2" icon={faFlag} />
<Icon className="mr-2" src={Flag} />
{flags}
</div>
)}

View File

@@ -1,11 +1,10 @@
import React from 'react';
import { faSort } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useDispatch } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { SearchField } from '@edx/paragon';
import { Icon, SearchField } from '@edx/paragon';
import { ArrowDropUpDown } from '@edx/paragon/icons';
import { SelectableDropdown } from '../../../components';
import { TopicOrdering } from '../../../data/constants';
@@ -27,9 +26,7 @@ function TopicSearchBar({ intl }) {
<SelectableDropdown
options={topicOrderingOptions}
onChange={(option) => dispatch(setSortBy(option.value))}
label={
<FontAwesomeIcon icon={faSort} />
}
label={<Icon src={ArrowDropUpDown} />}
/>
</div>
);