diff --git a/assets/js/editor.js b/assets/js/editor.js index 9debd906b..8aa6f2f05 100644 --- a/assets/js/editor.js +++ b/assets/js/editor.js @@ -258,6 +258,190 @@ const PrplLessonItemsHTML = () => { ); }; +/** + * Render the Remind Me button section. + * + * @return {Element} Element to render. + */ +const PrplRemindMeSection = () => { + const [ selectedDate, setSelectedDate ] = useState( '' ); + + // Callback function for the Remind Me button + const handleRemindMeClick = () => { + // Validate that a date is selected + if ( ! selectedDate ) { + wp.data + .dispatch( 'core/notices' ) + .createErrorNotice( + prplL10n( 'remindMeToReviewContentError' ), + { + type: 'snackbar', + isDismissible: true, + } + ); + return; + } + + // Get the current post ID + const postId = wp.data.select( 'core/editor' ).getCurrentPostId(); + + // Get the current post title + const postTitle = wp.data + .select( 'core/editor' ) + .getEditedPostAttribute( 'title' ); + + // Show loading state + wp.data + .dispatch( 'core/notices' ) + .createInfoNotice( prplL10n( 'remindMeToReviewContentSetting' ), { + type: 'snackbar', + isDismissible: true, + } ); + + // Make AJAX request to set reminder + const formData = new FormData(); + formData.append( 'action', 'progress_planner_set_reminder' ); + formData.append( 'post_id', postId ); + formData.append( 'post_title', postTitle ); + formData.append( 'reminder_date', selectedDate ); + formData.append( 'nonce', progressPlannerEditor.nonce ); + + fetch( progressPlannerEditor.ajaxUrl, { + method: 'POST', + credentials: 'same-origin', + body: formData, + } ) + .then( ( response ) => response.json() ) + .then( ( responseData ) => { + if ( responseData.success ) { + // Show success notification + wp.data + .dispatch( 'core/notices' ) + .createSuccessNotice( + responseData.data.message || + prplL10n( 'remindMeToReviewContentSuccess' ) + + postTitle, + { + type: 'snackbar', + isDismissible: true, + } + ); + // Clear the selected date after successful reminder set + setSelectedDate( '' ); + } else { + // Show error notification + wp.data + .dispatch( 'core/notices' ) + .createErrorNotice( + responseData.data.message || + prplL10n( 'remindMeToReviewContentError' ), + { + type: 'snackbar', + isDismissible: true, + } + ); + } + } ) + .catch( ( error ) => { + console.error( 'Error setting reminder:', error ); + // Show error notification + wp.data + .dispatch( 'core/notices' ) + .createErrorNotice( + prplL10n( 'remindMeToReviewContentError' ), + { + type: 'snackbar', + isDismissible: true, + } + ); + } ); + }; + + // Get minimum date (today) + const today = new Date().toISOString().split( 'T' )[ 0 ]; + + return el( + PanelBody, + { + key: 'progress-planner-sidebar-remind-me-section', + title: prplL10n( 'remindMeToReviewContent' ), + initialOpen: true, + }, + el( + 'div', + { + style: { + padding: '10px 0', + }, + }, + // Date picker + el( + 'div', + { + style: { + marginBottom: '15px', + }, + }, + el( + 'label', + { + style: { + display: 'block', + marginBottom: '5px', + fontWeight: 'bold', + color: '#38296D', + }, + }, + prplL10n( 'remindMeToReviewContentDate' ) + ), + el( 'input', { + type: 'date', + value: selectedDate, + min: today, + onChange: ( event ) => + setSelectedDate( event.target.value ), + style: { + width: '100%', + padding: '8px 12px', + border: '1px solid #ddd', + borderRadius: '4px', + fontSize: '14px', + color: '#38296D', + }, + } ) + ), + el( + Button, + { + key: 'progress-planner-sidebar-remind-me-button', + onClick: handleRemindMeClick, + variant: 'secondary', + disabled: ! selectedDate, + style: { + width: '100%', + margin: '15px 0', + color: selectedDate ? '#38296D' : '#999', + boxShadow: selectedDate + ? 'inset 0 0 0 1px #38296D' + : 'inset 0 0 0 1px #ddd', + whiteSpace: 'normal', + height: 'auto', + minHeight: '60px', + padding: '10px 15px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + textAlign: 'center', + lineHeight: '1.4', + }, + }, + prplL10n( 'remindMeToReviewContent' ) + ) + ) + ); +}; + /** * Render the Progress Planner sidebar. * This sidebar will display the lessons and videos for the current page. @@ -294,7 +478,8 @@ const PrplProgressPlannerSidebar = () => }, }, PrplRenderPageTypeSelector(), - PrplLessonItemsHTML() + PrplLessonItemsHTML(), + PrplRemindMeSection() ) ) ); diff --git a/classes/admin/class-editor.php b/classes/admin/class-editor.php index 208cf82be..a48ee3169 100644 --- a/classes/admin/class-editor.php +++ b/classes/admin/class-editor.php @@ -59,6 +59,8 @@ public function enqueue_editor_script() { 'lessons' => \progress_planner()->get_lessons()->get_items(), 'pageTypes' => $page_types, 'defaultPageType' => $prpl_preselected_page_type, + 'ajaxUrl' => \admin_url( 'admin-ajax.php' ), + 'nonce' => \wp_create_nonce( 'progress_planner' ), ], ] ); diff --git a/classes/admin/class-enqueue.php b/classes/admin/class-enqueue.php index c92215148..2eed7df18 100644 --- a/classes/admin/class-enqueue.php +++ b/classes/admin/class-enqueue.php @@ -235,6 +235,18 @@ public function localize_script( $handle, $localize_data = [] ) { [ 'post_status' => [ 'publish', 'trash' ], 'include_provider' => [ 'user' ], + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'relation' => 'OR', + [ + 'key' => 'prpl_available_at', + 'compare' => 'NOT EXISTS', + ], + [ + 'key' => 'prpl_available_at', + 'value' => \time(), + 'compare' => '<', + ], + ], ] ); @@ -341,65 +353,70 @@ private function get_badge_urls() { public function get_localized_strings() { // Strings alphabetically ordered. return [ - 'badge' => \esc_html__( 'Badge', 'progress-planner' ), - 'checklistProgressDescription' => \sprintf( + 'badge' => \esc_html__( 'Badge', 'progress-planner' ), + 'checklistProgressDescription' => \sprintf( /* translators: %s: the checkmark icon. */ \esc_html__( 'Check off all required elements %s in the element checks below', 'progress-planner' ), '' ), - 'close' => \esc_html__( 'Close', 'progress-planner' ), - 'doneBtnText' => \esc_html__( 'Finish', 'progress-planner' ), - 'howLong' => \esc_html__( 'How long?', 'progress-planner' ), - 'info' => \esc_html__( 'Info', 'progress-planner' ), - 'markAsComplete' => \esc_html__( 'Mark as completed', 'progress-planner' ), - 'nextBtnText' => \esc_html__( 'Next →', 'progress-planner' ), - 'prevBtnText' => \esc_html__( '← Previous', 'progress-planner' ), - 'pageType' => \esc_html__( 'Page type', 'progress-planner' ), - 'progressPlannerSidebar' => \esc_html__( 'Progress Planner Sidebar', 'progress-planner' ), - 'progressText' => \sprintf( + 'close' => \esc_html__( 'Close', 'progress-planner' ), + 'doneBtnText' => \esc_html__( 'Finish', 'progress-planner' ), + 'howLong' => \esc_html__( 'How long?', 'progress-planner' ), + 'info' => \esc_html__( 'Info', 'progress-planner' ), + 'markAsComplete' => \esc_html__( 'Mark as completed', 'progress-planner' ), + 'nextBtnText' => \esc_html__( 'Next →', 'progress-planner' ), + 'prevBtnText' => \esc_html__( '← Previous', 'progress-planner' ), + 'pageType' => \esc_html__( 'Page type', 'progress-planner' ), + 'progressPlannerSidebar' => \esc_html__( 'Progress Planner Sidebar', 'progress-planner' ), + 'progressText' => \sprintf( /* translators: %1$s: The current step number. %2$s: The total number of steps. */ \esc_html__( 'Step %1$s of %2$s', 'progress-planner' ), '{{current}}', '{{total}}' ), - 'saving' => \esc_html__( 'Saving...', 'progress-planner' ), - 'snooze' => \esc_html__( 'Snooze', 'progress-planner' ), - 'snoozeDurationOneWeek' => \esc_html__( '1 week', 'progress-planner' ), - 'snoozeDurationOneMonth' => \esc_html__( '1 month', 'progress-planner' ), - 'snoozeDurationThreeMonths' => \esc_html__( '3 months', 'progress-planner' ), - 'snoozeDurationSixMonths' => \esc_html__( '6 months', 'progress-planner' ), - 'snoozeDurationOneYear' => \esc_html__( '1 year', 'progress-planner' ), - 'snoozeDurationForever' => \esc_html__( 'forever', 'progress-planner' ), - 'snoozeThisTask' => \esc_html__( 'Snooze this task?', 'progress-planner' ), - 'subscribed' => \esc_html__( 'Subscribed...', 'progress-planner' ), - 'subscribing' => \esc_html__( 'Subscribing...', 'progress-planner' ), + 'saving' => \esc_html__( 'Saving...', 'progress-planner' ), + 'snooze' => \esc_html__( 'Snooze', 'progress-planner' ), + 'snoozeDurationOneWeek' => \esc_html__( '1 week', 'progress-planner' ), + 'snoozeDurationOneMonth' => \esc_html__( '1 month', 'progress-planner' ), + 'snoozeDurationThreeMonths' => \esc_html__( '3 months', 'progress-planner' ), + 'snoozeDurationSixMonths' => \esc_html__( '6 months', 'progress-planner' ), + 'snoozeDurationOneYear' => \esc_html__( '1 year', 'progress-planner' ), + 'snoozeDurationForever' => \esc_html__( 'forever', 'progress-planner' ), + 'snoozeThisTask' => \esc_html__( 'Snooze this task?', 'progress-planner' ), + 'subscribed' => \esc_html__( 'Subscribed...', 'progress-planner' ), + 'subscribing' => \esc_html__( 'Subscribing...', 'progress-planner' ), /* translators: %s: The task content. */ - 'taskCompleted' => \esc_html__( "Task '%s' completed and moved to the bottom", 'progress-planner' ), + 'taskCompleted' => \esc_html__( "Task '%s' completed and moved to the bottom", 'progress-planner' ), /* translators: %s: The task content. */ - 'taskDelete' => \esc_html__( "Delete task '%s'", 'progress-planner' ), - 'taskMovedDown' => \esc_html__( 'Task moved down', 'progress-planner' ), - 'taskMovedUp' => \esc_html__( 'Task moved up', 'progress-planner' ), + 'taskDelete' => \esc_html__( "Delete task '%s'", 'progress-planner' ), + 'taskMovedDown' => \esc_html__( 'Task moved down', 'progress-planner' ), + 'taskMovedUp' => \esc_html__( 'Task moved up', 'progress-planner' ), /* translators: %s: The task content. */ - 'taskMoveDown' => \esc_html__( "Move task '%s' down", 'progress-planner' ), + 'taskMoveDown' => \esc_html__( "Move task '%s' down", 'progress-planner' ), /* translators: %s: The task content. */ - 'taskMoveUp' => \esc_html__( "Move task '%s' up", 'progress-planner' ), + 'taskMoveUp' => \esc_html__( "Move task '%s' up", 'progress-planner' ), /* translators: %s: The task content. */ - 'taskNotCompleted' => \esc_html__( "Task '%s' marked as not completed and moved to the top", 'progress-planner' ), - 'video' => \esc_html__( 'Video', 'progress-planner' ), - 'watchVideo' => \esc_html__( 'Watch video', 'progress-planner' ), - 'disabledRRCheckboxTooltip' => \esc_html__( 'Don\'t worry! This task will be checked off automatically when you\'ve completed it.', 'progress-planner' ), - 'opensInNewWindow' => \esc_html__( 'Opens in new window', 'progress-planner' ), + 'taskNotCompleted' => \esc_html__( "Task '%s' marked as not completed and moved to the top", 'progress-planner' ), + 'video' => \esc_html__( 'Video', 'progress-planner' ), + 'watchVideo' => \esc_html__( 'Watch video', 'progress-planner' ), + 'disabledRRCheckboxTooltip' => \esc_html__( 'Don\'t worry! This task will be checked off automatically when you\'ve completed it.', 'progress-planner' ), + 'remindMeToReviewContent' => \esc_html__( 'Remind me to review content', 'progress-planner' ), + 'remindMeToReviewContentDate' => \esc_html__( 'Reminder date', 'progress-planner' ), + 'remindMeToReviewContentSuccess' => \esc_html__( 'Reminder set for:', 'progress-planner' ), + 'remindMeToReviewContentError' => \esc_html__( 'Failed to set reminder. Please try again.', 'progress-planner' ), + 'remindMeToReviewContentSetting' => \esc_html__( 'Setting reminder...', 'progress-planner' ), + 'opensInNewWindow' => \esc_html__( 'Opens in new window', 'progress-planner' ), /* translators: %s: The plugin name. */ - 'installPlugin' => \esc_html__( 'Install and activate the "%s" plugin', 'progress-planner' ), + 'installPlugin' => \esc_html__( 'Install and activate the "%s" plugin', 'progress-planner' ), /* translators: %s: The plugin name. */ - 'activatePlugin' => \esc_html__( 'Activate plugin "%s"', 'progress-planner' ), - 'installing' => \esc_html__( 'Installing...', 'progress-planner' ), - 'installed' => \esc_html__( 'Installed', 'progress-planner' ), - 'alreadyInstalled' => \esc_html__( 'Already installed', 'progress-planner' ), - 'installFailed' => \esc_html__( 'Install failed', 'progress-planner' ), - 'activating' => \esc_html__( 'Activating...', 'progress-planner' ), - 'activated' => \esc_html__( 'Activated', 'progress-planner' ), - 'activateFailed' => \esc_html__( 'Activation failed', 'progress-planner' ), + 'activatePlugin' => \esc_html__( 'Activate plugin "%s"', 'progress-planner' ), + 'installing' => \esc_html__( 'Installing...', 'progress-planner' ), + 'installed' => \esc_html__( 'Installed', 'progress-planner' ), + 'alreadyInstalled' => \esc_html__( 'Already installed', 'progress-planner' ), + 'installFailed' => \esc_html__( 'Install failed', 'progress-planner' ), + 'activating' => \esc_html__( 'Activating...', 'progress-planner' ), + 'activated' => \esc_html__( 'Activated', 'progress-planner' ), + 'activateFailed' => \esc_html__( 'Activation failed', 'progress-planner' ), ]; } diff --git a/classes/class-suggested-tasks-db.php b/classes/class-suggested-tasks-db.php index 960886cf9..9c5c9eb43 100644 --- a/classes/class-suggested-tasks-db.php +++ b/classes/class-suggested-tasks-db.php @@ -175,6 +175,7 @@ public function update_recommendation( $id, $data ) { switch ( $key ) { case 'points': case 'prpl_points': + case 'prpl_available_at': case 'prpl_popover_id': $update_meta[ 'prpl_' . \str_replace( 'prpl_', '', (string) $key ) ] = $value; break; diff --git a/classes/class-suggested-tasks.php b/classes/class-suggested-tasks.php index 1adcf03c9..adb955656 100644 --- a/classes/class-suggested-tasks.php +++ b/classes/class-suggested-tasks.php @@ -489,13 +489,17 @@ public function get_tasks_in_rest_format( array $args = [] ) { continue; } - $category_tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( - [ - 'category' => $category_slug, - 'posts_per_page' => 0 < $args['posts_per_page'] ? $args['posts_per_page'] : $max_items, - 'post_status' => $args['post_status'], - ] - ); + $get_tasks_args = [ + 'category' => $category_slug, + 'posts_per_page' => 0 < $args['posts_per_page'] ? $args['posts_per_page'] : $max_items, + 'post_status' => $args['post_status'], + ]; + + if ( ! empty( $args['meta_query'] ) ) { + $get_tasks_args['meta_query'] = $args['meta_query']; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + } + + $category_tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( $get_tasks_args ); if ( ! empty( $category_tasks ) ) { $tasks[ $category_slug ] = []; diff --git a/classes/class-todo.php b/classes/class-todo.php index 49ac52824..dcaa40a08 100644 --- a/classes/class-todo.php +++ b/classes/class-todo.php @@ -23,6 +23,9 @@ public function __construct() { // Handle user tasks creation. \add_action( 'rest_after_insert_prpl_recommendations', [ $this, 'handle_creating_user_task' ], 10, 3 ); + + // Set a reminder for the current post. + \add_action( 'wp_ajax_progress_planner_set_reminder', [ $this, 'set_reminder' ] ); } /** @@ -96,5 +99,93 @@ public function handle_creating_user_task( $post, $request, $creating ) { return; } } + + /** + * Set a reminder for the current post. + * + * @return void + */ + public function set_reminder() { + // Check the nonce. + if ( ! \check_ajax_referer( 'progress_planner', 'nonce', false ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid nonce.', 'progress-planner' ) ] ); + } + + $post_id = isset( $_POST['post_id'] ) ? \sanitize_text_field( \wp_unslash( $_POST['post_id'] ) ) : ''; + if ( ! $post_id ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Missing post ID.', 'progress-planner' ) ] ); + } + + $post_title = isset( $_POST['post_title'] ) ? \sanitize_text_field( \wp_unslash( $_POST['post_title'] ) ) : ''; + if ( ! $post_title ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Missing post title.', 'progress-planner' ) ] ); + } + + $reminder_date = isset( $_POST['reminder_date'] ) ? \sanitize_text_field( \wp_unslash( $_POST['reminder_date'] ) ) : ''; + if ( ! $reminder_date ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Missing reminder date.', 'progress-planner' ) ] ); + } + + $reminder_date_timestamp = \strtotime( $reminder_date ); + if ( ! $reminder_date_timestamp ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid reminder date.', 'progress-planner' ) ] ); + } + + // Check if we have an existing reminder for this post. + $posts = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( + [ + 'post_status' => [ 'publish' ], + 'numberposts' => 1, + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + [ + 'key' => 'prpl_target_post_id', + 'value' => $post_id, + 'compare' => '=', + ], + [ + 'key' => 'prpl_available_at', + 'compare' => 'EXISTS', + ], + ], + 'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + [ + 'taxonomy' => 'prpl_recommendations_provider', + 'field' => 'slug', + 'terms' => 'user', + ], + ], + ] + ); + + // If we have an existing task, skip. + if ( ! empty( $posts ) ) { + // Update the existing task. + \progress_planner()->get_suggested_tasks_db()->update_recommendation( + $posts[0]->ID, + [ + 'post_title' => $post_title, + 'prpl_available_at' => $reminder_date_timestamp, + ] + ); + } else { + // We're creating a new task. + \progress_planner()->get_suggested_tasks_db()->add( + [ + 'task_id' => 'user-task-' . \md5( $post_id . '-' . \microtime( true ) ), + /* translators: %s: The post title. */ + 'post_title' => \sprintf( __( 'Review %s', 'progress-planner' ), $post_title ), + 'provider_id' => 'user', + 'category' => 'user', + 'status' => 'publish', + 'available_at' => $reminder_date_timestamp, + 'target_post_id' => $post_id, + 'dismissable' => true, + 'snoozable' => false, + ] + ); + } + + \wp_send_json_success( [ 'message' => \esc_html__( 'Reminder set.', 'progress-planner' ) ] ); + } } // phpcs:enable Generic.Commenting.Todo