From 0e34b790b142ec38bc84c021d5f2e0a9918e4e0f Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Mon, 26 Jan 2026 10:33:02 +0100 Subject: [PATCH 1/6] Revert to old onboarding system This reverts the new onboarding introduced in PR #714 and subsequent onboarding-related PRs (#730, #733, #736, #741). The new onboarding work is preserved in branch: filip/new-onboarding-preserved Co-Authored-By: Claude Opus 4.5 --- assets/css/admin.css | 18 - assets/css/onboard.css | 23 + assets/css/onboarding/onboarding.css | 1359 ----------------- assets/css/welcome.css | 77 + assets/images/onboarding/icon_image.svg | 3 - assets/images/onboarding/icon_info_solid.svg | 3 - .../images/onboarding/neglected_site_ravi.svg | 1 - assets/images/onboarding/success_ravi.svg | 1 - .../images/onboarding/thumbs_up_ravi_rtl.svg | 1 - assets/js/license-generator.js | 140 -- assets/js/onboard.js | 168 ++ assets/js/onboarding/OnboardTask.js | 470 ------ assets/js/onboarding/onboarding.js | 501 ------ assets/js/onboarding/steps/BadgesStep.js | 62 - .../js/onboarding/steps/EmailFrequencyStep.js | 208 --- assets/js/onboarding/steps/FirstTaskStep.js | 68 - assets/js/onboarding/steps/MoreTasksStep.js | 143 -- assets/js/onboarding/steps/OnboardingStep.js | 326 ---- assets/js/onboarding/steps/SettingsStep.js | 492 ------ assets/js/onboarding/steps/WelcomeStep.js | 165 -- assets/js/onboarding/steps/WhatsWhatStep.js | 34 - assets/js/settings.js | 52 +- classes/admin/class-page-settings.php | 10 +- classes/admin/class-page.php | 9 + classes/class-base.php | 8 +- classes/class-onboard-wizard.php | 604 -------- classes/class-suggested-tasks.php | 27 +- .../suggested-tasks/class-tasks-interface.php | 17 - .../providers/class-blog-description.php | 35 +- .../providers/class-select-locale.php | 77 +- .../providers/class-select-timezone.php | 91 +- .../providers/class-site-icon.php | 35 +- .../suggested-tasks/providers/class-tasks.php | 33 +- classes/ui/class-chart.php | 10 +- classes/utils/class-date.php | 11 +- classes/utils/class-debug-tools.php | 73 - classes/utils/class-playground.php | 56 +- tests/e2e/sequential/onboarding.spec.js | 76 +- tests/phpunit/test-class-onboard-wizard.php | 485 ------ tests/phpunit/test-class-onboarding-tasks.php | 42 - views/admin-page.php | 9 +- views/onboarding/badges.php | 67 - views/onboarding/email-frequency.php | 107 -- views/onboarding/first-task.php | 47 - views/onboarding/form-inputs/checkbox.php | 40 - views/onboarding/form-inputs/radio.php | 40 - views/onboarding/more-tasks.php | 85 -- views/onboarding/quit-confirmation.php | 46 - views/onboarding/settings.php | 216 --- .../onboarding/tasks/core-blogdescription.php | 38 - views/onboarding/tasks/core-siteicon.php | 55 - views/onboarding/tasks/select-locale.php | 63 - views/onboarding/tasks/select-timezone.php | 37 - views/onboarding/welcome.php | 83 - views/onboarding/whats-what.php | 63 - views/welcome.php | 157 ++ 56 files changed, 622 insertions(+), 6545 deletions(-) create mode 100644 assets/css/onboard.css delete mode 100644 assets/css/onboarding/onboarding.css create mode 100644 assets/css/welcome.css delete mode 100644 assets/images/onboarding/icon_image.svg delete mode 100644 assets/images/onboarding/icon_info_solid.svg delete mode 100644 assets/images/onboarding/neglected_site_ravi.svg delete mode 100644 assets/images/onboarding/success_ravi.svg delete mode 100644 assets/images/onboarding/thumbs_up_ravi_rtl.svg delete mode 100644 assets/js/license-generator.js create mode 100644 assets/js/onboard.js delete mode 100644 assets/js/onboarding/OnboardTask.js delete mode 100644 assets/js/onboarding/onboarding.js delete mode 100644 assets/js/onboarding/steps/BadgesStep.js delete mode 100644 assets/js/onboarding/steps/EmailFrequencyStep.js delete mode 100644 assets/js/onboarding/steps/FirstTaskStep.js delete mode 100644 assets/js/onboarding/steps/MoreTasksStep.js delete mode 100644 assets/js/onboarding/steps/OnboardingStep.js delete mode 100644 assets/js/onboarding/steps/SettingsStep.js delete mode 100644 assets/js/onboarding/steps/WelcomeStep.js delete mode 100644 assets/js/onboarding/steps/WhatsWhatStep.js delete mode 100644 classes/class-onboard-wizard.php delete mode 100644 tests/phpunit/test-class-onboard-wizard.php delete mode 100644 tests/phpunit/test-class-onboarding-tasks.php delete mode 100644 views/onboarding/badges.php delete mode 100644 views/onboarding/email-frequency.php delete mode 100644 views/onboarding/first-task.php delete mode 100644 views/onboarding/form-inputs/checkbox.php delete mode 100644 views/onboarding/form-inputs/radio.php delete mode 100644 views/onboarding/more-tasks.php delete mode 100644 views/onboarding/quit-confirmation.php delete mode 100644 views/onboarding/settings.php delete mode 100644 views/onboarding/tasks/core-blogdescription.php delete mode 100644 views/onboarding/tasks/core-siteicon.php delete mode 100644 views/onboarding/tasks/select-locale.php delete mode 100644 views/onboarding/tasks/select-timezone.php delete mode 100644 views/onboarding/welcome.php delete mode 100644 views/onboarding/whats-what.php create mode 100644 views/welcome.php diff --git a/assets/css/admin.css b/assets/css/admin.css index 09733f4e85..192f8d591b 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -80,24 +80,6 @@ body.toplevel_page_progress-planner { margin-top: var(--prpl-padding); } -/*------------------------------------*\ - Styles for the container of the page when the privacy policy is not accepted. -\*------------------------------------*/ -.prpl-pp-not-accepted { - - .prpl-start-onboarding-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--prpl-padding); - } - - .prpl-start-onboarding-graphic { - width: 250px; - } -} - /*------------------------------------*\ Generic styles. \*------------------------------------*/ diff --git a/assets/css/onboard.css b/assets/css/onboard.css new file mode 100644 index 0000000000..bac24ea7dd --- /dev/null +++ b/assets/css/onboard.css @@ -0,0 +1,23 @@ +#prpl-onboarding-form .prpl-form-fields label { + display: grid; + grid-template-columns: 1fr 3fr; + margin-bottom: 0.5em; + gap: var(--prpl-padding); +} + +#prpl-onboarding-form label > span:has(input[type="checkbox"]) { + display: flex; + align-items: baseline; +} + +.prpl-onboard-form-radio-select { + + label { + display: block !important; + } +} + +#prpl-onboarding-submit-wrapper { + display: flex; + align-items: center; +} diff --git a/assets/css/onboarding/onboarding.css b/assets/css/onboarding/onboarding.css deleted file mode 100644 index ae11f3057c..0000000000 --- a/assets/css/onboarding/onboarding.css +++ /dev/null @@ -1,1359 +0,0 @@ -.prpl-popover-onboarding { - - --prpl-color-text: #4b5563; - - /* Paper */ - --prpl-onboarding-popover-background: var(--prpl-background-paper, #fff); - - /* Steps navigation */ - --prpl-background-steps: var(--prpl-background, #f6f7f9); - --prpl-background-info: var(--prpl-background-content, #f6f5fb); - --prpl-background-step-active: var(--prpl-graph-color-4, #534786); - --prpl-color-number-step-active: var(--prpl-background-paper, #fff); - --prpl-color-ui-icon: #6b7280; /* already exists in variables-color.css */ - --prpl-color-text-step-active: var(--prpl-color-headings, #38296d); - --prpl-color-border: #d1d5db; /* already exists in variables-color.css */ - - /* Button secondary */ - --prpl-color-button-secondary: var(--prpl-background-point, #f9b23c ); - --prpl-color-button-secondary-hover: var(--prpl-color-monthly, #faa310); - --prpl-color-button-secondary-text: #374151; - --prpl-color-button-secondary-icon: var(--prpl-color-button-secondary-text); - - /* Button disabled */ - --prpl-color-button-inactive: var(--prpl-color-gauge-remain, #e1e3e7 ); - --prpl-color-button-inactive-text: var(--prpl-color-text, #4b5563); - - --prpl-background-step-label: var(--prpl-background-step-active); - --prpl-color-step-label: var(--prpl-background-paper, #fff); - - /* Required text */ - --prpl-color-text-error: var(--prpl-color-button-primary, #dd324f); - --prpl-color-alert-error: #e73136; /* already exists in variables-color.css */ - - /* General error */ - --prpl-background-alert-error: #fdeded;/* already exists in variables-color.css */ - --prpl-color-alert-error-text: #7f1d1d; /* already exists in variables-color.css */ - - /* Custom contros (inputs, radio, checkbox) */ - --prpl-color-selection-controls-inactive: #9ca3af; - --prpl-color-selection-controls: var(--prpl-graph-color-4, #534786); - - --prpl-color-field-border: var(--prpl-color-border); - --prpl-color-field-border-active: var(--prpl-color-alert-info, #2563eb); - --prpl-background-field-active: var(--prpl-background-alert-info, #eff6ff); - - - font-family: system-ui, Arial, sans-serif; - font-size: 16px; - - padding: 0; - box-sizing: border-box; - - border-radius: 8px; - font-weight: 400; - max-height: 82vh; - width: 1200px; - max-width: 80vw; - color: var(--prpl-color-text); - background-color: var(--prpl-onboarding-popover-background); - border: 1px solid var(--prpl-color-border); - box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.07),-2px 0 6px rgba(0, 0, 0, 0.07); - - /* Popover backdrop. */ - &::backdrop { - background: rgba(0, 0, 0, 0.5); - } - - /* Popover close button. */ - .prpl-popover-close { - position: absolute; - top: 5px; - right: 5px; - padding: 8px; - cursor: pointer; - background: none; - border: none; - color: var(--prpl-color-text); - z-index: 10; - } - - /* General styles. */ - & * { - box-sizing: border-box; - } - - p { - font-size: 16px; - margin-top: 0; - margin-bottom: 8px; - - &:last-child { - margin-bottom: 0; - } - } - - h2 { - font-family: system-ui, Arial, sans-serif; - margin-top: 0; - margin-bottom: 12px; - font-size: 20px; - } - - h3 { - font-family: system-ui, Arial, sans-serif; - margin-top: 0; - margin-bottom: 8px; - font-size: 16px; - font-weight: 600; - color: var(--prpl-color-text); - - &:last-child { - margin-bottom: 0; - } - } - - /* Copied from WP Core CSS. */ - select { - font-size: 14px; - line-height: 2; - color: #2c3338; - border-color: var(--prpl-color-field-border); - box-shadow: none; - border-radius: 3px; - padding: 0 24px 0 8px; - min-height: 30px; - - /* max-width: 400px; */ - width: 100%; - -webkit-appearance: none; - background: #fff url(data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%206l5%205%205-5%202%201-7%207-7-7%202-1z%22%20fill%3D%22%23555%22%2F%3E%3C%2Fsvg%3E) no-repeat right 5px top 55%; - background-size: 16px 16px; - cursor: pointer; - vertical-align: middle; - } - - input[type="text"], - input[type="email"] { - width: 100%; - padding: 8px; - border: 1px solid var(--prpl-color-field-border); - border-radius: 4px; - font-size: 14px; - line-height: 1.4; - color: var(--prpl-color-text); - } - - /* Used for radio and checkbox inputs. */ - .prpl-custom-inputs-wrapper{ - padding-left: 3px; - - /* To prevent custom radio and checkbox from being cut off. */ - display: flex; - flex-direction: column; - gap: 8px; - - .prpl-checkbox-wrapper, - .prpl-radio-wrapper { - display: flex; - flex-direction: column; - justify-content: flex-start; - } - - /* Hide the default input, because WP has it's own styles (which include pseudo-elements). */ - .prpl-custom-checkbox input[type="checkbox"], - .prpl-custom-radio input[type="radio"] { - position: absolute; - opacity: 0; - width: 0; - height: 0; - } - - /* Shared styles for the custom radios and checkboxes. */ - .prpl-custom-control { - display: inline-block; - vertical-align: middle; - margin-right: 12px; - width: 20px; - height: 20px; - box-sizing: border-box; - position: relative; - transition: border-color 0.2s, background 0.2s; - flex-shrink: 0; - } - - /* Label text styling */ - .prpl-custom-checkbox, - .prpl-custom-radio { - display: flex; - align-items: center; - margin-bottom: 8px; - cursor: pointer; - user-select: none; - } - - /* Checkbox styles */ - .prpl-custom-checkbox { - - .prpl-custom-control { - border: 1px solid var(--prpl-color-selection-controls-inactive); - - /* border-radius: 6px; */ - background-color: var(--prpl-onboarding-popover-background); - } - - input[type="checkbox"] { - - /* Checkbox checked (on) */ - &:checked+.prpl-custom-control { - background: var(--prpl-color-selection-controls); - border-color: var(--prpl-color-selection-controls); - } - } - - /* Checkmark */ - .prpl-custom-control::after { - content: ""; - position: absolute; - left: 6px; - top: 2px; - width: 4px; - height: 9px; - border: solid var(--prpl-onboarding-popover-background); - border-width: 0 2px 2px 0; - opacity: 0; - transform: scale(0.8) rotate(45deg); - transition: opacity 0.2s, transform 0.2s; - } - - input[type="checkbox"]:checked+.prpl-custom-control::after { - opacity: 1; - transform: scale(1) rotate(45deg); - } - } - - /* Radio styles */ - .prpl-custom-radio { - - .prpl-custom-control { - border: 1px solid var(--prpl-color-selection-controls-inactive); - border-radius: 50%; - background-color: var(--prpl-onboarding-popover-background); - } - - /* Radio hover (off) */ - input[type="radio"] { - - /* Radio checked (on) */ - &:checked+.prpl-custom-control { - background: var(--prpl-color-selection-controls); - border-color: var(--prpl-color-selection-controls); - } - } - - /* Radio dot */ - .prpl-custom-control::after { - content: ""; - position: absolute; - top: 5px; - left: 5px; - width: 8px; - height: 8px; - background-color: var(--prpl-onboarding-popover-background); - border-radius: 50%; - opacity: 0; - transition: opacity 0.2s; - } - - input[type="radio"]:checked+.prpl-custom-control::after { - opacity: 1; - background-color: var(--prpl-onboarding-popover-background); - } - } - } - - /* Main layout container */ - .prpl-onboarding-layout { - display: flex; - - /* gap: 24px; */ - min-height: 350px; - } - - /* Left column: Step navigation */ - .prpl-onboarding-navigation { - width: 340px; - background: var(--prpl-background-steps); - border-right: none; - padding: 32px 24px 24px 24px; - flex-shrink: 0; - display: flex; - flex-direction: column; - justify-content: space-between; - gap: 24px; - } - - .prpl-onboarding-logo { - height: 60px; - max-height: 100%; - - img, svg { - height: 100%; - width: auto; - max-width: 100%; - } - } - - .prpl-step-list { - list-style: none; - margin: 0; - padding: 0; - } - - .prpl-nav-step-item { - margin-top: 10px; - display: flex; - align-items: flex-start; - gap: 14px; - padding: 0; - margin-bottom: 4px; - cursor: default; - - &:first-child { - margin-top: 0; - } - } - - .prpl-step-icon { - width: 20px; - height: 20px; - border-radius: 50%; - border: 1px solid var(--prpl-color-border); - color: var(--prpl-color-text); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - margin-top: 2px; - font-size: 14px; - } - - .prpl-nav-step-item.prpl-active .prpl-step-icon { - background: var(--prpl-background-step-active); - border-color: var(--prpl-background-step-active); - color: var(--prpl-color-number-step-active); - } - - .prpl-nav-step-item.prpl-completed .prpl-step-icon { - background: var(--prpl-color-ui-icon); - border-color: var(--prpl-color-ui-icon); - color: var(--prpl-background-paper); - } - - .prpl-step-label { - font-size: 15px; - color: var(--prpl-color-ui-icon); - line-height: 1.5; - } - - .prpl-nav-step-item.prpl-active .prpl-step-label { - color: var(--prpl-color-text-step-active); - font-weight: 700; - } - - .prpl-nav-step-item.prpl-completed .prpl-step-label { - color: var(--prpl-color-text-step-active); - } - - #prpl-onboarding-mobile-step-label { - display: none; - margin-bottom: 4px; - font-size: 14px; - color: var(--prpl-background-step-active); - font-weight: 600; - } - - /* Right section: Content area */ - .prpl-onboarding-content { - padding: 32px 24px; - flex: 1; - display: flex; - flex-direction: column; - - /* gap: 24px; */ - } - - .tour-content-wrapper { - flex: 1; - - /* overflow-y: auto; */ - } - - .tour-header { - - .tour-title { - color: var(--prpl-background-step-active); - font-weight: 600; - } - } - - .tour-footer { - margin-top: 16px; - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 10px; - width: 100%; - - .prpl-tour-next-wrapper { - margin-left: auto; - display: flex; - align-items: center; - gap: 10px; - } - } - - .tour-content { - font-size: 16px; - line-height: 1.5; - - img { - max-width: 100%; - height: auto; - } - - } - - .prpl-columns-wrapper-flex { - display: flex; - gap: 24px; - - /* overflow: hidden; */ - - /* padding-bottom: 10px; */ - - .prpl-background-content { - padding: 20px; - border-radius: 12px; - background-color: var(--prpl-background-info); - } - - .prpl-column { - flex-grow: 1; - flex-basis: 50%; - flex-direction: column; - } - - &.prpl-columns-2-1 { - - .prpl-column:first-child { - flex: 2 1 0; - min-width: 0; - } - - .prpl-column:last-child { - flex: 1 1 0; - min-width: 0; - } - } - - &.prpl-columns-1-2 { - - .prpl-column:first-child { - flex: 1 1 0; - min-width: 0; - } - - .prpl-column:last-child { - flex: 2 1 0; - min-width: 0; - } - } - } - - .prpl-btn { - display: inline-block; - margin: 0; - padding: 12px 20px; - - text-decoration: none; - cursor: pointer; - font-size: 16px; - - /* color: var(--prpl-color-button-primary-text); */ - - /* background: var(--prpl-color-button-primary); */ - background-color: var(--prpl-color-button-secondary); - color: var(--prpl-color-button-secondary-text); - line-height: 1.25; - box-shadow: none; - border: none; - border-radius: 6px; - transition: all 0.25s ease-in-out; - font-weight: 600; - text-align: center; - box-sizing: border-box; - position: relative; - z-index: 1; - - flex-shrink: 0; - - /* &:not([disabled]):hover, - &:not([disabled]):focus { - background: var(--prpl-color-button-primary-hover); - } */ - - &.prpl-btn-secondary { - background-color: var(--prpl-color-button-secondary); - color: var(--prpl-color-button-secondary-text); - - &:not([disabled]):not(.prpl-btn-disabled):hover, - &:not([disabled]):not(.prpl-btn-disabled):focus { - background-color: var(--prpl-color-button-secondary-hover); - color: var(--prpl-color-button-secondary-text); - } - } - - &.prpl-btn-disabled, - &:disabled { - - &, - &:hover{ - color: var(--prpl-color-button-inactive-text); - color: rgb(from var(--prpl-color-button-inactive-text) r g b / 0.88); - background-color: var(--prpl-color-button-inactive); - cursor: not-allowed; - } - } - } - - .prpl-complete-task-btn:not(.prpl-btn) { - border: none; - background: none; - cursor: pointer; - padding: 0; - margin: 0; - font-size: 16px; - color: var(--prpl-color-link); - } - -/* - .prpl-complete-task-btn-completed:not(.prpl-btn) { - color: #059669; - pointer-events: none; - opacity: 0.5; - } */ - - .prpl-complete-task-btn-error { - color: #9f0712; - } - - /* Generic error message (reusable for all steps) */ - .prpl-error-message { - animation: prpl-slide-down 0.3s ease-out; - } - - @keyframes prpl-slide-down { - - from { - opacity: 0; - transform: translateY(-10px); - } - - to { - opacity: 1; - transform: translateY(0); - } - } - - .prpl-error-box { - display: flex; - align-items: flex-start; - gap: 15px; - background: var(--prpl-background-alert-error); - padding: 20px; - border-radius: 6px; - - .prpl-error-icon { - color: var(--prpl-color-alert-error); - width: 20px; - height: 20px; - flex-shrink: 0; - - img, svg { - width: 100%; - height: 100%; - } - } - - h3 { - margin: 0 0 8px 0; - font-size: 16px; - font-weight: 700; - color: var(--prpl-color-alert-error-text); - } - - p { - margin: 0; - font-size: 16px; - line-height: 1.4; - color: var(--prpl-color-alert-error-text); - } - } - - /* Popover: Quit confirmation */ - &:has(.prpl-quit-confirmation) { - - #prpl-tour-close-btn, - .prpl-onboarding-navigation { - display: none; - } - } - - .prpl-column:has(.prpl-quit-confirmation) { - justify-content: flex-start !important; - } - - .prpl-quit-confirmation { - display: flex; - flex-direction: column; - gap: 30px; - } - - .prpl-quit-message { - display: flex; - align-items: flex-start; - gap: 15px; - background: var(--prpl-background-alert-error); - padding: 20px; - border-radius: 6px; - border-left: 4px solid var(--prpl-color-alert-error); - - h3 { - font-size: 18px; - font-weight: 700; - } - - p { - font-size: 15px; - line-height: 1.6; - } - } - - .prpl-quit-actions { - display: flex; - gap: 40px; - justify-content: space-between; - align-items: center; - } - - .prpl-quit-link { - font-size: 15px; - color: var(--prpl-color-link); - text-decoration: underline; - cursor: pointer; - - &.prpl-quit-link-primary { - font-weight: 600; - font-size: 16px; - } - } - - #prpl-quit-confirmation-graphic { - display: flex; - justify-content: center; - align-items: center; - - img, - svg { - height: 250px; - - /* Set height since it is what we do in the PP dashboard. */ - max-width: 100%; - } - } - - /* Form for onboarding tasks. */ - .prpl-onboarding-task-form { - display: flex; - align-items: center; - gap: 8px; - border: none; - padding: 0; - margin: 0; - background: none; - - .prpl-complete-task-btn { - flex-shrink: 0; - margin-top: 16px; - } - } - - /* Welcome Step */ - &[data-prpl-step="0"] { - - .prpl-column:not(.prpl-column-content) { - display: flex; - justify-content: center; - align-items: center; - } - - #prpl-welcome-graphic { - display: flex; - justify-content: center; - align-items: center; - - img, svg { - height: 250px; /* Set height since it is what we do in the PP dashboard. */ - max-width: 100%; - } - } - - .prpl-welcome-note{ - font-size: 14px; - font-style: italic; - } - } - - /* Privacy checkbox */ - .prpl-privacy-checkbox-wrapper { - margin-top: 20px; - - label { - gap: 10px; - cursor: pointer; - font-size: 15px; - color: var(--prpl-color-text); - } - - input[type="checkbox"] { - width: 18px; - height: 18px; - cursor: pointer; - } - - a { - color: var(--prpl-color-link); - text-decoration: underline; - } - - .prpl-required-indicator { - font-size: 14px; - font-style: italic; - - &::before { - content: '('; - } - - &::after { - content: ')'; - } - - &.prpl-required-indicator-active { - color: var(--prpl-color-text-error); - font-size: 16px; - font-style: normal; - - &::before { - display: inline-flex; - align-items: center; - justify-content: center; - content: '!'; - width: 16px; - height: 16px; - border-radius: 50%; - background-color: var(--prpl-color-alert-error); - color: #fff; - font-size: 12px; - margin-right: 5px; - } - - &::after { - content: '!'; - } - } - } - } - - /* What's Next Step */ - .prpl-suggested-task-points { - font-size: var(--prpl-font-size-xs, 12px); - font-weight: 700; - color: var(--prpl-text-point); - background-color: var(--prpl-background-point); - width: 24px; - height: 24px; - border-radius: 50%; - display: inline-flex; - align-items: center; - justify-content: center; - } - - /* First Task Step */ - .prpl-onboarding-task { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 10px; - - /* .prpl-onboarding-task-title { - margin: 0 0 16px 0; - font-size: 18px; - font-weight: 600; - } */ - - .prpl-onboarding-task-form { - margin-top: 16px; - width: 100%; - flex-direction: column; - - .prpl-complete-task-btn { - align-self: flex-end; - - &.prpl-complete-task-btn-completed { - opacity: 0.5; - pointer-events: none; - } - } - } - } - - /* Badges Step */ - .prpl-gauge-wrapper { - max-width: 100%; - text-align: center; - } - - /* Email Frequency Step */ - - /* .prpl-email-frequency-options { - - . - display: flex; - gap: 10px; - flex-direction: column; - } */ - - #prpl-email-form { - margin-top: 16px; - - .prpl-form-field { - margin-top: 16px; - - &:first-child { - margin-top: 0; - } - - label { - display: inline-block; - margin-bottom: 8px; - } - } - } - - /* Settings Step */ - .tour-content-wrapper:has(.prpl-setting-item) { - display: flex; - flex-direction: column; - flex-grow: 1; - - .tour-content, - .prpl-setting-item, - .prpl-setting-content { - display: flex; - flex-direction: column; - flex-grow: 1; - gap: 24px; - } - - .prpl-setting-title { - margin: 0 0 16px 0; - } - - .prpl-settings-progress { - font-size: 14px; - color: var(--prpl-color-step-label); - padding: 4px 8px; - border-radius: 20px; - background-color: var(--prpl-background-step-active); - } - - .prpl-setting-footer { - display: flex; - justify-content: flex-end; - align-items: flex-start; - gap: 10px; - margin-top: 16px; - - .prpl-save-setting-btn { - flex-shrink: 0; - } - } - - .prpl-select-page { - - &.prpl-disabled { - opacity: 0.5; - pointer-events: none; - } - } - - .prpl-setting-note { - display: none; - gap: 10px; - border-radius: 12px; - padding: 16px; - color: var(--prpl-color-field-border-active); - font-size: 14px; - background-color: var(--prpl-background-field-active); - - .prpl-setting-note-icon { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - border-radius: 50%; - color: var(--prpl-color-field-border-active); - } - } - - /* Post types sub-step */ - #prpl-post-types-include-wrapper { - display: flex; - flex-direction: column; - gap: 12px; - } - - /* Toggle checkbox for post types. */ - .prpl-post-type-toggle-wrapper { - display: flex; - align-items: center; - } - - .prpl-post-type-toggle-label { - display: flex; - align-items: center; - gap: 12px; - cursor: pointer; - position: relative; - } - - .prpl-post-type-toggle-input { - position: absolute; - opacity: 0; - width: 0; - height: 0; - margin: 0; - padding: 0; - } - - .prpl-post-type-toggle-switch { - position: relative; - width: 44px; - height: 24px; - border-radius: 12px; - background-color: var(--prpl-color-selection-controls-inactive); - transition: background-color 0.2s; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - - &::after { - content: ''; - position: absolute; - top: 2px; - left: 2px; - width: 20px; - height: 20px; - border-radius: 50%; - background-color: var(--prpl-onboarding-popover-background); - transition: transform 0.2s; - z-index: 1; - } - - svg { - position: absolute; - width: 12px; - height: 12px; - top: 50%; - left: 6px; - transform: translateY(-50%); - z-index: 2; - transition: opacity 0.2s, left 0.2s, color 0.2s; - color: var(--prpl-color-ui-icon); - } - - .prpl-toggle-icon-check { - display: none; - } - - .prpl-toggle-icon-x { - display: block; - } - } - - .prpl-post-type-toggle-label:hover .prpl-post-type-toggle-switch svg { - opacity: 0.6; - } - - .prpl-post-type-toggle-input:checked ~ .prpl-post-type-toggle-switch { - background-color: var(--prpl-background-step-active); - - &::after { - transform: translateX(20px); - } - - svg { - left: 26px; - color: var(--prpl-background-step-active); - transform: translateY(-50%); - } - - .prpl-toggle-icon-check { - display: block; - } - - .prpl-toggle-icon-x { - display: none; - } - } - - .prpl-post-type-toggle-text { - font-size: 16px; - line-height: 1.5; - transition: opacity 0.2s; - } - - .prpl-post-type-toggle-input:not(:checked) ~ .prpl-post-type-toggle-text { - opacity: 0.78; - } - - .prpl-post-type-toggle-input:checked ~ .prpl-post-type-toggle-text { - opacity: 1; - } - } - - /* More Tasks Step */ - - #prpl-success-graphic { - display: flex; - justify-content: center; - align-items: center; - - img, svg { - height: 250px; - max-width: 100%; - } - } - - /* Intro substep */ - .prpl-more-tasks-substep[data-substep="more-tasks-intro"] { - display: flex; - flex-direction: column; - gap: 24px; - } - - .prpl-more-tasks-intro-buttons { - margin-top: 16px; - display: flex; - align-items: center; - justify-content: center; - gap: 24px; - flex-wrap: wrap; - } - - .prpl-finish-onboarding { - background: none; - border: none; - color: var(--prpl-color-link, #534786); - text-decoration: underline; - font-size: 16px; - cursor: pointer; - padding: 12px 0; - } - - .prpl-finish-onboarding:hover, - .prpl-finish-onboarding:focus { - color: var(--prpl-color-link-hover, #38296d); - } - - /* Tasks substep */ - .prpl-more-tasks-substep[data-substep="more-tasks-tasks"] .prpl-task-list { - margin: 0; - } - - .prpl-column:has(.prpl-task-list) { - display: flex; - align-items: center; - } - - .prpl-task-list { - list-style: none; - padding: 0; - margin: 0; - width: 100%; - border: 1px solid #ccc; - - li { - padding: 7px 5px; - margin: 0; - - &:nth-child(odd) { - background-color: var(--prpl-background-steps); - } - - .task-title { - color: var(--prpl-color-text); - font-weight: 500; - } - } - } - - .prpl-complete-task-item { - display: flex; - gap: 30px; - justify-content: space-between; - } - - .prpl-task-arrow { - padding-right: 8px; - } - - .prpl-task-item-button-wrapper { - display: flex; - align-items: center; - gap: 8px; - flex-shrink: 0; - } - - .prpl-task-completed-icon { - display: none; - width: 20px; - height: 20px; - border-radius: 50%; - align-items: center; - justify-content: center; - color: #fff; - background-color: #059669; - } - - .prpl-task-completed { - - /* Show the completed icon. */ - .prpl-task-completed-icon { - display: inline-flex; - } - - /* Hide the trigger button and +1. */ - .prpl-task-item-button-wrapper { - display: none; - } - } - - .tour-content-wrapper:has(.prpl-task-content-active) { - display: flex; - flex-direction: column; - flex-grow: 1; - - /* Task content active, TOOD: change markup so this is simpler. */ - .prpl-task-content-active { - display: flex; - flex-direction: column; - flex-grow: 1; - gap: 24px; - width: 100%; - - .prpl-onboarding-task { - display: flex; - flex-direction: column; - flex-grow: 1; - - .prpl-onboarding-task-form, - .tour-content { - display: flex; - flex-direction: column; - flex-grow: 1; - } - - .prpl-onboarding-task-form { - justify-content: space-between; - gap: 48px; - } - } - } - - /* Task buttons. */ - .prpl-task-buttons { - display: flex; - justify-content: flex-end; - gap: 16px; - width: 100%; - - .prpl-btn { - margin-top: 0; - margin-bottom: 0; - - &.prpl-task-close-btn { - background-color: var(--prpl-background-banner); - color: var(--prpl-color-text); - } - } - } - } - - /* File drop zone. */ - .prpl-file-drop-zone { - width: 100%; - border-radius: 12px; - padding: 40px; - text-align: center; - color: var(--prpl-color-text); - transition: background 0.2s, border-color 0.2s; - cursor: pointer; - border: 2px dashed var(--prpl-color-field-border); - - svg { - - path { - stroke: var(--prpl-color-ui-icon); - } - } - } - - .prpl-file-drop-zone.dragover { - background-color: var(--prpl-background-field-active); - border-color: var(--prpl-color-field-border-active); - } - - /* When an image has been uploaded. */ - .prpl-file-drop-zone.has-image { - border-color: var(--prpl-color-field-border-active); - background-color: var(--prpl-background-field-active); - - & > .prpl-icon-image, - & > p, - & > .prpl-file-upload-hints { - display: none; - } - - .prpl-file-preview img { - border: 2px solid var(--prpl-color-field-border); - border-radius: 8px; - padding: 8px; - background: var(--prpl-color-background-white); - } - } - - .prpl-file-browse-link { - color: var(--prpl-color-link); - text-decoration: underline; - cursor: pointer; - } - - .prpl-file-remove-btn { - border: none; - background: none; - cursor: pointer; - padding: 0; - margin: 12px 0 0; - font-size: 16px; - color: var(--prpl-color-link); - text-decoration: underline; - } - - .prpl-file-upload-hints { - display: flex; - flex-direction: column; - font-size: 14px; - color: var(--prpl-color-ui-icon); - } - - /* WIP */ - #prpl-upload-status { - margin-top: 10px; - font-family: monospace; - } - - .prpl-file-preview { - display: none; /* Hidden by default. */ - margin-top: 10px; - margin-left: auto; - margin-right: auto; - max-width: 200px; - height: auto; - } -} - -@media (max-width: 1023px) { - - .prpl-popover-onboarding { - --prpl-mobile-nav-height: 60px; - max-width: 90vw; - inset: 0 0 var(--prpl-mobile-nav-height) 0; /* TODO: Adjust this for smallest screen sizes. */ - margin: auto auto var(--prpl-mobile-nav-height) auto; /* Center in available space above nav */ - - /* Hide graphics */ - .prpl-hide-on-mobile { - display: none !important; - } - - /* Quit confirmation */ - .prpl-quit-actions { - flex-direction: column; - gap: 16px; - } - - /* Naviation section */ - .prpl-onboarding-navigation { - position: fixed; - left: 0; - bottom: 0; - width: 100%; - height: var(--prpl-mobile-nav-height); - z-index: 1000; - - flex-direction: row; - gap: 16px; - justify-content: center; - align-items: center; - padding: 8px; - - .prpl-step-list { - display: flex; - gap: 8px; - - .prpl-nav-step-item { - margin: 0; - } - - .prpl-step-label { - display: none; - } - } - - #prpl-onboarding-mobile-step-label { - display: block; - } - } - - /* Content section */ - .prpl-columns-wrapper-flex { - flex-direction: column; - } - - .prpl-setting-footer, /* On the settings steps */ - .tour-footer { - flex-direction: column; /* So the info / error message is on top of the button. */ - } - - /* Badges step */ - .prpl-gauge-wrapper { - max-width: 400px; - margin-left: auto; - margin-right: auto; - } - } -} diff --git a/assets/css/welcome.css b/assets/css/welcome.css new file mode 100644 index 0000000000..20004600b4 --- /dev/null +++ b/assets/css/welcome.css @@ -0,0 +1,77 @@ +.prpl-wrap.prpl-pp-not-accepted { + padding: 0; + background-color: var(--prpl-background-paper); + border: 1px solid var(--prpl-color-border); + border-radius: var(--prpl-border-radius); +} + +.prpl-welcome { + + .inner-content { + padding: calc(var(--prpl-gap) * 1.5); + padding-bottom: 0; + margin-bottom: calc(var(--prpl-gap) * 1.5); + display: flex; + grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); + gap: calc(var(--prpl-gap) * 2); + + .left { + flex-grow: 1; + } + + img { + max-width: 100%; + width: 550px; + height: auto; + } + } + + .welcome-header { + background: var(--prpl-background-banner); + display: flex; + justify-content: space-between; + align-items: center; + border-top-left-radius: var(--prpl-border-radius); + border-top-right-radius: var(--prpl-border-radius); + overflow: hidden; + + h1 { + font-size: var(--prpl-font-size-3xl); + padding: var(--prpl-padding) calc(var(--prpl-gap) * 1.5); + font-weight: 600; + } + + .welcome-header-icon { + background: var(--prpl-background-banner); + background: linear-gradient(105deg, var(--prpl-background-banner) 25%, var(--prpl-background-monthly) 25%); + padding: var(--prpl-padding); + padding-left: 100px; + padding-right: calc(var(--prpl-gap) * 1.5); + + svg { + height: 100px; + } + } + } + + .prpl-form-notice-title { + font-size: var(--prpl-font-size-lg); + } + + ul { + list-style: disc; + margin-left: 1rem; + } + + .prpl-onboard-form-radio-select { + margin-top: 0.75rem; + + label { + margin-top: 0.5rem; + + &:first-child { + margin-top: 0; + } + } + } +} diff --git a/assets/images/onboarding/icon_image.svg b/assets/images/onboarding/icon_image.svg deleted file mode 100644 index 24f79c262a..0000000000 --- a/assets/images/onboarding/icon_image.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/images/onboarding/icon_info_solid.svg b/assets/images/onboarding/icon_info_solid.svg deleted file mode 100644 index 9b6e2d98f2..0000000000 --- a/assets/images/onboarding/icon_info_solid.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/images/onboarding/neglected_site_ravi.svg b/assets/images/onboarding/neglected_site_ravi.svg deleted file mode 100644 index 14acee56f5..0000000000 --- a/assets/images/onboarding/neglected_site_ravi.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/images/onboarding/success_ravi.svg b/assets/images/onboarding/success_ravi.svg deleted file mode 100644 index 45e8535d4e..0000000000 --- a/assets/images/onboarding/success_ravi.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/onboarding/thumbs_up_ravi_rtl.svg b/assets/images/onboarding/thumbs_up_ravi_rtl.svg deleted file mode 100644 index 27234bc510..0000000000 --- a/assets/images/onboarding/thumbs_up_ravi_rtl.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/js/license-generator.js b/assets/js/license-generator.js deleted file mode 100644 index 5dfc2bb109..0000000000 --- a/assets/js/license-generator.js +++ /dev/null @@ -1,140 +0,0 @@ -/** - * License Generator - Handles license key generation during onboarding - * Adapted from onboard.js - */ -/* global progressPlanner */ - -// eslint-disable-next-line no-unused-vars -class LicenseGenerator { - /** - * Store config for use in other methods. - * - * @type {Object} - */ - static config = null; - - /** - * Get the default config from progressPlanner global. - * - * @return {Object} Default configuration object. - */ - static getDefaultConfig() { - // eslint-disable-next-line no-undef - if ( typeof progressPlanner !== 'undefined' ) { - return { - // eslint-disable-next-line no-undef - onboardNonceURL: progressPlanner.onboardNonceURL, - // eslint-disable-next-line no-undef - onboardAPIUrl: progressPlanner.onboardAPIUrl, - // eslint-disable-next-line no-undef - adminAjaxUrl: progressPlanner.ajaxUrl, - // eslint-disable-next-line no-undef - nonce: progressPlanner.nonce, - }; - } - return {}; - } - - /** - * Make a request to save the license key. - * - * @param {string} licenseKey The license key. - * @return {Promise} Promise that resolves when license is saved - */ - static saveLicenseKey( licenseKey ) { - console.log( 'License key: ' + licenseKey ); - return LicenseGenerator.ajaxRequest( { - url: LicenseGenerator.config.adminAjaxUrl, - data: { - action: 'progress_planner_save_onboard_data', - _ajax_nonce: LicenseGenerator.config.nonce, - key: licenseKey, - }, - } ); - } - - /** - * Make the AJAX request to the API. - * - * @param {Object} data The data to send with the request. - * @return {Promise} Promise that resolves when request completes - */ - static ajaxAPIRequest( data ) { - return LicenseGenerator.ajaxRequest( { - url: LicenseGenerator.config.onboardAPIUrl, - data, - } ) - .then( ( response ) => { - // Make a local request to save the response data. - return LicenseGenerator.saveLicenseKey( response.license_key ); - } ) - .catch( ( error ) => { - console.warn( error ); - throw error; - } ); - } - - /** - * Make the AJAX request. - * - * Make a request to get the nonce. - * Once the nonce is received, make a request to the API. - * - * @param {Object} data The data to send with the request. - * @param {Object} config Optional configuration object. Falls back to progressPlanner global. - * @return {Promise} Promise that resolves when license is generated - */ - static generateLicense( data = {}, config = null ) { - // Store config for use in other methods, fall back to default if not provided. - LicenseGenerator.config = config || LicenseGenerator.getDefaultConfig(); - - return LicenseGenerator.ajaxRequest( { - url: LicenseGenerator.config.onboardNonceURL, - data, - } ).then( ( response ) => { - if ( 'ok' === response.status ) { - // Add the nonce to our data object. - data.nonce = response.nonce; - - // Make the request to the API. - return LicenseGenerator.ajaxAPIRequest( data ); - } - // Handle error response - const errorMessage = - response.message || - 'Failed to get nonce for license generation'; - throw new Error( errorMessage ); - } ); - } - - /** - * Helper function to make AJAX requests - * - * @param {Object} options Request options - * @param {string} options.url The URL to send the request to - * @param {Object} options.data The data to send with the request - * @return {Promise} Promise that resolves with response data - */ - static ajaxRequest( options ) { - const { url, data } = options; - - return fetch( url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams( data ), - credentials: 'same-origin', - } ) - .then( ( response ) => { - if ( ! response.ok ) { - throw new Error( 'Request failed: ' + response.status ); - } - return response.json(); - } ) - .catch( ( error ) => { - console.error( 'AJAX request error:', error ); - throw error; - } ); - } -} diff --git a/assets/js/onboard.js b/assets/js/onboard.js new file mode 100644 index 0000000000..529b05831b --- /dev/null +++ b/assets/js/onboard.js @@ -0,0 +1,168 @@ +/* global progressPlanner, progressPlannerAjaxRequest */ +/* + * Onboard + * + * A script to handle the onboarding process. + * + * Dependencies: progress-planner/ajax-request, progress-planner/upgrade-tasks + */ + +/** + * Make a request to save the license key. + * + * @param {string} licenseKey The license key. + */ +const progressPlannerSaveLicenseKey = ( licenseKey ) => { + console.log( 'License key: ' + licenseKey ); + return progressPlannerAjaxRequest( { + url: progressPlanner.ajaxUrl, + data: { + action: 'progress_planner_save_onboard_data', + _ajax_nonce: progressPlanner.nonce, + key: licenseKey, + }, + } ); +}; + +/** + * Make the AJAX request. + * + * @param {Object} data The data to send with the request. + */ +const progressPlannerAjaxAPIRequest = ( data ) => { + progressPlannerAjaxRequest( { + url: progressPlanner.onboardAPIUrl, + data, + } ) + .then( ( response ) => { + // Make a local request to save the response data. + progressPlannerSaveLicenseKey( response.license_key ).then( () => { + // Refresh the page. + window.location.reload(); + } ); + } ) + .catch( ( error ) => { + console.warn( error ); + } ); +}; + +/** + * Make the AJAX request. + * + * Make a request to get the nonce. + * Once the nonce is received, make a request to the API. + * + * @param {Object} data The data to send with the request. + */ +const progressPlannerOnboardCall = ( data ) => { + progressPlannerAjaxRequest( { + url: progressPlanner.onboardNonceURL, + data, + } ).then( ( response ) => { + if ( 'ok' === response.status ) { + // Add the nonce to our data object. + data.nonce = response.nonce; + + // Make the request to the API. + progressPlannerAjaxAPIRequest( data ); + } + } ); +}; + +if ( document.getElementById( 'prpl-onboarding-form' ) ) { + document + .querySelectorAll( 'input[name="with-email"]' ) + .forEach( ( input ) => { + input.addEventListener( 'change', function () { + if ( 'no' === this.value ) { + document + .getElementById( 'prpl-onboarding-form' ) + .querySelectorAll( 'input' ) + .forEach( ( inputField ) => { + inputField.required = false; + } ); + } else { + document + .getElementById( 'prpl-onboarding-form' ) + .querySelectorAll( 'input' ) + .forEach( ( inputField ) => { + if ( + 'name' === inputField.name || + 'email' === inputField.name + ) { + inputField.required = true; + } + } ); + } + document + .getElementById( 'prpl-onboarding-form' ) + .querySelectorAll( + '.prpl-form-fields, .prpl-form-fields, .prpl-button-primary, .prpl-button-secondary--no-email' + ) + .forEach( ( el ) => el.classList.toggle( 'prpl-hidden' ) ); + } ); + } ); + + document + .querySelector( '#prpl-onboarding-form input[name="privacy-policy"]' ) + .addEventListener( 'change', function () { + const privacyPolicyAccepted = !! this.checked; + + if ( privacyPolicyAccepted ) { + document + .getElementById( 'prpl-onboarding-submit-wrapper' ) + .classList.remove( 'prpl-disabled' ); + } else { + document + .getElementById( 'prpl-onboarding-submit-wrapper' ) + .classList.add( 'prpl-disabled' ); + } + } ); + + document + .getElementById( 'prpl-onboarding-form' ) + .addEventListener( 'submit', function ( event ) { + event.preventDefault(); + + const privacyPolicyAccepted = !! document.querySelector( + '#prpl-onboarding-form input[name="privacy-policy"]' + ).checked; + + // Make sure the user accepted the privacy policy. + if ( ! privacyPolicyAccepted ) { + return; + } + + // Disable all (both buttons) submit buttons. + document + .querySelectorAll( + '#prpl-onboarding-form input[type="submit"]' + ) + .forEach( ( input ) => { + input.disabled = true; + } ); + + // Show the spinner. + const spinner = document.createElement( 'span' ); + spinner.classList.add( 'prpl-spinner' ); + spinner.innerHTML = + ''; // WP spinner. + + // Append spinner after submit button. + + document + .getElementById( 'prpl-onboarding-submit-wrapper' ) + .appendChild( spinner ); + + // Get all form data. + const data = Object.fromEntries( new FormData( event.target ) ); + + // If the user doesn't want to use email, remove the email and name. + if ( 'no' === data.with_email ) { + data.email = ''; + data.name = ''; + } + + progressPlannerOnboardCall( data ); + } ); +} diff --git a/assets/js/onboarding/OnboardTask.js b/assets/js/onboarding/OnboardTask.js deleted file mode 100644 index 2db23b7816..0000000000 --- a/assets/js/onboarding/OnboardTask.js +++ /dev/null @@ -1,470 +0,0 @@ -/** - * OnboardTask - Handles individual tasks that open within the column - * Used by the MoreTasksStep for tasks that require user input - * Toggles visibility of task list and shows task content in the same column - */ -/* global ProgressPlannerOnboardData, ProgressPlannerTourUtils */ - -// eslint-disable-next-line no-unused-vars -class PrplOnboardTask { - constructor( el, wizard ) { - this.el = el; - this.id = el.dataset.taskId; - this.wizard = wizard; - this.taskContent = null; - this.formValues = {}; - this.openTaskBtn = el.querySelector( '[prpl-open-task]' ); - - // Register task open event - this.openTaskBtn?.addEventListener( 'click', () => this.open() ); - } - - /** - * Get the tour footer element via the current step - * @return {HTMLElement|null} The tour footer element or null if not found - */ - getTourFooter() { - // Get the current step and use its getTourFooter method - const currentStep = - this.wizard?.tourSteps?.[ this.wizard.state.currentStep ]; - if ( currentStep && typeof currentStep.getTourFooter === 'function' ) { - return currentStep.getTourFooter(); - } - - // Fallback in case step doesn't have the method - return this.wizard?.contentWrapper?.querySelector( '.tour-footer' ); - } - - registerEvents() { - this.taskContent.addEventListener( 'click', ( e ) => { - if ( e.target.classList.contains( 'prpl-complete-task-btn' ) ) { - const formData = new FormData( - this.taskContent.querySelector( 'form' ) - ); - this.formValues = Object.fromEntries( formData.entries() ); - this.complete(); - } - } ); - - // Close button handler - const closeBtn = this.taskContent.querySelector( - '.prpl-task-close-btn' - ); - closeBtn?.addEventListener( 'click', () => this.close() ); - - this.setupFormValidation(); - - // Initialize upload handling (only if upload field exists) - this.setupFileUpload(); - - this.el.addEventListener( 'prplFileUploaded', ( e ) => { - // Handle file upload for the 'set site icon' task. - if ( 'core-siteicon' === e.detail.fileInput.dataset.taskId ) { - // Element which will be used to store the file post ID. - const nextElementSibling = - e.detail.fileInput.nextElementSibling; - - nextElementSibling.value = e.detail.filePost.id; - - // Trigger change so validation is triggered and "Complete" button is enabled. - nextElementSibling.dispatchEvent( - new CustomEvent( 'change', { - bubbles: true, - } ) - ); - } - } ); - } - - open() { - if ( this.taskContent ) { - return; // Already open - } - - // Find the column containing the task list - const taskList = this.wizard.popover.querySelector( '.prpl-task-list' ); - if ( ! taskList ) { - return; - } - - const column = taskList.closest( '.prpl-column' ); - if ( ! column ) { - return; - } - - // Hide the task list - taskList.style.display = 'none'; - - // Hide the tour footer (it's part of the step content) - const tourFooter = this.getTourFooter(); - if ( tourFooter ) { - tourFooter.style.display = 'none'; - } - - // Get task content from template - const content = this.el - .querySelector( 'template' ) - .content.cloneNode( true ); - - // Create task content wrapper - this.taskContent = document.createElement( 'div' ); - this.taskContent.className = 'prpl-task-content-active'; - this.taskContent.appendChild( content ); - - // Find the complete button in the form - const completeBtn = this.taskContent.querySelector( - '.prpl-complete-task-btn' - ); - - if ( completeBtn ) { - // Create close button - const closeBtn = document.createElement( 'button' ); - closeBtn.type = 'button'; - closeBtn.className = 'prpl-btn prpl-task-close-btn'; - closeBtn.innerHTML = - ' ' + - ProgressPlannerOnboardData.l10n.backToRecommendations; - - // Create button wrapper - const buttonWrapper = document.createElement( 'div' ); - buttonWrapper.className = 'prpl-task-buttons'; - - // Move complete button into wrapper - completeBtn.parentNode.insertBefore( buttonWrapper, completeBtn ); - buttonWrapper.appendChild( closeBtn ); - buttonWrapper.appendChild( completeBtn ); - } - - // Add task content to the column - column.appendChild( this.taskContent ); - - // Hide the popover close button - const popoverCloseBtn = this.wizard.popover.querySelector( - '#prpl-tour-close-btn' - ); - if ( popoverCloseBtn ) { - popoverCloseBtn.style.display = 'none'; - } - - // Register events - this.registerEvents(); - } - - close() { - if ( ! this.taskContent ) { - return; - } - - // Remove task content - this.taskContent.remove(); - - // Show the task list - const taskList = this.wizard.popover.querySelector( '.prpl-task-list' ); - if ( taskList ) { - taskList.style.display = ''; - } - - // Show the tour footer (it's part of the step content) - const tourFooter = this.getTourFooter(); - if ( tourFooter ) { - tourFooter.style.display = ''; - } - - // Show the popover close button - const popoverCloseBtn = this.wizard.popover.querySelector( - '#prpl-tour-close-btn' - ); - if ( popoverCloseBtn ) { - popoverCloseBtn.style.display = ''; - } - - // Clean up - this.taskContent = null; - } - - complete() { - ProgressPlannerTourUtils.completeTask( this.id, this.formValues ) - .then( () => { - this.el.classList.add( 'prpl-task-completed' ); - const taskBtn = this.el.querySelector( - '.prpl-complete-task-btn' - ); - if ( taskBtn ) { - taskBtn.disabled = true; - } - - this.close(); - this.notifyParent(); - } ) - .catch( ( error ) => { - console.error( error ); - // TODO: Handle error. - } ); - } - - notifyParent() { - const event = new CustomEvent( 'taskCompleted', { - bubbles: true, - detail: { id: this.id, formValues: this.formValues }, - } ); - this.el.dispatchEvent( event ); - } - - setupFormValidation() { - const form = this.taskContent.querySelector( 'form' ); - const submitButton = this.taskContent.querySelector( - '.prpl-complete-task-btn' - ); - - if ( ! form || ! submitButton ) { - return; - } - - const validateElements = form.querySelectorAll( '[data-validate]' ); - if ( validateElements.length === 0 ) { - return; - } - - const checkValidation = () => { - let isValid = true; - - validateElements.forEach( ( element ) => { - const validationType = element.getAttribute( 'data-validate' ); - let elementValid = false; - - switch ( validationType ) { - case 'required': - elementValid = - element.value !== null && - element.value !== undefined && - element.value !== ''; - break; - case 'not-empty': - elementValid = element.value.trim() !== ''; - break; - default: - elementValid = true; - } - - if ( ! elementValid ) { - isValid = false; - } - } ); - - submitButton.disabled = ! isValid; - }; - - checkValidation(); - validateElements.forEach( ( element ) => { - element.addEventListener( 'change', checkValidation ); - element.addEventListener( 'input', checkValidation ); - } ); - } - - /** - * Handles drag-and-drop or manual file upload for specific tasks. - * Only runs if the form contains an upload field. - */ - setupFileUpload() { - const uploadContainer = this.taskContent.querySelector( - '[data-upload-field]' - ); - if ( ! uploadContainer ) { - return; - } // no upload for this task - - const fileInput = uploadContainer.querySelector( 'input[type="file"]' ); - const statusDiv = uploadContainer.querySelector( - '.prpl-upload-status' - ); - - // Visual drag behavior - [ 'dragenter', 'dragover' ].forEach( ( event ) => { - uploadContainer.addEventListener( event, ( e ) => { - e.preventDefault(); - uploadContainer.classList.add( 'dragover' ); - } ); - } ); - - [ 'dragleave', 'drop' ].forEach( ( event ) => { - uploadContainer.addEventListener( event, ( e ) => { - e.preventDefault(); - uploadContainer.classList.remove( 'dragover' ); - } ); - } ); - - uploadContainer.addEventListener( 'drop', ( e ) => { - const file = e.dataTransfer.files[ 0 ]; - if ( file ) { - this.uploadFile( file, statusDiv ).then( ( response ) => { - this.el.dispatchEvent( - new CustomEvent( 'prplFileUploaded', { - detail: { file, filePost: response, fileInput }, - bubbles: true, - } ) - ); - } ); - } - } ); - - fileInput?.addEventListener( 'change', ( e ) => { - const file = e.target.files[ 0 ]; - if ( file ) { - this.uploadFile( file, statusDiv, fileInput ).then( - ( response ) => { - this.el.dispatchEvent( - new CustomEvent( 'prplFileUploaded', { - detail: { file, filePost: response, fileInput }, - bubbles: true, - } ) - ); - } - ); - } - } ); - - // Remove button handler. - const removeBtn = uploadContainer.querySelector( '.prpl-file-remove-btn' ); - const previewDiv = uploadContainer.querySelector( '.prpl-file-preview' ); - removeBtn?.addEventListener( 'click', () => { - this.removeUploadedFile( uploadContainer, previewDiv ); - } ); - } - - async uploadFile( file, statusDiv ) { - // Validate file extension - if ( ! this.isValidFaviconFile( file ) ) { - const fileInput = - this.taskContent.querySelector( 'input[type="file"]' ); - const acceptedTypes = fileInput?.accept || 'supported file types'; - statusDiv.textContent = `Invalid file type. Please upload a file with one of these formats: ${ acceptedTypes }`; - return; - } - - statusDiv.textContent = `Uploading ${ file.name }...`; - - const formData = new FormData(); - formData.append( 'file', file ); - formData.append( 'prplFileUpload', '1' ); - - return fetch( '/wp-json/wp/v2/media', { - method: 'POST', - headers: { - 'X-WP-Nonce': ProgressPlannerOnboardData.nonceWPAPI, // usually wp_localize_script adds this - }, - body: formData, - credentials: 'same-origin', - } ) - .then( ( res ) => { - if ( 201 !== res.status ) { - throw new Error( 'Failed to upload file' ); - } - return res.json(); - } ) - .then( ( response ) => { - // Testing only, no need to display file name in production. - // statusDiv.textContent = `${ file.name } uploaded.`; - statusDiv.style.display = 'none'; - - // Update the file preview. - const previewDiv = - this.taskContent.querySelector( '.prpl-file-preview' ); - if ( previewDiv ) { - previewDiv.innerHTML = `${ file.name }`; - previewDiv.style.display = 'block'; - - // Add has-image class to drop zone to update styling. - const dropZone = this.taskContent.querySelector( - '.prpl-file-drop-zone' - ); - if ( dropZone ) { - dropZone.classList.add( 'has-image' ); - - // Show the remove button. - const removeBtn = dropZone.querySelector( - '.prpl-file-remove-btn' - ); - if ( removeBtn ) { - removeBtn.hidden = false; - } - } - } - return response; - } ) - .catch( ( error ) => { - console.error( error ); - statusDiv.textContent = `Error: ${ error.message }`; - } ); - } - - /** - * Validate if file matches the accepted file types from the input - * @param {File} file The file to validate - * @return {boolean} True if file extension is supported - */ - isValidFaviconFile( file ) { - const fileInput = - this.taskContent.querySelector( 'input[type="file"]' ); - if ( ! fileInput || ! fileInput.accept ) { - return true; // No restrictions if no accept attribute - } - - const acceptedTypes = fileInput.accept - .split( ',' ) - .map( ( type ) => type.trim() ); - const fileName = file.name.toLowerCase(); - - return acceptedTypes.some( ( type ) => { - if ( type.startsWith( '.' ) ) { - // Extension-based validation - return fileName.endsWith( type ); - } else if ( type.includes( '/' ) ) { - // MIME type-based validation - return file.type === type; - } - return false; - } ); - } - - /** - * Remove uploaded file and reset the drop zone state. - * @param {HTMLElement} dropZone The drop zone element. - * @param {HTMLElement} previewDiv The preview container element. - */ - removeUploadedFile( dropZone, previewDiv ) { - // Clear the preview. - previewDiv.innerHTML = ''; - previewDiv.style.display = 'none'; - - // Remove has-image class. - dropZone.classList.remove( 'has-image' ); - - // Hide the remove button. - const removeBtn = dropZone.querySelector( '.prpl-file-remove-btn' ); - if ( removeBtn ) { - removeBtn.hidden = true; - } - - // Clear the file input. - const fileInput = dropZone.querySelector( 'input[type="file"]' ); - if ( fileInput ) { - fileInput.value = ''; - } - - // Clear the hidden post_id input and trigger validation. - const postIdInput = dropZone.querySelector( 'input[name="post_id"]' ); - if ( postIdInput ) { - postIdInput.value = ''; - postIdInput.dispatchEvent( - new CustomEvent( 'change', { bubbles: true } ) - ); - } - - // Show status div again. - const statusDiv = dropZone.querySelector( '.prpl-upload-status' ); - if ( statusDiv ) { - statusDiv.style.display = ''; - statusDiv.textContent = ''; - } - } -} diff --git a/assets/js/onboarding/onboarding.js b/assets/js/onboarding/onboarding.js deleted file mode 100644 index 3bda0afeb6..0000000000 --- a/assets/js/onboarding/onboarding.js +++ /dev/null @@ -1,501 +0,0 @@ -/** - * Progress Planner Onboarding Wizard - * Handles the onboarding wizard functionality - * - * Dependencies: progress-planner/license-generator - */ -/* global ProgressPlannerOnboardData */ - -// eslint-disable-next-line no-unused-vars -class ProgressPlannerOnboardWizard { - constructor( config ) { - this.config = config; - this.state = { - currentStep: 0, - data: { - moreTasksCompleted: {}, - firstTaskCompleted: false, - finished: false, - }, - cleanup: null, - }; - - // Store previously focused element for accessibility - this.previouslyFocusedElement = null; - - // Restore saved progress if available - this.restoreSavedProgress(); - - // Make state work with reactive updates. - this.setupStateProxy(); - - // Set DOM related properties FIRST. - this.popover = document.getElementById( this.config.popoverId ); - this.contentWrapper = this.popover.querySelector( - '.tour-content-wrapper' - ); - - // Popover buttons. - this.closeBtn = this.popover.querySelector( '#prpl-tour-close-btn' ); - - // Initialize tour steps AFTER popover is set - this.tourSteps = this.initializeTourSteps(); - - // Setup event listeners after DOM is ready - this.setupEventListeners(); - } - - /** - * Restore saved progress from server - */ - restoreSavedProgress() { - if ( - ! this.config.savedProgress || - typeof this.config.savedProgress !== 'object' - ) { - return; - } - - const savedState = this.config.savedProgress; - - // Restore currentStep if valid - if ( - typeof savedState.currentStep === 'number' && - savedState.currentStep >= 0 - ) { - this.state.currentStep = savedState.currentStep; - console.log( - 'Restored onboarding progress to step:', - this.state.currentStep - ); - } - - // Restore data object if present - if ( savedState.data && typeof savedState.data === 'object' ) { - // Merge saved data with default state - this.state.data = { - ...this.state.data, - ...savedState.data, - }; - - // Ensure moreTasksCompleted is an object - if ( - ! this.state.data.moreTasksCompleted || - typeof this.state.data.moreTasksCompleted !== 'object' - ) { - this.state.data.moreTasksCompleted = {}; - } - - console.log( 'Restored onboarding data:', this.state.data ); - } - } - - /** - * Initialize tour steps configuration - * Creates instances of step components - */ - initializeTourSteps() { - // Create instances of step components. - const steps = this.config.steps.map( ( stepName ) => { - if ( - window[ `Prpl${ stepName }` ] && - typeof window[ `Prpl${ stepName }` ] === 'object' - ) { - return window[ `Prpl${ stepName }` ]; - } - - console.error( - `Step class "${ stepName }" not found. Available on window:`, - Object.keys( window ).filter( ( key ) => - key.includes( 'Step' ) - ) - ); - - return null; - } ); - - // Set wizard reference for each step - steps.forEach( ( step ) => step.setWizard( this ) ); - - return steps; - } - - /** - * Render current step - */ - renderStep() { - const step = this.tourSteps[ this.state.currentStep ]; - - // Render step content - this.contentWrapper.innerHTML = step.render(); - - // Cleanup previous step - if ( this.state.cleanup ) { - this.state.cleanup(); - this.state.cleanup = null; - } - - // Mount current step and store cleanup function - this.state.cleanup = step.onMount( this.state ); - - // Setup next button (handled by step now) - step.setupNextButton(); - - // Update step indicator - this.popover.dataset.prplStep = this.state.currentStep; - this.updateStepNavigation(); - } - - /** - * Update step navigation in left column - */ - updateStepNavigation() { - const stepItems = this.popover.querySelectorAll( - '.prpl-nav-step-item' - ); - let activeStepTitle = ''; - - stepItems.forEach( ( item, index ) => { - const icon = item.querySelector( '.prpl-step-icon' ); - const stepNumber = index + 1; - - // Remove all state classes - item.classList.remove( 'prpl-active', 'prpl-completed' ); - - // Add appropriate class and update icon - if ( index < this.state.currentStep ) { - // Completed step: show checkmark - item.classList.add( 'prpl-completed' ); - icon.textContent = 'âś“'; - } else if ( index === this.state.currentStep ) { - // Active step: show number - item.classList.add( 'prpl-active' ); - icon.textContent = stepNumber; - activeStepTitle = - item.querySelector( '.prpl-step-label' ).textContent; - } else { - // Future step: show number - icon.textContent = stepNumber; - } - } ); - - // Update mobile step label - const mobileStepLabel = this.popover.querySelector( - '#prpl-onboarding-mobile-step-label' - ); - if ( mobileStepLabel ) { - mobileStepLabel.textContent = activeStepTitle; - } - } - - /** - * Move to next step - */ - async nextStep() { - console.log( - 'nextStep() called, current step:', - this.state.currentStep - ); - const step = this.tourSteps[ this.state.currentStep ]; - - // Check if user can proceed from current step - if ( ! step.canProceed( this.state ) ) { - console.log( 'Cannot proceed - step requirements not met' ); - return; - } - - // Call beforeNextStep if step has it (for async operations like license generation) - if ( step.beforeNextStep ) { - try { - await step.beforeNextStep(); - } catch ( error ) { - console.error( 'Error in beforeNextStep:', error ); - return; // Don't proceed if beforeNextStep fails - } - } - - if ( this.state.currentStep < this.tourSteps.length - 1 ) { - this.state.currentStep++; - console.log( 'Moving to step:', this.state.currentStep ); - this.saveProgressToServer(); - this.renderStep(); - } else { - console.log( 'Finishing tour - reached last step' ); - this.state.data.finished = true; - this.closeTour(); - - // Redirect to the Progress Planner dashboard - if ( - this.config.lastStepRedirectUrl && - this.config.lastStepRedirectUrl.length > 0 - ) { - window.location.href = this.config.lastStepRedirectUrl; - } - } - } - - /** - * Move to previous step, currently not used. - */ - prevStep() { - if ( this.state.currentStep > 0 ) { - this.state.currentStep--; - this.renderStep(); - } - } - - /** - * Close the tour - */ - closeTour() { - if ( this.popover ) { - this.popover.hidePopover(); - } - this.saveProgressToServer(); - - // Cleanup active step - if ( this.state.cleanup ) { - this.state.cleanup(); - } - - // Reset cleanup - this.state.cleanup = null; - - // Restore focus to previously focused element for accessibility - if ( - this.previouslyFocusedElement && - typeof this.previouslyFocusedElement.focus === 'function' - ) { - this.previouslyFocusedElement.focus(); - this.previouslyFocusedElement = null; - } - } - - /** - * Start the onboarding - */ - startOnboarding() { - if ( this.popover ) { - // Store currently focused element for accessibility - this.previouslyFocusedElement = - this.popover.ownerDocument.activeElement; - - this.popover.showPopover(); - this.updateStepNavigation(); - this.renderStep(); - - // Move focus to popover for keyboard accessibility - // Use setTimeout to ensure popover is visible before focusing - setTimeout( () => { - this.popover.focus(); - }, 0 ); - } - } - - /** - * Save progress to server - */ - async saveProgressToServer() { - try { - const response = await fetch( this.config.adminAjaxUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams( { - state: JSON.stringify( this.state ), - nonce: this.config.nonceProgressPlanner, - action: 'progress_planner_onboarding_save_progress', - } ), - credentials: 'same-origin', - } ); - - if ( ! response.ok ) { - throw new Error( 'Request failed: ' + response.status ); - } - - return response.json(); - } catch ( error ) { - console.error( 'Failed to save tour progress:', error ); - } - } - - /** - * Update next button state - * Delegates to current step's updateNextButton method - */ - updateNextButton() { - const step = this.tourSteps[ this.state.currentStep ]; - if ( step && typeof step.updateNextButton === 'function' ) { - step.updateNextButton(); - } - } - - /** - * Update DOM, used for reactive updates. - * All changes which should happen when the state changes should be done here. - */ - updateDOM() { - this.updateNextButton(); - } - - /** - * Setup event listeners - */ - setupEventListeners() { - console.log( 'Setting up event listeners...' ); - if ( this.popover ) { - console.log( 'Popover found:', this.popover ); - - this.popover.addEventListener( 'beforetoggle', ( event ) => { - if ( event.newState === 'open' ) { - console.log( 'Tour opened' ); - } - if ( event.newState === 'closed' ) { - console.log( 'Tour closed' ); - } - } ); - - // Note: nextBtn click handler is now set up in renderStep() - // since the button is part of the step content - - if ( this.closeBtn ) { - this.closeBtn.addEventListener( 'click', ( e ) => { - console.log( 'Close button clicked!' ); - - // Display quit confirmation if on welcome step (since privacy policy is accepted there) - if ( this.state.currentStep === 0 ) { - e.preventDefault(); - this.showQuitConfirmation(); - return; - } - - this.state.data.finished = - this.state.currentStep === this.tourSteps.length - 1; - this.closeTour(); - - // If on PP Dashboard page and privacy was accepted during onboarding, - // refresh the page to properly initialize dashboard components. - if ( - this.state.data.privacyAccepted && - window.location.href.includes( - 'admin.php?page=progress-planner' - ) - ) { - window.location.reload(); - } - } ); - } - } else { - console.error( 'Popover not found!' ); - } - } - - /** - * Show quit confirmation when trying to close without accepting privacy - */ - showQuitConfirmation() { - // Replace content with confirmation message - const originalContent = this.contentWrapper.innerHTML; - - // Get template from DOM - const template = document.getElementById( - 'prpl-onboarding-quit-confirmation' - ); - if ( ! template ) { - console.error( 'Quit confirmation template not found' ); - return; - } - - this.contentWrapper.innerHTML = template.innerHTML; - - // Add event listeners - const quitYes = this.contentWrapper.querySelector( '#prpl-quit-yes' ); - const quitNo = this.contentWrapper.querySelector( '#prpl-quit-no' ); - - if ( quitYes ) { - quitYes.addEventListener( 'click', ( e ) => { - e.preventDefault(); - this.closeTour(); - } ); - } - - if ( quitNo ) { - quitNo.addEventListener( 'click', ( e ) => { - e.preventDefault(); - // Restore original content - this.contentWrapper.innerHTML = originalContent; - - // Re-mount the step - this.renderStep(); - } ); - } - } - - /** - * Setup state proxy for reactive updates - */ - setupStateProxy() { - this.state.data = this.createDeepProxy( this.state.data, () => - this.updateDOM() - ); - } - - /** - * Create deep proxy for nested object changes - * @param {Object} target - * @param {Function} callback - */ - createDeepProxy( target, callback ) { - // Recursively wrap existing nested objects first - for ( const key of Object.keys( target ) ) { - if ( - target[ key ] && - typeof target[ key ] === 'object' && - ! Array.isArray( target[ key ] ) - ) { - target[ key ] = this.createDeepProxy( target[ key ], callback ); - } - } - - return new Proxy( target, { - set: ( obj, prop, value ) => { - if ( - value && - typeof value === 'object' && - ! Array.isArray( value ) - ) { - value = this.createDeepProxy( value, callback ); - } - obj[ prop ] = value; - callback(); - return true; - }, - } ); - } -} - -class ProgressPlannerTourUtils { - /** - * Complete a task via AJAX - * @param {string} taskId - * @param {Object} formValues - */ - static async completeTask( taskId, formValues = {} ) { - const response = await fetch( ProgressPlannerOnboardData.adminAjaxUrl, { - method: 'POST', - body: new URLSearchParams( { - form_values: JSON.stringify( formValues ), - task_id: taskId, - nonce: ProgressPlannerOnboardData.nonceProgressPlanner, - action: 'progress_planner_onboarding_complete_task', - } ), - } ); - - if ( ! response.ok ) { - throw new Error( 'Request failed: ' + response.status ); - } - - return response.json(); - } -} diff --git a/assets/js/onboarding/steps/BadgesStep.js b/assets/js/onboarding/steps/BadgesStep.js deleted file mode 100644 index 516636fadb..0000000000 --- a/assets/js/onboarding/steps/BadgesStep.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Badges step - Explains the badge system to users - * Simple informational step with no user interaction required - */ -/* global OnboardingStep */ - -class PrplBadgesStep extends OnboardingStep { - constructor() { - super( { - templateId: 'onboarding-step-badges', - } ); - } - - /** - * Mount badges step and lazy-load badge graphic - * Badge is only loaded after privacy policy is accepted - * @return {Function} Cleanup function - */ - onMount() { - const gaugeElement = document.getElementById( 'prpl-gauge-onboarding' ); - - if ( ! gaugeElement ) { - return () => {}; - } - - // Create badge element using innerHTML to properly instantiate the custom element - const badgeId = gaugeElement.getAttribute( 'data-badge-id' ); - const badgeName = gaugeElement.getAttribute( 'data-badge-name' ); - const brandingId = gaugeElement.getAttribute( 'data-branding-id' ); - - gaugeElement.innerHTML = ` - - `; - - // Increment badge point(s) after badge is loaded - setTimeout( () => { - if ( gaugeElement ) { - // Check if the first task was completed. - if ( this.wizard.state.data.firstTaskCompleted ) { - gaugeElement.value += 1; - } - } - }, 1500 ); - - return () => {}; - } - - /** - * User can always proceed from badges step - * @return {boolean} Always returns true - */ - canProceed() { - return true; - } -} - -window.PrplBadgesStep = new PrplBadgesStep(); diff --git a/assets/js/onboarding/steps/EmailFrequencyStep.js b/assets/js/onboarding/steps/EmailFrequencyStep.js deleted file mode 100644 index ff9ffc56ea..0000000000 --- a/assets/js/onboarding/steps/EmailFrequencyStep.js +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Email Frequency step - Allow users to opt in/out of weekly emails - * If opted in, collects name and email for subscription - */ -/* global OnboardingStep, ProgressPlannerOnboardData, LicenseGenerator */ - -class PrplEmailFrequencyStep extends OnboardingStep { - constructor() { - super( { - templateId: 'onboarding-step-email-frequency', - } ); - } - - /** - * Mount the email frequency step - * Sets up radio button and form field listeners - * @param {Object} state - Wizard state - * @return {Function} Cleanup function - */ - onMount( state ) { - const emailWeeklyRadio = - this.popover.querySelector( '#prpl-email-weekly' ); - const dontEmailRadio = this.popover.querySelector( '#prpl-dont-email' ); - const emailForm = this.popover.querySelector( '#prpl-email-form' ); - const nameInput = this.popover.querySelector( '#prpl-email-name' ); - const emailInput = this.popover.querySelector( '#prpl-email-address' ); - - if ( ! emailWeeklyRadio || ! dontEmailRadio || ! emailForm ) { - return () => {}; - } - - // Initialize state - if ( ! state.data.emailFrequency ) { - state.data.emailFrequency = { - choice: 'weekly', // Default to 'weekly' - name: nameInput ? nameInput.value.trim() : '', // Get pre-populated value - email: emailInput ? emailInput.value.trim() : '', // Get pre-populated value - }; - } - - // Set radio button state from wizard state - if ( state.data.emailFrequency.choice === 'weekly' ) { - emailWeeklyRadio.checked = true; - emailForm.style.display = 'block'; - } else if ( state.data.emailFrequency.choice === 'none' ) { - dontEmailRadio.checked = true; - emailForm.style.display = 'none'; - } - - // Set form values from state (or keep pre-populated values) - if ( nameInput ) { - nameInput.value = state.data.emailFrequency.name || nameInput.value; - } - if ( emailInput ) { - emailInput.value = - state.data.emailFrequency.email || emailInput.value; - } - - // Radio button change handlers - const weeklyHandler = ( e ) => { - if ( e.target.checked ) { - state.data.emailFrequency.choice = 'weekly'; - emailForm.style.display = 'block'; - - // Update button state - this.updateNextButton(); - } - }; - - const dontEmailHandler = ( e ) => { - if ( e.target.checked ) { - state.data.emailFrequency.choice = 'none'; - emailForm.style.display = 'none'; - - // Update button state - this.updateNextButton(); - } - }; - - // Form input handlers - const nameHandler = ( e ) => { - state.data.emailFrequency.name = e.target.value.trim(); - this.updateNextButton(); - }; - - const emailHandler = ( e ) => { - state.data.emailFrequency.email = e.target.value.trim(); - this.updateNextButton(); - }; - - // Add event listeners - emailWeeklyRadio.addEventListener( 'change', weeklyHandler ); - dontEmailRadio.addEventListener( 'change', dontEmailHandler ); - - if ( nameInput ) { - nameInput.addEventListener( 'input', nameHandler ); - } - if ( emailInput ) { - emailInput.addEventListener( 'input', emailHandler ); - } - - // Cleanup function - return () => { - emailWeeklyRadio.removeEventListener( 'change', weeklyHandler ); - dontEmailRadio.removeEventListener( 'change', dontEmailHandler ); - - if ( nameInput ) { - nameInput.removeEventListener( 'input', nameHandler ); - } - if ( emailInput ) { - emailInput.removeEventListener( 'input', emailHandler ); - } - }; - } - - /** - * User can proceed if: - * - "Don't email me" is selected, OR - * - "Email me weekly" is selected AND both name and email fields are filled - * @param {Object} state - Wizard state - * @return {boolean} True if can proceed - */ - canProceed( state ) { - // Initialize state if needed (defensive check) - if ( ! state.data.emailFrequency ) { - state.data.emailFrequency = { - choice: null, - name: '', - email: '', - }; - } - - const emailFrequency = state.data.emailFrequency; - - if ( ! emailFrequency.choice ) { - return false; - } - - // If user chose "don't email", they can proceed immediately - if ( emailFrequency.choice === 'none' ) { - return true; - } - - // If user chose "weekly", check that name and email are filled - if ( emailFrequency.choice === 'weekly' ) { - return !! ( emailFrequency.name && emailFrequency.email ); - } - - return false; - } - - /** - * Called before advancing to next step - * Fires AJAX request to subscribe user if "Email me weekly" was selected - * @return {Promise} Resolves when action is complete - */ - async beforeNextStep() { - const state = this.getState(); - - // Only send AJAX if user chose to receive emails - if ( state.data.emailFrequency.choice !== 'weekly' ) { - return Promise.resolve(); - } - - // Show spinner - const spinner = this.showSpinner( this.nextBtn ); - - try { - // Use LicenseGenerator to handle the license generation process - await LicenseGenerator.generateLicense( - { - name: state.data.emailFrequency.name, - email: state.data.emailFrequency.email, - site: ProgressPlannerOnboardData.site, - timezone_offset: ProgressPlannerOnboardData.timezone_offset, - 'with-email': 'yes', - }, - { - onboardNonceURL: ProgressPlannerOnboardData.onboardNonceURL, - onboardAPIUrl: ProgressPlannerOnboardData.onboardAPIUrl, - adminAjaxUrl: ProgressPlannerOnboardData.adminAjaxUrl, - nonce: ProgressPlannerOnboardData.nonceProgressPlanner, - } - ); - - console.log( 'Successfully subscribed' ); - } catch ( error ) { - console.error( 'Failed to subscribe:', error ); - - // Display error message to user - this.showErrorMessage( - error.message || 'Failed to subscribe. Please try again.', - 'Error subscribing' - ); - - // Re-enable the button so user can retry - this.setNextButtonDisabled( false ); - - // Don't proceed to next step - throw error; - } finally { - // Remove spinner - spinner.remove(); - } - } -} - -window.PrplEmailFrequencyStep = new PrplEmailFrequencyStep(); diff --git a/assets/js/onboarding/steps/FirstTaskStep.js b/assets/js/onboarding/steps/FirstTaskStep.js deleted file mode 100644 index 633f109432..0000000000 --- a/assets/js/onboarding/steps/FirstTaskStep.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * First Task step - User completes their first task - * Handles task completion and form submission - */ -/* global OnboardingStep, ProgressPlannerTourUtils */ -class PrplFirstTaskStep extends OnboardingStep { - constructor() { - super( { - templateId: 'onboarding-step-first-task', - } ); - } - - /** - * Mount the first task step - * Sets up event listener for task completion - * @param {Object} state - Wizard state - * @return {Function} Cleanup function - */ - onMount( state ) { - const btn = this.popover.querySelector( '.prpl-complete-task-btn' ); - if ( ! btn ) { - return () => {}; - } - - const handler = ( e ) => { - const thisBtn = e.target.closest( 'button' ); - const form = thisBtn.closest( 'form' ); - let formValues = {}; - - if ( form ) { - const formData = new FormData( form ); - formValues = Object.fromEntries( formData.entries() ); - } - - ProgressPlannerTourUtils.completeTask( - thisBtn.dataset.taskId, - formValues - ) - .then( () => { - thisBtn.classList.add( 'prpl-complete-task-btn-completed' ); - this.updateState( 'firstTaskCompleted', { - [ thisBtn.dataset.taskId ]: true, - } ); - - // Automatically advance to the next step - this.nextStep(); - } ) - .catch( ( error ) => { - console.error( error ); - thisBtn.classList.add( 'prpl-complete-task-btn-error' ); - } ); - }; - - btn.addEventListener( 'click', handler ); - return () => btn.removeEventListener( 'click', handler ); - } - - /** - * User can only proceed if they've completed the first task - * @param {Object} state - Wizard state - * @return {boolean} True if first task is completed - */ - canProceed( state ) { - return !! state.data.firstTaskCompleted; - } -} - -window.PrplFirstTaskStep = new PrplFirstTaskStep(); diff --git a/assets/js/onboarding/steps/MoreTasksStep.js b/assets/js/onboarding/steps/MoreTasksStep.js deleted file mode 100644 index 998d943ade..0000000000 --- a/assets/js/onboarding/steps/MoreTasksStep.js +++ /dev/null @@ -1,143 +0,0 @@ -/** - * More Tasks step - User completes additional tasks - * Handles multiple tasks that can be completed in any order - * Each task may open a sub-popover with its own form - * Split into 2 substeps: intro screen and task list - */ -/* global OnboardingStep, PrplOnboardTask */ - -class PrplMoreTasksStep extends OnboardingStep { - subSteps = [ 'more-tasks-intro', 'more-tasks-tasks' ]; - - constructor() { - super( { - templateId: 'onboarding-step-more-tasks', - } ); - this.tasks = []; - this.currentSubStep = 0; - } - - /** - * Mount the more tasks step - * Initializes all tasks and sets up event listeners - * @param {Object} state - Wizard state - * @return {Function} Cleanup function - */ - onMount( state ) { - // Always start from first sub-step - this.currentSubStep = 0; - - // Hide footer initially (will show on tasks substep) - this.toggleStepFooter( false ); - - // Render the current sub-step - this.renderSubStep( state ); - - // Setup continue button listener - const continueBtn = this.popover.querySelector( - '.prpl-more-tasks-continue' - ); - if ( continueBtn ) { - continueBtn.addEventListener( 'click', () => { - this.advanceSubStep( state ); - } ); - } - - // Setup finish onboarding link in intro - const finishLink = this.popover.querySelector( - '.prpl-more-tasks-substep[data-substep="intro"] .prpl-finish-onboarding' - ); - if ( finishLink ) { - finishLink.addEventListener( 'click', ( e ) => { - e.preventDefault(); - this.wizard.finishOnboarding(); - } ); - } - - // Initialize task completion tracking - const moreTasks = this.popover.querySelectorAll( - '.prpl-task-item[data-task-id]' - ); - moreTasks.forEach( ( btn ) => { - if ( ! state.data.moreTasksCompleted ) { - state.data.moreTasksCompleted = {}; - } - state.data.moreTasksCompleted[ btn.dataset.taskId ] = false; - } ); - - // Initialize PrplOnboardTask instances for each task, passing wizard reference - this.tasks = Array.from( - this.popover.querySelectorAll( '[data-popover="task"]' ) - ).map( ( t ) => new PrplOnboardTask( t, this.wizard ) ); - - // Listen for task completion events - const handler = ( e ) => { - // Update state when a task is completed - state.data.moreTasksCompleted[ e.detail.id ] = true; - - // Update next button state - this.updateNextButton(); - }; - - this.popover.addEventListener( 'taskCompleted', handler ); - - // Return cleanup function - return () => { - this.popover.removeEventListener( 'taskCompleted', handler ); - // Clean up task instances - this.tasks = []; - // Show footer when leaving this step - this.toggleStepFooter( true ); - }; - } - - /** - * Render the current sub-step - * @param {Object} state - Wizard state - */ - renderSubStep( state ) { - const subStepName = this.subSteps[ this.currentSubStep ]; - - // Show/hide sub-step containers - this.subSteps.forEach( ( step ) => { - const container = this.popover.querySelector( - `.prpl-more-tasks-substep[data-substep="${ step }"]` - ); - if ( container ) { - container.style.display = step === subStepName ? '' : 'none'; - } - } ); - - // Show footer only on tasks substep - const isTasksSubStep = subStepName === 'more-tasks-tasks'; - this.toggleStepFooter( isTasksSubStep ); - - // Update Next button state if on tasks sub-step - if ( isTasksSubStep ) { - this.updateNextButton(); - } - } - - /** - * Advance to next sub-step - * @param {Object} state - Wizard state - */ - advanceSubStep( state ) { - if ( this.currentSubStep < this.subSteps.length - 1 ) { - this.currentSubStep++; - this.renderSubStep( state ); - } - } - - /** - * User can only proceed if on tasks substep - * @param {Object} state - Wizard state - * @return {boolean} True if can proceed - */ - canProceed( state ) { - // Can only proceed if on tasks substep - return this.currentSubStep === this.subSteps.length - 1; - } -} - -window.PrplMoreTasksStep = new PrplMoreTasksStep(); diff --git a/assets/js/onboarding/steps/OnboardingStep.js b/assets/js/onboarding/steps/OnboardingStep.js deleted file mode 100644 index 14a9c8f5ee..0000000000 --- a/assets/js/onboarding/steps/OnboardingStep.js +++ /dev/null @@ -1,326 +0,0 @@ -/** - * Base class for onboarding steps - * All step components should extend this class - */ -class OnboardingStep { - /** - * Constructor - * @param {Object} config - Step configuration - * @param {string} config.id - Unique step identifier - * @param {string} config.templateId - ID of the template element containing the step HTML - */ - constructor( config ) { - this.templateId = config.templateId; - this.wizard = null; // Reference to parent wizard - this.popover = null; // Reference to popover element - this.cleanup = null; // Cleanup function for event listeners - this.nextBtn = null; // Reference to next button element - } - - /** - * Set wizard reference - * @param {ProgressPlannerOnboardWizard} wizard - */ - setWizard( wizard ) { - this.wizard = wizard; - this.popover = wizard.popover; - } - - /** - * Get the step's HTML content - * @return {string} HTML content - */ - render() { - const template = document.getElementById( this.templateId ); - if ( ! template ) { - console.error( `Template not found: ${ this.templateId }` ); - return ''; - } - return template.innerHTML; - } - - /** - * Called when step is mounted to DOM - * Override this method to setup event listeners and step-specific logic - * @param {Object} state - Wizard state - * @return {Function} Cleanup function to be called when step unmounts - */ - onMount( state ) { - // Override in subclass - return () => {}; - } - - /** - * Check if user can proceed to next step - * Override this method to add step-specific validation - * @param {Object} state - Wizard state - * @return {boolean} True if user can proceed - */ - canProceed( state ) { - // Override in subclass - return true; - } - - /** - * Called when step is about to be unmounted - * Override this method for cleanup logic - */ - onUnmount() { - if ( this.cleanup ) { - this.cleanup(); - this.cleanup = null; - } - } - - /** - * Utility method to update wizard state - * @param {string} key - State key to update - * @param {*} value - New value - */ - updateState( key, value ) { - if ( this.wizard ) { - this.wizard.state.data[ key ] = value; - } - } - - /** - * Utility method to get current state - * @return {Object} Current wizard state - */ - getState() { - return this.wizard ? this.wizard.state : null; - } - - /** - * Utility method to advance to next step - */ - nextStep() { - if ( this.wizard ) { - this.wizard.nextStep(); - } - } - - /** - * Get the tour footer element - * @return {HTMLElement|null} The tour footer element or null if not found - */ - getTourFooter() { - return this.wizard?.contentWrapper?.querySelector( '.tour-footer' ); - } - - /** - * Show error message to user - * @param {string} message Error message to display - * @param {string} title Optional error title - */ - showErrorMessage( message, title = '' ) { - // Remove existing error if any - this.clearErrorMessage(); - - // Build title HTML if provided - const titleHtml = title ? `

