diff --git a/assets/css/page-widgets/suggested-tasks.css b/assets/css/page-widgets/suggested-tasks.css index 4f83b716c..d1e98f70b 100644 --- a/assets/css/page-widgets/suggested-tasks.css +++ b/assets/css/page-widgets/suggested-tasks.css @@ -495,3 +495,119 @@ } } } + +/*------------------------------------*\ + Page select setting. +\*------------------------------------*/ +.prpl-pages-item { + + &:has(input[type="radio"][value="yes"]:checked), + &:has(input[type="radio"][value="no"]:checked) { + + h3 { + + .icon-exclamation-circle { + display: block; + } + + .icon-check-circle { + display: none; + } + } + } + + &:has(option[value=""]:not(:checked)):has(input[type="radio"][value="yes"]:checked), + &:has(input[type="radio"][value="not-applicable"]:checked) { + + h3 { + + .icon-check-circle { + display: block; + } + + .icon-exclamation-circle { + display: none; + } + } + } + + .item-actions, + .prpl-select-page { + display: flex; + align-items: center; + gap: 1rem; + } + + .remind-button, + .assign-button { + + svg { + width: 1rem; + height: 1rem; + } + } + + h3 { + font-size: 1.15rem; + margin: 0; + + display: flex; + align-items: center; + gap: 0.5rem; + + .icon { + width: 1em; + height: 1em; + display: none; + } + } + + p { + margin-block-start: 0.5rem; + margin-block-end: 1rem; + } + + .radios { + margin-bottom: 1rem; + } + + .prpl-radio-wrapper { + display: flex; + justify-content: space-between; + align-items: center; + + [data-action="select"], + [data-action="create"] { + visibility: hidden; + } + + &:has(input[type="radio"]:checked) { + + [data-action="select"], + [data-action="create"] { + visibility: visible; + } + } + + &:has(input[type="radio"][value="not-applicable"]) { + padding-top: 0.25rem; + + /* Add bit height, because we dont have button or select */ + } + } +} + +/*------------------------------------*\ + Post types selection. +\*------------------------------------*/ +.prpl-post-types-selection { + + label { + display: block; + margin-top: 0.75rem; + + &:first-child { + margin-top: 0; + } + } +} diff --git a/assets/css/settings-page.css b/assets/css/settings-page.css index 70d431ed6..a5a1ebb46 100644 --- a/assets/css/settings-page.css +++ b/assets/css/settings-page.css @@ -211,7 +211,7 @@ } } - .prpl-button { + .prpl-button-link { color: var(--prpl-color-gray-7); text-decoration: none; border: 1px solid var(--prpl-color-border); diff --git a/assets/js/recommendations/set-page.js b/assets/js/recommendations/set-page.js new file mode 100644 index 000000000..fbca6755b --- /dev/null +++ b/assets/js/recommendations/set-page.js @@ -0,0 +1,140 @@ +/* global prplInteractiveTaskFormListener, progressPlanner, prplDocumentReady */ + +/* + * Set page settings (About, Contact, FAQ, etc.) + * + * Dependencies: progress-planner/recommendations/interactive-task + */ + +// Initialize custom submit handlers for all set-page tasks. +prplDocumentReady( function () { + // Find all set-page popovers. + const popovers = document.querySelectorAll( + '[id^="prpl-popover-set-page-"]' + ); + + popovers.forEach( function ( popover ) { + // Extract page name from popover ID (e.g., "prpl-popover-set-page-about" -> "about") + const popoverId = popover.id; + const match = popoverId.match( /prpl-popover-set-page-(.+)/ ); + if ( ! match ) { + return; + } + + const pageName = match[ 1 ]; + const taskId = 'set-page-' + pageName; + + // Skip if already initialized. + if ( popover.dataset.setPageInitialized ) { + return; + } + popover.dataset.setPageInitialized = 'true'; + + prplInteractiveTaskFormListener.customSubmit( { + taskId, + popoverId, + callback: () => { + return new Promise( ( resolve, reject ) => { + const pageValue = document.querySelector( + '#' + + popoverId + + ' input[name="pages[' + + pageName + + '][have_page]"]:checked' + ); + + if ( ! pageValue ) { + reject( { + success: false, + error: new Error( 'Page value not found' ), + } ); + return; + } + + const pageId = document.querySelector( + '#' + + popoverId + + ' select[name="pages[' + + pageName + + '][id]"]' + ); + + fetch( progressPlanner.ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams( { + action: 'prpl_interactive_task_submit_set-page', + nonce: progressPlanner.nonce, + have_page: pageValue.value, + id: pageId ? pageId.value : '', + task_id: taskId, + } ), + } ) + .then( ( response ) => response.json() ) + .then( ( data ) => { + if ( data.success ) { + resolve( { response: data, success: true } ); + } else { + reject( { success: false, error: data } ); + } + } ) + .catch( ( error ) => { + reject( { success: false, error } ); + } ); + } ); + }, + } ); + } ); +} ); + +const prplTogglePageSelectorSettingVisibility = function ( page, value ) { + const itemRadiosWrapperEl = document.querySelector( + `.prpl-pages-item-${ page } .radios` + ); + + if ( ! itemRadiosWrapperEl ) { + return; + } + + const selectPageWrapper = + itemRadiosWrapperEl.querySelector( '.prpl-select-page' ); + + if ( ! selectPageWrapper ) { + return; + } + + // Show only create button. + if ( 'no' === value || 'not-applicable' === value ) { + // Hide
'; + } +} diff --git a/classes/suggested-tasks/providers/class-set-page-contact.php b/classes/suggested-tasks/providers/class-set-page-contact.php new file mode 100644 index 000000000..d97dc4a68 --- /dev/null +++ b/classes/suggested-tasks/providers/class-set-page-contact.php @@ -0,0 +1,55 @@ +'; + \esc_html_e( 'The Contact page is a page that provides a way for your visitors to contact you. It is a great way to help your visitors get in touch with you and ask questions.', 'progress-planner' ); + echo ''; + } +} diff --git a/classes/suggested-tasks/providers/class-set-page-faq.php b/classes/suggested-tasks/providers/class-set-page-faq.php new file mode 100644 index 000000000..001d2d5e2 --- /dev/null +++ b/classes/suggested-tasks/providers/class-set-page-faq.php @@ -0,0 +1,55 @@ +'; + \esc_html_e( 'The FAQ page is a page that provides answers to frequently asked questions about your website. It is a great way to help your visitors find the information they need quickly and easily.', 'progress-planner' ); + echo ''; + } +} diff --git a/classes/suggested-tasks/providers/class-set-page-task.php b/classes/suggested-tasks/providers/class-set-page-task.php new file mode 100644 index 000000000..fe2624b8f --- /dev/null +++ b/classes/suggested-tasks/providers/class-set-page-task.php @@ -0,0 +1,199 @@ +get_admin__enqueue()->enqueue_script( + 'progress-planner/recommendations/set-page', + $this->get_enqueue_data() + ); + self::$script_enqueued = true; + } + } + + /** + * Check if the task condition is satisfied. + * (bool) true means that the task condition is satisfied, meaning that we don't need to add the task or task was completed. + * + * @return bool + */ + public function should_add_task() { + $pages = \progress_planner()->get_admin__page_settings()->get_settings(); + + if ( ! isset( $pages[ static::PAGE_NAME ] ) ) { + return false; + } + + return 'no' === $pages[ static::PAGE_NAME ]['isset']; + } + + /** + * Print the popover input field for the form. + * + * @return void + */ + public function print_popover_form_contents() { + $pages = \progress_planner()->get_admin__page_settings()->get_settings(); + $page = $pages[ static::PAGE_NAME ]; + + \progress_planner()->the_view( + 'setting/page-select.php', + [ + 'prpl_setting' => $page, + 'context' => 'popover', + ] + ); + $this->print_submit_button( \__( 'Set page', 'progress-planner' ) ); + } + + /** + * Handle the interactive task submit. + * + * This is only for interactive tasks that change core permalink settings. + * The $_POST data is expected to be: + * - have_page: (string) The value to update the setting to. + * - id: (int) The ID of the page to update. + * - task_id: (string) The task ID (e.g., "set-page-about") to identify the page type. + * - nonce: (string) The nonce. + * + * @return void + */ + public static function handle_interactive_task_specific_submit() { + + // Check if the user has the necessary capabilities. + if ( ! \current_user_can( 'manage_options' ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'You do not have permission to update settings.', 'progress-planner' ) ] ); + } + + // Check the nonce. + if ( ! \check_ajax_referer( 'progress_planner', 'nonce', false ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid nonce.', 'progress-planner' ) ] ); + } + + if ( ! isset( $_POST['have_page'] ) || ! isset( $_POST['task_id'] ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Missing value.', 'progress-planner' ) ] ); + } + + $have_page = \trim( \sanitize_text_field( \wp_unslash( $_POST['have_page'] ) ) ); + $id = isset( $_POST['id'] ) ? (int) \trim( \sanitize_text_field( \wp_unslash( $_POST['id'] ) ) ) : 0; + $task_id = \trim( \sanitize_text_field( \wp_unslash( $_POST['task_id'] ) ) ); + + if ( empty( $have_page ) || empty( $task_id ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid page value.', 'progress-planner' ) ] ); + } + + // Extract page name from task ID (e.g., "set-page-about" -> "about"). + $page_name = \str_replace( 'set-page-', '', $task_id ); + if ( empty( $page_name ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid task ID.', 'progress-planner' ) ] ); + } + + // Validate page name against allowed page types. + $pages = \progress_planner()->get_admin__page_settings()->get_settings(); + if ( ! isset( $pages[ $page_name ] ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid page name.', 'progress-planner' ) ] ); + } + + // Update the page value. + \progress_planner()->get_admin__page_settings()->set_page_values( + [ + $page_name => [ + 'id' => (int) $id, + 'have_page' => $have_page, // yes, no, not-applicable. + ], + ] + ); + + \wp_send_json_success( [ 'message' => \esc_html__( 'Page updated.', 'progress-planner' ) ] ); + } + + /** + * Add task actions specific to this task. + * + * @param array $data The task data. + * @param array $actions The existing actions. + * + * @return array + */ + public function add_task_actions( $data = [], $actions = [] ) { + $actions[] = [ + 'priority' => 10, + 'html' => '' . \esc_html__( 'Set', 'progress-planner' ) . '', + ]; + + return $actions; + } +} diff --git a/classes/suggested-tasks/providers/class-set-valuable-post-types.php b/classes/suggested-tasks/providers/class-set-valuable-post-types.php index b93aea305..e54233acb 100644 --- a/classes/suggested-tasks/providers/class-set-valuable-post-types.php +++ b/classes/suggested-tasks/providers/class-set-valuable-post-types.php @@ -10,7 +10,7 @@ /** * Add tasks for settings saved. */ -class Set_Valuable_Post_Types extends Tasks { +class Set_Valuable_Post_Types extends Tasks_Interactive { /** * The provider ID. @@ -19,6 +19,13 @@ class Set_Valuable_Post_Types extends Tasks { */ protected const PROVIDER_ID = 'set-valuable-post-types'; + /** + * The provider ID. + * + * @var string + */ + public const POPOVER_ID = 'set-valuable-post-types'; + /** * Whether the task is an onboarding task. * @@ -56,6 +63,59 @@ protected function get_url() { */ public function init() { \add_action( 'progress_planner_settings_form_options_stored', [ $this, 'remove_upgrade_option' ] ); + \add_action( 'wp_ajax_prpl_interactive_task_submit_set-valuable-post-types', [ $this, 'handle_interactive_task_specific_submit' ] ); + + // On late init hook we need to check if the public post types are changed. + \add_action( 'init', [ $this, 'check_public_post_types' ], PHP_INT_MAX - 1 ); + } + + /** + * Check if the public post types are changed. + * + * @return void + */ + public function check_public_post_types() { + $previosly_set_public_post_types = \array_unique( \get_option( 'progress_planner_public_post_types', [] ) ); + $public_post_types = \array_unique( \progress_planner()->get_settings()->get_public_post_types() ); + + // Sort the public post types. + \sort( $previosly_set_public_post_types ); + \sort( $public_post_types ); + + // Compare the previously set public post types with the current public post types. + if ( $previosly_set_public_post_types === $public_post_types ) { + return; + } + + // Update the previously set public post types. + \update_option( 'progress_planner_public_post_types', $public_post_types ); + + // Exit if post type was removed, or it is not public anymore, since the user will not to able to make different selection. + if ( count( $public_post_types ) < count( $previosly_set_public_post_types ) ) { + return; + } + + // If we're here that means that there is new public post type. + + // Check if the task exists, if it does and it is published do nothing. + $task = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'provider_id' => static::PROVIDER_ID ] ); + if ( ! empty( $task ) && 'publish' === $task[0]->post_status ) { + return; + } + + // If it is trashed, change it's status to publish. + if ( ! empty( $task ) && 'trash' === $task[0]->post_status ) { + \wp_update_post( + [ + 'ID' => $task[0]->ID, + 'post_status' => 'publish', + ] + ); + return; + } + + // If we're here then we need to add it. + \progress_planner()->get_suggested_tasks_db()->add( $this->modify_injection_task_data( $this->get_task_details() ) ); } /** @@ -119,6 +179,77 @@ public function is_task_completed( $task_id = '' ) { return false === \get_option( 'progress_planner_set_valuable_post_types', false ); } + /** + * Handle the interactive task submit. + * + * @return void + */ + public function handle_interactive_task_specific_submit() { + // Check if the user has the necessary capabilities. + if ( ! \current_user_can( 'manage_options' ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'You do not have permission to update settings.', 'progress-planner' ) ] ); + } + + // Check the nonce. + if ( ! \check_ajax_referer( 'progress_planner', 'nonce', false ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid nonce.', 'progress-planner' ) ] ); + } + + if ( ! isset( $_POST['prpl-post-types-include'] ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Missing post types.', 'progress-planner' ) ] ); + } + + $post_types = \wp_unslash( $_POST['prpl-post-types-include'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- array elements are sanitized below. + $post_types = explode( ',', $post_types ); + $post_types = array_map( 'sanitize_text_field', $post_types ); + + \progress_planner()->get_admin__page_settings()->save_post_types( $post_types ); + + \wp_send_json_success( [ 'message' => \esc_html__( 'Setting updated.', 'progress-planner' ) ] ); + } + + /** + * Print the popover instructions. + * + * @return void + */ + public function print_popover_instructions() { + echo ''; + \esc_html_e( 'You\'re in control of what counts as valuable content. We\'ll track and reward activity only for the post types you select here.', 'progress-planner' ); + echo '
'; + } + + /** + * Print the popover form contents. + * + * @return void + */ + public function print_popover_form_contents() { + $prpl_saved_settings = \progress_planner()->get_settings()->get_post_types_names(); + $prpl_post_types = \progress_planner()->get_settings()->get_public_post_types(); + + // Early exit if there are no public post types. + if ( empty( $prpl_post_types ) ) { + return; + } + ?> +