+ +
++ +
+ label( __( 'Create Your First Course', 'tutor' ) ) + ->variant( Variant::PRIMARY ) + ->size( Size::MEDIUM ) + ->icon( Icon::ARROW_RIGHT_2, 'right', 20 ) + ->attr( 'class', 'tutor-create-new-course' ) + ->render(); + ?> ++ +
++ +
+diff --git a/assets/icons/info-octagon-fill.svg b/assets/icons/info-octagon-fill.svg new file mode 100644 index 0000000000..94d69821a3 --- /dev/null +++ b/assets/icons/info-octagon-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/illustrations/confetti.svg b/assets/images/illustrations/confetti.svg index dd7ef638dc..c32d31b647 100644 --- a/assets/images/illustrations/confetti.svg +++ b/assets/images/illustrations/confetti.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/illustrations/dashboard-empty.svg b/assets/images/illustrations/dashboard-empty.svg index c2ac5a4c16..f536d1af08 100644 --- a/assets/images/illustrations/dashboard-empty.svg +++ b/assets/images/illustrations/dashboard-empty.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/illustrations/instructor-approved.svg b/assets/images/illustrations/instructor-approved.svg new file mode 100644 index 0000000000..07f489dd34 --- /dev/null +++ b/assets/images/illustrations/instructor-approved.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/illustrations/instructor-pending.svg b/assets/images/illustrations/instructor-pending.svg new file mode 100644 index 0000000000..8c6eab90aa --- /dev/null +++ b/assets/images/illustrations/instructor-pending.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/src/js/frontend/services/common.ts b/assets/src/js/frontend/services/common.ts index bee2dd13b4..17446b513f 100644 --- a/assets/src/js/frontend/services/common.ts +++ b/assets/src/js/frontend/services/common.ts @@ -45,14 +45,14 @@ export const initializeCommon = () => { const createCourseButton = document.querySelector('.tutor-create-new-course'); createCourseButton?.addEventListener('click', async (e) => { e.preventDefault(); - createCourseButton.classList.add('tutor-loading'); + createCourseButton.classList.add(...['tutor-loading', 'tutor-btn-loading']); createCourseButton.setAttribute('disabled', 'true'); const target = e.target as HTMLElement; target.innerHTML = 'Creating...'; try { await handler.handleCreateCourse(); } finally { - createCourseButton.classList.remove('tutor-loading'); + createCourseButton.classList.remove(...['tutor-loading', 'tutor-btn-loading']); createCourseButton.removeAttribute('disabled'); } }); diff --git a/assets/src/js/v3/shared/icons/types.ts b/assets/src/js/v3/shared/icons/types.ts index e7310b53fb..5fe65f2f03 100644 --- a/assets/src/js/v3/shared/icons/types.ts +++ b/assets/src/js/v3/shared/icons/types.ts @@ -216,6 +216,7 @@ export const icons = [ 'infoColorize', 'infoFill', 'infoOctagon', + 'infoOctagonFill', 'instructor', 'interactiveQuiz', 'interface', diff --git a/assets/src/scss/frontend/dashboard/pages/_student-dashboard.scss b/assets/src/scss/frontend/dashboard/pages/_student-dashboard.scss index 78a670295c..741f8d8342 100644 --- a/assets/src/scss/frontend/dashboard/pages/_student-dashboard.scss +++ b/assets/src/scss/frontend/dashboard/pages/_student-dashboard.scss @@ -5,7 +5,6 @@ @include tutor-flex(row, center, space-between); @include tutor-card-base(); @include tutor-card-radius(2xl); - padding: $tutor-spacing-8; margin-bottom: $tutor-spacing-7; @include tutor-breakpoint-down(sm) { @@ -15,7 +14,12 @@ } .tutor-dashboard-welcome-content { - max-width: 270px; + padding: $tutor-spacing-8; + max-width: 318px; + + [data-tutor-instructor-status="pending"] & { + max-width: 350px; + } } .tutor-dashboard-welcome-badge { @@ -29,7 +33,13 @@ } .tutor-dashboard-welcome-banner { - padding-inline-end: $tutor-spacing-6; + width: 248px; + height: 248px; + @include tutor-flex-center(); + + @include tutor-breakpoint-down(sm) { + width: 100%; + } } } diff --git a/assets/src/scss/frontend/kids/_dashboard.scss b/assets/src/scss/frontend/kids/_dashboard.scss index ed79162f10..2d3624d6cf 100644 --- a/assets/src/scss/frontend/kids/_dashboard.scss +++ b/assets/src/scss/frontend/kids/_dashboard.scss @@ -68,6 +68,10 @@ border-radius: $tutor-radius-6xl; } + .tutor-instructor-request-alert { + border-radius: $tutor-radius-5xl; + } + .tutor-account-section .tutor-account-cover-photo { border-radius: $tutor-radius-4xl; } diff --git a/classes/Icon.php b/classes/Icon.php index 97e17b2a65..32eaf82132 100644 --- a/classes/Icon.php +++ b/classes/Icon.php @@ -232,6 +232,7 @@ final class Icon { const INFO_COLORIZE = 'info-colorize'; const INFO_FILL = 'info-fill'; const INFO_OCTAGON = 'info-octagon'; + const INFO_OCTAGON_FILL = 'info-octagon-fill'; const INSTRUCTOR = 'instructor'; const INTERACTIVE_QUIZ = 'interactive-quiz'; const INTERFACE = 'interface'; diff --git a/classes/Instructor.php b/classes/Instructor.php index 08566ae70c..9c0f44b354 100644 --- a/classes/Instructor.php +++ b/classes/Instructor.php @@ -232,6 +232,7 @@ public function apply_instructor() { } else { update_user_meta( $user_id, '_is_tutor_instructor', tutor_time() ); update_user_meta( $user_id, '_tutor_instructor_status', apply_filters( 'tutor_initial_instructor_status', 'pending' ) ); + update_user_meta( $user_id, User::APPLICATION_SOURCE_META, User::SOURCE_STUDENT_DASHBOARD ); do_action( 'tutor_new_instructor_after', $user_id ); } @@ -350,6 +351,7 @@ public function instructor_approval_action() { update_user_meta( $instructor_id, '_tutor_instructor_status', 'approved' ); update_user_meta( $instructor_id, '_tutor_instructor_approved', tutor_time() ); + update_user_meta( $instructor_id, User::INSTRUCTOR_APPROVAL_NOTICE_META, true ); $instructor = new \WP_User( $instructor_id ); $instructor->add_role( tutor()->instructor_role ); @@ -395,6 +397,8 @@ public function instructor_approval_action() { public function hide_instructor_notice() { if ( 'hide_instructor_notice' === Input::get( 'tutor_action' ) ) { delete_user_meta( get_current_user_id(), 'tutor_instructor_show_rejection_message' ); + } elseif ( 'hide_instructor_approval_notice' === Input::get( 'tutor_action' ) ) { + delete_user_meta( get_current_user_id(), User::INSTRUCTOR_APPROVAL_NOTICE_META ); } } @@ -430,6 +434,7 @@ public function can_publish_tutor_courses() { public function update_instructor_meta( int $user_id ) { update_user_meta( $user_id, '_is_tutor_instructor', tutor_time() ); update_user_meta( $user_id, '_tutor_instructor_status', apply_filters( 'tutor_initial_instructor_status', 'pending' ) ); + update_user_meta( $user_id, User::APPLICATION_SOURCE_META, User::SOURCE_INSTRUCTOR_REGISTRATION ); do_action( 'tutor_new_instructor_after', $user_id ); } diff --git a/classes/Instructors_List.php b/classes/Instructors_List.php index e357f15127..5769d45b21 100644 --- a/classes/Instructors_List.php +++ b/classes/Instructors_List.php @@ -281,6 +281,7 @@ protected static function add_instructor_role( int $instructor_id, string $statu update_user_meta( $instructor_id, '_tutor_instructor_status', $status ); update_user_meta( $instructor_id, '_tutor_instructor_approved', tutor_time() ); + update_user_meta( $instructor_id, User::INSTRUCTOR_APPROVAL_NOTICE_META, true ); $instructor = new \WP_User( $instructor_id ); $instructor->add_role( tutor()->instructor_role ); diff --git a/classes/Template.php b/classes/Template.php index 385a59c3c2..f1d3da1e84 100644 --- a/classes/Template.php +++ b/classes/Template.php @@ -406,7 +406,11 @@ public function tutor_dashboard( $template ) { $dashboard_pages = tutor_utils()->tutor_dashboard_pages(); $dashboard_page_item = tutor_utils()->array_get( $query_var, $dashboard_pages ); $auth_cap = tutor_utils()->array_get( 'auth_cap', $dashboard_page_item ); - if ( $auth_cap && ! User::is_admin() && ! current_user_can( $auth_cap ) ) { + + $can_access_instructor_item = tutor()->instructor_role === $auth_cap && User::can_view_instructor_dashboard(); + + if ( $auth_cap && ! User::is_admin() && ! current_user_can( $auth_cap ) && ! $can_access_instructor_item + ) { $template = tutor_get_template( 'permission-denied' ); } diff --git a/classes/User.php b/classes/User.php index 6bf7d2dd63..7897c8e82c 100644 --- a/classes/User.php +++ b/classes/User.php @@ -15,6 +15,7 @@ use Tutor\Helpers\HttpHelper; use Tutor\Models\UserModel; use Tutor\Traits\JsonResponse; +use TUTOR\InstructorList; /** * User class @@ -31,15 +32,20 @@ class User { /** * User meta keys. */ - const REVIEW_POPUP_META = 'tutor_review_course_popup'; - const LAST_LOGIN_META = 'tutor_last_login'; - const TIMEZONE_META = '_tutor_timezone'; - const PROFILE_PHOTO_META = '_tutor_profile_photo'; - const PHONE_NUMBER_META = 'phone_number'; - const COVER_PHOTO_META = '_tutor_cover_photo'; - const PROFILE_BIO_META = '_tutor_profile_bio'; - const PROFILE_JOB_TITLE_META = '_tutor_profile_job_title'; - const TUTOR_STUDENT_META = '_is_tutor_student'; + const REVIEW_POPUP_META = 'tutor_review_course_popup'; + const LAST_LOGIN_META = 'tutor_last_login'; + const TIMEZONE_META = '_tutor_timezone'; + const PROFILE_PHOTO_META = '_tutor_profile_photo'; + const PHONE_NUMBER_META = 'phone_number'; + const COVER_PHOTO_META = '_tutor_cover_photo'; + const PROFILE_BIO_META = '_tutor_profile_bio'; + const PROFILE_JOB_TITLE_META = '_tutor_profile_job_title'; + const TUTOR_STUDENT_META = '_is_tutor_student'; + const APPLICATION_SOURCE_META = '_tutor_application_source'; + const INSTRUCTOR_APPROVAL_NOTICE_META = 'tutor_instructor_show_approval_message'; + + const SOURCE_INSTRUCTOR_REGISTRATION = 'instructor_registration'; + const SOURCE_STUDENT_DASHBOARD = 'student_dashboard'; /** * View as constants @@ -208,6 +214,76 @@ public static function is_instructor( $user_id = 0, $is_approved = true ) { return tutils()->is_instructor( $user_id, $is_approved ); } + /** + * Get Tutor application source for a user. + * + * @since 4.0.0 + * + * @param int $user_id user id. + * + * @return string + */ + public static function get_application_source( $user_id = 0 ): string { + return (string) get_user_meta( + tutor_utils()->get_user_id( $user_id ), + self::APPLICATION_SOURCE_META, + true + ); + } + + /** + * Check if the user came through instructor registration. + * + * @since 4.0.0 + * + * @param int $user_id user id. + * + * @return boolean + */ + public static function used_instructor_registration( $user_id = 0 ): bool { + return self::SOURCE_INSTRUCTOR_REGISTRATION === self::get_application_source( $user_id ); + } + + /** + * Check if a user can view instructor dashboard screens. + * + * @since 4.0.0 + * + * @param int $user_id user id. + * + * @return boolean + */ + public static function can_view_instructor_dashboard( $user_id = 0 ): bool { + $user_id = tutor_utils()->get_user_id( $user_id ); + + if ( self::is_admin( $user_id ) || self::is_instructor( $user_id ) ) { + return true; + } + + if ( ! self::used_instructor_registration( $user_id ) ) { + return false; + } + + return in_array( + tutor_utils()->instructor_status( $user_id, false ), + array( Instructors_List::STATUS_PENDING, Instructors_List::STATUS_APPROVED ), + true + ); + } + + /** + * Check if user has a pending instructor application. + * + * @since 4.0.0 + * + * @param int $user_id user id. + * + * @return boolean + */ + public static function has_pending_instructor_application( $user_id = 0 ): bool { + return Instructors_List::STATUS_PENDING === tutor_utils()->instructor_status( $user_id, false ); + } + /** * Check current user is only instructor as admin also has instructor role. * @@ -728,12 +804,20 @@ public function ajax_switch_profile() { */ public static function get_current_view_mode(): string { $user_id = get_current_user_id(); - $default_mode = self::can_switch_mode( $user_id ) ? self::VIEW_AS_INSTRUCTOR : self::VIEW_AS_STUDENT; $current_mode = get_user_meta( $user_id, self::VIEW_MODE_USER_META, true ); - return in_array( $current_mode, array( self::VIEW_AS_INSTRUCTOR, self::VIEW_AS_STUDENT ), true ) - ? $current_mode - : $default_mode; + if ( self::can_switch_mode( $user_id ) && in_array( $current_mode, array( self::VIEW_AS_INSTRUCTOR, self::VIEW_AS_STUDENT ), true ) ) { + return $current_mode; + } + + if ( self::used_instructor_registration( $user_id ) ) { + $instructor_status = tutor_utils()->instructor_status( $user_id, false ); + if ( in_array( $instructor_status, array( Instructors_List::STATUS_PENDING, Instructors_List::STATUS_APPROVED ), true ) ) { + return self::VIEW_AS_INSTRUCTOR; + } + } + + return self::VIEW_AS_STUDENT; } /** @@ -759,17 +843,15 @@ public static function is_student_view(): bool { } /** - * Check if the user can switch mode + * Check if the user can switch between learner and instructor dashboard modes. * - * @since 1.0.0 + * @since 4.0.0 * - * @param integer $user_id User id. + * @param int $user_id User ID. * - * @return boolean + * @return bool */ public static function can_switch_mode( int $user_id = 0 ): bool { - $user_id = tutor_utils()->get_user_id( $user_id ); - - return self::is_instructor( $user_id ) || self::is_admin( $user_id ); + return self::is_admin( $user_id ) || self::is_instructor( $user_id ); } } diff --git a/classes/Utils.php b/classes/Utils.php index 5c23a36a21..b23f2dbd0e 100644 --- a/classes/Utils.php +++ b/classes/Utils.php @@ -2826,6 +2826,10 @@ public function tutor_dashboard_permalinks() { public function tutor_dashboard_nav_ui_items() { $nav_items = $this->tutor_dashboard_pages(); + if ( ! tutor_utils()->count( $nav_items ) ) { + return $nav_items; + } + foreach ( $nav_items as $key => $nav_item ) { if ( is_array( $nav_item ) ) { @@ -2833,7 +2837,13 @@ public function tutor_dashboard_nav_ui_items() { unset( $nav_items[ $key ] ); } - if ( isset( $nav_item['auth_cap'] ) && ! User::is_admin() && ! current_user_can( $nav_item['auth_cap'] ) ) { + if ( ! isset( $nav_item['auth_cap'] ) ) { + continue; + } + + $can_access_instructor_item = tutor()->instructor_role === $nav_item['auth_cap'] && User::can_view_instructor_dashboard(); + + if ( ! User::is_admin() && ! current_user_can( $nav_item['auth_cap'] ) && ! $can_access_instructor_item ) { unset( $nav_items[ $key ] ); } } diff --git a/templates/dashboard.php b/templates/dashboard.php index 6d8b0691c7..c54ecb73d4 100644 --- a/templates/dashboard.php +++ b/templates/dashboard.php @@ -10,7 +10,10 @@ defined( 'ABSPATH' ) || exit; +use Tutor\Components\Alert; +use Tutor\Components\EmptyState; use TUTOR\Dashboard; +use TUTOR\Icon; use TUTOR\User; global $wp_query; @@ -107,7 +110,15 @@
+ +
+ label( __( 'Create Your First Course', 'tutor' ) ) + ->variant( Variant::PRIMARY ) + ->size( Size::MEDIUM ) + ->icon( Icon::ARROW_RIGHT_2, 'right', 20 ) + ->attr( 'class', 'tutor-create-new-course' ) + ->render(); + ?> ++ +
+