${ this.escapeHtml( title ) }

` : ''; - - // Get error icon from wizard config - const errorIcon = this.wizard?.config?.errorIcon || ''; - - // Create error message element - const errorDiv = document.createElement( 'div' ); - errorDiv.className = 'prpl-error-message'; - errorDiv.innerHTML = ` -
- - ${ errorIcon } - -
- ${ titleHtml } -

${ this.escapeHtml( message ) }

-
-
- `; - - // Add error message to tour footer - const footer = this.getTourFooter(); - if ( footer ) { - footer.prepend( errorDiv ); - } - } - - /** - * Clear error message - */ - clearErrorMessage() { - const existingError = this.wizard?.popover?.querySelector( - '.prpl-error-message' - ); - if ( existingError ) { - existingError.remove(); - } - } - - /** - * Escape HTML to prevent XSS - * @param {string} text Text to escape - * @return {string} Escaped text - */ - escapeHtml( text ) { - const div = document.createElement( 'div' ); - div.textContent = text; - return div.innerHTML; - } - - /** - * Show spinner before a button and disable the button - * @param {HTMLElement} button Button element to show spinner before and disable - * @return {HTMLElement} The created spinner element - */ - showSpinner( button ) { - const spinner = document.createElement( 'span' ); - spinner.classList.add( 'prpl-spinner' ); - spinner.innerHTML = - ''; - - button.parentElement.insertBefore( spinner, button ); - button.disabled = true; - - return spinner; - } - - /** - * Toggle visibility of the footer in this step's template - * @param {boolean} visible - Whether to show the footer - */ - toggleStepFooter( visible ) { - const stepFooter = this.getTourFooter(); - if ( stepFooter ) { - stepFooter.style.display = visible ? 'flex' : 'none'; - } - } - - /** - * Called before advancing to next step - * Fires AJAX request to subscribe user if "Email me weekly" was selected - * @return {Promise} Resolves when action is complete - */ - async beforeNextStep() { - // Override in subclass - return Promise.resolve(); - } - - /** - * Setup next button after step is rendered - * Finds button, attaches click handler, and initializes state - * Called automatically by wizard after rendering - */ - setupNextButton() { - // Find the next button in the rendered step content - this.nextBtn = - this.wizard?.contentWrapper?.querySelector( '.prpl-tour-next' ); - - if ( ! this.nextBtn ) { - // Step doesn't have a next button (e.g., SettingsStep with sub-steps) - return; - } - - // Remove any existing listeners by cloning the button - const newBtn = this.nextBtn.cloneNode( true ); - if ( this.nextBtn.parentNode ) { - this.nextBtn.parentNode.replaceChild( newBtn, this.nextBtn ); - } - this.nextBtn = newBtn; - - // Add click listener - this.nextBtn.addEventListener( 'click', () => { - console.log( 'Next button clicked!' ); - this.nextStep(); - } ); - - // Initialize button state - this.updateNextButton(); - - // Call hook for subclasses to add custom button behavior - // Returns optional cleanup function - const customCleanup = this.onNextButtonSetup(); - - // If step provided a cleanup function, chain it with existing cleanup - if ( customCleanup && typeof customCleanup === 'function' ) { - const originalCleanup = this.cleanup; - this.cleanup = () => { - customCleanup(); - if ( originalCleanup ) { - originalCleanup(); - } - }; - } - } - - /** - * Called after next button is setup - * Override to add custom button behavior - * @return {Function|void} Optional cleanup function - */ - onNextButtonSetup() { - // Override in subclass - // Return a cleanup function if you need to remove event listeners - } - - /** - * Update next button state (text and enabled/disabled) - * Called when step state changes - */ - updateNextButton() { - if ( ! this.nextBtn ) { - return; - } - - const state = this.getState(); - const canProceed = this.canProceed( state ); - - // Update enabled/disabled state - this.setNextButtonDisabled( ! canProceed ); - - // Update button text - this.updateNextButtonText(); - } - - /** - * Update next button text based on step configuration and wizard state - * Currently this is only used to change button text on the last step to "Take me to the Recommendations dashboard" - */ - updateNextButtonText() { - if ( ! this.nextBtn || ! this.wizard ) { - return; - } - - const isLastStep = - this.wizard.state.currentStep === this.wizard.tourSteps.length - 1; - - // Check if step provides custom button text - if ( isLastStep ) { - // On last step, use "Take me to the Recommendations dashboard" text - const dashboardText = - this.wizard.config?.l10n?.dashboard || - 'Take me to the Recommendations dashboard'; - this.nextBtn.innerHTML = dashboardText; - } - } - - /** - * Enable or disable the next button - * Separated into its own method for easy customization - * @param {boolean} disabled - Whether to disable the button - */ - setNextButtonDisabled( disabled ) { - if ( ! this.nextBtn ) { - return; - } - - // Using prpl-btn-disabled CSS class instead of the disabled attribute - if ( disabled ) { - this.nextBtn.classList.add( 'prpl-btn-disabled' ); - this.nextBtn.setAttribute( 'aria-disabled', 'true' ); - } else { - this.nextBtn.classList.remove( 'prpl-btn-disabled' ); - this.nextBtn.setAttribute( 'aria-disabled', 'false' ); - } - } -} diff --git a/assets/js/onboarding/steps/SettingsStep.js b/assets/js/onboarding/steps/SettingsStep.js deleted file mode 100644 index 8986db6e74..0000000000 --- a/assets/js/onboarding/steps/SettingsStep.js +++ /dev/null @@ -1,492 +0,0 @@ -/** - * Settings step - Configure About, Contact, FAQ pages, and Post Types - * Multi-step process with 5 sub-steps - */ -/* global OnboardingStep, ProgressPlannerOnboardData */ - -class PrplSettingsStep extends OnboardingStep { - subSteps = [ 'homepage', 'about', 'contact', 'faq', 'post-types' ]; - - defaultSettings = { - homepage: { - hasPage: true, // true if checkbox is NOT checked (default: unchecked) - pageId: null, - }, - about: { - hasPage: true, // true if checkbox is NOT checked (default: unchecked) - pageId: null, - }, - contact: { - hasPage: true, - pageId: null, - }, - faq: { - hasPage: true, - pageId: null, - }, - 'post-types': { - selectedTypes: [], // Array of selected post type slugs - }, - }; - - constructor() { - super( { - templateId: 'onboarding-step-settings', - } ); - this.currentSubStep = 0; - } - - /** - * Mount the settings step - * Sets up event listeners for page select and save button - * @param {Object} state - Wizard state - * @return {Function} Cleanup function - */ - onMount( state ) { - // Initialize state - if ( ! state.data.settings ) { - state.data.settings = {}; - } - - // Initialize missing sub-steps - for ( const [ key, defaultValue ] of Object.entries( - this.defaultSettings - ) ) { - if ( ! state.data.settings[ key ] ) { - state.data.settings[ key ] = { ...defaultValue }; - } - } - - // Always start from first sub-step - this.currentSubStep = 0; - - // Hide footer in step template initially (will show on last sub-step) - this.toggleStepFooter( false ); - - // Render the current sub-step - this.renderSubStep( state ); - - // Return cleanup function - return () => { - // Show footer when leaving this step (for other steps that might need it) - this.toggleStepFooter( true ); - }; - } - - /** - * Render the current sub-step - * @param {Object} state - Wizard state - */ - renderSubStep( state ) { - const subStepName = this.subSteps[ this.currentSubStep ]; - const subStepData = state.data.settings[ subStepName ]; - - // Update progress indicator - /* - const progressIndicator = this.popover.querySelector( - '.prpl-settings-progress' - ); - if ( progressIndicator ) { - progressIndicator.textContent = `${ this.currentSubStep + 1 }/${ - this.subSteps.length - }`; - } - */ - - // Show/hide sub-step containers - this.subSteps.forEach( ( step, index ) => { - const container = this.popover.querySelector( - `.prpl-setting-item[data-page="${ step }"]` - ); - if ( container ) { - container.style.display = - index === this.currentSubStep ? 'flex' : 'none'; - } - } ); - - // Hide "Save setting" button on last sub-step (show Next/Dashboard instead) - const isLastSubStep = this.currentSubStep === this.subSteps.length - 1; - const saveButton = this.popover.querySelector( - `#prpl-save-${ subStepName }-setting` - ); - if ( saveButton ) { - saveButton.style.display = isLastSubStep ? 'none' : ''; - } - - // Setup event listeners for current sub-step - this.setupSubStepListeners( subStepName, subStepData, state ); - - // Show/hide footer based on sub-step - this.toggleStepFooter( isLastSubStep ); - - // Update Next/Dashboard button state if on last sub-step - if ( isLastSubStep ) { - this.updateNextButton(); - } - } - - /** - * Setup event listeners for a sub-step - * @param {string} subStepName - Name of sub-step (about/contact/faq/post-types) - * @param {Object} subStepData - Data for this sub-step - * @param {Object} state - Wizard state - */ - setupSubStepListeners( subStepName, subStepData, state ) { - // Handle page selection sub-steps (about, contact, faq) - if ( - [ 'homepage', 'about', 'contact', 'faq' ].includes( subStepName ) - ) { - this.setupPageSelectListeners( subStepName, subStepData, state ); - return; - } - - // Handle post types sub-step - if ( subStepName === 'post-types' ) { - this.setupPostTypesListeners( subStepName, subStepData, state ); - } - } - - /** - * Setup event listeners for page select sub-steps (about, contact, faq) - * @param {string} subStepName - Name of sub-step - * @param {Object} subStepData - Data for this sub-step - * @param {Object} state - Wizard state - */ - setupPageSelectListeners( subStepName, subStepData, state ) { - // Get select and checkbox - const pageSelect = this.popover.querySelector( - `select[name="pages[${ subStepName }][id]"]` - ); - const noPageCheckbox = this.popover.querySelector( - `#prpl-no-${ subStepName }-page` - ); - - // Get save button - const saveButton = this.popover.querySelector( - `#prpl-save-${ subStepName }-setting` - ); - - if ( ! pageSelect || ! noPageCheckbox || ! saveButton ) { - return; - } - - // Get select wrapper - const selectWrapper = this.popover.querySelector( - `.prpl-setting-item[data-page="${ subStepName }"] .prpl-select-page` - ); - - // Set initial states from saved data - if ( subStepData.pageId ) { - pageSelect.value = subStepData.pageId; - } - - if ( ! subStepData.hasPage ) { - noPageCheckbox.checked = true; - if ( selectWrapper ) { - selectWrapper.classList.add( 'prpl-disabled' ); - } - } - - // Page select handler - pageSelect.addEventListener( 'change', ( e ) => { - subStepData.pageId = e.target.value; - this.updateSaveButtonState( saveButton, subStepData ); - - // Update Next/Dashboard button if on last sub-step - if ( this.currentSubStep === this.subSteps.length - 1 ) { - this.updateNextButton(); - } - } ); - - // Checkbox handler - noPageCheckbox.addEventListener( 'change', ( e ) => { - subStepData.hasPage = ! e.target.checked; - - // Display the note if the checkbox is checked. - const note = this.popover.querySelector( - `.prpl-setting-item[data-page="${ subStepName }"] .prpl-setting-footer .prpl-setting-note` - ); - - // Hide/show select based on checkbox - if ( e.target.checked ) { - // Checkbox is checked - hide select - if ( selectWrapper ) { - selectWrapper.classList.add( 'prpl-disabled' ); - } - pageSelect.value = ''; // Reset selection - subStepData.pageId = null; - if ( note ) { - note.style.display = 'flex'; - } - } else if ( selectWrapper ) { - // Checkbox is unchecked - show select - if ( selectWrapper ) { - selectWrapper.classList.remove( 'prpl-disabled' ); - } - - if ( note ) { - note.style.display = 'none'; - } - } - - this.updateSaveButtonState( saveButton, subStepData ); - - // Update Next/Dashboard button if on last sub-step - if ( this.currentSubStep === this.subSteps.length - 1 ) { - this.updateNextButton(); - } - } ); - - // Save button handler - just advances to next sub-step - saveButton.addEventListener( 'click', () => { - this.advanceSubStep( state ); - } ); - - // Initial button state - this.updateSaveButtonState( saveButton, subStepData ); - } - - /** - * Setup event listeners for post types sub-step - * @param {string} subStepName - Name of sub-step - * @param {Object} subStepData - Data for this sub-step - * @param {Object} state - Wizard state - */ - setupPostTypesListeners( subStepName, subStepData, state ) { - const container = this.popover.querySelector( - `.prpl-setting-item[data-page="${ subStepName }"]` - ); - const saveButton = this.popover.querySelector( - `#prpl-save-${ subStepName }-setting` - ); - - if ( ! container || ! saveButton ) { - return; - } - - // Get all checkboxes - const checkboxes = container.querySelectorAll( - 'input[type="checkbox"][name="prpl-post-types-include[]"]' - ); - - // Initialize selected types from checkboxes that are already checked (from template) - // or from saved data if available - if ( - subStepData.selectedTypes && - subStepData.selectedTypes.length > 0 - ) { - // Use saved data if available - checkboxes.forEach( ( checkbox ) => { - checkbox.checked = subStepData.selectedTypes.includes( - checkbox.value - ); - } ); - } else { - // Initialize from checkboxes that are already checked in the template - subStepData.selectedTypes = Array.from( checkboxes ) - .filter( ( cb ) => cb.checked ) - .map( ( cb ) => cb.value ); - - // If no checkboxes are checked, default to all checked - if ( subStepData.selectedTypes.length === 0 ) { - checkboxes.forEach( ( checkbox ) => { - checkbox.checked = true; - subStepData.selectedTypes.push( checkbox.value ); - } ); - } - } - - // Add change listeners to checkboxes - checkboxes.forEach( ( checkbox ) => { - checkbox.addEventListener( 'change', () => { - // Update selected types array - subStepData.selectedTypes = Array.from( checkboxes ) - .filter( ( cb ) => cb.checked ) - .map( ( cb ) => cb.value ); - - this.updateSaveButtonState( saveButton, subStepData ); - - // Update Next/Dashboard button if on last sub-step - if ( this.currentSubStep === this.subSteps.length - 1 ) { - this.updateNextButton(); - } - } ); - } ); - - // Save button handler - just advances to next sub-step - saveButton.addEventListener( 'click', () => { - this.advanceSubStep( state ); - } ); - - // Initial button state - this.updateSaveButtonState( saveButton, subStepData ); - } - - /** - * Advance to next sub-step - * @param {Object} state - Wizard state - */ - advanceSubStep( state ) { - if ( this.currentSubStep < this.subSteps.length - 1 ) { - this.currentSubStep++; - this.renderSubStep( state ); - // Footer visibility is handled in renderSubStep() - } - } - - /** - * Update save button state - * @param {HTMLElement} button - Save button element - * @param {Object} subStepData - Sub-step data - */ - updateSaveButtonState( button, subStepData ) { - const canSave = this.canSaveSubStep( subStepData ); - button.disabled = ! canSave; - } - - /** - * Check if sub-step can be saved - * @param {Object} subStepData - Sub-step data - * @return {boolean} True if can save - */ - canSaveSubStep( subStepData ) { - // Handle page selection sub-steps (about, contact, faq) - if ( subStepData.hasPage !== undefined ) { - // If user has the page, they must select one - if ( subStepData.hasPage && ! subStepData.pageId ) { - return false; - } - - // If checkbox is checked (don't have page), can save - if ( ! subStepData.hasPage ) { - return true; - } - - // If page is selected, can save - return !! subStepData.pageId; - } - - // Handle post types sub-step - at least one must be selected - if ( subStepData.selectedTypes !== undefined ) { - return subStepData.selectedTypes.length > 0; - } - - return false; - } - - /** - * User can proceed if on last sub-step and it's valid - * @param {Object} state - Wizard state - * @return {boolean} True if can proceed - */ - canProceed( state ) { - if ( ! state.data.settings ) { - return false; - } - - // Can only proceed if on last sub-step - if ( this.currentSubStep !== this.subSteps.length - 1 ) { - return false; - } - - // Check if all sub-steps have valid data - return this.subSteps.every( ( step ) => { - const subStepData = state.data.settings[ step ]; - return this.canSaveSubStep( subStepData ); - } ); - } - - /** - * Called before advancing to next step - * Saves all settings via AJAX - * @return {Promise} Resolves when settings are saved - */ - async beforeNextStep() { - const state = this.getState(); - - // Show spinner on Next button - const spinner = this.showSpinner( this.nextBtn ); - - try { - // Collect all settings data for a single AJAX request - const formDataObj = new FormData(); - formDataObj.append( 'action', 'prpl_save_all_onboarding_settings' ); - formDataObj.append( - 'nonce', - ProgressPlannerOnboardData.nonceProgressPlanner - ); - - // Collect page settings (about, contact, faq) - const pages = {}; - for ( const subStepName of this.subSteps ) { - const subStepData = state.data.settings[ subStepName ]; - - if ( - [ 'homepage', 'about', 'contact', 'faq' ].includes( - subStepName - ) - ) { - pages[ subStepName ] = { - id: subStepData.pageId || '', - have_page: subStepData.hasPage ? 'yes' : 'no', - }; - } - } - - // Add pages data as JSON - if ( Object.keys( pages ).length > 0 ) { - formDataObj.append( 'pages', JSON.stringify( pages ) ); - } - - // Add post types - const postTypesData = state.data.settings[ 'post-types' ]; - if ( postTypesData && postTypesData.selectedTypes ) { - postTypesData.selectedTypes.forEach( ( postType ) => { - formDataObj.append( 'prpl-post-types-include[]', postType ); - } ); - } - - // Send single AJAX request - const response = await fetch( - ProgressPlannerOnboardData.adminAjaxUrl, - { - method: 'POST', - body: formDataObj, - credentials: 'same-origin', - } - ); - - if ( ! response.ok ) { - throw new Error( 'Request failed: ' + response.status ); - } - - const result = await response.json(); - - if ( ! result.success ) { - throw new Error( - result.data?.message || 'Failed to save settings' - ); - } - - console.log( 'Successfully saved all onboarding settings' ); - } catch ( error ) { - console.error( 'Failed to save settings:', error ); - - // Display error message - this.showErrorMessage( - error.message || 'Failed to save settings. Please try again.', - 'Error saving setting' - ); - - // Re-enable button - this.setNextButtonDisabled( false ); - - // Don't proceed to next step - throw error; - } finally { - spinner.remove(); - } - } -} - -window.PrplSettingsStep = new PrplSettingsStep(); diff --git a/assets/js/onboarding/steps/WelcomeStep.js b/assets/js/onboarding/steps/WelcomeStep.js deleted file mode 100644 index 8ad4498be3..0000000000 --- a/assets/js/onboarding/steps/WelcomeStep.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Welcome step - First step in the onboarding flow - * Displays a welcome message, logo, and privacy policy checkbox - */ -/* global OnboardingStep, LicenseGenerator, ProgressPlannerOnboardData */ - -class PrplWelcomeStep extends OnboardingStep { - constructor() { - super( { - templateId: 'onboarding-step-welcome', - } ); - this.isGeneratingLicense = false; - } - - /** - * Mount the welcome step - * Sets up checkbox listener and initializes state - * @param {Object} state - Wizard state - * @return {Function} Cleanup function - */ - onMount( state ) { - const checkbox = this.popover.querySelector( '#prpl-privacy-checkbox' ); - - if ( ! checkbox ) { - return () => {}; - } - - // Initialize state from checkbox if not already set in saved state - if ( state.data.privacyAccepted === undefined ) { - state.data.privacyAccepted = checkbox.checked; - } else { - // Set checkbox state from wizard state - checkbox.checked = state.data.privacyAccepted; - } - - const handler = ( e ) => { - state.data.privacyAccepted = e.target.checked; - - // Remove active class from required indicator. - this.popover - .querySelector( - '.prpl-privacy-checkbox-wrapper .prpl-required-indicator' - ) - .classList.remove( 'prpl-required-indicator-active' ); - }; - - checkbox.addEventListener( 'change', handler ); - - return () => { - checkbox.removeEventListener( 'change', handler ); - }; - } - - /** - * Setup custom handler for disabled button clicks - * Shows error message when user tries to proceed without accepting privacy policy - * @return {Function} Cleanup function - */ - onNextButtonSetup() { - const disabledClickHandler = ( e ) => { - if ( this.nextBtn.classList.contains( 'prpl-btn-disabled' ) ) { - e.preventDefault(); - e.stopPropagation(); - - this.popover - .querySelector( - '.prpl-privacy-checkbox-wrapper .prpl-required-indicator' - ) - .classList.add( 'prpl-required-indicator-active' ); - } - }; - - this.nextBtn.addEventListener( 'click', disabledClickHandler ); - - // Return cleanup function - return () => { - this.nextBtn?.removeEventListener( 'click', disabledClickHandler ); - }; - } - - /** - * User can only proceed if privacy policy is accepted - * Sites with existing license bypass this check (no privacy checkbox shown). - * @param {Object} state - Wizard state - * @return {boolean} True if privacy is accepted or license exists - */ - canProceed( state ) { - // Sites with license already skip the privacy checkbox. - if ( ProgressPlannerOnboardData.hasLicense ) { - return true; - } - return !! state.data.privacyAccepted; - } - - /** - * Called before advancing to next step - * Generates license and shows spinner - * Branded sites with existing license skip this step. - * @return {Promise} Resolves when license is generated - */ - async beforeNextStep() { - // Skip license generation if site already has a license (branded sites). - if ( ProgressPlannerOnboardData.hasLicense ) { - return; - } - - if ( this.isGeneratingLicense ) { - return; - } - - this.isGeneratingLicense = true; - - // Clear any existing error messages - this.clearErrorMessage(); - - // Show spinner - const spinner = this.showSpinner( this.nextBtn ); - - try { - // Generate license - await this.generateLicense(); - } catch ( error ) { - console.error( 'Failed to generate license:', error ); - - // Display error message to user - this.showErrorMessage( error.message, 'Error generating license' ); - - // Re-enable the button so user can retry - this.setNextButtonDisabled( false ); - - // Don't proceed to next step - throw error; - } finally { - // Remove spinner - spinner.remove(); - this.isGeneratingLicense = false; - } - } - - /** - * Generate license on server - * Uses LicenseGenerator utility class - * @return {Promise} Resolves when license is generated - */ - async generateLicense() { - // Use LicenseGenerator to handle the license generation process - return LicenseGenerator.generateLicense( - { - name: '', - email: '', - 'with-email': 'no', - site: ProgressPlannerOnboardData.site, - timezone_offset: ProgressPlannerOnboardData.timezone_offset, - }, - { - onboardNonceURL: ProgressPlannerOnboardData.onboardNonceURL, - onboardAPIUrl: ProgressPlannerOnboardData.onboardAPIUrl, - adminAjaxUrl: ProgressPlannerOnboardData.adminAjaxUrl, - nonce: ProgressPlannerOnboardData.nonceProgressPlanner, - } - ); - } -} - -window.PrplWelcomeStep = new PrplWelcomeStep(); diff --git a/assets/js/onboarding/steps/WhatsWhatStep.js b/assets/js/onboarding/steps/WhatsWhatStep.js deleted file mode 100644 index c46a49c90a..0000000000 --- a/assets/js/onboarding/steps/WhatsWhatStep.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Whats What step - Explains the badge system to users - * Simple informational step with no user interaction required - */ -/* global OnboardingStep */ -class PrplWhatsWhatStep extends OnboardingStep { - constructor() { - super( { - templateId: 'onboarding-step-whats-what', - } ); - } - - /** - * No special mounting logic needed for badges step - * @param {Object} state - Wizard state - * @return {Function} Cleanup function - */ - onMount( state ) { - // Whats Next step is informational only - // No special logic needed - return () => {}; - } - - /** - * User can always proceed from badges step - * @param {Object} state - Wizard state - * @return {boolean} Always returns true - */ - canProceed( state ) { - return true; - } -} - -window.PrplWhatsWhatStep = new PrplWhatsWhatStep(); diff --git a/assets/js/settings.js b/assets/js/settings.js index 32d7670391..6695710039 100644 --- a/assets/js/settings.js +++ b/assets/js/settings.js @@ -1,10 +1,10 @@ -/* global prplL10n, LicenseGenerator */ +/* global progressPlanner, progressPlannerAjaxRequest, progressPlannerSaveLicenseKey, prplL10n */ /* * Settings * * A script to handle the settings page. * - * Dependencies: progress-planner/l10n, progress-planner/license-generator + * Dependencies: progress-planner/ajax-request, progress-planner/onboard, wp-util, progress-planner/l10n */ // Submit the email. @@ -22,23 +22,47 @@ if ( !! settingsLicenseForm ) { data[ key ] = value; } - document.getElementById( 'submit-license-key' ).disabled = true; - document.getElementById( 'submit-license-key' ).innerHTML = - prplL10n( 'subscribing' ); + progressPlannerAjaxRequest( { + url: progressPlanner.onboardNonceURL, + data, + } ) + .then( ( response ) => { + if ( 'ok' === response.status ) { + // Add the nonce to our data object. + data.nonce = response.nonce; + + // Make the request to the API. + progressPlannerAjaxRequest( { + url: progressPlanner.onboardAPIUrl, + data, + } ) + .then( ( apiResponse ) => { + // Make a local request to save the response data. + progressPlannerSaveLicenseKey( + apiResponse.license_key + ); - LicenseGenerator.generateLicense( data ) - .then( () => { - document.getElementById( 'submit-license-key' ).innerHTML = - prplL10n( 'subscribed' ); + document.getElementById( + 'submit-license-key' + ).innerHTML = prplL10n( 'subscribed' ); - // Timeout so the license key is saved. - setTimeout( () => { - // Reload the page. - window.location.reload(); - }, 500 ); + // Timeout so the license key is saved. + setTimeout( () => { + // Reload the page. + window.location.reload(); + }, 500 ); + } ) + .catch( ( error ) => { + console.warn( error ); + } ); + } } ) .catch( ( error ) => { console.warn( error ); } ); + + document.getElementById( 'submit-license-key' ).disabled = true; + document.getElementById( 'submit-license-key' ).innerHTML = + prplL10n( 'subscribing' ); } ); } diff --git a/classes/admin/class-page-settings.php b/classes/admin/class-page-settings.php index ba5e396897..891cefc3af 100644 --- a/classes/admin/class-page-settings.php +++ b/classes/admin/class-page-settings.php @@ -140,11 +140,15 @@ public function set_page_values( $pages ) { /** * Save the settings. * - * @param bool $redirect_on_login Whether to redirect on login. * @return void */ - public function save_settings( $redirect_on_login ) { - \update_user_meta( \get_current_user_id(), 'prpl_redirect_on_login', $redirect_on_login ); + public function save_settings() { + // Nonce is already checked in store_settings_form_options() which calls this method. + $redirect_on_login = isset( $_POST['prpl-redirect-on-login'] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing + ? \sanitize_text_field( \wp_unslash( $_POST['prpl-redirect-on-login'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Missing + : false; + + \update_user_meta( \get_current_user_id(), 'prpl_redirect_on_login', (bool) $redirect_on_login ); } /** diff --git a/classes/admin/class-page.php b/classes/admin/class-page.php index f5f36fc302..b4424fb24a 100644 --- a/classes/admin/class-page.php +++ b/classes/admin/class-page.php @@ -295,6 +295,15 @@ public function enqueue_styles() { // Enqueue ugprading (onboarding) tasks styles, these are needed both when privacy policy is accepted and when it is not. \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/upgrade-tasks' ); } + + $prpl_privacy_policy_accepted = \progress_planner()->is_privacy_policy_accepted(); + if ( ! $prpl_privacy_policy_accepted ) { + // Enqueue welcome styles. + \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/welcome' ); + + // Enqueue onboarding styles. + \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/onboard' ); + } } /** diff --git a/classes/class-base.php b/classes/class-base.php index 7978a9367d..b789b375bc 100644 --- a/classes/class-base.php +++ b/classes/class-base.php @@ -51,11 +51,10 @@ * @method \Progress_Planner\UI\Popover get_ui__popover() * @method \Progress_Planner\Admin\Widgets\Content_Activity get_admin__widgets__content_activity() * @method \Progress_Planner\UI\Chart get_ui__chart() - * @method \Progress_Planner\Activities\Content_Helpers|null get_activities__content_helpers() + * @method \Progress_Planner\Activities\Content_Helpers get_activities__content_helpers() * @method \Progress_Planner\Admin\Widgets\Challenge get_admin__widgets__challenge() * @method \Progress_Planner\Admin\Widgets\Activity_Scores get_admin__widgets__activity_scores() * @method \Progress_Planner\Utils\Date get_utils__date() - * @method \Progress_Planner\Onboard_Wizard get_onboard_wizard() */ class Base { @@ -181,9 +180,6 @@ public function init() { // Init the enqueue class. $this->get_admin__enqueue()->init(); - - // TODO: Decide when this needs to be initialized. - $this->get_onboard_wizard(); } /** @@ -563,7 +559,7 @@ public function is_on_progress_planner_dashboard_page() { * @return bool */ public function is_debug_mode_enabled() { - return ( ( \defined( 'PRPL_DEBUG' ) && PRPL_DEBUG ) || \get_option( 'prpl_debug' ) ) && \current_user_can( 'manage_options' ); + return ( \defined( 'PRPL_DEBUG' ) && PRPL_DEBUG ) || \get_option( 'prpl_debug' ); } } // phpcs:enable Generic.Commenting.Todo diff --git a/classes/class-onboard-wizard.php b/classes/class-onboard-wizard.php deleted file mode 100644 index f484b487e5..0000000000 --- a/classes/class-onboard-wizard.php +++ /dev/null @@ -1,604 +0,0 @@ -get_ui__branding()->get_branding_id(); - $show_onboarding = ! \progress_planner()->is_privacy_policy_accepted() - || \get_option( self::PROGRESS_OPTION_NAME, false ) - || $is_branded; - - /** - * Filter whether to show the onboarding wizard. - * - * Hosting integrations can use this filter to force showing - * or hiding the onboarding wizard. - * - * @param bool $show_onboarding Whether to show the onboarding wizard. - */ - $show_onboarding = \apply_filters( 'progress_planner_show_onboarding', $show_onboarding ); - - if ( ! $show_onboarding ) { - return; - } - - // Add popover on front end. - \add_action( 'wp_footer', [ $this, 'add_popover' ] ); - \add_action( 'wp_footer', [ $this, 'add_popover_step_templates' ] ); - \add_action( 'wp_enqueue_scripts', [ $this, 'add_popover_scripts' ] ); - - // Add popover on admin. - \add_action( 'admin_footer', [ $this, 'add_popover' ] ); - \add_action( 'admin_footer', [ $this, 'add_popover_step_templates' ] ); - \add_action( 'admin_enqueue_scripts', [ $this, 'add_popover_scripts' ] ); - - // Trigger the onboarding wizard on the front end. - \add_action( 'wp_footer', [ $this, 'trigger_onboarding' ] ); - \add_action( 'admin_footer', [ $this, 'trigger_onboarding' ] ); - - // Define steps and their order. - \add_action( 'init', [ $this, 'define_steps_and_order' ], 101 ); - - // Allow only images for the front-end upload. - \add_filter( 'rest_pre_insert_attachment', [ $this, 'rest_pre_insert_attachment' ], 10, 2 ); - } - - /** - * Define steps and their order. - * - * @return void - */ - public function define_steps_and_order() { - $saved_progress = $this->get_saved_progress(); - - // We need to know if the first task is already completed, in case user resumes the onboarding. - $was_first_task_completed = isset( $saved_progress['data'] ) && ! empty( $saved_progress['data']['firstTaskCompleted'] ); - - // Get the onboarding tasks. - $onboarding_tasks = [ - 'core-blogdescription', - 'select-timezone', - 'select-locale', - 'core-siteicon', - ]; - - $tasks = []; - - foreach ( $onboarding_tasks as $task_id ) { - $task = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $task_id ] ); - $task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $task_id ); - - // If there is no task, create it. - if ( ! $task && $task_provider ) { - $task_data = $task_provider->get_task_details(); - - // Task will not be inserted if it already exists. - \progress_planner()->get_suggested_tasks_db()->add( $task_data ); - - // Now get the task. - $task = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $task_id ] ); - } - - // Safety check: Skip if task could not be created or retrieved. - if ( empty( $task ) ) { - \error_log( 'Onboarding: Could not retrieve or create task: ' . $task_id ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log - continue; - } - - $task_formatted = [ - 'task_id' => $task[0]->get_task_id(), - 'title' => $task[0]->post_title ?? '', - 'url' => $task[0]->url ?? '', - 'provider_id' => $task[0]->get_provider_id(), - 'points' => $task[0]->points ?? 0, - 'action_label' => $task_provider ? $task_provider->get_task_action_label() : \esc_html__( 'Do it', 'progress-planner' ), - ]; - - // Add task specific data. - if ( 'core-blogdescription' === $task_id ) { - $task_formatted['site_description'] = \get_bloginfo( 'description' ); - } - - $tasks[ $task_id ] = $task_formatted; - } - - $this->steps = [ - [ - 'script_file_name' => 'WelcomeStep', - 'template_file_name' => 'welcome', - 'template_id' => 'onboarding-step-welcome', - /* translators: %s: Progress Planner name. */ - 'title' => sprintf( esc_html__( 'Welcome to %s', 'progress-planner' ), \esc_html( \progress_planner()->get_ui__branding()->get_admin_menu_name() ) ), - ], - [ - 'script_file_name' => 'WhatsWhatStep', - 'template_file_name' => 'whats-what', - 'template_id' => 'onboarding-step-whats-what', - 'title' => esc_html__( 'What\'s what?', 'progress-planner' ), - ], - ]; - - // Add first task step if there are tasks or if the first task is already completed. - if ( ! empty( $tasks ) || $was_first_task_completed ) { - $this->steps[] = [ - 'script_file_name' => 'FirstTaskStep', - 'template_file_name' => 'first-task', - 'template_data' => ! $was_first_task_completed ? [ 'task' => \array_shift( $tasks ) ] : [], - 'template_id' => 'onboarding-step-first-task', - 'title' => esc_html__( 'Complete your first task!', 'progress-planner' ), - ]; - } - - $this->steps[] = [ - 'script_file_name' => 'BadgesStep', - 'template_file_name' => 'badges', - 'template_id' => 'onboarding-step-badges', - 'title' => esc_html__( 'Our badges are waiting for you', 'progress-planner' ), - ]; - - $this->steps[] = [ - 'script_file_name' => 'EmailFrequencyStep', - 'template_file_name' => 'email-frequency', - 'template_id' => 'onboarding-step-email-frequency', - 'title' => esc_html__( 'Email Frequency', 'progress-planner' ), - ]; - - $this->steps[] = [ - 'script_file_name' => 'SettingsStep', - 'template_file_name' => 'settings', - 'template_id' => 'onboarding-step-settings', - 'title' => esc_html__( 'Settings', 'progress-planner' ), - ]; - - // Add more-tasks step if there are remaining tasks. - if ( ! empty( $tasks ) ) { - $this->steps[] = [ - 'script_file_name' => 'MoreTasksStep', - 'template_file_name' => 'more-tasks', - 'template_data' => [ 'tasks' => $tasks ], - 'template_id' => 'onboarding-step-more-tasks', - 'title' => esc_html__( 'Finish onboarding!', 'progress-planner' ), - ]; - } - } - - /** - * Allow only images for the front-end upload. - * - * @param array $attachment The attachment. - * @param \WP_REST_Request $request The request. - * @return array|\WP_Error The attachment or WP_Error. - */ - public function rest_pre_insert_attachment( $attachment, $request ) { - - // Only run for our file upload. - if ( isset( $request['prplFileUpload'] ) && $request['prplFileUpload'] ) { - - $files = $request->get_file_params(); - - if ( empty( $files['file'] ) ) { - return new \WP_Error( - 'rest_no_file', - __( 'No file uploaded.', 'progress-planner' ), - [ 'status' => 400 ] - ); - } - - $file = $files['file']; - - // Check MIME type. - if ( strpos( $file['type'], 'image/' ) !== 0 ) { - return new \WP_Error( - 'rest_invalid_file_type', - __( 'Only images are allowed for this upload.', 'progress-planner' ), - [ 'status' => 400 ] - ); - } - } - - return $attachment; - } - - /** - * Add popover scripts. - * - * @return void - */ - public function add_popover_scripts() { - // Enqueue variables-color.css. - \wp_enqueue_style( 'prpl-variables-color', \constant( 'PROGRESS_PLANNER_URL' ) . '/assets/css/variables-color.css', [], \progress_planner()->get_plugin_version() ); - - \wp_add_inline_style( 'prpl-variables-color', \progress_planner()->get_ui__branding()->get_custom_css() ); - - // Enqueue onboarding.css. - progress_planner()->get_admin__enqueue()->enqueue_style( 'onboarding/onboarding' ); - - // Enqueue PrplOnboardTask (used by MoreTasksStep). - \progress_planner()->get_admin__enqueue()->enqueue_script( 'onboarding/OnboardTask' ); - - // Enqueue base step class. - \progress_planner()->get_admin__enqueue()->enqueue_script( 'onboarding/steps/OnboardingStep' ); - - // Enqueue step components. - foreach ( $this->steps as $step ) { - \progress_planner()->get_admin__enqueue()->enqueue_script( 'onboarding/steps/' . $step['script_file_name'] ); - } - - \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-gauge' ); - - // Get saved progress from user meta. - $saved_progress = $this->get_saved_progress(); - - // Enqueue main onboarding.js (depends on all step components). - \progress_planner()->get_admin__enqueue()->enqueue_script( - 'onboarding/onboarding', - [ - 'name' => 'ProgressPlannerOnboardData', - 'data' => [ - 'adminAjaxUrl' => \esc_url_raw( admin_url( 'admin-ajax.php' ) ), - 'nonceProgressPlanner' => \esc_js( \wp_create_nonce( 'progress_planner' ) ), - 'nonceWPAPI' => \esc_js( \wp_create_nonce( 'wp_rest' ) ), - 'popoverId' => 'prpl-popover-onboarding', - 'onboardAPIUrl' => \progress_planner()->get_utils__onboard()->get_remote_url( 'onboard' ), - 'onboardNonceURL' => \progress_planner()->get_utils__onboard()->get_remote_url( 'get-nonce' ), - 'site' => \esc_attr( \set_url_scheme( \site_url() ) ), - 'timezone_offset' => (float) ( \wp_timezone()->getOffset( new \DateTime( 'midnight' ) ) / 3600 ), - 'savedProgress' => $saved_progress, - 'lastStepRedirectUrl' => \esc_url_raw( admin_url( 'admin.php?page=progress-planner' ) ), - 'fullscreenMode' => true, // Enable fullscreen takeover mode. - 'hasLicense' => false !== \progress_planner()->get_license_key(), - 'l10n' => [ - 'next' => \esc_html__( 'Next', 'progress-planner' ), - 'startOnboarding' => \esc_html__( 'Start onboarding', 'progress-planner' ), - /* translators: %s: Progress Planner name. */ - 'privacyPolicyError' => sprintf( \esc_html__( 'You need to agree with the privacy policy to use the %s plugin.', 'progress-planner' ), \esc_html( \progress_planner()->get_ui__branding()->get_admin_menu_name() ) ), - /* translators: %s: arrow icon */ - 'dashboard' => sprintf( \esc_html__( 'Take me to the dashboard %s', 'progress-planner' ), '' ), - 'backToRecommendations' => \esc_html__( 'Back to recommendations', 'progress-planner' ), - ], - 'errorIcon' => \progress_planner()->get_asset( 'images/icon_exclamation_circle.svg' ), - 'steps' => array_column( $this->steps, 'script_file_name' ), - ], - ] - ); - } - - /** - * Get saved progress from user meta. - * - * @return array|null - */ - protected function get_saved_progress() { - if ( ! \get_current_user_id() ) { - return null; - } - - $onboarding_progress = \get_option( self::PROGRESS_OPTION_NAME, true ); - if ( ! $onboarding_progress ) { - return null; - } - - $decoded = \json_decode( $onboarding_progress, true ); - if ( ! $decoded || ! \is_array( $decoded ) ) { - return null; - } - - return $decoded; - } - - /** - * Verify AJAX security (capability and nonce). - * - * @return void - */ - protected function verify_ajax_security() { - if ( ! \current_user_can( 'manage_options' ) ) { - \wp_send_json_error( [ 'message' => \esc_html__( 'You do not have permission to perform this action.', 'progress-planner' ) ] ); - } - - if ( ! \check_ajax_referer( 'progress_planner', 'nonce', false ) ) { - \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid nonce.', 'progress-planner' ) ] ); - } - } - - /** - * Save the tour progress. - * - * @return void - */ - public function ajax_save_onboarding_progress() { - $this->verify_ajax_security(); - - if ( ! isset( $_POST['state'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in verify_ajax_security(). - \wp_send_json_error( [ 'message' => \esc_html__( 'State is required.', 'progress-planner' ) ] ); - } - $progress = \sanitize_text_field( \wp_unslash( $_POST['state'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in verify_ajax_security(). - - \error_log( print_r( $progress, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r, WordPress.PHP.DevelopmentFunctions.error_log_error_log - - // Save as user meta? - \update_option( self::PROGRESS_OPTION_NAME, $progress ); - - \wp_send_json_success( [ 'message' => \esc_html__( 'Tour progress saved.', 'progress-planner' ) ] ); - } - - /** - * Complete a task. - * - * @return void - */ - public function ajax_complete_task() { - $this->verify_ajax_security(); - - if ( ! isset( $_POST['task_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in verify_ajax_security(). - \wp_send_json_error( [ 'message' => \esc_html__( 'Task ID is required.', 'progress-planner' ) ] ); - } - - $task_id = \sanitize_text_field( \wp_unslash( $_POST['task_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in verify_ajax_security(). - - // Aditional data for the task, besides the task ID. - $form_values = []; - if ( isset( $_POST['form_values'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in verify_ajax_security(). - $form_values = \sanitize_text_field( \wp_unslash( $_POST['form_values'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in verify_ajax_security(). - $form_values = \json_decode( $form_values, true ); - } - - // Safety check: Ensure form_values is an array after decoding. - if ( ! \is_array( $form_values ) ) { - $form_values = []; - } - - // Get the task. - $task = \progress_planner()->get_suggested_tasks_db()->get_post( $task_id ); - if ( ! $task ) { - \wp_send_json_error( [ 'message' => \esc_html__( 'Task not found.', 'progress-planner' ) ] ); - } - - // To get the provider and complete the task, we need to use the provider. - $provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $task->get_provider_id() ); - if ( ! $provider ) { - \wp_send_json_error( [ 'message' => \esc_html__( 'Provider not found.', 'progress-planner' ) ] ); - } - - // Complete the task. - $task_completed = $provider->complete_task( $form_values, $task_id ); - - // It will skip the celebration and set the task's post status to trash. - $task_post_marked_as_completed = \progress_planner()->get_suggested_tasks()->mark_task_as_completed( $task_id, null, true ); - - if ( ! $task_completed || ! $task_post_marked_as_completed ) { - \error_log( 'Task not completed: ' . $task_id ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log - \wp_send_json_error( [ 'message' => \esc_html__( 'Task not completed.', 'progress-planner' ) ] ); - } - - \error_log( 'Task completed: ' . $task_id ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log - \wp_send_json_success( [ 'message' => \esc_html__( 'Task completed.', 'progress-planner' ) ] ); - } - - /** - * Handle saving all onboarding settings in a single request. - * - * @return void - */ - public function ajax_save_all_onboarding_settings() { - $this->verify_ajax_security(); - - $page_settings = \progress_planner()->get_admin__page_settings(); - - // Handle page settings (about, contact, faq). - if ( isset( $_POST['pages'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing - $pages_json = \sanitize_text_field( \wp_unslash( $_POST['pages'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing - $pages = \json_decode( $pages_json, true ); - - if ( \is_array( $pages ) ) { - // Convert to the format expected by set_page_values. - $pages_formatted = []; - foreach ( $pages as $page_type => $page_data ) { - if ( isset( $page_data['id'] ) && isset( $page_data['have_page'] ) ) { - $pages_formatted[ $page_type ] = [ - 'id' => (int) $page_data['id'], - 'have_page' => $page_data['have_page'], - ]; - } - } - - if ( ! empty( $pages_formatted ) ) { - $page_settings->set_page_values( $pages_formatted ); - } - } - } - - // Handle post types. - $include_post_types = isset( $_POST['prpl-post-types-include'] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing - ? \array_map( 'sanitize_text_field', \wp_unslash( $_POST['prpl-post-types-include'] ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing - : []; - $page_settings->save_post_types( $include_post_types ); - - // Handle login destination. - $redirect_on_login = isset( $_POST['prpl-redirect-on-login'] ) ? (bool) \sanitize_text_field( \wp_unslash( $_POST['prpl-redirect-on-login'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Missing - $page_settings->save_settings( $redirect_on_login ); - - \wp_send_json_success( [ 'message' => \esc_html__( 'All settings saved successfully.', 'progress-planner' ) ] ); - } - - /** - * Trigger the onboarding wizard on the front end. - * - * @return void - */ - public function trigger_onboarding() { - - // If the request is an AJAX request, do not trigger the onboarding wizard. - if ( \wp_doing_ajax() ) { - return; - } - - // Dont trigger it if user is not logged in and is not a admin. - if ( ! \is_user_logged_in() || ! \current_user_can( 'manage_options' ) ) { - return; - } - - $get_saved_progress = $this->get_saved_progress(); - - // If there is no saved progress, trigger the onboarding wizard. - if ( ! $get_saved_progress ) { - ?> - - -
-
- -
-
-
- steps[0]['title'] ); ?> -
-
    - steps as $step ) : - ?> -
  1. - - [] ] ); ?> -
  2. - -
