diff --git a/assets/core/scss/components/_tooltip.scss b/assets/core/scss/components/_tooltip.scss index ad1f7d9ad0..c7bd1246f9 100644 --- a/assets/core/scss/components/_tooltip.scss +++ b/assets/core/scss/components/_tooltip.scss @@ -21,17 +21,18 @@ @include tutor-typography('tiny', 'regular', 'primary-inverse'); position: fixed; z-index: 1070; - max-width: 200px; + max-width: 180px; padding: $tutor-spacing-4; background-color: $tutor-surface-dark; border-radius: $tutor-radius-sm; box-shadow: $tutor-shadow-md; word-wrap: break-word; - text-align: start; + text-align: center; &-large { max-width: 320px; padding: $tutor-spacing-5; + text-align: start; } &-arrow-start { diff --git a/assets/core/scss/mixins/_buttons.scss b/assets/core/scss/mixins/_buttons.scss index 826a86bdc7..b602d1ded9 100644 --- a/assets/core/scss/mixins/_buttons.scss +++ b/assets/core/scss/mixins/_buttons.scss @@ -166,7 +166,7 @@ color: $tutor-text-primary; svg { - color: $tutor-icon-idle; + color: $tutor-icon-secondary; } &:hover:not(:disabled) { diff --git a/assets/core/ts/services/Query.ts b/assets/core/ts/services/Query.ts index 601fc83ff2..e9bcfae198 100644 --- a/assets/core/ts/services/Query.ts +++ b/assets/core/ts/services/Query.ts @@ -232,21 +232,16 @@ export class QueryService { return result; } catch (err) { - const error = { - message: (err as Error).message || 'Mutation failed', - code: (err as { code?: string }).code, - } as TError; - - (this as unknown as MutationState).error = error; + (this as unknown as MutationState).error = err as TError; (this as unknown as MutationState).isError = true; if (options.onError) { - options.onError(error, variables); + options.onError(err as TError, variables); } // onSettled callback - always called if (options.onSettled) { - options.onSettled(null, error, variables); + options.onSettled(null, err as TError, variables); } throw err; diff --git a/assets/icons/read.svg b/assets/icons/read.svg new file mode 100644 index 0000000000..69a02a2132 --- /dev/null +++ b/assets/icons/read.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/src/js/frontend/dashboard/pages/discussions.ts b/assets/src/js/frontend/dashboard/pages/discussions.ts index 81ca6df3a1..b26540c36d 100644 --- a/assets/src/js/frontend/dashboard/pages/discussions.ts +++ b/assets/src/js/frontend/dashboard/pages/discussions.ts @@ -26,10 +26,21 @@ interface ReplyQnAPayload { question_id: number; answer: string; } +interface UpdateQnAPayload { + question_id: number; + answer: string; +} + +interface DeleteQnAPayload { + question_id: number; + context?: 'question' | 'reply'; +} const FORM_ID_PREFIXES = { COMMENT_EDIT: 'lesson-comment-edit-', COMMENT_REPLY: 'lesson-comment-reply-form-', + QNA_EDIT: 'qna-edit-', + QNA_REPLY: 'qna-reply-form-', }; const MODALS = { @@ -39,10 +50,12 @@ const MODALS = { const ELEMENT_IDS = { COMMENT_TEXT_PREFIX: 'tutor-lesson-comment-text-', + QNA_TEXT_PREFIX: 'tutor-qna-text-', REPLIES_LIST_CONTAINER: 'tutor-discussion-replies-list', }; const URL_PARAMS = { + TAB: 'tab', ID: 'id', ORDER: 'order', }; @@ -65,6 +78,8 @@ const discussionsPage = () => { qnaSingleActionMutation: null as MutationState | null, deleteQnAMutation: null as MutationState | null, replyQnAMutation: null as MutationState | null, + updateQnAMutation: null as MutationState | null, + loadQnARepliesMutation: null as MutationState | null, currentAction: null as string | null, currentQuestionId: null as number | null, isSolved: false, @@ -139,7 +154,7 @@ const discussionsPage = () => { // Q&A single action mutation (read, unread, solved, important, archived). this.qnaSingleActionMutation = this.query.useMutation(this.qnaSingleAction, { - onSuccess: (response, payload: QnASingleActionPayload) => { + onSuccess: (_, payload: QnASingleActionPayload) => { const action = payload.qna_action; if (action === 'solved') { this.isSolved = !this.isSolved; @@ -162,10 +177,16 @@ const discussionsPage = () => { // Q&A delete mutation. this.deleteQnAMutation = this.query.useMutation(this.deleteQnA, { - onSuccess: () => { - const url = new URL(window.location.href); - url.searchParams.delete(URL_PARAMS.ID); - window.location.href = url.toString(); + onSuccess: (_, payload) => { + if (payload.context === 'reply') { + toast.success(__('Reply deleted successfully', 'tutor')); + modal.closeModal(MODALS.QNA_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) => { toast.error(convertToErrorMessage(error)); @@ -174,9 +195,33 @@ const discussionsPage = () => { // Q&A reply mutation. this.replyQnAMutation = this.query.useMutation(this.replyQnA, { - onSuccess: () => { + onSuccess: (_, payload) => { toast.success(__('Reply saved successfully', 'tutor')); - window.location.reload(); + this.reloadReplies(); + const formId = `${FORM_ID_PREFIXES.QNA_REPLY}${payload.question_id}`; + if (form.hasForm(formId)) { + form.reset(formId); + } + }, + onError: (error: Error) => { + toast.error(convertToErrorMessage(error)); + }, + }); + + // Q&A update mutation. + this.updateQnAMutation = this.query.useMutation(this.updateQnA, { + onSuccess: (_, payload) => { + toast.success(__('Updated successfully', 'tutor')); + + // Update DOM directly for immediate feedback + const element = document.getElementById(`${ELEMENT_IDS.QNA_TEXT_PREFIX}${payload.question_id}`); + if (element) { + element.innerHTML = payload.answer; + } + + if (this.editingId === payload.question_id) { + this.setEditing(null); + } }, onError: (error: Error) => { toast.error(convertToErrorMessage(error)); @@ -191,12 +236,14 @@ const discussionsPage = () => { const url = new URL(window.location.href); const commentId = parseInt(url.searchParams.get(URL_PARAMS.ID) || '0'); + const tab = url.searchParams.get(URL_PARAMS.TAB) || 'qna'; if (!commentId) return; this.loadingReplies = true; try { - const response = await wpAjaxInstance.post(endpoints.LOAD_DISCUSSION_REPLIES, { + const endpoint = tab === 'qna' ? endpoints.LOAD_QNA_REPLIES : endpoints.LOAD_COMMENT_REPLIES; + const response = await wpAjaxInstance.post(endpoint, { comment_id: commentId, order: this.repliesOrder, }); @@ -234,7 +281,7 @@ const discussionsPage = () => { return wpAjaxInstance.post(endpoints.QNA_SINGLE_ACTION, payload); }, - deleteQnA(payload: { question_id: number }) { + deleteQnA(payload: DeleteQnAPayload) { return wpAjaxInstance.post(endpoints.DELETE_DASHBOARD_QNA, payload); }, @@ -242,6 +289,10 @@ const discussionsPage = () => { return wpAjaxInstance.post(endpoints.CREATE_UPDATE_QNA, payload); }, + updateQnA(payload: UpdateQnAPayload) { + return wpAjaxInstance.post(endpoints.UPDATE_QNA, payload); + }, + async handleQnASingleAction(questionId: number, action: string, extras: Record = {}) { this.currentAction = action; this.currentQuestionId = questionId; @@ -278,15 +329,18 @@ const discussionsPage = () => { } }, - setEditing(id: number | null) { + setEditing(id: number | null, context = 'comment') { + const prefix = context === 'qna' ? FORM_ID_PREFIXES.QNA_EDIT : FORM_ID_PREFIXES.COMMENT_EDIT; + const field = context === 'qna' ? 'answer' : 'comment'; + this.editingId = id; - const formId = id ? `${FORM_ID_PREFIXES.COMMENT_EDIT}${id}` : null; + const formId = id ? `${prefix}${id}` : null; this.editingFormId = formId; if (id && formId) { this.$nextTick?.(() => { if (form.hasForm(formId)) { - form.setFocus(formId, 'comment'); + form.setFocus(formId, field); } }); } diff --git a/assets/src/js/frontend/learning-area/pages/qna.ts b/assets/src/js/frontend/learning-area/pages/qna.ts index 7627522a74..7035babb68 100644 --- a/assets/src/js/frontend/learning-area/pages/qna.ts +++ b/assets/src/js/frontend/learning-area/pages/qna.ts @@ -9,6 +9,11 @@ interface CreateQnaPayload { question: string; } +interface UpdateQnAPayload { + question_id: number; + answer: string; +} + interface ReplyQnaPayload { course_id: number; question_id: number; @@ -20,57 +25,116 @@ interface DeleteQnaPayload { context: string; } +const FORM_ID_PREFIXES = { + QNA_FORM: 'learning-area-qna-form', + QNA_EDIT: 'qna-edit-', + QNA_REPLY: 'qna-reply-form-', +}; + +const ELEMENT_IDS = { + QNA_TEXT_PREFIX: 'tutor-qna-text-', + REPLIES_LIST_CONTAINER: 'tutor-discussion-replies-list', +}; + +const MODALS = { + QNA_DELETE: 'tutor-qna-delete-modal', +}; + +const URL_PARAMS = { + QUESTION_ID: 'question_id', + ORDER: 'order', +}; + /** * Q&A Page Component * Handles Q&A related action in learning area */ const qnaPage = () => { const query = window.TutorCore.query; + const form = window.TutorCore.form; + const modal = window.TutorCore.modal; + const toast = window.TutorCore.toast; return { query, - createQnaMutation: null as MutationState | null, - replyQnaMutation: null as MutationState | null, - deleteQnaMutation: null as MutationState | null, - focused: false, + createQnAMutation: null as MutationState | null, + updateQnAMutation: null as MutationState | null, + replyQnAMutation: null as MutationState | null, + deleteQnAMutation: null as MutationState | null, + editingId: null as number | null, + editingFormId: null as string | null, + loadingReplies: false, + repliesOrder: 'DESC', + $nextTick: undefined as ((callback: () => void) => void) | undefined, init() { - this.createQnaMutation = this.query.useMutation(this.createQnA, { + this.createQnAMutation = this.query.useMutation(this.createQnA, { onSuccess: () => { - window.TutorCore.toast.success(__('Question saved successfully', 'tutor')); + toast.success(__('Question saved successfully', 'tutor')); + + const formId = FORM_ID_PREFIXES.QNA_FORM; + if (form.hasForm(formId)) { + form.reset(formId); + } + window.location.reload(); }, onError: (error: Error) => { - window.TutorCore.toast.error(convertToErrorMessage(error)); + toast.error(convertToErrorMessage(error)); }, }); - this.replyQnaMutation = this.query.useMutation(this.replyQna, { - onSuccess: () => { - window.TutorCore.toast.success(__('Reply saved successfully', 'tutor')); - window.location.reload(); + this.updateQnAMutation = this.query.useMutation(this.updateQnA, { + onSuccess: (_, payload) => { + toast.success(__('Updated successfully', 'tutor')); + + // Update DOM directly for immediate feedback + const element = document.getElementById(`${ELEMENT_IDS.QNA_TEXT_PREFIX}${payload.question_id}`); + if (element) { + element.innerHTML = payload.answer; + } + + if (this.editingId === payload.question_id) { + this.setEditing(null); + } }, onError: (error: Error) => { - window.TutorCore.toast.error(convertToErrorMessage(error)); + toast.error(convertToErrorMessage(error)); }, }); - this.deleteQnaMutation = this.query.useMutation(this.deleteQnA, { - onSuccess: (result, payload) => { - if (payload?.context === 'question') { - window.TutorCore.toast.success(__('Question deleted successfully', 'tutor')); + this.replyQnAMutation = this.query.useMutation(this.replyQnA, { + onSuccess: (_, payload) => { + toast.success(__('Reply saved successfully', 'tutor')); + this.reloadReplies(); + const formId = `${FORM_ID_PREFIXES.QNA_REPLY}${payload.question_id}`; + if (form.hasForm(formId)) { + form.reset(formId); + } + }, + onError: (error: Error) => { + toast.error(convertToErrorMessage(error)); + }, + }); + + this.deleteQnAMutation = this.query.useMutation(this.deleteQnA, { + onSuccess: (_, payload) => { + if (payload.context === 'reply') { + toast.success(__('Reply deleted successfully', 'tutor')); + modal.closeModal(MODALS.QNA_DELETE); + this.reloadReplies(); + } else { + toast.success(__('Question deleted successfully', 'tutor')); + modal.closeModal(MODALS.QNA_DELETE); + + // Reload the page. const url = new URL(window.location.href); - url.searchParams.delete('question_id'); + url.searchParams.delete(URL_PARAMS.QUESTION_ID); window.location.href = url.toString(); - } else if (payload?.context === 'reply') { - window.TutorCore.toast.success(__('Reply deleted successfully', 'tutor')); - window.location.reload(); - } else { - window.location.reload(); } }, onError: (error: Error) => { - window.TutorCore.toast.error(convertToErrorMessage(error)); + toast.error(convertToErrorMessage(error)); }, }); }, @@ -79,13 +143,72 @@ const qnaPage = () => { return wpAjaxInstance.post(endpoints.CREATE_UPDATE_QNA, payload); }, - replyQna(payload: ReplyQnaPayload) { + updateQnA(payload: UpdateQnAPayload) { + return wpAjaxInstance.post(endpoints.UPDATE_QNA, payload); + }, + + replyQnA(payload: ReplyQnaPayload) { return wpAjaxInstance.post(endpoints.CREATE_UPDATE_QNA, payload); }, deleteQnA(payload: DeleteQnaPayload) { return wpAjaxInstance.post(endpoints.DELETE_DASHBOARD_QNA, payload); }, + + async reloadReplies(order?: string) { + if (order) { + this.repliesOrder = order; + } + + const url = new URL(window.location.href); + const commentId = parseInt(url.searchParams.get(URL_PARAMS.QUESTION_ID) || '0'); + + if (!commentId) return; + + this.loadingReplies = true; + try { + const response = await wpAjaxInstance.post(endpoints.LOAD_QNA_REPLIES, { + comment_id: commentId, + order: this.repliesOrder, + context: 'learning-area', + }); + + 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; + } + }, + + setEditing(id: number | null) { + this.editingId = id; + const formId = id ? `${FORM_ID_PREFIXES.QNA_EDIT}${id}` : null; + this.editingFormId = formId; + + if (id && formId) { + this.$nextTick?.(() => { + if (form.hasForm(formId)) { + form.setFocus(formId, 'answer'); + } + }); + } + }, + + handleKeydown(event: KeyboardEvent) { + if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { + (event.target as HTMLFormElement).closest('form')?.requestSubmit(); + } + }, }; }; @@ -93,7 +216,7 @@ export const initializeQna = () => { window.TutorComponentRegistry.register({ type: 'component', meta: { - name: 'qna', + name: 'QnA', component: qnaPage, }, }); diff --git a/assets/src/js/v2/qna.js b/assets/src/js/v2/qna.js index d8ad8b97d7..03d8703dc9 100644 --- a/assets/src/js/v2/qna.js +++ b/assets/src/js/v2/qna.js @@ -202,6 +202,16 @@ window.jQuery(document).ready($=>{ } } }, + error: (xhr) => { + let message = __('Something went wrong.', 'tutor'); + + try { + const resp = JSON.parse(xhr.responseText); + message = resp.message || message; + } catch (e) {} + + tutor_toast(__('Error!', 'tutor'), message, 'error'); + }, complete: () =>{ button.removeClass('is-loading'); } diff --git a/assets/src/js/v3/shared/icons/types.ts b/assets/src/js/v3/shared/icons/types.ts index a81f07f584..6d7f59b98d 100644 --- a/assets/src/js/v3/shared/icons/types.ts +++ b/assets/src/js/v3/shared/icons/types.ts @@ -289,6 +289,7 @@ export const icons = [ 'quizShortAnswer', 'quizTrueFalse', 'ratings', + 'read', 'receiptPercent', 'redo', 'refresh', diff --git a/assets/src/js/v3/shared/utils/endpoints.ts b/assets/src/js/v3/shared/utils/endpoints.ts index 1047891568..cd6e002405 100644 --- a/assets/src/js/v3/shared/utils/endpoints.ts +++ b/assets/src/js/v3/shared/utils/endpoints.ts @@ -93,6 +93,8 @@ const endpoints = { QNA_SINGLE_ACTION: 'tutor_qna_single_action', DELETE_DASHBOARD_QNA: 'tutor_delete_dashboard_question', CREATE_UPDATE_QNA: 'tutor_qna_create_update', + UPDATE_QNA: 'tutor_qna_update', + LOAD_QNA_REPLIES: 'tutor_qna_load_replies', // ASSIGNMENT GET_ASSIGNMENT_DETAILS: 'tutor_assignment_details', diff --git a/classes/Icon.php b/classes/Icon.php index 9c5728dd8e..b52c0ba3cd 100644 --- a/classes/Icon.php +++ b/classes/Icon.php @@ -306,6 +306,7 @@ final class Icon { const QUIZ_SHORT_ANSWER = 'quiz-short-answer'; const QUIZ_TRUE_FALSE = 'quiz-true-false'; const RATINGS = 'ratings'; + const READ = 'read'; const RECEIPT_PERCENT = 'receipt-percent'; const REDO = 'redo'; const REFRESH = 'refresh'; diff --git a/classes/Q_And_A.php b/classes/Q_And_A.php index 59c59976ba..593facc79c 100644 --- a/classes/Q_And_A.php +++ b/classes/Q_And_A.php @@ -11,6 +11,8 @@ namespace TUTOR; use Tutor\Helpers\QueryHelper; +use Tutor\Helpers\UrlHelper; +use Tutor\Traits\JsonResponse; if ( ! defined( 'ABSPATH' ) ) { exit; @@ -21,6 +23,7 @@ * @since 1.0.0 */ class Q_And_A { + use JsonResponse; /** * List of all possible Q&A question statuses. @@ -47,28 +50,53 @@ public function __construct( $register_hooks = true ) { return; } - add_action( 'wp_ajax_tutor_qna_create_update', array( $this, 'tutor_qna_create_update' ) ); + add_filter( 'tutor_learning_area_sub_page_nav_item', array( $this, 'add_learning_area_menu' ), 10, 2 ); - /** - * Delete question - * - * @since v.1.6.4 - */ + add_action( 'wp_ajax_tutor_qna_create_update', array( $this, 'tutor_qna_create_update' ) ); + add_action( 'wp_ajax_tutor_qna_update', array( $this, 'ajax_qna_update' ) ); add_action( 'wp_ajax_tutor_delete_dashboard_question', array( $this, 'tutor_delete_dashboard_question' ) ); - - /** - * Take action against single qna - * - * @since v2.0.0 - */ add_action( 'wp_ajax_tutor_qna_single_action', array( $this, 'tutor_qna_single_action' ) ); add_action( 'wp_ajax_tutor_qna_bulk_action', array( $this, 'process_bulk_action' ) ); - /** - * Q & A load more - * - * @since v2.0.6 - */ - add_action( 'wp_ajax_tutor_q_and_a_load_more', __CLASS__ . '::load_more' ); + add_action( 'wp_ajax_tutor_q_and_a_load_more', array( $this, 'load_more' ) ); + add_action( 'wp_ajax_tutor_qna_load_replies', array( $this, 'load_replies' ) ); + } + + /** + * Add learning area menu + * + * @since 4.0.0 + * + * @param array $menu_items the array of nav items. + * @param string $base_url the base url. + * + * @return array + */ + public function add_learning_area_menu( $menu_items, $base_url ) { + global $tutor_course_id; + + $user_id = get_current_user_id(); + + $enable_q_and_a_on_course = (bool) get_tutor_option( 'enable_q_and_a_on_course' ); + $is_enabled_course_wise = (bool) get_post_meta( $tutor_course_id, '_tutor_enable_qa', true ); + $can_access = tutor_utils()->has_user_course_content_access( $user_id, $tutor_course_id ); + + if ( $is_enabled_course_wise && $enable_q_and_a_on_course && $can_access ) { + $qna_item = array( + 'qna' => array( + 'title' => __( 'Q&A', 'tutor' ), + 'icon' => Icon::QA, + 'url' => UrlHelper::add_query_params( $base_url, array( 'subpage' => 'qna' ) ), + 'template' => tutor_get_template( 'learning-area.subpages.qna' ), + ), + ); + + // Remove existing Q&A if Tutor already added it. + unset( $menu_items['qna'] ); + + $menu_items = $qna_item + $menu_items; + } + + return $menu_items; } /** @@ -105,14 +133,13 @@ public function tutor_qna_create_update() { $course_id = Input::post( 'course_id', 0, Input::TYPE_INT ); if ( ! $this->has_qna_access( $user_id, $course_id ) ) { - wp_send_json_error( array( 'message' => tutor_utils()->error_message() ) ); + $this->response_bad_request( tutor_utils()->error_message() ); } $qna_text = Input::post( 'answer', '', tutor()->has_pro ? Input::TYPE_KSES_POST : Input::TYPE_TEXTAREA ); if ( ! $qna_text ) { - // Content validation. - wp_send_json_error( array( 'message' => __( 'Empty Content Not Allowed!', 'tutor' ) ) ); + $this->response_bad_request( __( 'Empty Content Not Allowed!', 'tutor' ) ); } // Prepare course, question info. @@ -207,22 +234,50 @@ public function inset_qna( $qna_object ) { return $question_id; } + /** + * Update question [frontend dashboard] + * + * @since 4.0.0 + */ + public function ajax_qna_update() { + tutor_utils()->checking_nonce(); + + $question_id = Input::post( 'question_id', 0, Input::TYPE_INT ); + if ( ! $question_id || ! tutor_utils()->can_user_manage( 'qa_question', $question_id ) ) { + $this->response_bad_request( tutor_utils()->error_message( 'authorization' ) ); + } + + $qna_text = Input::post( 'answer', '', tutor()->has_pro ? Input::TYPE_KSES_POST : Input::TYPE_TEXTAREA ); + if ( ! $qna_text ) { + $this->response_bad_request( __( 'Empty Content Not Allowed!', 'tutor' ) ); + } + + $data = array( + 'comment_content' => $qna_text, + ); + + global $wpdb; + $wpdb->update( $wpdb->comments, $data, array( 'comment_ID' => $question_id ) ); + + $this->json_response( __( 'Comment edited successfully', 'tutor' ) ); + } + /** * Delete question [frontend dashboard] * - * @since v.1.6.4 + * @since 1.6.4 */ public function tutor_delete_dashboard_question() { tutor_utils()->checking_nonce(); $question_id = Input::post( 'question_id', 0, Input::TYPE_INT ); if ( ! $question_id || ! tutor_utils()->can_user_manage( 'qa_question', $question_id ) ) { - wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) ); + $this->response_bad_request( tutor_utils()->error_message( 'authorization' ) ); } $this->delete_qna_permanently( array( $question_id ) ); - wp_send_json_success(); + $this->json_response( __( 'Comment deleted successfully.', 'tutor' ) ); } /** @@ -337,7 +392,7 @@ public function tutor_qna_single_action() { $question_id = Input::post( 'question_id', 0, Input::TYPE_INT ); if ( ! tutor_utils()->can_user_manage( 'qa_question', $question_id ) ) { - wp_send_json_error( array( 'message' => __( 'Permission Denied!', 'tutor' ) ) ); + $this->response_bad_request( tutor_utils()->error_message( 'authorization' ) ); } // Get who asked the question. @@ -433,4 +488,44 @@ public static function load_more() { $html = ob_get_clean(); wp_send_json_success( array( 'html' => $html ) ); } + + /** + * Load replies + * + * @since 4.0.0 + * + * @return void send wp_json response + */ + public function load_replies() { + tutor_utils()->checking_nonce(); + + $comment_id = Input::post( 'comment_id', 0, Input::TYPE_INT ); + $replies_order = QueryHelper::get_valid_sort_order( Input::post( 'order', 'DESC' ) ); + $context = Input::post( 'context', 'dashboard' ); + + if ( ! $comment_id ) { + $this->response_bad_request( __( 'Invalid comment ID', 'tutor' ) ); + } + + $user_id = get_current_user_id(); + $replies = tutor_utils()->get_qa_answer_by_question( $comment_id, $replies_order, 'frontend' ); + + $template = 'dashboard.discussions.qna-replies'; + if ( 'learning-area' === $context ) { + $template = 'learning-area.subpages.qna.replies'; + } + + ob_start(); + tutor_load_template( + $template, + array( + 'replies' => $replies, + 'replies_order' => $replies_order, + 'user_id' => $user_id, + ) + ); + $html = ob_get_clean(); + + $this->json_response( '', array( 'html' => $html ) ); + } } diff --git a/classes/Template.php b/classes/Template.php index 0ef91125c2..16407c37f3 100644 --- a/classes/Template.php +++ b/classes/Template.php @@ -532,12 +532,6 @@ public static function make_learning_area_sub_page_nav_items( $base_url = '' ): } $menu_items = array( - 'qna' => array( - 'title' => __( 'Q&A', 'tutor' ), - 'icon' => Icon::QA, - 'url' => UrlHelper::add_query_params( $base_url, array( 'subpage' => 'qna' ) ), - 'template' => tutor_get_template( 'learning-area.subpages.qna' ), - ), 'course-info' => array( 'title' => __( 'Course Info', 'tutor' ), 'icon' => Icon::INFO_OCTAGON, diff --git a/templates/dashboard/discussions.php b/templates/dashboard/discussions.php index f20745e8fa..3f002870f2 100644 --- a/templates/dashboard/discussions.php +++ b/templates/dashboard/discussions.php @@ -41,6 +41,10 @@ ); ?>
+

+ +

+ path . 'templates/dashboard/discussions/qna-single.php'; diff --git a/templates/dashboard/discussions/qna-card.php b/templates/dashboard/discussions/qna-card.php index b52854b178..fcff775dbc 100644 --- a/templates/dashboard/discussions/qna-card.php +++ b/templates/dashboard/discussions/qna-card.php @@ -50,6 +50,7 @@ ?>
id( $question->course_id )->render(); ?>
-
+
@@ -148,7 +187,7 @@ class="tutor-btn tutor-btn-link tutor-btn-x-small tutor-btn-icon tutor-text-subd @@ -156,20 +195,20 @@ class="tutor-btn tutor-btn-text tutor-btn-x-small tutor-btn-icon tutor-discussio
+ + + +
@@ -238,3 +284,20 @@ class="tutor-popover-menu-item tutor-sm-border-t" + + +
+ 'qna-edit-' . (int) $question_id, + 'default_value' => $question->comment_content, + 'submit_handler' => '(data) => updateQnAMutation?.mutate({ ...data, question_id: ' . (int) $question->comment_ID . ' })', + 'cancel_handler' => 'setEditing(null)', + 'is_pending' => 'updateQnAMutation?.isPending', + ) + ); + ?> +
+ diff --git a/templates/dashboard/discussions/qna-form.php b/templates/dashboard/discussions/qna-form.php new file mode 100644 index 0000000000..b53cd9d64f --- /dev/null +++ b/templates/dashboard/discussions/qna-form.php @@ -0,0 +1,87 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +use Tutor\Components\Button; +use Tutor\Components\Constants\InputType; +use Tutor\Components\Constants\Size; +use Tutor\Components\Constants\Variant; +use Tutor\Components\InputField; +use TUTOR\Icon; + +$form_id = $form_id ?? ''; +$label = $label ?? ''; +$submit_label = $submit_label ?? __( 'Update', 'tutor' ); +$form_class = $form_class ?? ''; +$default_value = $default_value ?? ''; +$submit_handler = $submit_handler ?? ''; +$cancel_handler = $cancel_handler ?? ''; +$is_pending = $is_pending ?? ''; +$placeholder = $placeholder ?? __( 'Write your question', 'tutor' ); + +?> + +
+ type( InputType::TEXTAREA ) + ->name( 'answer' ) + ->placeholder( $placeholder ) + ->attr( 'x-bind', "register('answer', { required: '" . esc_js( __( 'Please enter your response.', 'tutor' ) ) . "' })" ) + ->attr( '@keydown', 'handleKeydown($event)' ) + ->attr( '@focus', 'focused = true' ); + + if ( $label ) { + $input->label( $label ); + } + + $input->render(); + ?> + +
+
+ render_svg_icon( Icon::COMMAND, 12, 12 ); ?> + + render_svg_icon( Icon::ENTER, 12, 12 ); ?> + +
+
+ label( __( 'Cancel', 'tutor' ) ) + ->variant( Variant::GHOST ) + ->size( Size::X_SMALL ) + ->attr( 'type', 'button' ) + ->attr( '@click', $cancel_handler ) + ->attr( ':disabled', $is_pending ) + ->render(); + + Button::make() + ->label( $submit_label ) + ->size( Size::X_SMALL ) + ->attr( 'type', 'submit' ) + ->attr( ':disabled', $is_pending ) + ->attr( ':class', "{ 'tutor-btn-loading': {$is_pending} }" ) + ->render(); + ?> +
+
+
diff --git a/templates/dashboard/discussions/qna-list.php b/templates/dashboard/discussions/qna-list.php index d114822e6f..097d5a9ee4 100644 --- a/templates/dashboard/discussions/qna-list.php +++ b/templates/dashboard/discussions/qna-list.php @@ -100,7 +100,7 @@ ->title( __( 'Delete This Question?', 'tutor' ) ) ->message( __( 'Are you sure you want to delete this question permanently? Please confirm your choice.', 'tutor' ) ) ->confirm_text( __( 'Yes, Delete This', 'tutor' ) ) - ->confirm_handler( 'deleteQnAMutation?.mutate({ question_id: payload?.questionId })' ) + ->confirm_handler( 'deleteQnAMutation?.mutate(payload)' ) ->mutation_state( 'deleteQnAMutation' ) ->render(); } diff --git a/templates/dashboard/discussions/qna-replies.php b/templates/dashboard/discussions/qna-replies.php new file mode 100644 index 0000000000..d5af23aaf9 --- /dev/null +++ b/templates/dashboard/discussions/qna-replies.php @@ -0,0 +1,110 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +use Tutor\Components\Avatar; +use Tutor\Components\Constants\Size; +use Tutor\Components\Sorting; +use TUTOR\Icon; +use TUTOR\User; + +?> + +
+
+ + () +
+ order( $replies_order ) + ->on_change( 'reloadReplies' ) + ->bind_active_order( 'repliesOrder' ) + ->render(); + ?> +
+ +
+ +
+
+ user( $reply->user_id )->size( Size::SIZE_40 )->render(); ?> +
+
+ + comment_author ); ?> + + + comment_date_gmt ) ) ) ); + ?> + +
+
+ comment_content ); ?> +
+
+ user_id; + $can_delete = User::is_instructor_view() || $user_id === (int) $reply->user_id; + $has_menu = $can_edit || $can_delete; + ?> + +
+ +
+
+ + + + + + +
+
+
+ +
+ + +
+ 'qna-edit-' . (int) $reply->comment_ID, + 'default_value' => $reply->comment_content, + 'submit_handler' => '(data) => updateQnAMutation?.mutate({ ...data, question_id: ' . (int) $reply->comment_ID . ' })', + 'cancel_handler' => 'setEditing(null)', + 'is_pending' => 'updateQnAMutation?.isPending', + 'placeholder' => __( 'Write your answer', 'tutor' ), + ) + ); + ?> +
+ +
+ +
+ diff --git a/templates/dashboard/discussions/qna-single.php b/templates/dashboard/discussions/qna-single.php index dd8716f750..4af691b419 100644 --- a/templates/dashboard/discussions/qna-single.php +++ b/templates/dashboard/discussions/qna-single.php @@ -12,27 +12,22 @@ defined( 'ABSPATH' ) || exit; use Tutor\Components\Avatar; -use Tutor\Components\Button; use Tutor\Components\ConfirmationModal; -use Tutor\Components\Constants\InputType; use Tutor\Components\Constants\Size; -use Tutor\Components\Constants\Variant; -use Tutor\Components\EmptyState; -use Tutor\Components\InputField; use Tutor\Components\PreviewTrigger; -use Tutor\Components\Sorting; use TUTOR\Icon; use TUTOR\Input; use TUTOR\User; $question = tutor_utils()->get_qa_question( (int) $discussion_id ); if ( ! $question ) { - EmptyState::make()->render(); + wp_safe_redirect( $discussion_url ); return; } -$use_id = get_current_user_id(); -$is_user_asker = $use_id === (int) $question->user_id; +$user_id = get_current_user_id(); +$is_user_asker = $user_id === (int) $question->user_id; +$qna_delete_modal_id = 'tutor-qna-delete-modal'; $replies_order = Input::get( 'order', '' ); $replies = tutor_utils()->get_qa_answer_by_question( $discussion_id, $replies_order, 'frontend' ); @@ -113,10 +108,9 @@ class="tutor-btn tutor-btn-link tutor-btn-x-small tutor-gap-2 tutor-text-subdued
-
- - -
-
+ +
comment_content ); ?>
-
- comment_ID; ?> -
- type( InputType::TEXTAREA ) - ->name( 'answer' ) - ->label( __( 'Reply', 'tutor' ) ) - ->placeholder( __( 'Just drop your response here!', 'tutor' ) ) - ->attr( 'x-bind', "register('answer', { required: '" . esc_js( __( 'Please enter a response', 'tutor' ) ) . "' })" ) - ->attr( '@focus', 'focused = true' ) - ->attr( '@keydown', 'handleKeydown($event)' ) - ->render(); - ?> -
-
- render_svg_icon( Icon::COMMAND, 12, 12 ); ?> - - render_svg_icon( Icon::ENTER, 12, 12 ); ?> - -
-
+ +
label( __( 'Cancel', 'tutor' ) ) - ->variant( Variant::GHOST ) - ->size( Size::X_SMALL ) - ->attr( 'type', 'button' ) - ->attr( '@click', 'reset(); focused = false' ) - ->attr( ':disabled', 'replyQnAMutation?.isPending' ) - ->render(); - - Button::make() - ->label( __( 'Save', 'tutor' ) ) - ->variant( Variant::PRIMARY_SOFT ) - ->size( Size::X_SMALL ) - ->attr( 'type', 'submit' ) - ->attr( ':disabled', 'replyQnAMutation?.isPending' ) - ->attr( ':class', "{ 'tutor-btn-loading': replyQnAMutation?.isPending }" ) - ->render(); + tutor_load_template( + 'dashboard.discussions.qna-form', + array( + 'form_id' => 'qna-edit-' . (int) $question->comment_ID, + 'default_value' => $question->comment_content, + 'submit_handler' => '(data) => updateQnAMutation?.mutate({ ...data, question_id: ' . (int) $question->comment_ID . ' })', + 'cancel_handler' => 'setEditing(null)', + 'is_pending' => 'updateQnAMutation?.isPending', + ) + ); ?>
-
- -
-
- - () -
- order( $replies_order )->render(); ?> +
- -
- -
- user( $reply->user_id )->size( Size::SIZE_40 )->render(); ?> -
-
- - comment_author ); ?> - - - comment_date_gmt ) ) ) ); - ?> - -
-
- comment_content ); ?> -
-
-
- + + 'qna-reply-form-' . $question->comment_ID, + 'submit_handler' => '(data) => replyQnAMutation?.mutate({ ...data, question_id: ' . (int) $question->comment_ID . ', course_id: ' . (int) $question->course_id . ' })', + 'cancel_handler' => 'reset(); focused = false', + 'is_pending' => 'replyQnAMutation?.isPending', + 'placeholder' => __( 'Just drop your response here!', 'tutor' ), + 'label' => __( 'Reply', 'tutor' ), + 'submit_label' => __( 'Save', 'tutor' ), + 'form_class' => 'tutor-discussion-single-reply-form tutor-p-6', + ) + ); + ?> + +
+ $replies, + 'replies_order' => $replies_order, + 'user_id' => $user_id, + ) + ); + ?>
- id( 'tutor-qna-delete-modal' ) + ->id( $qna_delete_modal_id ) ->title( __( 'Delete This Question?', 'tutor' ) ) ->message( __( 'Are you sure you want to delete this question permanently? Please confirm your choice.', 'tutor' ) ) ->confirm_text( __( 'Yes, Delete This', 'tutor' ) ) - ->confirm_handler( 'deleteQnAMutation?.mutate({ question_id: payload?.questionId })' ) + ->confirm_handler( 'deleteQnAMutation?.mutate(payload)' ) ->mutation_state( 'deleteQnAMutation' ) ->render(); ?>
- diff --git a/templates/learning-area/subpages/qna-single.php b/templates/learning-area/subpages/qna-single.php deleted file mode 100644 index d562edc8ef..0000000000 --- a/templates/learning-area/subpages/qna-single.php +++ /dev/null @@ -1,246 +0,0 @@ - - * @link https://themeum.com - * @since 4.0.0 - */ - -defined( 'ABSPATH' ) || exit; - -use Tutor\Components\Avatar; -use Tutor\Components\Button; -use Tutor\Components\ConfirmationModal; -use TUTOR\Icon; -use TUTOR\Input; -use Tutor\Components\InputField; -use Tutor\Components\Constants\InputType; -use Tutor\Components\Constants\Size; -use Tutor\Components\Constants\Variant; -use Tutor\Components\EmptyState; -use Tutor\Components\Popover; -use Tutor\Components\Sorting; - -// Get course ID and question ID. -global $tutor_course_id; -$question_id = Input::get( 'question_id', 0, Input::TYPE_INT ); -$order_by = Input::get( 'order', 'DESC' ); - -if ( $question_id ) { - // Get question data. - $question = tutor_utils()->get_qa_question( $question_id ); - - // Get answers. - if ( $question ) { - $answers = tutor_utils()->get_qa_answer_by_question( $question_id, $order_by, 'frontend' ); - $current_user_id = get_current_user_id(); - } -} - -$back_url = remove_query_arg( 'question_id' ); - -?> -
-
- label( __( 'Back', 'tutor' ) ) - ->tag( 'a' ) - ->variant( 'secondary' ) - ->size( Size::SMALL ) - ->icon( Icon::ARROW_LEFT_2 ) - ->attr( 'href', esc_url( $back_url ) ) - ->attr( 'class', 'tutor-gap-2' ) - ->render(); - ?> -
- title( 'Question not found!' )->render(); - return; - } - ?> - -
-
- user( $question->user_id ) - ->size( Size::SIZE_32 ) - ->render(); - ?> -
-
- display_name ); ?> - - comment_date ) ) ) ); - ?> - -
-
- user_id || current_user_can( 'manage_tutor' ) ) : ?> -
- placement( 'bottom-end' ) - ->trigger( - Button::make() - ->variant( Variant::GHOST ) - ->icon( tutor_utils()->get_svg_icon( Icon::THREE_DOTS_VERTICAL, 16, 16 ) ) - ->size( Size::SMALL ) - ->attr( 'x-ref', 'trigger' ) - ->attr( '@click', 'toggle()' ) - ->get() - ) - ->menu_item( - array( - 'tag' => 'button', - 'content' => esc_html__( 'Delete', 'tutor' ), - 'icon' => tutor_utils()->get_svg_icon( Icon::DELETE_2 ), - 'attr' => array( - '@click' => "hide(); TutorCore.modal.showModal('tutor-qna-delete-modal', { questionId: " . esc_html( $question->comment_ID ) . ", context: 'question' });", - ), - ) - ) - ->render(); - ?> -
- -
-
- comment_content ) ); ?> -
-
- -
-
-
- - type( InputType::TEXTAREA ) - ->name( 'response' ) - ->placeholder( __( 'Just drop your response here!', 'tutor' ) ) - ->clearable() - ->attr( '@focus', 'focused = true' ) - ->attr( 'x-bind', "register('answer', { required: '" . esc_html( __( 'Please enter a response', 'tutor' ) ) . "' })" ) - ->render(); - ?> -
-
- label( __( 'Cancel', 'tutor' ) ) - ->variant( Variant::GHOST ) - ->size( Size::X_SMALL ) - ->attr( 'type', 'button' ) - ->attr( '@click', 'reset(); focused = false' ) - ->attr( ':disabled', 'replyQnaMutation?.isPending' ) - ->render(); - Button::make() - ->label( __( 'Save', 'tutor' ) ) - ->variant( Variant::PRIMARY_SOFT ) - ->size( Size::X_SMALL ) - ->attr( 'type', 'submit' ) - ->attr( ':disabled', 'replyQnaMutation?.isPending' ) - ->attr( ':class', "{ 'tutor-btn-loading': replyQnaMutation?.isPending }" ) - ->render(); - ?> -
-
-
- -
-
- - () -
-
- order( $order_by ) - ->render(); - ?> -
-
- - count( $answers ) ) : ?> -
- -
- user( $answer->user_id ) - ->size( Size::SIZE_32 ) - ->render(); - ?> -
-
- display_name ); ?> - - comment_date ) ) ) ); - ?> - -
-
- comment_content ) ); ?> -
-
- user_id || current_user_can( 'manage_tutor' ) ) : ?> -
- placement( 'bottom-end' ) - ->trigger( - Button::make() - ->variant( Variant::GHOST ) - ->icon( tutor_utils()->get_svg_icon( Icon::THREE_DOTS_VERTICAL, 16, 16 ) ) - ->size( Size::SMALL ) - ->attr( 'x-ref', 'trigger' ) - ->attr( '@click', 'toggle()' ) - ->get() - ) - ->menu_item( - array( - 'tag' => 'button', - 'content' => esc_html__( 'Delete', 'tutor' ), - 'icon' => tutor_utils()->get_svg_icon( Icon::DELETE_2 ), - 'attr' => array( - '@click' => "hide(); TutorCore.modal.showModal('tutor-qna-delete-modal', { questionId: " . esc_html( $answer->comment_ID ) . ", context: 'reply' });", - ), - ) - ) - ->render(); - ?> -
- -
- -
- -
- title( 'No replies yet. Be the first to reply!' )->render(); ?> -
- - - id( 'tutor-qna-delete-modal' ) - ->title( __( 'Delete This Question?', 'tutor' ) ) - ->message( __( 'Are you sure you want to delete this question permanently? Please confirm your choice.', 'tutor' ) ) - ->confirm_text( __( 'Yes, Delete This', 'tutor' ) ) - ->confirm_handler( 'deleteQnaMutation?.mutate({ question_id: payload?.questionId, context: payload?.context })' ) - ->mutation_state( 'deleteQnaMutation' ) - ->render(); - ?> - -
diff --git a/templates/learning-area/subpages/qna.php b/templates/learning-area/subpages/qna.php index a9e6463b3d..c02f36ccf4 100644 --- a/templates/learning-area/subpages/qna.php +++ b/templates/learning-area/subpages/qna.php @@ -10,12 +10,12 @@ defined( 'ABSPATH' ) || exit; -use Tutor\Components\Avatar; use TUTOR\Icon; use TUTOR\Input; use Tutor\Components\Constants\InputType; use Tutor\Components\InputField; use Tutor\Components\Button; +use Tutor\Components\ConfirmationModal; use Tutor\Components\Constants\Size; use Tutor\Components\Constants\Variant; use Tutor\Components\EmptyState; @@ -27,7 +27,7 @@ $question_id = Input::get( 'question_id', 0, Input::TYPE_INT ); if ( $question_id ) { - tutor_load_template( 'learning-area.subpages.qna-single' ); + tutor_load_template( 'learning-area.subpages.qna.single' ); return; } @@ -42,7 +42,7 @@ $order_by = Input::get( 'order', 'DESC' ); // Get questions for this course. -$total_items = tutor_utils()->get_qa_questions( +$total_items = (int) tutor_utils()->get_qa_questions( $offset, $question_per_page, $search_query, @@ -72,135 +72,64 @@ ); ?> -
- - - - - -
-
- -
- type( InputType::TEXTAREA ) - ->name( 'answer' ) - ->placeholder( 'Asked questions...' ) - ->attr( '@focus', 'focused = true' ) - ->attr( 'x-bind', "register('answer', { required: '" . esc_html( __( 'Please enter a response', 'tutor' ) ) . "' })" ) - ->attr( 'rows', 4 ) - ->render(); - ?> -
-
-
-
- render_svg_icon( Icon::COMMAND, 12, 12 ); ?> - - render_svg_icon( Icon::ENTER, 12, 12 ); ?> - -
-
- label( __( 'Cancel', 'tutor' ) ) - ->variant( Variant::GHOST ) - ->size( Size::X_SMALL ) - ->attr( 'type', 'button' ) - ->attr( '@click', 'reset(); focused = false' ) - ->attr( ':disabled', 'createQnaMutation?.isPending' ) - ->render(); - Button::make() - ->label( __( 'Save', 'tutor' ) ) - ->variant( Variant::PRIMARY_SOFT ) - ->size( Size::X_SMALL ) - ->attr( 'type', 'submit' ) - ->attr( ':disabled', 'createQnaMutation?.isPending' ) - ->attr( ':class', "{ 'tutor-btn-loading': createQnaMutation?.isPending }" ) +
+
+ + ?>
- - -
-
- - () -
-
+
+ order( $order_by ) - ->render(); + tutor_load_template( + 'learning-area.subpages.qna.form', + array( + 'form_id' => 'learning-area-qna-form', + 'submit_handler' => '(data) => createQnAMutation?.mutate({ ...data, course_id: ' . (int) $tutor_course_id . ' })', + 'cancel_handler' => 'reset(); focused = false', + 'is_pending' => 'createQnAMutation?.isPending', + 'placeholder' => __( 'Asked questions...', 'tutor' ), + 'submit_label' => __( 'Save', 'tutor' ), + ) + ); ?>
-
- - count( $questions ) ) : ?> -
- meta; - $is_important = (int) tutor_utils()->array_get( 'tutor_qna_important', $meta, 0 ); - $question_url = add_query_arg( - array( - 'subpage' => 'qna', - 'question_id' => $question->comment_ID, - ), - get_permalink( $tutor_course_id ) - ); - $content = wp_strip_all_tags( stripslashes( $question->comment_content ) ); - $content = strlen( $content ) > 100 ? substr( $content, 0, 100 ) . '...' : $content; - ?> - - +
+
+ + () +
+ order( $order_by )->render(); ?>
+ + title( 'No Questions Found!' )->render(); ?> + +
+ $question, + ) + ); + endforeach; + ?> +
+ + current( $current_page ) @@ -208,10 +137,17 @@ class="tutor-discussion-form tutor-p-6 tutor-border-b tutor-qna-form" ->limit( $question_per_page ) ->attr( 'class', 'tutor-px-6 tutor-pb-6 tutor-sm-p-5 tutor-sm-border-t' ) ->render(); - else : ?> -
- title( 'No Questions Found!' )->render(); ?> -
- + + id( 'tutor-qna-delete-modal' ) + ->title( __( 'Delete This Question?', 'tutor' ) ) + ->message( __( 'Are you sure you want to delete this question permanently? Please confirm your choice.', 'tutor' ) ) + ->confirm_text( __( 'Yes, Delete This', 'tutor' ) ) + ->confirm_handler( 'deleteQnAMutation?.mutate(payload)' ) + ->mutation_state( 'deleteQnAMutation' ) + ->render(); + ?> +
diff --git a/templates/learning-area/subpages/qna/card.php b/templates/learning-area/subpages/qna/card.php new file mode 100644 index 0000000000..1d55e93f3e --- /dev/null +++ b/templates/learning-area/subpages/qna/card.php @@ -0,0 +1,132 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +use Tutor\Components\Constants\Size; +use TUTOR\Icon; +use Tutor\Components\Avatar; +use Tutor\Helpers\UrlHelper; + +global $tutor_course_id; + +$current_user_id = get_current_user_id(); +$is_user_asker = $current_user_id === (int) $question->user_id; + +$limit = 60; // Content text limit. +$content = wp_strip_all_tags( $question->comment_content ); +$content = strlen( $content ) > $limit ? substr( $content, 0, $limit ) . '...' : $content; + +$question_id = $question->comment_ID; +$last_reply = null; +$answers = tutor_utils()->get_qa_answer_by_question( $question_id, 'DESC', 'frontend' ); + +if ( ! empty( $answers ) ) { + $last_reply = $answers[0]; +} + +$single_url = UrlHelper::add_query_params( + get_permalink( $tutor_course_id ), + array( + 'subpage' => 'qna', + 'question_id' => $question_id, + ) +); +?> +
+ user( $question->user_id )->size( Size::SIZE_32 )->render(); ?> +
+
+
comment_author ); ?>
+
+ comment_date_gmt ) ) ) ); + ?> +
+
+
+
+
+ + + +
+ render_svg_icon( Icon::COMMENTS, 20, 20 ); ?> + answer_count ); ?> +
+ + +
+ user( $last_reply->user_id )->size( Size::SIZE_20 )->render(); ?> +
+ comment_date_gmt ) ) ) ); + ?> +
+
+ +
+
+
+
+ + + + +
+ +
+
+ + +
+
+
+ +
+
+ + +
+ 'qna-edit-' . (int) $question_id, + 'default_value' => $question->comment_content, + 'submit_handler' => '(data) => updateQnAMutation?.mutate({ ...data, question_id: ' . (int) $question->comment_ID . ' })', + 'cancel_handler' => 'setEditing(null)', + 'is_pending' => 'updateQnAMutation?.isPending', + ) + ); + ?> +
+ diff --git a/templates/learning-area/subpages/qna/form.php b/templates/learning-area/subpages/qna/form.php new file mode 100644 index 0000000000..b53cd9d64f --- /dev/null +++ b/templates/learning-area/subpages/qna/form.php @@ -0,0 +1,87 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +use Tutor\Components\Button; +use Tutor\Components\Constants\InputType; +use Tutor\Components\Constants\Size; +use Tutor\Components\Constants\Variant; +use Tutor\Components\InputField; +use TUTOR\Icon; + +$form_id = $form_id ?? ''; +$label = $label ?? ''; +$submit_label = $submit_label ?? __( 'Update', 'tutor' ); +$form_class = $form_class ?? ''; +$default_value = $default_value ?? ''; +$submit_handler = $submit_handler ?? ''; +$cancel_handler = $cancel_handler ?? ''; +$is_pending = $is_pending ?? ''; +$placeholder = $placeholder ?? __( 'Write your question', 'tutor' ); + +?> + +
+ type( InputType::TEXTAREA ) + ->name( 'answer' ) + ->placeholder( $placeholder ) + ->attr( 'x-bind', "register('answer', { required: '" . esc_js( __( 'Please enter your response.', 'tutor' ) ) . "' })" ) + ->attr( '@keydown', 'handleKeydown($event)' ) + ->attr( '@focus', 'focused = true' ); + + if ( $label ) { + $input->label( $label ); + } + + $input->render(); + ?> + +
+
+ render_svg_icon( Icon::COMMAND, 12, 12 ); ?> + + render_svg_icon( Icon::ENTER, 12, 12 ); ?> + +
+
+ label( __( 'Cancel', 'tutor' ) ) + ->variant( Variant::GHOST ) + ->size( Size::X_SMALL ) + ->attr( 'type', 'button' ) + ->attr( '@click', $cancel_handler ) + ->attr( ':disabled', $is_pending ) + ->render(); + + Button::make() + ->label( $submit_label ) + ->size( Size::X_SMALL ) + ->attr( 'type', 'submit' ) + ->attr( ':disabled', $is_pending ) + ->attr( ':class', "{ 'tutor-btn-loading': {$is_pending} }" ) + ->render(); + ?> +
+
+
diff --git a/templates/learning-area/subpages/qna/replies.php b/templates/learning-area/subpages/qna/replies.php new file mode 100644 index 0000000000..398ff030c1 --- /dev/null +++ b/templates/learning-area/subpages/qna/replies.php @@ -0,0 +1,99 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +use Tutor\Components\Avatar; +use Tutor\Components\Constants\Size; +use Tutor\Components\Sorting; +use TUTOR\Icon; + +?> + +
+
+ + () +
+ order( $replies_order ) + ->on_change( 'reloadReplies' ) + ->bind_active_order( 'repliesOrder' ) + ->render(); + ?> +
+ +
+ +
+
+ user( $reply->user_id )->size( Size::SIZE_40 )->render(); ?> +
+
+ + comment_author ); ?> + + + comment_date_gmt ) ) ) ); + ?> + +
+
+ comment_content ); ?> +
+
+ user_id ) : ?> +
+ +
+
+ + +
+
+
+ +
+ + user_id ) : ?> +
+ 'qna-edit-' . (int) $reply->comment_ID, + 'default_value' => $reply->comment_content, + 'submit_handler' => '(data) => updateQnAMutation?.mutate({ ...data, question_id: ' . (int) $reply->comment_ID . ' })', + 'cancel_handler' => 'setEditing(null)', + 'is_pending' => 'updateQnAMutation?.isPending', + 'placeholder' => __( 'Write your answer', 'tutor' ), + ) + ); + ?> +
+ +
+ +
+ diff --git a/templates/learning-area/subpages/qna/single.php b/templates/learning-area/subpages/qna/single.php new file mode 100644 index 0000000000..c93e30cacf --- /dev/null +++ b/templates/learning-area/subpages/qna/single.php @@ -0,0 +1,152 @@ + + * @link https://themeum.com + * @since 4.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +use Tutor\Components\Avatar; +use Tutor\Components\ConfirmationModal; +use Tutor\Components\Constants\Size; +use TUTOR\Icon; +use TUTOR\Input; + +$question_id = Input::get( 'question_id', 0, Input::TYPE_INT ); +$replies_order = Input::get( 'order', '' ); + +$back_url = remove_query_arg( 'question_id' ); + +$question = tutor_utils()->get_qa_question( $question_id ); +if ( ! $question ) { + wp_safe_redirect( $back_url ); + return; +} + +$user_id = get_current_user_id(); +$is_user_asker = $user_id === (int) $question->user_id; +$qna_delete_modal_id = 'tutor-qna-delete-modal'; + +$replies = tutor_utils()->get_qa_answer_by_question( $question_id, $replies_order, 'frontend' ); +?> +
+
+ +
+
+ user( $question->user_id )->size( Size::SIZE_40 )->render(); ?> +
+
+ comment_author ); ?> + + comment_date_gmt ) ) ) ); + ?> + +
+
+ +
+
+ +
+
+ + +
+
+
+
+ +
+ +
+ comment_content ); ?> +
+ + +
+ 'qna-edit-' . (int) $question->comment_ID, + 'default_value' => $question->comment_content, + 'submit_handler' => '(data) => updateQnAMutation?.mutate({ ...data, question_id: ' . (int) $question->comment_ID . ' })', + 'cancel_handler' => 'setEditing(null)', + 'is_pending' => 'updateQnAMutation?.isPending', + ) + ); + ?> +
+ +
+ + 'qna-reply-form-' . $question->comment_ID, + 'submit_handler' => '(data) => replyQnAMutation?.mutate({ ...data, question_id: ' . (int) $question->comment_ID . ', course_id: ' . (int) $question->course_id . ' })', + 'cancel_handler' => 'reset(); focused = false', + 'is_pending' => 'replyQnAMutation?.isPending', + 'placeholder' => __( 'Just drop your response here!', 'tutor' ), + 'label' => __( 'Reply', 'tutor' ), + 'submit_label' => __( 'Save', 'tutor' ), + 'form_class' => 'tutor-discussion-single-reply-form tutor-p-6', + ) + ); + ?> + +
+ $replies, + 'replies_order' => $replies_order, + 'user_id' => $user_id, + ) + ); + ?> +
+ + id( $qna_delete_modal_id ) + ->title( __( 'Delete This Question?', 'tutor' ) ) + ->message( __( 'Are you sure you want to delete this question permanently? Please confirm your choice.', 'tutor' ) ) + ->confirm_text( __( 'Yes, Delete This', 'tutor' ) ) + ->confirm_handler( 'deleteQnAMutation?.mutate(payload)' ) + ->mutation_state( 'deleteQnAMutation' ) + ->render(); + ?> +
+