diff --git a/assets/core/scss/themes/_dark.scss b/assets/core/scss/themes/_dark.scss index f5f39160e3..7eed4a1904 100644 --- a/assets/core/scss/themes/_dark.scss +++ b/assets/core/scss/themes/_dark.scss @@ -3,7 +3,7 @@ @use '../tokens' as *; -[data-theme='dark'] { +[data-tutor-theme='dark'] { // ============================================================================= // SURFACE COLORS // ============================================================================= diff --git a/assets/core/scss/themes/_light.scss b/assets/core/scss/themes/_light.scss index 4390cafb3b..58b8036faf 100644 --- a/assets/core/scss/themes/_light.scss +++ b/assets/core/scss/themes/_light.scss @@ -4,7 +4,7 @@ @use '../tokens' as *; :root, -[data-theme="light"] { +[data-tutor-theme="light"] { // ============================================================================= // SURFACE COLORS // ============================================================================= diff --git a/assets/core/ts/components/calendar.ts b/assets/core/ts/components/calendar.ts index 5744f301ba..0ebe12dad4 100644 --- a/assets/core/ts/components/calendar.ts +++ b/assets/core/ts/components/calendar.ts @@ -71,7 +71,7 @@ const TUTOR_CALENDAR_VALUES = { apply: 'apply', clear: 'clear', calendarZIndex: '100001', - themeAttrDetect: 'body[data-theme]', + themeAttrDetect: 'body[data-tutor-theme]', calendarClasses: 'vc tutor-vc-calendar', } as const; diff --git a/assets/core/ts/services/Preference.ts b/assets/core/ts/services/Preference.ts index aa18f8ec3e..c6dc2f163b 100644 --- a/assets/core/ts/services/Preference.ts +++ b/assets/core/ts/services/Preference.ts @@ -9,7 +9,7 @@ class PreferenceService { private readonly BASE_FONT_SIZE = 16; private readonly SCALE_PERCENTAGE_BASE = 100; private readonly STYLE_ID = 'tutor-font-scale'; - private readonly DATA_THEME_ATTR = 'data-theme'; + private readonly DATA_THEME_ATTR = 'data-tutor-theme'; constructor() { this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); diff --git a/assets/icons/learning-mood.svg b/assets/icons/learning-mood.svg new file mode 100644 index 0000000000..7f4f116a41 --- /dev/null +++ b/assets/icons/learning-mood.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/illustrations/kids-reset-preference.svg b/assets/images/illustrations/kids-reset-preference.svg new file mode 100644 index 0000000000..5d63fc4d35 --- /dev/null +++ b/assets/images/illustrations/kids-reset-preference.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/illustrations/reset-preference.svg b/assets/images/illustrations/reset-preference.svg new file mode 100644 index 0000000000..3968d5e1c6 --- /dev/null +++ b/assets/images/illustrations/reset-preference.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/src/js/frontend/dashboard/pages/settings.ts b/assets/src/js/frontend/dashboard/pages/settings.ts index 43950ffd38..5b7a2d3ddc 100644 --- a/assets/src/js/frontend/dashboard/pages/settings.ts +++ b/assets/src/js/frontend/dashboard/pages/settings.ts @@ -60,9 +60,15 @@ interface PreferencesFormProps { auto_play_next: boolean; theme: string; font_scale: number; + learning_mood: boolean; formId?: string; } +interface ResetPreferencesPayload { + formId: string; + modalId: string; +} + interface UpdateNotificationProps { [key: string]: boolean | string; } @@ -77,6 +83,7 @@ interface ResetPasswordResponse { const settings = () => { const query = window.TutorCore.query; const form = window.TutorCore.form; + const modal = window.TutorCore.modal; const toast = window.TutorCore.toast; return { @@ -90,6 +97,7 @@ const settings = () => { resetPasswordMutation: null as MutationState | null, handleUpdateNotification: null as MutationState | null, savePreferencesMutation: null as MutationState> | null, + resetPreferencesMutation: null as MutationState> | null, init() { if (!this.$el) { @@ -103,6 +111,7 @@ const settings = () => { this.handleSaveBillingInfo = this.handleSaveBillingInfo.bind(this); this.handleSaveWithdrawMethod = this.handleSaveWithdrawMethod.bind(this); this.handleResetPassword = this.handleResetPassword.bind(this); + this.handleResetPreferences = this.handleResetPreferences.bind(this); this.handleUpdateNotification = query.useMutation(this.updateNotification, { onSuccess: (data: TutorMutationResponse, payload: UpdateNotificationProps) => { @@ -190,6 +199,16 @@ const settings = () => { toast.error(convertToErrorMessage(error)); }, }); + + this.resetPreferencesMutation = query.useMutation(this.resetPreferences, { + onSuccess: (data: TutorMutationResponse) => { + toast.success(data?.message ?? __('Preferences reset successfully', 'tutor')); + window.location.reload(); + }, + onError: (error: Error) => { + toast.error(convertToErrorMessage(error)); + }, + }); }, async updatePreferences(payload: PreferencesFormProps) { @@ -198,6 +217,16 @@ const settings = () => { >; }, + async resetPreferences(payload: ResetPreferencesPayload) { + return wpAjaxInstance.post(endpoints.RESET_USER_PREFERENCES, payload) as unknown as Promise< + TutorMutationResponse + >; + }, + + handleResetPreferences(formId: string, modalId: string) { + this.resetPreferencesMutation?.mutate({ formId, modalId }); + }, + async updateNotification(payload: UpdateNotificationProps) { const transformedPayload = Object.keys(payload).reduce( (formattedPayload, key) => { diff --git a/assets/src/js/v3/shared/icons/types.ts b/assets/src/js/v3/shared/icons/types.ts index 1d661dbe6e..358e1e7a5b 100644 --- a/assets/src/js/v3/shared/icons/types.ts +++ b/assets/src/js/v3/shared/icons/types.ts @@ -212,6 +212,7 @@ export const icons = [ 'landscape', 'landscapeFilled', 'league', + 'learningMood', 'left', 'lesson', 'lessonCompleted', diff --git a/assets/src/js/v3/shared/utils/endpoints.ts b/assets/src/js/v3/shared/utils/endpoints.ts index 06873a19f2..366ec0612d 100644 --- a/assets/src/js/v3/shared/utils/endpoints.ts +++ b/assets/src/js/v3/shared/utils/endpoints.ts @@ -211,6 +211,7 @@ const endpoints = { RESET_PASSWORD: 'tutor_profile_password_reset', UPDATE_PROFILE_NOTIFICATION: 'tutor_save_notification_preference', UPDATE_USER_PREFERENCES: 'tutor_save_user_preferences', + RESET_USER_PREFERENCES: 'tutor_reset_user_preferences', REMOVE_DEVICE_MANUALLY: 'tutor_remove_device_manually', REMOVE_ALL_ACTIVE_LOGINS: 'tutor_remove_all_active_logins', diff --git a/assets/src/scss/frontend/dashboard/settings/_preferences.scss b/assets/src/scss/frontend/dashboard/settings/_preferences.scss index d9cd3c1c1a..98be40c795 100644 --- a/assets/src/scss/frontend/dashboard/settings/_preferences.scss +++ b/assets/src/scss/frontend/dashboard/settings/_preferences.scss @@ -2,13 +2,11 @@ @use '@Core/scss/mixins' as *; .tutor-preferences { - &-section-header { - @include tutor-typography('h5', 'semibold', 'primary', 'heading'); - margin: $tutor-spacing-none $tutor-spacing-none $tutor-spacing-4 $tutor-spacing-none; - - @include tutor-breakpoint-down(sm) { - @include tutor-typography('medium', 'semibold', 'primary', 'heading'); - } + &-reset-default { + @include tutor-flex(row, center, flex-end); + color: $tutor-text-secondary; + gap: $tutor-spacing-2; + cursor: pointer; } &-setting-item { @@ -47,4 +45,4 @@ flex-shrink: 0; } } -} +} \ No newline at end of file diff --git a/classes/Icon.php b/classes/Icon.php index b0a30c99f6..66c51a6077 100644 --- a/classes/Icon.php +++ b/classes/Icon.php @@ -228,6 +228,7 @@ final class Icon { const LANDSCAPE = 'landscape'; const LANDSCAPE_FILLED = 'landscape-filled'; const LEAGUE = 'league'; + const LEARNING_MOOD = 'learning-mood'; const LEFT = 'left'; const LESSON = 'lesson'; const LESSON_COMPLETED = 'lesson-completed'; diff --git a/classes/UserPreference.php b/classes/UserPreference.php index c5d4e0b6e2..fda8096933 100644 --- a/classes/UserPreference.php +++ b/classes/UserPreference.php @@ -17,6 +17,7 @@ use Tutor\Cache\TutorCache; use Tutor\Helpers\HttpHelper; +use Tutor\Options_V2; use Tutor\Traits\JsonResponse; /** @@ -113,6 +114,7 @@ public function __construct( $register_hooks = true ) { } add_action( 'wp_ajax_tutor_save_user_preferences', array( $this, 'ajax_save_user_preferences' ) ); + add_action( 'wp_ajax_tutor_reset_user_preferences', array( $this, 'ajax_reset_user_preferences' ) ); add_filter( 'body_class', array( $this, 'add_theme_attribute' ) ); add_action( 'wp_head', array( $this, 'apply_font_scale' ) ); } @@ -152,7 +154,7 @@ public function add_theme_attribute( $classes ) { if ( ! in_array( $theme, self::THEMES, true ) ) { $theme = self::DEFAULT_THEME; } - echo ' data-theme="' . esc_attr( $theme ) . '"'; + echo ' data-tutor-theme="' . esc_attr( $theme ) . '"'; return $classes; } @@ -226,10 +228,15 @@ public function save_preferences( array $prefs, $user_id = 0 ) { return false; } - $current_preferences = $this->get_preferences( $user_id ); - $preferences = array_merge( $current_preferences, $prefs ); + // Get from database. + $current_preferences = get_user_meta( $user_id, self::META_KEY, true ); + if ( ! is_array( $current_preferences ) ) { + $current_preferences = array(); + } - $preferences = apply_filters( 'tutor_user_preference_data', $preferences, $user_id ); + $combined_preferences = array_merge( $current_preferences, $prefs ); + + $preferences = apply_filters( 'tutor_user_preference_data', $combined_preferences, $user_id ); update_user_meta( $user_id, self::META_KEY, $preferences ); @@ -248,24 +255,49 @@ public function save_preferences( array $prefs, $user_id = 0 ) { public function ajax_save_user_preferences() { tutor_utils()->check_nonce(); - if ( ! is_user_logged_in() ) { + $auto_play_next = Input::post( 'auto_play_next', null ); + $theme = Input::post( 'theme', null ); + $font_scale = Input::post( 'font_scale', null ); + $learning_mood = Input::post( 'learning_mood', null ); + + $preferences_settings = array(); + + if ( null !== $auto_play_next ) { + $default_auto_play_next = (bool) tutor_utils()->get_option( 'autoload_next_course_content' ); + $auto_play_next = 'true' === $auto_play_next ? true : false; + if ( $auto_play_next !== $default_auto_play_next ) { + $preferences_settings['auto_play_next'] = $auto_play_next; + } + } + + if ( null !== $theme ) { + $preferences_settings['theme'] = $theme; + } + + if ( null !== $font_scale ) { + $preferences_settings['font_scale'] = (int) $font_scale; + } + + if ( null !== $learning_mood ) { + // Validate learning_mood against allowed values. + $allowed_moods = array( Options_V2::LEARNING_MODE_MODERN, Options_V2::LEARNING_MODE_KIDS ); + if ( ! in_array( $learning_mood, $allowed_moods, true ) ) { + $learning_mood = Options_V2::LEARNING_MODE_MODERN; + } + $default_learning_mood = tutor_utils()->get_option( 'learning_mode', Options_V2::LEARNING_MODE_MODERN ); + if ( $learning_mood !== $default_learning_mood ) { + $preferences_settings['learning_mood'] = $learning_mood; + } + } + + if ( empty( $preferences_settings ) ) { $this->json_response( - tutor_utils()->error_message( 'forbidden' ), + __( 'No changes detected', 'tutor' ), null, - HttpHelper::STATUS_UNAUTHORIZED + HttpHelper::STATUS_OK ); } - $auto_play_next = Input::post( 'auto_play_next', false, INPUT::TYPE_BOOL ); - $theme = Input::post( 'theme', self::DEFAULT_THEME ); - $font_scale = Input::post( 'font_scale', self::DEFAULT_FONT_SCALE, INPUT::TYPE_INT ); - - $preferences_settings = array( - 'auto_play_next' => $auto_play_next, - 'theme' => $theme, - 'font_scale' => $font_scale, - ); - $preference_data = $this->save_preferences( $preferences_settings, get_current_user_id() ); if ( false === $preference_data ) { @@ -282,6 +314,34 @@ public function ajax_save_user_preferences() { ); } + /** + * AJAX handler: reset current user's preferences back to defaults. + * + * @since 4.0.0 + * + * @return void + */ + public function ajax_reset_user_preferences() { + tutor_utils()->check_nonce(); + + $user_id = get_current_user_id(); + if ( ! $user_id ) { + $this->json_response( + __( 'Failed to reset preferences', 'tutor' ), + null, + HttpHelper::STATUS_NOT_FOUND + ); + } + + // Delete user meta. + delete_user_meta( $user_id, self::META_KEY ); + + $this->json_response( + __( 'Preferences reset successfully', 'tutor' ), + null + ); + } + /** * Get default preferences. * @@ -297,6 +357,7 @@ private static function get_default_preferences() { 'auto_play_next' => (bool) tutor_utils()->get_option( 'autoload_next_course_content' ), 'theme' => self::DEFAULT_THEME, 'font_scale' => self::DEFAULT_FONT_SCALE, + 'learning_mood' => tutor_utils()->get_option( 'learning_mode', Options_V2::LEARNING_MODE_MODERN ), ) ); @@ -327,6 +388,26 @@ public static function get_theme_options() { ); } + /** + * Get learning mood options for UI selects. + * + * @since 4.0.0 + * + * @return array + */ + public static function get_learning_mood_options() { + return array( + array( + 'label' => __( 'Modern', 'tutor' ), + 'value' => Options_V2::LEARNING_MODE_MODERN, + ), + array( + 'label' => __( 'Kids', 'tutor' ), + 'value' => Options_V2::LEARNING_MODE_KIDS, + ), + ); + } + /** * Get font scale options for UI selects. * diff --git a/classes/Utils.php b/classes/Utils.php index c25fb7453c..bc89739a96 100644 --- a/classes/Utils.php +++ b/classes/Utils.php @@ -11132,13 +11132,19 @@ public static function get_icon_by_post_type( $post_type ): string { } /** - * Is kids mode active? + * Is kids mode active * * @since 4.0.0 * * @return bool */ - public static function is_kids_mode(): bool { + public function is_kids_mode(): bool { + $user_id = get_current_user_id(); + if ( $user_id && User::is_student_view() ) { + $user_learning_mood = UserPreference::get( 'learning_mood', Options_V2::LEARNING_MODE_MODERN ); + return Options_V2::LEARNING_MODE_KIDS === $user_learning_mood; + } + return Options_V2::LEARNING_MODE_KIDS === tutor_utils()->get_option( 'learning_mode' ) && User::is_student_view(); } diff --git a/templates/dashboard/account/settings/preferences.php b/templates/dashboard/account/settings/preferences.php index b1a9c6decd..a62acbe758 100644 --- a/templates/dashboard/account/settings/preferences.php +++ b/templates/dashboard/account/settings/preferences.php @@ -13,22 +13,32 @@ use TUTOR\Icon; use Tutor\Components\SvgIcon; use TUTOR\UserPreference; +use Tutor\Components\ConfirmationModal; use Tutor\Components\InputField; use Tutor\Components\Constants\InputType; use Tutor\Components\Constants\Size; +use Tutor\Helpers\UrlHelper; +use Tutor\Options_V2; +use TUTOR\User; $theme_options = UserPreference::get_theme_options(); +$learning_mood_options = UserPreference::get_learning_mood_options(); + $font_scale_options = UserPreference::get_font_scale_options(); // Load current user preferences to seed the form. $user_preferences = UserPreference::get_preferences(); +// Confirmation modal id for resetting user preferences. +$reset_modal_id = 'tutor-preferences-reset-modal'; + ?>
'}); })($event)" > -
- -
+
+
+ +
+
+ name( Icon::RELOAD_3 )->size( 16 )->render(); ?> + +
+
+ is_kids_mode() ? UrlHelper::asset( 'images/illustrations/kids-reset-preference.svg' ) : UrlHelper::asset( 'images/illustrations/reset-preference.svg' ); + ConfirmationModal::make() + ->id( $reset_modal_id ) + ->title( __( 'Reset your Preferences?', 'tutor' ) ) + ->message( __( 'This will reset your learning preferences to the default settings. Your progress and account data won’t be affected.', 'tutor' ) ) + ->cancel_text( __( 'Cancel', 'tutor' ) ) + ->confirm_text( __( 'Reset Preferences', 'tutor' ) ) + ->icon( $reset_modal_illustration ) + ->confirm_handler( "handleResetPreferences('" . esc_js( $form_id ) . "','" . esc_js( $reset_modal_id ) . "')" ) + ->mutation_state( 'resetPreferencesMutation' ) + ->render(); + ?> +
@@ -105,6 +135,30 @@ ?>
+ +
+
+
+ name( Icon::LEARNING_MOOD )->size( 20 )->render(); ?> +
+ +
+
+ type( InputType::SELECT ) + ->size( Size::SM ) + ->name( 'learning_mood' ) + ->options( $learning_mood_options ) + ->value( $user_preferences['learning_mood'] ?? Options_V2::LEARNING_MODE_MODERN ) + ->placeholder( __( 'Select mode', 'tutor' ) ) + ->attr( 'x-bind', "register('learning_mood')" ) + ->attr( 'style', 'min-width: 140px;' ) + ->render(); + ?> +
+
+