-
- -
- - -
-
- - -
-
-
- - -
- steps as $step ) { - \progress_planner()->the_view( 'onboarding/' . $step['template_file_name'] . '.php', isset( $step['template_data'] ) ? $step['template_data'] : [] ); - } - - // Add quit confirmation template. - \progress_planner()->the_view( 'onboarding/quit-confirmation.php' ); - ?> - - mark_task_as_completed( $task_id, $user_id ); - } - - /** - * Complete a task. - * - * @param string $task_id The task ID. - * @param int|null $user_id Optional. The user ID for token deletion. If provided, the token will be deleted. - * @param bool $skip_celebration Optional. Whether to skip the celebration. - * - * @return bool - */ - public function mark_task_as_completed( $task_id, $user_id = null, $skip_celebration = false ) { if ( ! $this->was_task_completed( $task_id ) ) { $task = \progress_planner()->get_suggested_tasks_db()->get_post( $task_id ); if ( $task ) { - $post_status = $skip_celebration ? 'trash' : 'pending'; - \progress_planner()->get_suggested_tasks_db()->update_recommendation( $task->ID, [ 'post_status' => $post_status ] ); + \progress_planner()->get_suggested_tasks_db()->update_recommendation( $task->ID, [ 'post_status' => 'pending' ] ); // Insert an activity. $this->insert_activity( $task_id ); - // Delete the token after successful use (one-time use) if user_id is provided. - if ( $user_id ) { - $this->delete_task_completion_token( $task_id, $user_id ); - } - - return true; + // Delete the token after successful use (one-time use). + $this->delete_task_completion_token( $task_id, $user_id ); } } - - return false; } /** diff --git a/classes/suggested-tasks/class-tasks-interface.php b/classes/suggested-tasks/class-tasks-interface.php index bf9a9ca598..18b3f8f3aa 100644 --- a/classes/suggested-tasks/class-tasks-interface.php +++ b/classes/suggested-tasks/class-tasks-interface.php @@ -114,13 +114,6 @@ public function get_popover_id(); */ public function add_task_actions( $data = [], $actions = [] ); - /** - * Get the task action label. - * - * @return string - */ - public function get_task_action_label(); - /** * Check if the task has activity. * @@ -129,14 +122,4 @@ public function get_task_action_label(); * @return bool */ public function task_has_activity( $task_id = '' ); - - /** - * Complete the task. - * - * @param array $args The task data. - * @param string $task_id The task ID. - * - * @return bool - */ - public function complete_task( $args = [], $task_id = '' ); } diff --git a/classes/suggested-tasks/providers/class-blog-description.php b/classes/suggested-tasks/providers/class-blog-description.php index add79362a3..61ec8bb519 100644 --- a/classes/suggested-tasks/providers/class-blog-description.php +++ b/classes/suggested-tasks/providers/class-blog-description.php @@ -142,42 +142,9 @@ public function print_popover_form_contents() { public function add_task_actions( $data = [], $actions = [] ) { $actions[] = [ 'priority' => 10, - 'html' => '' . \esc_html( $this->get_task_action_label() ) . '', + 'html' => '' . \esc_html__( 'Set tagline', 'progress-planner' ) . '', ]; return $actions; } - - /** - * Get the task action label. - * - * @return string - */ - public function get_task_action_label() { - return \__( 'Set tagline', 'progress-planner' ); - } - - /** - * Complete the task. - * - * @param array $args The task data. - * @param string $task_id The task ID. - * - * @return bool - */ - public function complete_task( $args = [], $task_id = '' ) { - - if ( ! $this->capability_required() ) { - return false; - } - - if ( ! isset( $args['blogdescription'] ) ) { - return false; - } - - // update_option will return false if the option value is the same as the one being set. - \update_option( 'blogdescription', \sanitize_text_field( $args['blogdescription'] ) ); - - return true; - } } diff --git a/classes/suggested-tasks/providers/class-select-locale.php b/classes/suggested-tasks/providers/class-select-locale.php index b15251b393..313d4aa5cf 100644 --- a/classes/suggested-tasks/providers/class-select-locale.php +++ b/classes/suggested-tasks/providers/class-select-locale.php @@ -269,13 +269,27 @@ public function handle_interactive_task_specific_submit() { \wp_send_json_error( [ 'message' => \esc_html__( 'Missing setting path.', 'progress-planner' ) ] ); } + $option_updated = false; $language_for_update = \sanitize_text_field( \wp_unslash( $_POST['value'] ) ); if ( empty( $language_for_update ) ) { \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid language.', 'progress-planner' ) ] ); } - $option_updated = $this->update_language( $language_for_update ); + // Handle translation installation. + if ( \current_user_can( 'install_languages' ) ) { + // @phpstan-ignore-next-line requireOnce.fileNotFound + require_once ABSPATH . 'wp-admin/includes/translation-install.php'; + + if ( \wp_can_install_language_pack() ) { + $language = \wp_download_language_pack( $language_for_update ); + if ( $language ) { + $language_for_update = $language; + + $option_updated = \update_option( 'WPLANG', $language_for_update ); + } + } + } if ( $option_updated ) { \wp_send_json_success( [ 'message' => \esc_html__( 'Setting updated.', 'progress-planner' ) ] ); @@ -295,68 +309,9 @@ public function handle_interactive_task_specific_submit() { public function add_task_actions( $data = [], $actions = [] ) { $actions[] = [ 'priority' => 10, - 'html' => '' . \esc_html( $this->get_task_action_label() ) . '', + 'html' => '' . \esc_html__( 'Select locale', 'progress-planner' ) . '', ]; return $actions; } - - /** - * Get the task action label. - * - * @return string - */ - public function get_task_action_label() { - return \__( 'Select locale', 'progress-planner' ); - } - - /** - * Complete the task. - * - * @param array $args The task data. - * @param string $task_id The task ID. - * - * @return bool - */ - public function complete_task( $args = [], $task_id = '' ) { - - if ( ! $this->capability_required() ) { - return false; - } - - if ( ! isset( $args['language'] ) ) { - return false; - } - - return $this->update_language( \sanitize_text_field( \wp_unslash( $args['language'] ) ) ); - } - - /** - * Update the language. - * - * @param string $language_for_update The language to update. - * - * @return bool - */ - protected function update_language( $language_for_update ) { - // Handle translation installation. - if ( \current_user_can( 'install_languages' ) ) { - // @phpstan-ignore-next-line requireOnce.fileNotFound - require_once ABSPATH . 'wp-admin/includes/translation-install.php'; - - if ( \wp_can_install_language_pack() ) { - $language = \wp_download_language_pack( $language_for_update ); - if ( $language ) { - $language_for_update = $language; - - // update_option will return false if the option value is the same as the one being set. - \update_option( 'WPLANG', $language_for_update ); - - return true; - } - } - } - - return false; - } } diff --git a/classes/suggested-tasks/providers/class-select-timezone.php b/classes/suggested-tasks/providers/class-select-timezone.php index 14807d3bfe..3eb54c8bf1 100644 --- a/classes/suggested-tasks/providers/class-select-timezone.php +++ b/classes/suggested-tasks/providers/class-select-timezone.php @@ -192,75 +192,6 @@ public function handle_interactive_task_specific_submit() { \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid timezone.', 'progress-planner' ) ] ); } - $option_updated = $this->update_timezone( $timezone_string ); - - if ( $option_updated ) { - - // We're not checking for the return value of the update_option calls, because it will return false if the value is the same (for example if gmt_offset is already set to ''). - \wp_send_json_success( [ 'message' => \esc_html__( 'Setting updated.', 'progress-planner' ) ] ); - } - - \wp_send_json_error( [ 'message' => \esc_html__( 'Failed to update setting.', '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( $this->get_task_action_label() ) . '', - ]; - - return $actions; - } - - /** - * Get the task action label. - * - * @return string - */ - public function get_task_action_label() { - return \__( 'Select timezone', 'progress-planner' ); - } - - /** - * Complete the task. - * - * @param array $args The task data. - * @param string $task_id The task ID. - * - * @return bool - */ - public function complete_task( $args = [], $task_id = '' ) { - - if ( ! $this->capability_required() ) { - return false; - } - - if ( ! isset( $args['timezone'] ) ) { - return false; - } - - $timezone_string = \sanitize_text_field( \wp_unslash( $args['timezone'] ) ); - - return $this->update_timezone( $timezone_string ); - } - - /** - * Update the timezone. - * - * @param string $timezone_string The timezone string to update. - * - * @return bool - */ - protected function update_timezone( $timezone_string ) { - $update_options = false; // Map UTC+- timezones to gmt_offsets and set timezone_string to empty. @@ -283,9 +214,27 @@ protected function update_timezone( $timezone_string ) { \update_option( 'timezone_string', $timezone_string ); \update_option( 'gmt_offset', $gmt_offset ); - return true; + // We're not checking for the return value of the update_option calls, because it will return false if the value is the same (for example if gmt_offset is already set to ''). + \wp_send_json_success( [ 'message' => \esc_html__( 'Setting updated.', 'progress-planner' ) ] ); } - return false; + \wp_send_json_error( [ 'message' => \esc_html__( 'Failed to update setting.', '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__( 'Select timezone', 'progress-planner' ) . '', + ]; + + return $actions; } } diff --git a/classes/suggested-tasks/providers/class-site-icon.php b/classes/suggested-tasks/providers/class-site-icon.php index 7dfd56f88d..7bf34c85f4 100644 --- a/classes/suggested-tasks/providers/class-site-icon.php +++ b/classes/suggested-tasks/providers/class-site-icon.php @@ -154,42 +154,9 @@ protected function get_enqueue_data() { public function add_task_actions( $data = [], $actions = [] ) { $actions[] = [ 'priority' => 10, - 'html' => '' . \esc_html( $this->get_task_action_label() ) . '', + 'html' => '' . \esc_html__( 'Set site icon', 'progress-planner' ) . '', ]; return $actions; } - - /** - * Get the task action label. - * - * @return string - */ - public function get_task_action_label() { - return \__( 'Set site icon', 'progress-planner' ); - } - - /** - * Complete the task. - * - * @param array $args The task data. - * @param string $task_id The task ID. - * - * @return bool - */ - public function complete_task( $args = [], $task_id = '' ) { - - if ( ! $this->capability_required() ) { - return false; - } - - if ( ! isset( $args['post_id'] ) ) { - return false; - } - - // update_option will return false if the option value is the same as the one being set. - \update_option( 'site_icon', \sanitize_text_field( $args['post_id'] ) ); - - return true; - } } diff --git a/classes/suggested-tasks/providers/class-tasks.php b/classes/suggested-tasks/providers/class-tasks.php index d074ed83a9..b3cac345b0 100644 --- a/classes/suggested-tasks/providers/class-tasks.php +++ b/classes/suggested-tasks/providers/class-tasks.php @@ -295,9 +295,7 @@ public function get_task_id( $task_data = [] ) { $parts[] = $task_data['target_taxonomy'] ?? false; // If the task is repetitive, add the date as the last part (format: YYYYWW, e.g., 202542 for week 42 of 2025). // This creates a new task instance each week for repetitive tasks. - // Note: We use 'oW' format (ISO year + ISO week) instead of 'YW' to handle year boundaries correctly. - // For example, Dec 29, 2025 is ISO week 01 of 2026, so 'oW' returns '202601' while 'YW' would incorrectly return '202501'. - $parts[] = $this->is_repetitive() ? \gmdate( 'oW' ) : false; + $parts[] = $this->is_repetitive() ? \gmdate( 'YW' ) : false; // Remove empty parts to keep IDs clean. $parts = \array_filter( $parts ); @@ -449,7 +447,7 @@ public function is_task_relevant() { * - Returns the task object if completed, false otherwise * * Repetitive tasks: - * - Must be completed within the same week they were created (using oW format: ISO year + ISO week number) + * - Must be completed within the same week they were created (using YW format: year + week number) * - For example, a task created in week 42 of 2025 must be completed in 2025W42 * - This prevents tasks from previous weeks being marked as complete * - Allows child classes to add completion data (e.g., post_id for "create post" tasks) @@ -485,8 +483,8 @@ public function evaluate_task( $task_id ) { $task->provider && $task->provider->slug === $this->get_provider_id() && \DateTime::createFromFormat( 'Y-m-d H:i:s', $task->post_date ) && - // Check if the task was created in the current week (oW format: ISO year + ISO week, e.g., 202542 = week 42 of 2025). - \gmdate( 'oW' ) === \gmdate( 'oW', \DateTime::createFromFormat( 'Y-m-d H:i:s', $task->post_date )->getTimestamp() ) && // @phpstan-ignore-line + // Check if the task was created in the current week (YW format: e.g., 202542 = week 42 of 2025). + \gmdate( 'YW' ) === \gmdate( 'YW', \DateTime::createFromFormat( 'Y-m-d H:i:s', $task->post_date )->getTimestamp() ) && // @phpstan-ignore-line $this->is_task_completed( \progress_planner()->get_suggested_tasks()->get_task_id_from_slug( $task->post_name ) ) ) { // Allow adding more data, for example in case of 'create-post' tasks we are adding the post_id. @@ -601,7 +599,7 @@ public function get_task_details( $task_data = [] ) { 'parent' => $this->get_parent(), 'priority' => $this->get_priority(), 'points' => $this->get_points(), - 'date' => \gmdate( 'oW' ), + 'date' => \gmdate( 'YW' ), 'url' => $this->get_url_with_data( $task_data ), 'url_target' => $this->get_url_target(), 'link_setting' => $this->get_link_setting(), @@ -782,15 +780,6 @@ public function add_task_actions( $data = [], $actions = [] ) { return $actions; } - /** - * Get the task action label. - * - * @return string - */ - public function get_task_action_label() { - return \__( 'Do it', 'progress-planner' ); - } - /** * Check if the task has activity. * @@ -812,16 +801,4 @@ public function task_has_activity( $task_id = '' ) { return ! empty( $activity ); } - - /** - * Complete the task. - * - * @param array $args The task data. - * @param string $task_id The task ID. - * - * @return bool - */ - public function complete_task( $args = [], $task_id = '' ) { - return false; - } } diff --git a/classes/ui/class-chart.php b/classes/ui/class-chart.php index 587bd452ee..abf9911367 100644 --- a/classes/ui/class-chart.php +++ b/classes/ui/class-chart.php @@ -109,14 +109,8 @@ public function get_chart_data( $args = [] ) { */ $previous_period_activities = []; if ( $args['normalized'] ) { - /** - * The start date of the first period. - * - * @var \DateTime $first_period_start - */ - $first_period_start = $periods[0]['start_date']; - $previous_month_start = ( clone $first_period_start )->modify( '-1 month' ); - $previous_month_end = ( clone $first_period_start )->modify( '-1 day' ); + $previous_month_start = ( clone $periods[0]['start_date'] )->modify( '-1 month' ); + $previous_month_end = ( clone $periods[0]['start_date'] )->modify( '-1 day' ); $previous_period_activities = $args['items_callback']( $previous_month_start, $previous_month_end ); if ( $args['filter_results'] ) { $activities = $args['filter_results']( $activities ); diff --git a/classes/utils/class-date.php b/classes/utils/class-date.php index 2810fa8401..96b249e3c0 100644 --- a/classes/utils/class-date.php +++ b/classes/utils/class-date.php @@ -68,15 +68,8 @@ public function get_periods( $start_date, $end_date, $frequency ) { if ( empty( $date_ranges ) ) { return []; } - $last_range = \end( $date_ranges ); - /** - * The end date of the last range. - * - * @var \DateTime $last_end_date - */ - $last_end_date = $last_range['end_date']; - if ( $end_date->format( 'z' ) !== $last_end_date->format( 'z' ) ) { - $final_end = clone $last_end_date; + if ( $end_date->format( 'z' ) !== \end( $date_ranges )['end_date']->format( 'z' ) ) { + $final_end = clone \end( $date_ranges )['end_date']; $date_ranges[] = $this->get_range( $final_end->modify( '+1 day' ), $end_date ); } diff --git a/classes/utils/class-debug-tools.php b/classes/utils/class-debug-tools.php index 6b42a5ca69..d6f1d5e302 100644 --- a/classes/utils/class-debug-tools.php +++ b/classes/utils/class-debug-tools.php @@ -48,7 +48,6 @@ public function __construct() { \add_action( 'init', [ $this, 'check_toggle_migrations' ] ); \add_action( 'init', [ $this, 'check_delete_single_task' ] ); \add_action( 'init', [ $this, 'check_toggle_recommendations_ui' ] ); - \add_action( 'init', [ $this, 'check_delete_onboarding_progress' ] ); if ( \defined( '\IS_PLAYGROUND_PREVIEW' ) && \constant( '\IS_PLAYGROUND_PREVIEW' ) === true ) { \add_action( 'init', [ $this, 'check_toggle_placeholder_demo' ] ); } @@ -100,8 +99,6 @@ public function add_toolbar_items( $admin_bar ) { $this->add_toggle_recommendations_ui_submenu_item( $admin_bar ); $this->add_placeholder_demo_submenu_item( $admin_bar ); - - $this->add_onboarding_submenu_item( $admin_bar ); } /** @@ -730,74 +727,4 @@ public function filter_tasks_show_ui( $show_ui ) { } return $show_ui; } - - /** - * Add Onboarding submenu to the debug menu. - * - * @param \WP_Admin_Bar $admin_bar The WordPress admin bar object. - * @return void - */ - protected function add_onboarding_submenu_item( $admin_bar ) { - $admin_bar->add_node( - [ - 'id' => 'prpl-onboarding', - 'parent' => 'prpl-debug', - 'title' => 'Onboarding', - ] - ); - - // Start onboarding. - $admin_bar->add_node( - [ - 'id' => 'prpl-start-onboarding', - 'parent' => 'prpl-onboarding', - 'title' => 'Start Onboarding', - 'href' => '#', - 'meta' => [ - 'onclick' => 'window.prplOnboardWizard.startOnboarding(); return false;', - ], - ] - ); - - // Delete onboarding progress. - $admin_bar->add_node( - [ - 'id' => 'prpl-delete-onboarding-progress', - 'parent' => 'prpl-onboarding', - 'title' => 'Delete Onboarding Progress', - 'href' => \add_query_arg( 'prpl_delete_onboarding_progress', '1', $this->current_url ), - ] - ); - } - - /** - * Check and process the delete onboarding progress action. - * - * Deletes onboarding progress if the appropriate query parameter is set - * and user has required capabilities. - * - * @return void - */ - public function check_delete_onboarding_progress() { - if ( - ! isset( $_GET['prpl_delete_onboarding_progress'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $_GET['prpl_delete_onboarding_progress'] !== '1' || // phpcs:ignore WordPress.Security.NonceVerification.Recommended - ! \current_user_can( 'manage_options' ) - ) { - return; - } - - // Verify nonce for security. - $this->verify_nonce(); - - // Delete the onboarding progress. - \Progress_Planner\Onboard_Wizard::delete_progress(); - - // Delete the license key. - \delete_option( 'progress_planner_license_key' ); - - // Redirect to the same page without the parameter. - \wp_safe_redirect( \remove_query_arg( [ 'prpl_delete_onboarding_progress', '_wpnonce' ] ) ); - exit; - } } diff --git a/classes/utils/class-playground.php b/classes/utils/class-playground.php index 130a004315..988043db81 100644 --- a/classes/utils/class-playground.php +++ b/classes/utils/class-playground.php @@ -31,6 +31,7 @@ public function register_hooks() { if ( ! \progress_planner()->get_license_key() && ! \get_option( 'progress_planner_demo_data_generated', false ) ) { $this->generate_data(); \update_option( 'progress_planner_license_key', \str_replace( ' ', '-', $this->create_random_string( 20 ) ) ); + \update_option( 'progress_planner_force_show_onboarding', false ); \update_option( 'progress_planner_todo', [ @@ -47,6 +48,7 @@ public function register_hooks() { \update_option( 'progress_planner_demo_data_generated', true ); } \add_action( 'progress_planner_admin_page_header_before', [ $this, 'show_header_notice' ] ); + \add_action( 'wp_ajax_progress_planner_hide_onboarding', [ $this, 'hide_onboarding' ] ); \add_action( 'wp_ajax_progress_planner_show_onboarding', [ $this, 'show_onboarding' ] ); \progress_planner()->get_settings()->set( 'activation_date', ( new \DateTime() )->modify( '-2 months' )->format( 'Y-m-d' ) ); @@ -78,23 +80,48 @@ public function enable_debug_tools() { } /** - * Show the onboarding in the Playground environment. + * Toggle the onboarding visibility in the Playground environment. + * + * @param string $action Either 'show' or 'hide'. * * @return void */ - public function show_onboarding() { - \check_ajax_referer( 'progress_planner_show_onboarding', 'nonce' ); + private function toggle_onboarding( $action ) { + $nonce_action = "progress_planner_{$action}_onboarding"; + \check_ajax_referer( $nonce_action, 'nonce' ); if ( ! \current_user_can( 'manage_options' ) ) { \wp_die( \esc_html__( 'You do not have sufficient permissions to access this page.', 'progress-planner' ) ); } - // Delete onboarding progress to trigger fresh onboarding. - \Progress_Planner\Onboard_Wizard::delete_progress(); - // Delete the license key to trigger onboarding (privacy not accepted). - \delete_option( 'progress_planner_license_key' ); + if ( $action === 'hide' ) { + \add_option( 'progress_planner_license_key', \str_replace( ' ', '-', $this->create_random_string( 20 ) ) ); + $message = \esc_html__( 'Onboarding hidden successfully', 'progress-planner' ); + } else { + \delete_option( 'progress_planner_license_key' ); + $message = \esc_html__( 'Onboarding shown successfully', 'progress-planner' ); + } + \update_option( 'progress_planner_force_show_onboarding', $action !== 'hide' ); + + \wp_send_json_success( [ 'message' => $message ] ); + } + + /** + * Hide the onboarding in the Playground environment. + * + * @return void + */ + public function hide_onboarding() { + $this->toggle_onboarding( 'hide' ); + } - \wp_send_json_success( [ 'message' => \esc_html__( 'Onboarding shown successfully', 'progress-planner' ) ] ); + /** + * Show the onboarding in the Playground environment. + * + * @return void + */ + public function show_onboarding() { + $this->toggle_onboarding( 'show' ); } /** @@ -108,7 +135,10 @@ public function show_header_notice() { return; } - $nonce = \wp_create_nonce( 'progress_planner_show_onboarding' ); + $show_onboarding = \get_option( 'progress_planner_force_show_onboarding', false ); + $button_text = $show_onboarding ? \__( 'Hide onboarding', 'progress-planner' ) : \__( 'Show onboarding', 'progress-planner' ); + $action = $show_onboarding ? 'hide' : 'show'; + $nonce = \wp_create_nonce( "progress_planner_{$action}_onboarding" ); ?>
@@ -120,16 +150,16 @@ public function show_header_notice() {

-

diff --git a/views/onboarding/email-frequency.php b/views/onboarding/email-frequency.php deleted file mode 100644 index e0696a043e..0000000000 --- a/views/onboarding/email-frequency.php +++ /dev/null @@ -1,107 +0,0 @@ -display_name ?? ''; -$prpl_user_email = $prpl_current_user->user_email ?? ''; -?> - - - diff --git a/views/onboarding/first-task.php b/views/onboarding/first-task.php deleted file mode 100644 index 310043ffb1..0000000000 --- a/views/onboarding/first-task.php +++ /dev/null @@ -1,47 +0,0 @@ - - - - diff --git a/views/onboarding/form-inputs/checkbox.php b/views/onboarding/form-inputs/checkbox.php deleted file mode 100644 index f8abf8b017..0000000000 --- a/views/onboarding/form-inputs/checkbox.php +++ /dev/null @@ -1,40 +0,0 @@ - - -
-
- - - - - -
-
diff --git a/views/onboarding/form-inputs/radio.php b/views/onboarding/form-inputs/radio.php deleted file mode 100644 index deb6a46e4b..0000000000 --- a/views/onboarding/form-inputs/radio.php +++ /dev/null @@ -1,40 +0,0 @@ - - -
-
- - - - - -
-
diff --git a/views/onboarding/more-tasks.php b/views/onboarding/more-tasks.php deleted file mode 100644 index 5e0493a523..0000000000 --- a/views/onboarding/more-tasks.php +++ /dev/null @@ -1,85 +0,0 @@ - - - - diff --git a/views/onboarding/quit-confirmation.php b/views/onboarding/quit-confirmation.php deleted file mode 100644 index 4d38064c5d..0000000000 --- a/views/onboarding/quit-confirmation.php +++ /dev/null @@ -1,46 +0,0 @@ - - - - - diff --git a/views/onboarding/settings.php b/views/onboarding/settings.php deleted file mode 100644 index 015e619c86..0000000000 --- a/views/onboarding/settings.php +++ /dev/null @@ -1,216 +0,0 @@ - [ - 'id' => 'homepage', - 'title' => __( 'Home page', 'progress-planner' ), - 'description' => \esc_html__( 'Help us understand your site a little better so we can give you more useful recommendations. Let\'s start with the home page.', 'progress-planner' ), - 'note' => __( 'A Home page is important. We\'ll remind you to make one at a later time.', 'progress-planner' ), - ], - 'about' => [ - 'id' => 'about', - 'title' => __( 'About page', 'progress-planner' ), - 'description' => \esc_html__( 'Next up, pick the page you use as your about page.', 'progress-planner' ), - 'note' => __( 'An About page is important. We\'ll remind you to make one at a later time.', 'progress-planner' ), - ], - 'contact' => [ - 'id' => 'contact', - 'title' => __( 'Contact page', 'progress-planner' ), - 'description' => \esc_html__( 'Now choose the page you use as your contact page.', 'progress-planner' ), - 'note' => __( 'A Contact page is important. We\'ll remind you to make one at a later time.', 'progress-planner' ), - ], - 'faq' => [ - 'id' => 'faq', - 'title' => __( 'FAQ page', 'progress-planner' ), - 'description' => \esc_html__( 'Next, pick the page you use as your FAQ page.', 'progress-planner' ), - 'note' => __( 'An FAQ page is important. We\'ll remind you to make one at a later time.', 'progress-planner' ), - ], -]; - -// Get post types for the post types sub-step. -$prpl_saved_settings = \progress_planner()->get_settings()->get_post_types_names(); -$prpl_post_types = \progress_planner()->get_settings()->get_public_post_types(); - -$prpl_total_number_of_steps = 5; -$prpl_current_step_number = 0; - -?> - - - diff --git a/views/onboarding/tasks/core-blogdescription.php b/views/onboarding/tasks/core-blogdescription.php deleted file mode 100644 index 1cd7ba0e0e..0000000000 --- a/views/onboarding/tasks/core-blogdescription.php +++ /dev/null @@ -1,38 +0,0 @@ - - -
-
-

