Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 155 additions & 16 deletions assets/src/js/frontend/dashboard/pages/discussions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ interface ReplyCommentPayload {
comment_parent: number;
comment: string;
}

interface DeleteCommentPayload {
comment_id: number;
is_reply?: boolean;
}

interface QnASingleActionPayload {
question_id: number;
qna_action: string;
Expand All @@ -20,17 +26,41 @@ interface ReplyQnAPayload {
answer: string;
}

const FORM_ID_PREFIXES = {
COMMENT_EDIT: 'lesson-comment-edit-',
COMMENT_REPLY: 'lesson-comment-reply-form-',
};

const MODALS = {
COMMENT_DELETE: 'tutor-comment-delete-modal',
QNA_DELETE: 'tutor-qna-delete-modal',
};

const ELEMENT_IDS = {
COMMENT_TEXT_PREFIX: 'tutor-lesson-comment-text-',
REPLIES_LIST_CONTAINER: 'tutor-discussion-replies-list',
};

const URL_PARAMS = {
ID: 'id',
ORDER: 'order',
};

/**
* Discussions Page Component
* Handles Q&A, Lesson comments related actions
*/
const discussionsPage = () => {
const query = window.TutorCore.query;
const form = window.TutorCore.form;
const toast = window.TutorCore.toast;
const modal = window.TutorCore.modal;

return {
query,
deleteCommentMutation: null as MutationState<unknown, unknown> | null,
deleteCommentMutation: null as MutationState<unknown, DeleteCommentPayload> | null,
replyCommentMutation: null as MutationState<unknown, ReplyCommentPayload> | null,
editCommentMutation: null as MutationState<unknown, { comment_id: number; comment: string }> | null,
qnaSingleActionMutation: null as MutationState<unknown, unknown> | null,
deleteQnAMutation: null as MutationState<unknown, unknown> | null,
replyQnAMutation: null as MutationState<unknown, unknown> | null,
Expand All @@ -39,28 +69,70 @@ const discussionsPage = () => {
isSolved: false,
isImportant: false,
isArchived: false,
editingId: null as number | null,
editingFormId: null as string | null,
loadingReplies: false,
repliesOrder: 'DESC',
$nextTick: undefined as ((callback: () => void) => void) | undefined,

init() {
// Set initial state from URL
const url = new URL(window.location.href);
this.repliesOrder = url.searchParams.get(URL_PARAMS.ORDER) || 'DESC';

// Lesson comment delete mutation.
this.deleteCommentMutation = this.query.useMutation(this.deleteComment, {
onSuccess: () => {
const url = new URL(window.location.href);
url.searchParams.delete('id');
window.location.href = url.toString();
onSuccess: (_, payload) => {
if (payload.is_reply) {
toast.success(__('Reply deleted successfully', 'tutor'));
modal.closeModal(MODALS.COMMENT_DELETE);
this.reloadReplies();
} else {
const url = new URL(window.location.href);
url.searchParams.delete(URL_PARAMS.ID);
window.location.href = url.toString();
}
},
onError: (error: Error) => {
window.TutorCore.toast.error(error.message || __('Failed to delete Comment', 'tutor'));
toast.error(error.message || __('Failed to delete Comment', 'tutor'));
},
});

// Lesson comment reply mutation
this.replyCommentMutation = this.query.useMutation(this.replyComment, {
onSuccess: () => {
window.TutorCore.toast.success(__('Reply saved successfully', 'tutor'));
window.location.reload();
onSuccess: (_, payload) => {
toast.success(__('Reply saved successfully', 'tutor'));
this.reloadReplies();

const formId = `${FORM_ID_PREFIXES.COMMENT_REPLY}${payload.comment_parent}`;
if (form.hasForm(formId)) {
form.reset(formId);
}
},
onError: (error: Error) => {
window.TutorCore.toast.error(error.message || __('Failed to save reply', 'tutor'));
toast.error(error.message || __('Failed to save reply', 'tutor'));
},
});

// Lesson comment edit mutation.
this.editCommentMutation = this.query.useMutation(this.updateComment, {
onSuccess: (_, payload) => {
toast.success(__('Comment updated successfully.', 'tutor'));

const element = document.getElementById(`${ELEMENT_IDS.COMMENT_TEXT_PREFIX}${payload.comment_id}`);
if (element) {
element.innerHTML = payload.comment;
}

if (this.editingFormId && form.hasForm(this.editingFormId)) {
form.reset(this.editingFormId);
this.editingFormId = null;
}

this.editingId = null;
},
onError: (error: Error) => {
toast.error(error.message || __('Failed to update comment', 'tutor'));
},
});

Expand All @@ -83,42 +155,80 @@ const discussionsPage = () => {
);
},
onError: (error: Error) => {
window.TutorCore.toast.error(error.message || __('Action failed', 'tutor'));
toast.error(error.message || __('Action failed', 'tutor'));
},
});

// Q&A delete mutation.
this.deleteQnAMutation = this.query.useMutation(this.deleteQnA, {
onSuccess: () => {
const url = new URL(window.location.href);
url.searchParams.delete('id');
url.searchParams.delete(URL_PARAMS.ID);
window.location.href = url.toString();
},
onError: (error: Error) => {
window.TutorCore.toast.error(error.message || __('Failed to delete Q&A', 'tutor'));
toast.error(error.message || __('Failed to delete Q&A', 'tutor'));
},
});

// Q&A reply mutation.
this.replyQnAMutation = this.query.useMutation(this.replyQnA, {
onSuccess: () => {
window.TutorCore.toast.success(__('Reply saved successfully', 'tutor'));
toast.success(__('Reply saved successfully', 'tutor'));
window.location.reload();
},
onError: (error: Error) => {
window.TutorCore.toast.error(error.message || __('Failed to save reply', 'tutor'));
toast.error(error.message || __('Failed to save reply', 'tutor'));
},
});
},

deleteComment(payload: { comment_id: number }) {
async reloadReplies(order?: string) {
if (order) {
this.repliesOrder = order;
}

const url = new URL(window.location.href);
const commentId = parseInt(url.searchParams.get(URL_PARAMS.ID) || '0');

if (!commentId) return;

this.loadingReplies = true;
try {
const response = await wpAjaxInstance.post(endpoints.LOAD_DISCUSSION_REPLIES, {
comment_id: commentId,
order: this.repliesOrder,
});

const container = document.getElementById(ELEMENT_IDS.REPLIES_LIST_CONTAINER);
if (container && typeof response.data?.html === 'string') {
container.innerHTML = response.data.html;

// Update URL without reload
const url = new URL(window.location.href);
url.searchParams.set(URL_PARAMS.ORDER, this.repliesOrder);
window.history.pushState({}, '', url.toString());
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to reload replies:', error);
} finally {
this.loadingReplies = false;
}
},

deleteComment(payload: DeleteCommentPayload) {
return wpAjaxInstance.post(endpoints.DELETE_LESSON_COMMENT, payload);
},

replyComment(payload: ReplyCommentPayload) {
return wpAjaxInstance.post(endpoints.REPLY_LESSON_COMMENT, payload);
},

updateComment(payload: { comment_id: number; comment: string }) {
return wpAjaxInstance.post(endpoints.UPDATE_LESSON_COMMENT, payload);
},

qnaSingleAction(payload: QnASingleActionPayload) {
return wpAjaxInstance.post(endpoints.QNA_SINGLE_ACTION, payload);
},
Expand Down Expand Up @@ -146,11 +256,40 @@ const discussionsPage = () => {
}
},

handleReplyComment(data: { comment: string }, commentId: number, courseId: number) {
return this.replyCommentMutation?.mutate({
comment: data.comment,
comment_parent: commentId,
comment_post_ID: courseId,
});
},

handleEditComment(data: { comment: string }, commentId: number) {
return this.editCommentMutation?.mutate({
comment_id: commentId,
comment: data.comment,
});
},

handleKeydown(event: KeyboardEvent) {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
(event.target as HTMLFormElement).closest('form')?.requestSubmit();
}
},

setEditing(id: number | null) {
this.editingId = id;
const formId = id ? `${FORM_ID_PREFIXES.COMMENT_EDIT}${id}` : null;
this.editingFormId = formId;

if (id && formId) {
this.$nextTick?.(() => {
if (form.hasForm(formId)) {
form.setFocus(formId, 'comment');
}
});
}
},
};
};

Expand Down
1 change: 1 addition & 0 deletions assets/src/js/v3/shared/utils/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const endpoints = {
UPDATE_LESSON_COMMENT: 'tutor_update_lesson_comment',
DELETE_LESSON_COMMENT: 'tutor_delete_lesson_comment',
REPLY_LESSON_COMMENT: 'tutor_reply_lesson_comment',
LOAD_COMMENT_REPLIES: 'tutor_load_comment_replies',

// Q&A
QNA_SINGLE_ACTION: 'tutor_qna_single_action',
Expand Down
35 changes: 35 additions & 0 deletions classes/Lesson.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ public function __construct( $register_hooks = true ) {
add_action( 'wp_ajax_tutor_update_lesson_comment', array( $this, 'ajax_update_lesson_comment' ) );
add_action( 'wp_ajax_tutor_reply_lesson_comment', array( $this, 'reply_lesson_comment' ) );
add_action( 'wp_ajax_tutor_load_lesson_comments', array( $this, 'load_lesson_comments' ) );
add_action( 'wp_ajax_tutor_load_comment_replies', array( $this, 'load_comment_replies' ) );

// Add lesson title as nav item & render single content on the learning area.
add_action( "tutor_learning_area_nav_item_{$this->post_type}", array( $this, 'render_nav_item' ), 10, 2 );
Expand Down Expand Up @@ -1092,4 +1093,38 @@ public function load_lesson_comments() {
)
);
}

/**
* Load comment replies for dashboard.
*
* @since 4.0.0
*
* @return void
*/
public function load_comment_replies() {
tutor_utils()->check_nonce();

$comment_id = Input::post( 'comment_id', 0, Input::TYPE_INT );
$replies_order = Input::post( 'order', 'DESC' );

if ( ! $comment_id ) {
$this->response_bad_request( __( 'Invalid comment ID', 'tutor' ) );
}

$user_id = get_current_user_id();
$replies = self::get_comment_replies( $comment_id, $replies_order );

ob_start();
tutor_load_template(
'dashboard.discussions.comment-replies',
array(
'replies' => $replies,
'replies_order' => $replies_order,
'user_id' => $user_id,
)
);
$html = ob_get_clean();

wp_send_json_success( array( 'html' => $html ) );
}
}
4 changes: 2 additions & 2 deletions templates/dashboard/discussions.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
if ( $discussion_id ) {
$template = tutor()->path . 'templates/dashboard/discussions/qna-single.php';
if ( 'lesson-comments' === $current_tab ) {
$template = tutor()->path . 'templates/dashboard/discussions/lesson-comment-single.php';
$template = tutor()->path . 'templates/dashboard/discussions/comment-single.php';
}
require_once $template;
} else {
Expand All @@ -58,7 +58,7 @@
<?php
$template = tutor()->path . 'templates/dashboard/discussions/qna-list.php';
if ( 'lesson-comments' === $current_tab ) {
$template = tutor()->path . 'templates/dashboard/discussions/lesson-comment-list.php';
$template = tutor()->path . 'templates/dashboard/discussions/comment-list.php';
}

require_once $template;
Expand Down
Loading
Loading