diff --git a/assets/core/ts/config/config.ts b/assets/core/ts/config/config.ts index e495944feb..16008c8d53 100644 --- a/assets/core/ts/config/config.ts +++ b/assets/core/ts/config/config.ts @@ -23,6 +23,7 @@ const defaultTutorConfig = { wp_rest_nonce: '', is_admin: '', is_admin_bar_showing: '', + is_tour_completed: '', max_upload_size: '', content_change_event: '', is_tutor_course_edit: '', diff --git a/assets/core/ts/utils/endpoints.ts b/assets/core/ts/utils/endpoints.ts index 61d1abfac5..78bfd475f4 100644 --- a/assets/core/ts/utils/endpoints.ts +++ b/assets/core/ts/utils/endpoints.ts @@ -228,6 +228,9 @@ const endpoints = { // Instructor Dashboard SAVE_INSTRUCTOR_HOME_SECTIONS_ORDER: 'tutor_save_instructor_home_sections_order', SAVE_INSTRUCTOR_HOME_SECTIONS_VISIBILITY: 'tutor_save_instructor_home_sections_visibility', + + // Tour + COMPLETE_TOUR: 'tutor_complete_tour', } as const; export default endpoints; diff --git a/assets/src/js/frontend/components/tour.ts b/assets/src/js/frontend/components/tour.ts new file mode 100644 index 0000000000..da9d0c539e --- /dev/null +++ b/assets/src/js/frontend/components/tour.ts @@ -0,0 +1,83 @@ +import { tutorConfig } from '@Core/ts/config/config'; +import { TUTOR_CUSTOM_EVENTS } from '@Core/ts/constant'; + +interface SlideData { + title: string; + imageLarge: string; + imageSmall: string; +} + +const SLIDE_DIRECTION = { + NEXT: 'next', + BACK: 'back', +}; + +const tour = ({ slidesData, modalId }: { slidesData: SlideData[]; modalId: string }) => { + const { modal, api, endpoints } = window.TutorCore; + const isTourComplete = Number(tutorConfig.is_tour_completed); + + return { + currentSlide: 0, + slides: slidesData || [], + isOpen: false, + slideDirection: SLIDE_DIRECTION.NEXT, + $nextTick: undefined as ((callback: () => void) => void) | undefined, + _onModalClose: undefined as EventListener | undefined, + + init() { + this._onModalClose = ((e: CustomEvent) => { + if (e.detail?.id === modalId) { + this.isOpen = false; + } + }) as EventListener; + + document.addEventListener(TUTOR_CUSTOM_EVENTS.MODAL_CLOSE, this._onModalClose); + + if (!isTourComplete) { + this.isOpen = true; + this.$nextTick?.(() => { + modal.showModal(modalId); + }); + } + }, + + destroy() { + document.removeEventListener(TUTOR_CUSTOM_EVENTS.MODAL_CLOSE, this._onModalClose!); + }, + + next() { + this.slideDirection = SLIDE_DIRECTION.NEXT; + if (this.currentSlide < this.slides.length - 1) { + this.currentSlide++; + } else { + this.skip(); + } + }, + + back() { + this.slideDirection = SLIDE_DIRECTION.BACK; + if (this.currentSlide > 0) { + this.currentSlide--; + } + }, + + skip() { + // Fire-and-forget AJAX call to save user meta + api.wpPost(endpoints.COMPLETE_TOUR); + + this.isOpen = false; + modal.closeModal(modalId); + }, + }; +}; + +export const initializeTour = () => { + window.TutorComponentRegistry.register({ + type: 'component', + meta: { + name: 'tour', + component: tour, + }, + }); + window.TutorComponentRegistry.initWithAlpine(window.Alpine); +}; diff --git a/assets/src/js/frontend/dashboard/index.ts b/assets/src/js/frontend/dashboard/index.ts index f153db6e9d..b9572eeb1f 100644 --- a/assets/src/js/frontend/dashboard/index.ts +++ b/assets/src/js/frontend/dashboard/index.ts @@ -2,6 +2,7 @@ // Initializes dashboard functionality based on current page import { initializeReviews } from '@FrontendComponents/reviews'; +import { initializeTour } from '@FrontendComponents/tour'; import { initializeCommon } from '@FrontendServices/common'; import { initializeHeader } from './header'; @@ -74,6 +75,7 @@ const getCurrentPage = (): string => { const initializeDashboard = () => { initializeHeader(); initializeCommon(); + initializeTour(); const currentPage = getCurrentPage(); diff --git a/assets/src/js/frontend/learning-area/index.ts b/assets/src/js/frontend/learning-area/index.ts index b51d6c4449..867981b087 100644 --- a/assets/src/js/frontend/learning-area/index.ts +++ b/assets/src/js/frontend/learning-area/index.ts @@ -2,7 +2,9 @@ // Initializes learning area functionality based on current page import { initializeReviews } from '@FrontendComponents/reviews'; +import { initializeTour } from '@FrontendComponents/tour'; import { initializeCommon } from '@FrontendServices/common'; + import { initializeCommon as initializeLearningAreaCommon } from './common'; import { initializeLesson } from './lesson'; import { initializeCourseCourseInfo } from './pages/course-info'; @@ -14,6 +16,7 @@ const initializeLearningArea = () => { initializeLearningAreaCommon(); initializeCommon(); initializeSidebar(); + initializeTour(); initializeReviews(); const { pathname, search } = window.location; diff --git a/assets/src/js/v3/@types/index.d.ts b/assets/src/js/v3/@types/index.d.ts index 2987e98e7e..eda7184e80 100644 --- a/assets/src/js/v3/@types/index.d.ts +++ b/assets/src/js/v3/@types/index.d.ts @@ -84,6 +84,7 @@ declare global { wp_rest_nonce: string; is_admin: string; is_admin_bar_showing: string; + is_tour_completed: string; max_upload_size: string; // in bytes content_change_event: string; is_tutor_course_edit: string; diff --git a/assets/src/scss/frontend/components/_index.scss b/assets/src/scss/frontend/components/_index.scss index f5a5a6e8a8..eefc76093a 100644 --- a/assets/src/scss/frontend/components/_index.scss +++ b/assets/src/scss/frontend/components/_index.scss @@ -10,3 +10,4 @@ @forward 'quiz-attempt-details'; @forward 'quiz-summary'; @forward 'player'; +@forward 'tour'; diff --git a/assets/src/scss/frontend/components/_tour.scss b/assets/src/scss/frontend/components/_tour.scss new file mode 100644 index 0000000000..618617cefb --- /dev/null +++ b/assets/src/scss/frontend/components/_tour.scss @@ -0,0 +1,158 @@ +@use '@Core/scss/tokens' as *; +@use '@Core/scss/mixins' as *; + +.tutor-tour-modal { + &.tutor-modal-content { + max-width: 100%; + padding: $tutor-spacing-6; + } + + .tutor-tour-images { + position: relative; + overflow: hidden; + min-height: 366px; + width: 100%; + border-radius: $tutor-radius-2xl; + background: $tutor-surface-l1-hover; + + .tutor-tour-slide-image { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + picture, + img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: $tutor-radius-2xl; + } + + &::after { + content: ''; + position: absolute; + inset: 0; + border-radius: $tutor-radius-2xl; + pointer-events: none; + } + + &[class*='tutor-slide-4']::after { + box-shadow: inset 10px 0px 20px 0px $tutor-surface-l1-hover; + } + + @include tutor-breakpoint-up(sm) { + &[class*='tutor-slide-0']::after, + &[class*='tutor-slide-1']::after, + &[class*='tutor-slide-2']::after, + &[class*='tutor-slide-3']::after { + box-shadow: inset -6px -6px 20px 0px $tutor-surface-l1-hover; + } + } + + @include tutor-breakpoint-down(sm) { + &[class*='tutor-slide-0']::after, + &[class*='tutor-slide-2']::after, + &[class*='tutor-slide-3']::after { + box-shadow: inset -6px -6px 20px 0px $tutor-surface-l1-hover; + } + + &[class*='tutor-slide-1']::after { + box-shadow: inset 6px -6px 20px 0px $tutor-surface-l1-hover; + } + } + } + } + + .tutor-tour-dot { + width: 6px; + height: 6px; + border-radius: $tutor-radius-full; + background: $tutor-icon-disabled; + display: inline-block; + @include tutor-transition(background); + + &.is-active { + background: $tutor-icon-brand; + } + } + + .tutor-transition-slide-enter { + @include tutor-transition((transform, opacity)); + } + + .tutor-transition-slide-enter-start { + transform: translateX(100%); + opacity: 0; + } + + .tutor-transition-slide-enter-end { + transform: translateX(0); + opacity: 1; + } + + .tutor-transition-slide-leave { + transition: + transform 0.3s ease-in, + opacity 0.3s ease-in; + } + + .tutor-transition-slide-leave-start { + transform: translateX(0); + opacity: 1; + } + + .tutor-transition-slide-leave-end { + transform: translateX(-100%); + opacity: 0; + } + + .tutor-tour-images[data-direction='back'] { + .tutor-transition-slide-enter-start { + transform: translateX(-100%); + } + + .tutor-transition-slide-leave-end { + transform: translateX(100%); + } + } + + [dir='rtl'] & { + .tutor-transition-slide-enter-start { + transform: translateX(-100%); + } + + .tutor-transition-slide-leave-end { + transform: translateX(100%); + } + + .tutor-tour-images[data-direction='back'] { + .tutor-transition-slide-enter-start { + transform: translateX(100%); + } + + .tutor-transition-slide-leave-end { + transform: translateX(-100%); + } + } + } +} + +.tutor-tour-skip-wrapper { + .tutor-tour-skip-btn { + color: $tutor-text-primary-inverse; + + &:hover { + color: $tutor-text-primary; + } + } +} + +.tutor-modal-content:has(.tutor-tour-modal) { + background: transparent !important; + box-shadow: none !important; + border: none !important; + padding: 0 !important; + overflow: visible !important; +} diff --git a/classes/Assets.php b/classes/Assets.php index 20e07eaa12..e2363cfa78 100644 --- a/classes/Assets.php +++ b/classes/Assets.php @@ -224,6 +224,7 @@ function ( $file ) { 'course_slug' => tutor_utils()->get_option( 'course_permalink_base', 'courses' ), 'lesson_slug' => tutor_utils()->get_option( 'lesson_permalink_base', 'lessons' ), 'quiz_slug' => tutor_utils()->get_option( 'quiz_permalink_base', 'quizzes' ), + 'is_tour_completed' => (bool) get_user_meta( get_current_user_id(), User::TOUR_COMPLETED_META, true ), ); } diff --git a/classes/User.php b/classes/User.php index 7897c8e82c..58cb609c3d 100644 --- a/classes/User.php +++ b/classes/User.php @@ -41,6 +41,7 @@ class User { const PROFILE_BIO_META = '_tutor_profile_bio'; const PROFILE_JOB_TITLE_META = '_tutor_profile_job_title'; const TUTOR_STUDENT_META = '_is_tutor_student'; + const TOUR_COMPLETED_META = '_tutor_tour_completed'; const APPLICATION_SOURCE_META = '_tutor_application_source'; const INSTRUCTOR_APPROVAL_NOTICE_META = 'tutor_instructor_show_approval_message'; @@ -115,6 +116,7 @@ public function __construct( $register_hooks = true ) { add_action( 'wp_ajax_tutor_user_list', array( $this, 'ajax_user_list' ) ); add_action( 'wp_ajax_tutor_switch_profile', array( $this, 'ajax_switch_profile' ) ); + add_action( 'wp_ajax_tutor_complete_tour', array( $this, 'ajax_complete_tour' ) ); } /** @@ -854,4 +856,19 @@ public static function is_student_view(): bool { public static function can_switch_mode( int $user_id = 0 ): bool { return self::is_admin( $user_id ) || self::is_instructor( $user_id ); } + + /** + * Mark dashboard tour as completed for the current user. + * + * @since 4.0.0 + * + * @return void JSON response. + */ + public function ajax_complete_tour() { + tutor_utils()->check_nonce(); + + update_user_meta( $user_id, self::TOUR_COMPLETED_META, true ); + + $this->json_response( __( 'Tour completed', 'tutor' ) ); + } } diff --git a/templates/dashboard.php b/templates/dashboard.php index c54ecb73d4..d8168c236b 100644 --- a/templates/dashboard.php +++ b/templates/dashboard.php @@ -73,6 +73,7 @@ $user = get_user_by( 'ID', $user_id ); $enable_profile_completion = tutor_utils()->get_option( 'enable_profile_completion' ); $is_instructor = tutor_utils()->is_instructor(); +$is_tour_completed = get_user_meta( $user_id, User::TOUR_COMPLETED_META, true ); // URLS. $current_url = tutor()->current_url; @@ -149,6 +150,10 @@ + + + + diff --git a/templates/learning-area/index.php b/templates/learning-area/index.php index a35e3801b8..7648ef0088 100644 --- a/templates/learning-area/index.php +++ b/templates/learning-area/index.php @@ -22,6 +22,7 @@ use Tutor\Models\EnrollmentModel; use TUTOR\Quiz; use TUTOR\Template; +use TUTOR\User; $current_user_id = get_current_user_id(); $tutor_current_post = get_post(); @@ -37,6 +38,7 @@ $tutor_is_course_instructor = tutor_utils()->is_instructor_of_this_course( $current_user_id, $tutor_course_id, true ); $tutor_is_course_completed = tutor_utils()->is_completed_course( $tutor_course_id, $current_user_id ); $tutor_can_complete_course = CourseModel::can_complete_course( $tutor_course_id, $current_user_id ) && ! $tutor_is_course_completed; +$is_tour_completed = get_user_meta( $current_user_id, User::TOUR_COMPLETED_META, true ); $tutor_course_progress = tutor_utils()->get_course_completed_percent( $tutor_course_id, $current_user_id ); $tutor_completion_mode = tutor_utils()->get_option( 'course_completion_process' ); @@ -171,6 +173,11 @@ class="tutor-btn tutor-btn-outline tutor-btn-small tutor-btn-icon tutor-expand-b + + * @link https://themeum.com + * + * @since 4.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +use Tutor\Components\Button; +use Tutor\Components\Constants\Variant; + +?> + +
+ +