- -

-

- -

-
-
- - -
-
diff --git a/views/onboarding/tasks/core-siteicon.php b/views/onboarding/tasks/core-siteicon.php deleted file mode 100644 index 44baee3e39..0000000000 --- a/views/onboarding/tasks/core-siteicon.php +++ /dev/null @@ -1,55 +0,0 @@ - -
-

- -

-

- -

-
- -
- - the_file( 'assets/images/onboarding/icon_image.svg' ); ?> - -

- ', - '' - ); - ?> -

- - PNG, ICO, WEBP -
-

- - -
-
- -
- -
-
diff --git a/views/onboarding/tasks/select-locale.php b/views/onboarding/tasks/select-locale.php deleted file mode 100644 index a248d3c9d5..0000000000 --- a/views/onboarding/tasks/select-locale.php +++ /dev/null @@ -1,63 +0,0 @@ - - -
-

- -

-

- -

-
- 'language', - 'id' => 'language', - 'selected' => $prpl_locale, - 'languages' => $prpl_languages, - 'translations' => $prpl_translations, - 'show_available_translations' => \current_user_can( 'install_languages' ) && \wp_can_install_language_pack(), - 'echo' => true, - ] - ); - ?> - -
-
diff --git a/views/onboarding/tasks/select-timezone.php b/views/onboarding/tasks/select-timezone.php deleted file mode 100644 index 142b434934..0000000000 --- a/views/onboarding/tasks/select-timezone.php +++ /dev/null @@ -1,37 +0,0 @@ - - -
-

