From dd4c6bebe2b5716b8d0c7936e96dc89937dfc999 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Thu, 3 Apr 2025 13:06:28 +0200 Subject: [PATCH 1/2] generate "remind me to review post" task --- assets/js/editor.js | 127 +++++++++++++++++++++++++++++++- classes/admin/class-editor.php | 2 + classes/admin/class-enqueue.php | 70 +++++++++--------- classes/class-todo.php | 45 +++++++++++ 4 files changed, 210 insertions(+), 34 deletions(-) diff --git a/assets/js/editor.js b/assets/js/editor.js index 6f6ac18c5..241ab11c9 100644 --- a/assets/js/editor.js +++ b/assets/js/editor.js @@ -246,6 +246,130 @@ const PrplLessonItemsHTML = () => { ); }; +/** + * Render the Remind Me button section. + * + * @return {Element} Element to render. + */ +const PrplRemindMeSection = () => { + // Callback function for the Remind Me button + const handleRemindMeClick = () => { + // 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( '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, + } + ); + } 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, + } + ); + } ); + }; + + return el( + PanelBody, + { + key: 'progress-planner-sidebar-remind-me-section', + title: prplL10n( 'remindMeToReviewContent' ), + initialOpen: true, + }, + el( + 'div', + { + style: { + padding: '10px 0', + }, + }, + el( + Button, + { + key: 'progress-planner-sidebar-remind-me-button', + onClick: handleRemindMeClick, + icon: 'clock', + variant: 'secondary', + style: { + width: '100%', + margin: '15px 0', + color: '#38296D', + boxShadow: 'inset 0 0 0 1px #38296D', + 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. @@ -282,7 +406,8 @@ const PrplProgressPlannerSidebar = () => }, }, PrplRenderPageTypeSelector(), - PrplLessonItemsHTML() + PrplLessonItemsHTML(), + PrplRemindMeSection() ) ) ); diff --git a/classes/admin/class-editor.php b/classes/admin/class-editor.php index 2ec2058e0..a1a8ed584 100644 --- a/classes/admin/class-editor.php +++ b/classes/admin/class-editor.php @@ -56,6 +56,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 b6b1a18ed..02fd7863f 100644 --- a/classes/admin/class-enqueue.php +++ b/classes/admin/class-enqueue.php @@ -292,53 +292,57 @@ 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' ), + '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 in 30 days', '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' ), ]; } } diff --git a/classes/class-todo.php b/classes/class-todo.php index 20ca173dd..9e1bfe791 100644 --- a/classes/class-todo.php +++ b/classes/class-todo.php @@ -23,6 +23,9 @@ public function __construct() { \add_action( 'wp_ajax_progress_planner_save_user_suggested_task', [ $this, 'save_user_suggested_task' ] ); \add_action( 'wp_ajax_progress_planner_save_suggested_user_tasks_order', [ $this, 'save_suggested_user_tasks_order' ] ); + // Set a reminder for the current post. + \add_action( 'wp_ajax_progress_planner_set_reminder', [ $this, 'set_reminder' ] ); + \add_action( 'progress_planner_task_status_changed', [ $this, 'remove_order_from_completed_user_task' ], 10, 2 ); $this->maybe_change_first_item_points_on_monday(); @@ -121,6 +124,11 @@ public function get_pending_items() { continue; } + // Skip tasks that are not available yet. + if ( isset( $task['available_at'] ) && $task['available_at'] > time() ) { + continue; + } + if ( ! isset( $task['order'] ) ) { $task['order'] = $max_order + 1; ++$max_order; @@ -229,6 +237,43 @@ public function save_suggested_user_tasks_order() { \progress_planner()->get_settings()->set( 'local_tasks', $local_tasks ); } + /** + * 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' ) ] ); + } + + $task = [ + 'task_id' => 'user-task-' . md5( $post_id ), + 'provider_id' => 'user', + 'status' => 'pending', + /* translators: %s: The post title. */ + 'title' => sprintf( __( 'Review %s', 'progress-planner' ), $post_title ), + 'available_at' => time() + 30 * DAY_IN_SECONDS, + 'post_id' => $post_id, + ]; + + // Add the task to the local tasks. + progress_planner()->get_suggested_tasks()->get_local()->add_pending_task( $task ); + + \wp_send_json_success( [ 'message' => \esc_html__( 'Reminder set.', 'progress-planner' ) ] ); + } + /** * Get the points for a new task. * From e0af996a7bc0ac478007dcbd6002ed6f46d91fc2 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Thu, 17 Jul 2025 17:00:07 +0200 Subject: [PATCH 2/2] add date picker & update existing task if set --- assets/js/editor.js | 66 +++++++++++++++++++++++-- classes/admin/class-enqueue.php | 3 +- classes/class-suggested-tasks-db.php | 1 + classes/class-todo.php | 72 +++++++++++++++++++++++----- 4 files changed, 126 insertions(+), 16 deletions(-) diff --git a/assets/js/editor.js b/assets/js/editor.js index 241ab11c9..5f5d8a7f0 100644 --- a/assets/js/editor.js +++ b/assets/js/editor.js @@ -252,8 +252,24 @@ const PrplLessonItemsHTML = () => { * @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(); @@ -275,6 +291,7 @@ const PrplRemindMeSection = () => { 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, { @@ -297,6 +314,8 @@ const PrplRemindMeSection = () => { isDismissible: true, } ); + // Clear the selected date after successful reminder set + setSelectedDate( '' ); } else { // Show error notification wp.data @@ -326,6 +345,9 @@ const PrplRemindMeSection = () => { } ); }; + // Get minimum date (today) + const today = new Date().toISOString().split( 'T' )[ 0 ]; + return el( PanelBody, { @@ -340,18 +362,56 @@ const PrplRemindMeSection = () => { 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, - icon: 'clock', variant: 'secondary', + disabled: ! selectedDate, style: { width: '100%', margin: '15px 0', - color: '#38296D', - boxShadow: 'inset 0 0 0 1px #38296D', + color: selectedDate ? '#38296D' : '#999', + boxShadow: selectedDate + ? 'inset 0 0 0 1px #38296D' + : 'inset 0 0 0 1px #ddd', whiteSpace: 'normal', height: 'auto', minHeight: '60px', diff --git a/classes/admin/class-enqueue.php b/classes/admin/class-enqueue.php index 97018fae6..7838382b6 100644 --- a/classes/admin/class-enqueue.php +++ b/classes/admin/class-enqueue.php @@ -391,7 +391,8 @@ public function get_localized_strings() { '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 in 30 days', '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' ), diff --git a/classes/class-suggested-tasks-db.php b/classes/class-suggested-tasks-db.php index 3aa81db20..7b6af8012 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': $update_meta[ 'prpl_' . \str_replace( 'prpl_', '', (string) $key ) ] = $value; break; diff --git a/classes/class-todo.php b/classes/class-todo.php index 3d70a046f..594ccd712 100644 --- a/classes/class-todo.php +++ b/classes/class-todo.php @@ -122,22 +122,70 @@ public function set_reminder() { \wp_send_json_error( [ 'message' => \esc_html__( 'Missing post title.', 'progress-planner' ) ] ); } - // We're creating a new task. - \progress_planner()->get_suggested_tasks_db()->add( + $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( [ - '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' => \time() + 30 * DAY_IN_SECONDS, - 'target_post_id' => $post_id, - 'dismissable' => true, - 'snoozable' => false, + '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' ) ] ); } }