Basic discussions forum framework
Adds the basic structure for the Discussions MFE around which future development will happen.
This commit is contained in:
19
src/discussions/comments/CommentsView.jsx
Normal file
19
src/discussions/comments/CommentsView.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Comment, { commentShape } from './comment/Comment';
|
||||
|
||||
function CommentsView({ comments }) {
|
||||
return (
|
||||
<div className="discussion-comments d-flex flex-column">
|
||||
{
|
||||
comments.map(comment => <Comment comment={comment} key={comment.id} />)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CommentsView.propTypes = {
|
||||
comments: PropTypes.arrayOf(commentShape).isRequired,
|
||||
};
|
||||
|
||||
export default CommentsView;
|
||||
24
src/discussions/comments/CommentsViewContainer.jsx
Normal file
24
src/discussions/comments/CommentsViewContainer.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router';
|
||||
import CommentsView from './CommentsView';
|
||||
import { selectTopicComments } from './data/selectors';
|
||||
import { fetchTopicComments } from './data/thunks';
|
||||
|
||||
function CommentsViewContainer() {
|
||||
const { threadId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const comments = useSelector(selectTopicComments(threadId));
|
||||
useEffect(() => {
|
||||
// The courseId from the URL is the course we WANT to load.
|
||||
dispatch(fetchTopicComments(threadId));
|
||||
}, [threadId]);
|
||||
|
||||
return (
|
||||
<CommentsView comments={comments} />
|
||||
);
|
||||
}
|
||||
|
||||
CommentsViewContainer.propTypes = {};
|
||||
|
||||
export default CommentsViewContainer;
|
||||
56
src/discussions/comments/comment/Comment.jsx
Normal file
56
src/discussions/comments/comment/Comment.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import Button from '@edx/paragon/dist/Button';
|
||||
import { faFlag } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faEllipsisV, faStar } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import messages from './messages';
|
||||
|
||||
function Comment({ intl, comment }) {
|
||||
return (
|
||||
<div className="discussion-comment d-flex flex-column m-2 card">
|
||||
<div className="header d-flex m-1 card-header">
|
||||
<div className="avatar">
|
||||
[A]
|
||||
</div>
|
||||
<div className="d-flex flex-column m-1">
|
||||
<div className="title">
|
||||
{/* TODO: Get title from thread */ }
|
||||
Some title
|
||||
</div>
|
||||
<div className="status">
|
||||
{/* TODO: get type from thread */ }
|
||||
discussion posted about { comment.posted_on } by { comment.author }
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex icons m-1">
|
||||
<FontAwesomeIcon icon={faStar} />
|
||||
{ comment.abuse_flagged && <FontAwesomeIcon icon={faFlag} /> }
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="comment-body d-flex" dangerouslySetInnerHTML={{ __html: comment.rendered_body }} />
|
||||
<div className="visibility-comment d-flex">
|
||||
{/* TODO: Add parent group info */ }
|
||||
</div>
|
||||
<div className="actions d-flex">
|
||||
<Button>{ intl.formatMessage(messages.add_response) }</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const commentShape = PropTypes.shape({
|
||||
posted_on: PropTypes.string,
|
||||
abuse_flagged: PropTypes.bool,
|
||||
rendered_body: PropTypes.string,
|
||||
author: PropTypes.string,
|
||||
});
|
||||
|
||||
Comment.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
comment: commentShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(Comment);
|
||||
11
src/discussions/comments/comment/messages.js
Normal file
11
src/discussions/comments/comment/messages.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
add_response: {
|
||||
id: 'discussions.comments.comment.add-response',
|
||||
defaultMessage: 'Add a response',
|
||||
description: 'Button to add a response in a thread of forum posts',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
31
src/discussions/comments/data/api.js
Normal file
31
src/discussions/comments/data/api.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { API_BASE_URL } from '../../../data/constants';
|
||||
|
||||
export async function getThreadComments(
|
||||
threadId, {
|
||||
commentId, page, pageSize, requestedFields,
|
||||
} = {},
|
||||
) {
|
||||
const url = new URL(`${API_BASE_URL}/api/discussion/v1/comments/`);
|
||||
const paramsMap = {
|
||||
thread_id: threadId,
|
||||
comment_id: commentId,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
requested_fields: requestedFields,
|
||||
};
|
||||
Object.keys(paramsMap)
|
||||
.forEach(
|
||||
(param) => {
|
||||
const paramValue = paramsMap[param];
|
||||
if (paramValue) {
|
||||
url.searchParams.append(param, paramValue);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(url);
|
||||
return data;
|
||||
}
|
||||
1
src/discussions/comments/data/index.js
Normal file
1
src/discussions/comments/data/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './slices';
|
||||
4
src/discussions/comments/data/selectors.js
Normal file
4
src/discussions/comments/data/selectors.js
Normal file
@@ -0,0 +1,4 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export const selectTopicComments = topicId => state => state.comments.comments[topicId] || [];
|
||||
|
||||
export const courseTopicsStatus = state => state.comments.status;
|
||||
43
src/discussions/comments/data/slices.js
Normal file
43
src/discussions/comments/data/slices.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/* eslint-disable no-param-reassign,import/prefer-default-export */
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { LoadingStatus } from '../../../data/constants';
|
||||
|
||||
const commentsSlice = createSlice({
|
||||
name: 'comments',
|
||||
initialState: {
|
||||
status: LoadingStatus.LOADING,
|
||||
page: null,
|
||||
comments: {
|
||||
// Map thread ids to comments
|
||||
},
|
||||
totalPages: null,
|
||||
totalThreads: null,
|
||||
},
|
||||
reducers: {
|
||||
fetchCommentsRequest: (state) => {
|
||||
state.status = LoadingStatus.LOADING;
|
||||
},
|
||||
fetchCommentsSuccess: (state, { payload }) => {
|
||||
const { data, topicId } = payload;
|
||||
state.status = LoadingStatus.LOADED;
|
||||
state.comments[topicId] = data.results;
|
||||
state.page = data.pagination.page;
|
||||
state.totalPages = data.pagination.num_pages;
|
||||
state.totalThreads = data.pagination.count;
|
||||
},
|
||||
fetchCommentsFailed: (state) => {
|
||||
state.status = LoadingStatus.FAILED;
|
||||
},
|
||||
fetchCommentsDenied: (state) => {
|
||||
state.status = LoadingStatus.DENIED;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
fetchCommentsRequest,
|
||||
fetchCommentsSuccess,
|
||||
fetchCommentsFailed,
|
||||
} = commentsSlice.actions;
|
||||
|
||||
export const commentsReducer = commentsSlice.reducer;
|
||||
17
src/discussions/comments/data/thunks.js
Normal file
17
src/discussions/comments/data/thunks.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { getThreadComments } from './api';
|
||||
import { fetchCommentsFailed, fetchCommentsRequest, fetchCommentsSuccess } from './slices';
|
||||
|
||||
export function fetchTopicComments(topicId) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
dispatch(fetchCommentsRequest({ topicId }));
|
||||
const data = await getThreadComments(topicId);
|
||||
dispatch(fetchCommentsSuccess({ topicId, data }));
|
||||
} catch (error) {
|
||||
dispatch(fetchCommentsFailed());
|
||||
logError(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
2
src/discussions/comments/index.js
Normal file
2
src/discussions/comments/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as CommentsViewContainer } from './CommentsViewContainer';
|
||||
Reference in New Issue
Block a user