- -

-

- -

-
- - -
-
diff --git a/views/onboarding/welcome.php b/views/onboarding/welcome.php deleted file mode 100644 index 3e2a819835..0000000000 --- a/views/onboarding/welcome.php +++ /dev/null @@ -1,83 +0,0 @@ - - - - diff --git a/views/onboarding/whats-what.php b/views/onboarding/whats-what.php deleted file mode 100644 index 43cebc7f55..0000000000 --- a/views/onboarding/whats-what.php +++ /dev/null @@ -1,63 +0,0 @@ - - - - diff --git a/views/welcome.php b/views/welcome.php new file mode 100644 index 0000000000..5a55a3b742 --- /dev/null +++ b/views/welcome.php @@ -0,0 +1,157 @@ +get_license_key() ) { + return; +} + +// Enqueue styles. +\progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/welcome' ); +\progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/onboard' ); +?> +
+
+

+ + + the_asset( 'images/icon_progress_planner.svg' ); ?> + +
+
+
+
+
+ +
    +
  • + tag, %2$s: tag */ ?> + ', '' ); ?> +
  • +
  • + tag, %2$s: tag */ ?> + ', '' ); ?> +
  • +
  • + tag, %2$s: tag */ ?> + ', '' ); ?> +
  • +
+ get_ui__branding()->get_url( 'https://prpl.fyi/home' ) ) . '" target="_blank">progressplanner.com' + ) + ?> +
+
+ +
+ + +
+
+
+ + + + +
+
+
+ +
+
+
+
+ + + +
+ +
+
+
+
+ +
+
+
From ec957d680b288f8838bb461d3476c207e2bf597e Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Tue, 27 Jan 2026 22:08:27 +0100 Subject: [PATCH 2/6] add return type, to fix phpstan --- classes/class-base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/class-base.php b/classes/class-base.php index b789b375bc..66e6815f57 100644 --- a/classes/class-base.php +++ b/classes/class-base.php @@ -51,7 +51,7 @@ * @method \Progress_Planner\UI\Popover get_ui__popover() * @method \Progress_Planner\Admin\Widgets\Content_Activity get_admin__widgets__content_activity() * @method \Progress_Planner\UI\Chart get_ui__chart() - * @method \Progress_Planner\Activities\Content_Helpers get_activities__content_helpers() + * @method \Progress_Planner\Activities\Content_Helpers|null get_activities__content_helpers() * @method \Progress_Planner\Admin\Widgets\Challenge get_admin__widgets__challenge() * @method \Progress_Planner\Admin\Widgets\Activity_Scores get_admin__widgets__activity_scores() * @method \Progress_Planner\Utils\Date get_utils__date() From 62b64827556df71f7180de8051ebbf8ec3958b4d Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Tue, 27 Jan 2026 22:15:45 +0100 Subject: [PATCH 3/6] fix phpunit test --- classes/class-base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/class-base.php b/classes/class-base.php index 66e6815f57..7fa27f1ff3 100644 --- a/classes/class-base.php +++ b/classes/class-base.php @@ -559,7 +559,7 @@ public function is_on_progress_planner_dashboard_page() { * @return bool */ public function is_debug_mode_enabled() { - return ( \defined( 'PRPL_DEBUG' ) && PRPL_DEBUG ) || \get_option( 'prpl_debug' ); + return ( ( \defined( 'PRPL_DEBUG' ) && PRPL_DEBUG ) || \get_option( 'prpl_debug' ) ) && \current_user_can( 'manage_options' ); } } // phpcs:enable Generic.Commenting.Todo From 5e09e12034fe7a3a9f2420c1ad701a477ba778c9 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Wed, 28 Jan 2026 14:18:45 +0100 Subject: [PATCH 4/6] dont revert "Fix year-week boundary bug" PR --- classes/suggested-tasks/providers/class-tasks.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/classes/suggested-tasks/providers/class-tasks.php b/classes/suggested-tasks/providers/class-tasks.php index b3cac345b0..854bfebe99 100644 --- a/classes/suggested-tasks/providers/class-tasks.php +++ b/classes/suggested-tasks/providers/class-tasks.php @@ -268,7 +268,7 @@ public function get_external_link_url() { * Get the task ID. * * Generates a unique task ID by combining the provider ID with optional task-specific data. - * For repetitive tasks, includes the current year-week (YW format) to create weekly instances. + * For repetitive tasks, includes the current year-week (oW format) to create weekly instances. * * Example task IDs: * - Non-repetitive: "update-core" @@ -295,7 +295,9 @@ public function get_task_id( $task_data = [] ) { $parts[] = $task_data['target_taxonomy'] ?? false; // If the task is repetitive, add the date as the last part (format: YYYYWW, e.g., 202542 for week 42 of 2025). // This creates a new task instance each week for repetitive tasks. - $parts[] = $this->is_repetitive() ? \gmdate( 'YW' ) : false; + // Note: We use 'oW' format (ISO year + ISO week) instead of 'YW' to handle year boundaries correctly. + // For example, Dec 29, 2025 is ISO week 01 of 2026, so 'oW' returns '202601' while 'YW' would incorrectly return '202501'. + $parts[] = $this->is_repetitive() ? \gmdate( 'oW' ) : false; // Remove empty parts to keep IDs clean. $parts = \array_filter( $parts ); @@ -447,7 +449,7 @@ public function is_task_relevant() { * - Returns the task object if completed, false otherwise * * Repetitive tasks: - * - Must be completed within the same week they were created (using YW format: year + week number) + * - Must be completed within the same week they were created (using oW format: ISO year + ISO week number) * - For example, a task created in week 42 of 2025 must be completed in 2025W42 * - This prevents tasks from previous weeks being marked as complete * - Allows child classes to add completion data (e.g., post_id for "create post" tasks) @@ -483,8 +485,8 @@ public function evaluate_task( $task_id ) { $task->provider && $task->provider->slug === $this->get_provider_id() && \DateTime::createFromFormat( 'Y-m-d H:i:s', $task->post_date ) && - // Check if the task was created in the current week (YW format: e.g., 202542 = week 42 of 2025). - \gmdate( 'YW' ) === \gmdate( 'YW', \DateTime::createFromFormat( 'Y-m-d H:i:s', $task->post_date )->getTimestamp() ) && // @phpstan-ignore-line + // Check if the task was created in the current week (oW format: ISO year + ISO week, e.g., 202542 = week 42 of 2025). + \gmdate( 'oW' ) === \gmdate( 'oW', \DateTime::createFromFormat( 'Y-m-d H:i:s', $task->post_date )->getTimestamp() ) && // @phpstan-ignore-line $this->is_task_completed( \progress_planner()->get_suggested_tasks()->get_task_id_from_slug( $task->post_name ) ) ) { // Allow adding more data, for example in case of 'create-post' tasks we are adding the post_id. @@ -599,7 +601,7 @@ public function get_task_details( $task_data = [] ) { 'parent' => $this->get_parent(), 'priority' => $this->get_priority(), 'points' => $this->get_points(), - 'date' => \gmdate( 'YW' ), + 'date' => \gmdate( 'oW' ), 'url' => $this->get_url_with_data( $task_data ), 'url_target' => $this->get_url_target(), 'link_setting' => $this->get_link_setting(), From c7420f646f3bee7db67607d0e65ddf98f0133cc4 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Wed, 28 Jan 2026 14:45:09 +0100 Subject: [PATCH 5/6] more cherry picking --- classes/admin/class-page-settings.php | 13 +++++-------- classes/ui/class-chart.php | 5 +++-- classes/utils/class-date.php | 11 +++++++++-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/classes/admin/class-page-settings.php b/classes/admin/class-page-settings.php index 891cefc3af..e0976691cb 100644 --- a/classes/admin/class-page-settings.php +++ b/classes/admin/class-page-settings.php @@ -138,17 +138,14 @@ public function set_page_values( $pages ) { } /** - * Save the settings. + * Save the redirect on login setting. + * + * @param bool $redirect_on_login Whether to redirect on login. * * @return void */ - public function save_settings() { - // Nonce is already checked in store_settings_form_options() which calls this method. - $redirect_on_login = isset( $_POST['prpl-redirect-on-login'] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing - ? \sanitize_text_field( \wp_unslash( $_POST['prpl-redirect-on-login'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Missing - : false; - - \update_user_meta( \get_current_user_id(), 'prpl_redirect_on_login', (bool) $redirect_on_login ); + public function save_redirect_on_login( $redirect_on_login = false ) { + \update_user_meta( \get_current_user_id(), 'prpl_redirect_on_login', $redirect_on_login ); } /** diff --git a/classes/ui/class-chart.php b/classes/ui/class-chart.php index abf9911367..b0f3ff00fa 100644 --- a/classes/ui/class-chart.php +++ b/classes/ui/class-chart.php @@ -109,8 +109,9 @@ public function get_chart_data( $args = [] ) { */ $previous_period_activities = []; if ( $args['normalized'] ) { - $previous_month_start = ( clone $periods[0]['start_date'] )->modify( '-1 month' ); - $previous_month_end = ( clone $periods[0]['start_date'] )->modify( '-1 day' ); + $first_period_start = $periods[0]['start_date']; + $previous_month_start = ( clone $first_period_start )->modify( '-1 month' ); + $previous_month_end = ( clone $first_period_start )->modify( '-1 day' ); $previous_period_activities = $args['items_callback']( $previous_month_start, $previous_month_end ); if ( $args['filter_results'] ) { $activities = $args['filter_results']( $activities ); diff --git a/classes/utils/class-date.php b/classes/utils/class-date.php index 96b249e3c0..2810fa8401 100644 --- a/classes/utils/class-date.php +++ b/classes/utils/class-date.php @@ -68,8 +68,15 @@ public function get_periods( $start_date, $end_date, $frequency ) { if ( empty( $date_ranges ) ) { return []; } - if ( $end_date->format( 'z' ) !== \end( $date_ranges )['end_date']->format( 'z' ) ) { - $final_end = clone \end( $date_ranges )['end_date']; + $last_range = \end( $date_ranges ); + /** + * The end date of the last range. + * + * @var \DateTime $last_end_date + */ + $last_end_date = $last_range['end_date']; + if ( $end_date->format( 'z' ) !== $last_end_date->format( 'z' ) ) { + $final_end = clone $last_end_date; $date_ranges[] = $this->get_range( $final_end->modify( '+1 day' ), $end_date ); } From 89d3339fc9ba43bf2fe7da95371346e670848359 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Wed, 28 Jan 2026 14:56:25 +0100 Subject: [PATCH 6/6] add comment back as well --- classes/ui/class-chart.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/classes/ui/class-chart.php b/classes/ui/class-chart.php index b0f3ff00fa..587bd452ee 100644 --- a/classes/ui/class-chart.php +++ b/classes/ui/class-chart.php @@ -109,6 +109,11 @@ public function get_chart_data( $args = [] ) { */ $previous_period_activities = []; if ( $args['normalized'] ) { + /** + * The start date of the first period. + * + * @var \DateTime $first_period_start + */ $first_period_start = $periods[0]['start_date']; $previous_month_start = ( clone $first_period_start )->modify( '-1 month' ); $previous_month_end = ( clone $first_period_start )->modify( '-1 day' );