├── .distignore ├── .env.example ├── .eslintrc.js ├── .husky └── pre-commit ├── .stylelintrc.json ├── CHANGELOG.md ├── LICENSE ├── SECURITY.md ├── assets ├── css │ ├── admin.css │ ├── dashboard-widgets │ │ └── score.css │ ├── editor.css │ ├── focus-element.css │ ├── onboard.css │ ├── page-widgets │ │ ├── activity-scores.css │ │ ├── badge-streak.css │ │ ├── challenge.css │ │ ├── content-activity.css │ │ ├── latest-badge.css │ │ ├── suggested-tasks.css │ │ ├── todo.css │ │ └── whats-new.css │ ├── settings-page.css │ ├── upgrade-tasks.css │ ├── vendor │ │ └── driver.css │ ├── web-components │ │ ├── prpl-badge.css │ │ ├── prpl-suggested-task.css │ │ └── prpl-tooltip.css │ └── welcome.css ├── images │ ├── icon_check_circle.svg │ ├── icon_copywriting.svg │ ├── icon_exclamation_circle.svg │ ├── icon_exclamation_circle_solid.svg │ ├── icon_exclamation_triangle_solid.svg │ ├── icon_forward.svg │ ├── icon_info.svg │ ├── icon_key.svg │ ├── icon_pages.svg │ ├── icon_progress_planner.svg │ ├── icon_settings.svg │ ├── icon_snooze.svg │ ├── icon_tour.svg │ ├── icon_user.svg │ ├── image_onboaring_block.png │ ├── logo_progress_planner.svg │ ├── logo_progress_planner_pro.svg │ └── register_icon.svg └── js │ ├── ajax-request.js │ ├── celebrate.js │ ├── document-ready.js │ ├── editor.js │ ├── external-link-accessibility-helper.js │ ├── focus-element.js │ ├── grid-masonry.js │ ├── header-filters.js │ ├── l10n.js │ ├── onboard.js │ ├── scan-posts.js │ ├── settings-page.js │ ├── settings.js │ ├── tour.js │ ├── upgrade-tasks.js │ ├── vendor │ ├── driver.js.iife.js │ └── tsparticles.confetti.bundle.min.js │ ├── web-components │ ├── prpl-badge.js │ ├── prpl-big-counter.js │ ├── prpl-chart-bar.js │ ├── prpl-chart-line.js │ ├── prpl-gauge.js │ ├── prpl-interactive-task.js │ ├── prpl-suggested-task.js │ ├── prpl-task-sending-email.js │ └── prpl-tooltip.js │ ├── widgets │ ├── suggested-tasks.js │ └── todo.js │ └── yoast-focus-element.js ├── autoload.php ├── classes ├── actions │ ├── class-content-scan.php │ ├── class-content.php │ └── class-maintenance.php ├── activities │ ├── class-activity.php │ ├── class-content-helpers.php │ ├── class-content.php │ ├── class-maintenance.php │ ├── class-query.php │ └── class-suggested-task.php ├── admin │ ├── class-dashboard-widget-score.php │ ├── class-dashboard-widget-todo.php │ ├── class-dashboard-widget.php │ ├── class-editor.php │ ├── class-enqueue.php │ ├── class-page-settings.php │ ├── class-page.php │ ├── class-tour.php │ └── widgets │ │ ├── class-activity-scores.php │ │ ├── class-badge-streak.php │ │ ├── class-challenge.php │ │ ├── class-content-activity.php │ │ ├── class-latest-badge.php │ │ ├── class-suggested-tasks.php │ │ ├── class-todo.php │ │ ├── class-whats-new.php │ │ └── class-widget.php ├── badges │ ├── class-badge-content.php │ ├── class-badge-maintenance.php │ ├── class-badge.php │ ├── class-monthly.php │ ├── content │ │ ├── class-content-curator.php │ │ ├── class-purposeful-publisher.php │ │ └── class-revision-ranger.php │ └── maintenance │ │ ├── class-maintenance-maniac.php │ │ ├── class-progress-padawan.php │ │ └── class-super-site-specialist.php ├── class-badges.php ├── class-base.php ├── class-lessons.php ├── class-page-todos.php ├── class-page-types.php ├── class-plugin-deactivation.php ├── class-plugin-migrations.php ├── class-plugin-upgrade-tasks.php ├── class-settings.php ├── class-suggested-tasks.php ├── class-todo.php ├── goals │ ├── class-goal-recurring.php │ └── class-goal.php ├── rest │ ├── class-stats.php │ └── class-tasks.php ├── suggested-tasks │ ├── class-task-factory.php │ ├── class-task.php │ ├── class-tasks-interface.php │ ├── class-tasks-manager.php │ ├── data-collector │ │ ├── class-archive-format.php │ │ ├── class-base-data-collector.php │ │ ├── class-data-collector-manager.php │ │ ├── class-hello-world.php │ │ ├── class-inactive-plugins.php │ │ ├── class-last-published-post.php │ │ ├── class-post-author.php │ │ ├── class-post-tag-count.php │ │ ├── class-published-post-count.php │ │ ├── class-sample-page.php │ │ ├── class-terms-without-description.php │ │ ├── class-terms-without-posts.php │ │ ├── class-uncategorized-category.php │ │ └── class-yoast-orphaned-content.php │ └── providers │ │ ├── class-blog-description.php │ │ ├── class-content-create.php │ │ ├── class-content-review.php │ │ ├── class-core-update.php │ │ ├── class-debug-display.php │ │ ├── class-disable-comments.php │ │ ├── class-fewer-tags.php │ │ ├── class-hello-world.php │ │ ├── class-interactive.php │ │ ├── class-permalink-structure.php │ │ ├── class-php-version.php │ │ ├── class-remove-inactive-plugins.php │ │ ├── class-remove-terms-without-posts.php │ │ ├── class-rename-uncategorized-category.php │ │ ├── class-sample-page.php │ │ ├── class-search-engine-visibility.php │ │ ├── class-set-valuable-post-types.php │ │ ├── class-settings-saved.php │ │ ├── class-site-icon.php │ │ ├── class-tasks.php │ │ ├── class-update-term-description.php │ │ ├── class-user.php │ │ ├── integrations │ │ └── yoast │ │ │ ├── class-add-yoast-providers.php │ │ │ ├── class-archive-author.php │ │ │ ├── class-archive-date.php │ │ │ ├── class-archive-format.php │ │ │ ├── class-cornerstone-workout.php │ │ │ ├── class-crawl-settings-emoji-scripts.php │ │ │ ├── class-crawl-settings-feed-authors.php │ │ │ ├── class-crawl-settings-feed-global-comments.php │ │ │ ├── class-fix-orphaned-content.php │ │ │ ├── class-media-pages.php │ │ │ ├── class-organization-logo.php │ │ │ ├── class-orphaned-content-workout.php │ │ │ └── class-yoast-provider.php │ │ ├── interactive │ │ └── class-email-sending.php │ │ └── traits │ │ └── class-dismissable-task.php ├── ui │ ├── class-chart.php │ └── class-popover.php ├── update │ ├── class-update-111.php │ ├── class-update-130.php │ └── class-update-140.php └── utils │ ├── class-cache.php │ ├── class-date.php │ ├── class-debug-tools.php │ ├── class-onboard.php │ └── class-playground.php ├── playwright.config.js ├── progress-planner.php ├── readme.txt ├── uninstall.php └── views ├── admin-page-header.php ├── admin-page-settings.php ├── admin-page.php ├── dashboard-widgets ├── score.php └── todo.php ├── page-settings ├── license.php ├── pages.php ├── post-types.php └── settings.php ├── page-widgets ├── activity-scores.php ├── badge-streak.php ├── challenge.php ├── content-activity.php ├── latest-badge.php ├── parts │ └── monthly-badges.php ├── suggested-tasks.php ├── todo.php └── whats-new.php ├── popovers ├── badge-streak.php ├── monthly-badges.php ├── parts │ ├── badge-streak-badge.php │ ├── badge-streak-progressbar.php │ ├── icon.php │ └── upgrade-tasks.php ├── popover.php ├── subscribe-form.php └── upgrade-tasks.php ├── setting ├── page-select.php └── radio.php └── welcome.php /.distignore: -------------------------------------------------------------------------------- 1 | /.wordpress-org 2 | /.git 3 | /.github 4 | /coverage 5 | /tests 6 | /vendor 7 | .distignore 8 | .editorconfig 9 | .eslintrc.js 10 | .env.example 11 | .gitignore 12 | .gitattributes 13 | composer.json 14 | composer.lock 15 | package.json 16 | package-lock.json 17 | phpcs.xml.dist 18 | phpstan.neon.dist 19 | phpunit.xml.dist 20 | README.md 21 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # WordPress URL (without trailing slash) 2 | WP_URL=http://localhost:8080 3 | 4 | # WordPress admin credentials 5 | WP_USER=admin 6 | WP_PASS=password 7 | WP_EMAIL=admin@wordpress.test -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'plugin:@wordpress/eslint-plugin/recommended', 4 | 'plugin:eslint-comments/recommended', 5 | ], 6 | parserOptions: { 7 | ecmaVersion: "latest", 8 | }, 9 | rules: { 10 | "no-console": "off", 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | composer check-cs -- --warning-severity=6 2 | composer phpstan 3 | npm run lint:css 4 | npm run lint:js 5 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@wordpress/stylelint-config", 3 | "rules": { 4 | "no-descending-specificity": null, 5 | "selector-id-pattern": null, 6 | "function-url-quotes": null 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | You can report any security bugs found in the source code of this plugin through our [Patchstack Vulnerability Disclosure Program](https://patchstack.com/database/vdp/progress-planner). The Patchstack team will assist you with verification, CVE assignment and take care of notifying the developers of this plugin. 6 | 7 | ## Responding to Vulnerability Reports 8 | 9 | Emilia Projects takes security bugs seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. Patchstack will work with you and us to deal with the security issue as best as possible. 10 | 11 | ## Disclosing a Vulnerability 12 | 13 | Once an issue is reported, Emilia uses the following disclosure process: 14 | 15 | - When a report is received, we confirm the issue and determine its severity together with Patchstack. 16 | - If we know of specific third-party services or software that require mitigation before publication, those projects will be notified. 17 | - An advisory is prepared (but not published) which details the problem and steps for mitigation. 18 | - Patch releases are published and the advisory is published. 19 | - Release notes and our CHANGELOG.md will include a `Security` section with a link to the advisory. 20 | 21 | We credit reporters for identifying vulnerabilities, although we will keep your name confidential if you request it. 22 | -------------------------------------------------------------------------------- /assets/css/dashboard-widgets/score.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable max-line-length */ 2 | 3 | /** 4 | * Admin widget. 5 | * 6 | * Dependencies: progress-planner/web-components/prpl-suggested-task, progress-planner/web-components/prpl-badge 7 | */ 8 | #progress_planner_dashboard_widget_score { 9 | 10 | .prpl-dashboard-widget { 11 | padding-top: 5px; /* Total 16px top spacing */ 12 | display: grid; 13 | grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); 14 | grid-gap: calc(var(--prpl-gap) / 2); 15 | 16 | > div { 17 | border: 1px solid var(--prpl-color-gray-2); 18 | border-radius: var(--prpl-border-radius); 19 | font-size: 0.875rem; 20 | text-align: center; 21 | padding-bottom: 1em; 22 | } 23 | } 24 | 25 | h3 { 26 | font-weight: 500; 27 | } 28 | 29 | .prpl-suggested-task { 30 | 31 | h3 { 32 | margin-bottom: 0; 33 | } 34 | } 35 | 36 | .prpl-dashboard-widget-latest-activities { 37 | margin-top: 1em; 38 | padding-top: 1em; 39 | border-top: 1px solid #c3c4c7; /* same color as the one WP-Core uses */ 40 | li { 41 | display: flex; 42 | justify-content: space-between; 43 | } 44 | } 45 | 46 | .prpl-dashboard-widget-footer { 47 | margin-top: 1rem; 48 | padding-top: 1rem; 49 | border-top: 1px solid #c3c4c7; /* same color as the one WP-Core uses */ 50 | font-size: var(--prpl-font-size-base); 51 | display: flex; 52 | gap: 1rem; 53 | align-items: center; 54 | 55 | .prpl-button-primary { 56 | display: inline-block; 57 | margin: 0.5rem 0 0 0; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /assets/css/editor.css: -------------------------------------------------------------------------------- 1 | .components-button { 2 | 3 | svg.progress-planner-icon { 4 | 5 | #path1, 6 | #path3 { 7 | fill: #38296d !important; 8 | } 9 | 10 | #path2 { 11 | fill: #faa310 !important; 12 | } 13 | } 14 | 15 | &[aria-controls="progress-planner-sidebar:progress-planner-sidebar"] { 16 | background: #fff6eb !important; 17 | } 18 | 19 | &.is-pressed { 20 | 21 | &[aria-controls="progress-planner-sidebar:progress-planner-sidebar"] { 22 | background: #38296d !important; 23 | } 24 | 25 | svg.progress-planner-icon { 26 | 27 | #path1, 28 | #path3 { 29 | fill: #fff !important; 30 | } 31 | } 32 | } 33 | } 34 | 35 | #progress-planner-sidebar\:progress-planner-sidebar { 36 | 37 | .components-button { 38 | 39 | &.is-pressed { 40 | background: #38296d !important; 41 | } 42 | 43 | &.is-secondary { 44 | background: #fff6eb !important; 45 | font-weight: 700; 46 | color: #38296d !important; 47 | border-color: #faa310 !important; 48 | } 49 | } 50 | 51 | strong, 52 | h2 button { 53 | color: #38296d !important; 54 | } 55 | 56 | .dashicons-video-alt3 { 57 | color: #faa310 !important; 58 | } 59 | } 60 | 61 | .progress-planner-todo-item.required label::after { 62 | content: ""; 63 | background-color: #14b8a6; 64 | padding: 0.35em; 65 | margin: 0 0.25em; 66 | border-radius: 50%; 67 | display: inline-block; 68 | } 69 | 70 | .progress-planner-sidebar-lesson-items-invalid-license { 71 | padding: 15px; 72 | background-color: #d63638; 73 | color: #fff; 74 | 75 | a { 76 | color: #fff; 77 | text-decoration: underline; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /assets/css/focus-element.css: -------------------------------------------------------------------------------- 1 | .prpl-element-awards-points-icon-positioning-wrapper { 2 | position: relative; 3 | display: inline-block; 4 | height: 100%; 5 | 6 | .prpl-element-awards-points-icon-wrapper { 7 | position: absolute; 8 | top: -2em; 9 | left: 0; 10 | } 11 | } 12 | 13 | .prpl-element-awards-points-icon-wrapper { 14 | display: inline-flex; 15 | align-items: center; 16 | gap: 0.25rem; 17 | background-color: #fff9f0; 18 | font-size: 0.75rem; 19 | border: 2px solid #faa310 !important; 20 | border-radius: 1rem; 21 | color: #534786; 22 | font-weight: 600; 23 | padding: 0.25rem; 24 | margin: 0 1rem; 25 | transform: translateY(0.25rem); 26 | scroll-margin-top: 30px; 27 | 28 | img { 29 | width: 0.815rem; 30 | } 31 | 32 | &.focused { 33 | border-color: #14b8a6; 34 | background-color: #f2faf9; 35 | box-shadow: 2px 2px 0 0 #14b8a6; 36 | } 37 | 38 | &.complete { 39 | color: #14b8a6; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /assets/css/onboard.css: -------------------------------------------------------------------------------- 1 | #progress-planner-scan-progress progress { 2 | width: 100%; 3 | min-height: 1px; 4 | } 5 | 6 | #prpl-onboarding-form .prpl-form-fields label { 7 | display: grid; 8 | grid-template-columns: 1fr 3fr; 9 | margin-bottom: 0.5em; 10 | gap: var(--prpl-padding); 11 | } 12 | 13 | #prpl-onboarding-form label > span:has(input[type="checkbox"]) { 14 | display: flex; 15 | align-items: baseline; 16 | } 17 | 18 | .prpl-onboard-form-radio-select { 19 | 20 | label { 21 | display: block !important; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /assets/css/page-widgets/activity-scores.css: -------------------------------------------------------------------------------- 1 | .prpl-widget-wrapper.prpl-activity-scores { 2 | 3 | .prpl-graph-wrapper { 4 | max-height: 300px; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /assets/css/page-widgets/badge-streak.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | #popover-badge-streak-content, 4 | #popover-badge-streak-maintenance { 5 | display: grid; 6 | grid-template-columns: 1fr 1fr 1fr; 7 | grid-gap: var(--prpl-padding); 8 | 9 | .inner { 10 | border-radius: var(--prpl-border-radius-big); 11 | padding: var(--prpl-padding); 12 | font-size: var(--prpl-font-size-small); 13 | text-align: center; 14 | } 15 | } 16 | 17 | #popover-badge-streak-content .inner { 18 | background: var(--prpl-background-red); 19 | } 20 | 21 | #popover-badge-streak-maintenance .inner { 22 | background: var(--prpl-background-blue); 23 | } 24 | 25 | .badges-popover-progress-total { 26 | display: block; 27 | width: 100%; 28 | height: 20px; 29 | background: var(--prpl-color-gray-1); 30 | 31 | > span { 32 | display: block; 33 | height: 100%; 34 | background: var(--prpl-color-accent-red); 35 | } 36 | } 37 | 38 | /*------------------------------------*\ 39 | Badges popover. 40 | \*------------------------------------*/ 41 | 42 | #prpl-popover-badge-streak { 43 | 44 | .indicators { 45 | display: grid; 46 | grid-template-columns: 1fr 1fr 1fr; 47 | grid-gap: var(--prpl-padding); 48 | 49 | .indicator { 50 | text-align: center; 51 | line-height: 1.2; 52 | margin-top: 5px; 53 | 54 | .number { 55 | font-size: var(--prpl-font-size-2xl); 56 | font-weight: 500; 57 | display: block; 58 | } 59 | } 60 | } 61 | 62 | .prpl-widgets-container.in-popover { 63 | display: grid; 64 | grid-template-columns: repeat(auto-fit, minmax(var(--prpl-column-min-width), 1fr)); 65 | grid-gap: var(--prpl-gap); 66 | grid-auto-rows: auto; 67 | 68 | .prpl-widget-wrapper { 69 | display: flex; 70 | flex-direction: column; 71 | justify-content: space-between; 72 | } 73 | } 74 | } 75 | 76 | .string-freeze-explain { 77 | max-width: 42em; 78 | } 79 | -------------------------------------------------------------------------------- /assets/css/page-widgets/challenge.css: -------------------------------------------------------------------------------- 1 | .prpl-widget-wrapper.prpl-challenge { 2 | 3 | &:has(.prpl-challenge-promo-notice) { 4 | position: relative; 5 | 6 | &::after { 7 | content: ""; 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | width: 100%; 12 | height: 100%; 13 | background: var(--prpl-color-gray-2); 14 | opacity: 0.4; 15 | } 16 | 17 | .prpl-challenge-content { 18 | 19 | .prpl-challenge-promo-notice { 20 | position: absolute; 21 | bottom: var(--prpl-padding); 22 | left: var(--prpl-padding); 23 | z-index: 1; 24 | width: calc(100% - (var(--prpl-padding) * 4)); 25 | background-color: #fff; 26 | border: 1px solid var(--prpl-color-gray-2); 27 | padding: var(--prpl-padding); 28 | border-radius: var(--prpl-border-radius); 29 | 30 | .prpl-button-primary { 31 | margin-bottom: 0; 32 | } 33 | 34 | *:last-child { 35 | margin-bottom: 0; 36 | } 37 | } 38 | } 39 | } 40 | 41 | h2.prpl-widget-title { 42 | background: var(--prpl-background-purple); 43 | padding: 0.75rem 1rem; 44 | border-radius: 0.5rem; 45 | color: var(--prpl-color-headings); 46 | display: flex; 47 | gap: 0.5rem; 48 | align-items: center; 49 | 50 | img { 51 | max-width: 1em; 52 | max-height: 1em; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /assets/css/page-widgets/content-activity.css: -------------------------------------------------------------------------------- 1 | .prpl-widget-wrapper.prpl-content-activity { 2 | 3 | table { 4 | width: 100%; 5 | margin-bottom: 1em; 6 | } 7 | 8 | th, 9 | td { 10 | border: none; 11 | padding: 0.5em; 12 | 13 | &:not(:first-child) { 14 | text-align: center; 15 | } 16 | } 17 | 18 | th { 19 | text-align: start; 20 | } 21 | 22 | tbody th { 23 | font-weight: 400; 24 | } 25 | 26 | thead { 27 | 28 | th, 29 | td { 30 | border-bottom: 1px solid var(--prpl-color-gray-3); 31 | text-align: start; 32 | } 33 | } 34 | 35 | tfoot { 36 | 37 | th, 38 | td { 39 | border-top: 1px solid var(--prpl-color-gray-3); 40 | text-align: start; 41 | } 42 | } 43 | 44 | tr:nth-child(even) { 45 | background-color: #f9fafb; 46 | } 47 | 48 | tr:last-child td { 49 | border-bottom: none; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /assets/css/page-widgets/latest-badge.css: -------------------------------------------------------------------------------- 1 | .prpl-widget-wrapper.prpl-latest-badge { 2 | 3 | img { 4 | margin-top: 0.5rem; 5 | border: 1px solid var(--prpl-color-gray-2); 6 | border-radius: var(--prpl-border-radius); 7 | } 8 | 9 | .share-badge-wrapper { 10 | padding-top: 1rem; 11 | display: flex; 12 | justify-content: flex-end; 13 | } 14 | 15 | .prpl-button-share-badge { 16 | display: flex; 17 | align-items: center; 18 | gap: 0.5em; 19 | text-decoration: none; 20 | color: var(--prpl-color-gray-7); 21 | border: 1px solid var(--prpl-color-gray-2); 22 | border-radius: var(--prpl-border-radius); 23 | padding: 0.5em 1em; 24 | 25 | &:hover { 26 | color: var(--prpl-color-link); 27 | border-color: var(--prpl-color-link); 28 | background-color: var(--prpl-background-blue); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /assets/css/page-widgets/whats-new.css: -------------------------------------------------------------------------------- 1 | .prpl-widget-wrapper.prpl-whats-new { 2 | 3 | ul { 4 | margin: 0; 5 | 6 | p { 7 | margin: 0; 8 | } 9 | } 10 | 11 | li { 12 | 13 | h3 { 14 | margin-top: 0; 15 | font-size: 1.15em; 16 | font-weight: 600; 17 | 18 | > a { 19 | color: var(--prpl-color-headings); 20 | text-decoration: none; 21 | 22 | .prpl-external-link-icon { 23 | margin-inline-start: 0.15em; 24 | } 25 | } 26 | } 27 | 28 | 29 | img { 30 | width: 100%; 31 | } 32 | } 33 | 34 | .prpl-widget-footer { 35 | display: flex; 36 | justify-content: flex-end; 37 | } 38 | } 39 | 40 | .prpl-blog-post-image { 41 | width: 100%; 42 | min-height: 120px; 43 | aspect-ratio: 16 / 9; 44 | background-size: cover; 45 | margin-bottom: 1em; 46 | border-radius: var(--prpl-border-radius); 47 | box-shadow: 5px 5px 5px var(--prpl-color-gray-2); 48 | border: 1px solid var(--prpl-color-gray-2); 49 | background-color: var(--prpl-color-gray-1); /* Fallback, if remote host image is not accessible */ 50 | } 51 | -------------------------------------------------------------------------------- /assets/css/web-components/prpl-badge.css: -------------------------------------------------------------------------------- 1 | .prpl-badge { 2 | display: grid; 3 | grid-template-columns: 1fr; 4 | 5 | min-width: 0; 6 | gap: 10px; 7 | 8 | > * { 9 | align-self: center; 10 | } 11 | 12 | prpl-badge { 13 | 14 | img { 15 | transition: opacity 0.3s ease-in-out, filter 0.3s ease-in-out; 16 | } 17 | 18 | &[complete="false"] { 19 | 20 | img { 21 | opacity: 0.25; 22 | filter: grayscale(1); 23 | } 24 | } 25 | } 26 | } 27 | 28 | prpl-badge { 29 | width: 100%; 30 | margin-bottom: 1rem; 31 | } 32 | -------------------------------------------------------------------------------- /assets/css/web-components/prpl-tooltip.css: -------------------------------------------------------------------------------- 1 | .tooltip-actions { 2 | justify-content: flex-end; 3 | gap: 0.5em; 4 | display: flex; 5 | position: relative; 6 | 7 | .icon { 8 | width: 1.25rem; 9 | height: 1.25rem; 10 | display: inline-block; 11 | vertical-align: bottom; /* align with the text */ 12 | } 13 | } 14 | 15 | .prpl-tooltip { 16 | position: absolute; 17 | bottom: 0; 18 | left: 100%; 19 | transform: translate(-100%, calc(100% + 10px)); 20 | 21 | padding: 0.75rem 1.5rem 0.75rem 0.75rem; 22 | width: 150px; 23 | background: var(--prpl-background-green); 24 | border-radius: var(--prpl-border-radius); 25 | z-index: 2; /* above the gauges */ 26 | visibility: hidden; /* hidden by default */ 27 | 28 | font-size: 1rem; 29 | font-weight: 400; 30 | color: var(--prpl-color-text); 31 | 32 | &[data-tooltip-visible="true"] { 33 | visibility: visible; 34 | z-index: 10; 35 | } 36 | 37 | .close, 38 | .prpl-tooltip-close { 39 | position: absolute; 40 | top: 0; 41 | right: 0; 42 | padding: 0.1rem; 43 | line-height: 0; 44 | margin: 0; 45 | background: none; 46 | border: none; 47 | cursor: pointer; 48 | } 49 | 50 | /* Arrow */ 51 | &::after { 52 | content: ""; 53 | position: absolute; 54 | top: 0; 55 | right: 0; 56 | transform: translate(-10px, -10px) rotate(90deg); 57 | 58 | width: 0; 59 | height: 0; 60 | border-style: solid; 61 | border-width: 7.5px 10px 7.5px 0; 62 | border-color: transparent var(--prpl-background-green) transparent transparent; 63 | } 64 | } 65 | 66 | prpl-tooltip { 67 | display: inline-flex; 68 | align-items: center; 69 | position: relative; 70 | 71 | .prpl-tooltip { 72 | 73 | p { 74 | margin-bottom: 0; 75 | } 76 | 77 | p:first-child { 78 | margin-top: 0; 79 | } 80 | } 81 | } 82 | 83 | .prpl-overlay { 84 | display: none; 85 | } 86 | 87 | body:has([data-tooltip-visible="true"]) .prpl-overlay { 88 | display: block !important; 89 | position: fixed; 90 | top: 0; 91 | left: 0; 92 | width: 100%; 93 | height: 100%; 94 | z-index: 9; 95 | background-color: rgba(0, 0, 0, 0.5); 96 | } 97 | -------------------------------------------------------------------------------- /assets/css/welcome.css: -------------------------------------------------------------------------------- 1 | .prpl-wrap.prpl-pp-not-accepted { 2 | padding: 0; 3 | } 4 | 5 | .prpl-welcome { 6 | 7 | .inner-content { 8 | padding: calc(var(--prpl-gap) * 1.5); 9 | padding-bottom: 0; 10 | margin-bottom: calc(var(--prpl-gap) * 1.5); 11 | display: flex; 12 | grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); 13 | gap: calc(var(--prpl-gap) * 2); 14 | 15 | .left { 16 | flex-grow: 1; 17 | } 18 | 19 | img { 20 | max-width: 100%; 21 | width: 550px; 22 | height: auto; 23 | } 24 | } 25 | 26 | .welcome-header { 27 | background: var(--prpl-color-400-orange); 28 | display: flex; 29 | justify-content: space-between; 30 | align-items: center; 31 | border-top-left-radius: var(--prpl-border-radius); 32 | border-top-right-radius: var(--prpl-border-radius); 33 | overflow: hidden; 34 | 35 | h1 { 36 | font-size: var(--prpl-font-size-3xl); 37 | padding: var(--prpl-padding) calc(var(--prpl-gap) * 1.5); 38 | font-weight: 600; 39 | } 40 | 41 | .welcome-header-icon { 42 | background: var(--prpl-color-400-orange); 43 | background: linear-gradient(105deg, var(--prpl-color-400-orange) 25%, var(--prpl-background-orange) 25%); 44 | padding: var(--prpl-padding); 45 | padding-left: 100px; 46 | padding-right: calc(var(--prpl-gap) * 1.5); 47 | 48 | svg { 49 | height: 100px; 50 | } 51 | } 52 | } 53 | 54 | .prpl-form-notice-title { 55 | font-size: var(--prpl-font-size-lg); 56 | } 57 | 58 | ul { 59 | list-style: disc; 60 | margin-left: 1rem; 61 | } 62 | 63 | .prpl-onboard-form-radio-select { 64 | margin-top: 0.75rem; 65 | 66 | label { 67 | margin-top: 0.5rem; 68 | 69 | &:first-child { 70 | margin-top: 0; 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /assets/images/icon_check_circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icon_copywriting.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icon_exclamation_circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icon_exclamation_circle_solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/images/icon_exclamation_triangle_solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/images/icon_forward.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/images/icon_info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icon_key.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/images/icon_pages.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icon_progress_planner.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icon_settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icon_snooze.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icon_tour.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/icon_user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/images/image_onboaring_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgressPlanner/progress-planner/12b1e6da93c1082f0959270cc0d92b1e6d82fdc9/assets/images/image_onboaring_block.png -------------------------------------------------------------------------------- /assets/images/register_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/js/ajax-request.js: -------------------------------------------------------------------------------- 1 | /* global XMLHttpRequest */ 2 | 3 | /** 4 | * A helper to make AJAX requests. 5 | * 6 | * @param {Object} params The callback parameters. 7 | * @param {string} params.url The URL to send the request to. 8 | * @param {Object} params.data The data to send with the request. 9 | */ 10 | // eslint-disable-next-line no-unused-vars 11 | const progressPlannerAjaxRequest = ( { url, data } ) => { 12 | return new Promise( ( resolve, reject ) => { 13 | const http = new XMLHttpRequest(); 14 | http.open( 'POST', url, true ); 15 | http.onreadystatechange = () => { 16 | let response; 17 | try { 18 | response = JSON.parse( http.response ); 19 | } catch ( e ) { 20 | if ( http.readyState === 4 && http.status !== 200 ) { 21 | console.warn( http, e ); 22 | return http.response; 23 | } 24 | } 25 | 26 | if ( http.readyState === 4 ) { 27 | if ( http.status === 200 ) { 28 | resolve( response ); 29 | } 30 | 31 | // Request is completed, but the status is not 200. 32 | reject( response ); 33 | } 34 | }; 35 | 36 | const dataForm = new FormData(); 37 | 38 | // eslint-disable-next-line prefer-const 39 | for ( let [ key, value ] of Object.entries( data ) ) { 40 | dataForm.append( key, value ); 41 | } 42 | 43 | http.send( dataForm ); 44 | } ); 45 | }; 46 | -------------------------------------------------------------------------------- /assets/js/document-ready.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /** 3 | * Vanilla JS version of jQuery( document ).ready(). 4 | * 5 | * @param {Function} fn The function to run when the document is ready. 6 | */ 7 | const prplDocumentReady = function ( fn ) { 8 | if ( document.readyState !== 'loading' ) { 9 | fn(); 10 | return; 11 | } 12 | document.addEventListener( 'DOMContentLoaded', fn ); 13 | }; 14 | 15 | /* eslint-enable no-unused-vars */ 16 | -------------------------------------------------------------------------------- /assets/js/grid-masonry.js: -------------------------------------------------------------------------------- 1 | /* global prplDocumentReady */ 2 | /* 3 | * Grid Masonry 4 | * 5 | * A script to allow a grid to behave like a masonry layout. 6 | * Inspired by https://medium.com/@andybarefoot/a-masonry-style-layout-using-css-grid-8c663d355ebb 7 | * 8 | * Dependencies: progress-planner/document-ready 9 | */ 10 | 11 | /** 12 | * Trigger a resize event on the grid. 13 | */ 14 | const prplTriggerGridResize = () => { 15 | setTimeout( () => { 16 | window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); 17 | } ); 18 | }; 19 | 20 | prplDocumentReady( () => { 21 | prplTriggerGridResize(); 22 | setTimeout( prplTriggerGridResize, 1000 ); 23 | } ); 24 | 25 | window.addEventListener( 'resize', prplTriggerGridResize ); 26 | 27 | // Fire event after all images are loaded. 28 | window.addEventListener( 'load', prplTriggerGridResize ); 29 | 30 | // Listen for the event. 31 | window.addEventListener( 32 | 'prpl/grid/resize', 33 | () => { 34 | document 35 | .querySelectorAll( '.prpl-widget-wrapper' ) 36 | .forEach( ( item ) => { 37 | if ( ! item || item.classList.contains( 'in-popover' ) ) { 38 | return; 39 | } 40 | const innerContainer = item.querySelector( 41 | '.widget-inner-container' 42 | ); 43 | if ( ! innerContainer ) { 44 | return; 45 | } 46 | const rowSpan = Math.ceil( 47 | ( innerContainer.getBoundingClientRect().height + 48 | parseInt( 49 | window 50 | .getComputedStyle( item ) 51 | .getPropertyValue( 'padding-top' ) 52 | ) + 53 | parseInt( 54 | window 55 | .getComputedStyle( item ) 56 | .getPropertyValue( 'padding-bottom' ) 57 | ) ) / 58 | parseInt( 59 | window 60 | .getComputedStyle( 61 | document.querySelector( 62 | '.prpl-widgets-container' 63 | ) 64 | ) 65 | .getPropertyValue( 'grid-auto-rows' ) 66 | ) 67 | ); 68 | item.style.gridRowEnd = 'span ' + ( rowSpan + 1 ); 69 | } ); 70 | }, 71 | false 72 | ); 73 | -------------------------------------------------------------------------------- /assets/js/header-filters.js: -------------------------------------------------------------------------------- 1 | // Handle changes to the range dropdown. 2 | document 3 | .getElementById( 'prpl-select-range' ) 4 | .addEventListener( 'change', function () { 5 | const range = this.value; 6 | const url = new URL( window.location.href ); 7 | url.searchParams.set( 'range', range ); 8 | window.location.href = url.href; 9 | } ); 10 | 11 | // Handle changes to the frequency dropdown. 12 | document 13 | .getElementById( 'prpl-select-frequency' ) 14 | .addEventListener( 'change', function () { 15 | const frequency = this.value; 16 | const url = new URL( window.location.href ); 17 | url.searchParams.set( 'frequency', frequency ); 18 | window.location.href = url.href; 19 | } ); 20 | -------------------------------------------------------------------------------- /assets/js/l10n.js: -------------------------------------------------------------------------------- 1 | /* global prplL10nStrings */ 2 | window.prplL10n = ( key ) => prplL10nStrings[ key ] || ''; 3 | -------------------------------------------------------------------------------- /assets/js/scan-posts.js: -------------------------------------------------------------------------------- 1 | /* global progressPlanner, progressPlannerAjaxRequest */ 2 | /* 3 | * Scan Posts 4 | * 5 | * A script to scan posts for the Progress Planner. 6 | * 7 | * Dependencies: progress-planner/ajax-request, progress-planner/upgrade-tasks 8 | */ 9 | 10 | const progressPlannerTriggerScan = () => { 11 | document.getElementById( 'progress-planner-scan-progress' ).style.display = 12 | 'block'; 13 | 14 | return new Promise( async ( resolve, reject ) => { 15 | const progressBar = document.querySelector( 16 | '#progress-planner-scan-progress progress' 17 | ); 18 | 19 | if ( ! progressBar ) { 20 | resolve(); 21 | return; 22 | } 23 | 24 | let failCount = 0; 25 | let isComplete = false; 26 | 27 | while ( ! isComplete && 10 >= failCount ) { 28 | try { 29 | const response = await progressPlannerAjaxRequest( { 30 | url: progressPlanner.ajaxUrl, 31 | data: { 32 | action: 'progress_planner_scan_posts', 33 | _ajax_nonce: progressPlanner.nonce, 34 | }, 35 | } ); 36 | 37 | if ( response.data.progress > progressBar.value ) { 38 | progressBar.value = response.data.progress; 39 | } 40 | 41 | console.info( 42 | `Progress: ${ response.data.progress }%, (${ response.data.lastScanned }/${ response.data.lastPage })` 43 | ); 44 | 45 | if ( 100 <= response.data.progress ) { 46 | document.getElementById( 47 | 'progress-planner-scan-progress' 48 | ).style.display = 'none'; 49 | 50 | resolve(); 51 | isComplete = true; // Stops the loop. 52 | } 53 | 54 | failCount = 0; // Reset fail count on success. 55 | } catch ( error ) { 56 | failCount++; 57 | console.warn( 'Failed to scan posts. Retrying...', error ); 58 | } 59 | 60 | // 200ms delay between retries. 61 | if ( ! isComplete && 10 >= failCount ) { 62 | await new Promise( ( resolveTimeout ) => 63 | setTimeout( resolveTimeout, 200 ) 64 | ); 65 | } 66 | } 67 | 68 | if ( 10 <= failCount ) { 69 | reject( new Error( 'Max scan failures reached' ) ); 70 | } 71 | } ); 72 | }; 73 | 74 | if ( document.getElementById( 'prpl-scan-button' ) ) { 75 | document 76 | .getElementById( 'prpl-scan-button' ) 77 | .addEventListener( 'click', ( event ) => { 78 | event.preventDefault(); 79 | document.getElementById( 'prpl-scan-button' ).disabled = true; 80 | progressPlannerAjaxRequest( { 81 | url: progressPlanner.ajaxUrl, 82 | data: { 83 | action: 'progress_planner_reset_posts_data', 84 | _ajax_nonce: progressPlanner.nonce, 85 | }, 86 | } ) 87 | .then( () => { 88 | progressPlannerTriggerScan(); 89 | } ) 90 | .catch( ( error ) => { 91 | console.warn( error ); 92 | } ); 93 | } ); 94 | } 95 | -------------------------------------------------------------------------------- /assets/js/settings-page.js: -------------------------------------------------------------------------------- 1 | /* global alert, prplDocumentReady */ 2 | /* 3 | * Settings Page 4 | * 5 | * A script to handle the settings page. 6 | * 7 | * Dependencies: progress-planner/document-ready, wp-util 8 | */ 9 | const prplTogglePageSelectorSettingVisibility = function ( page, value ) { 10 | const itemRadiosWrapperEl = document.querySelector( 11 | `.prpl-pages-item-${ page } .radios` 12 | ); 13 | 14 | if ( ! itemRadiosWrapperEl ) { 15 | return; 16 | } 17 | 18 | // Show only create button. 19 | if ( 'no' === value || 'not-applicable' === value ) { 20 | // Hide wrapper. 29 | itemRadiosWrapperEl.querySelector( 30 | '.prpl-select-page' 31 | ).style.visibility = 'visible'; 32 | } 33 | }; 34 | 35 | prplDocumentReady( function () { 36 | document 37 | .querySelectorAll( 'input[type="radio"][data-page]' ) 38 | .forEach( function ( radio ) { 39 | const page = radio.getAttribute( 'data-page' ), 40 | value = radio.value; 41 | 42 | if ( radio ) { 43 | // Show/hide the page selector setting if radio is checked. 44 | if ( radio.checked ) { 45 | prplTogglePageSelectorSettingVisibility( page, value ); 46 | } 47 | 48 | // Add listeners for all radio buttons. 49 | radio.addEventListener( 'change', function () { 50 | prplTogglePageSelectorSettingVisibility( page, value ); 51 | } ); 52 | } 53 | } ); 54 | } ); 55 | 56 | /** 57 | * Handle the form submission. 58 | */ 59 | prplDocumentReady( function () { 60 | const prplFormSubmit = function ( event ) { 61 | event.preventDefault(); 62 | const formData = new FormData( 63 | document.getElementById( 'prpl-settings' ) 64 | ); 65 | const data = { 66 | action: 'prpl_settings_form', 67 | }; 68 | formData.forEach( function ( value, key ) { 69 | // Handle array notation in keys 70 | if ( key.endsWith( '[]' ) ) { 71 | const baseKey = key.slice( 0, -2 ); 72 | if ( ! data[ baseKey ] ) { 73 | data[ baseKey ] = []; 74 | } 75 | data[ baseKey ].push( value ); 76 | } else { 77 | data[ key ] = value; 78 | } 79 | } ); 80 | const request = wp.ajax.post( 'prpl_settings_form', data ); 81 | request.done( function () { 82 | window.location.reload(); 83 | } ); 84 | request.fail( function ( response ) { 85 | alert( response.licensingError || response ); // eslint-disable-line no-alert 86 | } ); 87 | }; 88 | document 89 | .getElementById( 'prpl-settings-submit' ) 90 | .addEventListener( 'click', prplFormSubmit ); 91 | document 92 | .getElementById( 'prpl-settings' ) 93 | .addEventListener( 'submit', prplFormSubmit ); 94 | } ); 95 | -------------------------------------------------------------------------------- /assets/js/settings.js: -------------------------------------------------------------------------------- 1 | /* global progressPlanner, progressPlannerAjaxRequest, progressPlannerSaveLicenseKey, prplL10n */ 2 | /* 3 | * Settings 4 | * 5 | * A script to handle the settings page. 6 | * 7 | * Dependencies: progress-planner/ajax-request, progress-planner/onboard, wp-util, progress-planner/l10n 8 | */ 9 | 10 | // Submit the email. 11 | const settingsLicenseForm = document.getElementById( 12 | 'prpl-settings-license-form' 13 | ); 14 | if ( !! settingsLicenseForm ) { 15 | settingsLicenseForm.addEventListener( 'submit', function ( event ) { 16 | event.preventDefault(); 17 | const form = new FormData( this ); 18 | const data = {}; 19 | 20 | // Build the onboarding data object. 21 | for ( const [ key, value ] of form.entries() ) { 22 | data[ key ] = value; 23 | } 24 | 25 | progressPlannerAjaxRequest( { 26 | url: progressPlanner.onboardNonceURL, 27 | data, 28 | } ) 29 | .then( ( response ) => { 30 | if ( 'ok' === response.status ) { 31 | // Add the nonce to our data object. 32 | data.nonce = response.nonce; 33 | 34 | // Make the request to the API. 35 | progressPlannerAjaxRequest( { 36 | url: progressPlanner.onboardAPIUrl, 37 | data, 38 | } ) 39 | .then( ( apiResponse ) => { 40 | // Make a local request to save the response data. 41 | progressPlannerSaveLicenseKey( 42 | apiResponse.license_key 43 | ); 44 | 45 | document.getElementById( 46 | 'submit-license-key' 47 | ).innerHTML = prplL10n( 'subscribed' ); 48 | 49 | // Timeout so the license key is saved. 50 | setTimeout( () => { 51 | // Reload the page. 52 | window.location.reload(); 53 | }, 500 ); 54 | } ) 55 | .catch( ( error ) => { 56 | console.warn( error ); 57 | } ); 58 | } 59 | } ) 60 | .catch( ( error ) => { 61 | console.warn( error ); 62 | } ); 63 | 64 | document.getElementById( 'submit-license-key' ).disabled = true; 65 | document.getElementById( 'submit-license-key' ).innerHTML = 66 | prplL10n( 'subscribing' ); 67 | } ); 68 | } 69 | -------------------------------------------------------------------------------- /assets/js/web-components/prpl-badge.js: -------------------------------------------------------------------------------- 1 | /* global customElements, HTMLElement, progressPlannerBadge, prplL10n */ 2 | /* 3 | * Badge 4 | * 5 | * A web component to display a badge. 6 | * 7 | * Dependencies: progress-planner/l10n 8 | */ 9 | 10 | /** 11 | * Register the custom web component. 12 | */ 13 | customElements.define( 14 | 'prpl-badge', 15 | class extends HTMLElement { 16 | constructor( badgeId ) { 17 | // Get parent class properties 18 | super(); 19 | 20 | badgeId = badgeId || this.getAttribute( 'badge-id' ); 21 | this.innerHTML = ` 22 | ${ prplL10n( 'badge' ) } 31 | `; 32 | } 33 | } 34 | ); 35 | -------------------------------------------------------------------------------- /assets/js/web-components/prpl-big-counter.js: -------------------------------------------------------------------------------- 1 | /* global customElements, HTMLElement */ 2 | 3 | /** 4 | * Register the custom web component. 5 | */ 6 | customElements.define( 7 | 'prpl-big-counter', 8 | class extends HTMLElement { 9 | constructor( number, content, backgroundColor ) { 10 | // Get parent class properties 11 | super(); 12 | number = number || this.getAttribute( 'number' ); 13 | content = content || this.getAttribute( 'content' ); 14 | backgroundColor = 15 | backgroundColor || this.getAttribute( 'background-color' ); 16 | backgroundColor = 17 | backgroundColor || 'var(--prpl-background-purple)'; 18 | 19 | const el = this; 20 | 21 | this.innerHTML = ` 22 |
35 |
36 | ${ number } 41 | 42 | ${ content } 43 | 44 |
45 | `; 46 | 47 | const resizeFont = () => { 48 | const element = el.querySelector( '.resize' ); 49 | if ( ! element ) { 50 | return; 51 | } 52 | 53 | element.style.fontSize = '100%'; 54 | 55 | let size = 100; 56 | while ( 57 | element.clientWidth > 58 | el.querySelector( '.container-width' ).clientWidth 59 | ) { 60 | if ( size < 80 ) { 61 | element.style.fontSize = size + '%'; 62 | element.style.width = '100%'; 63 | break; 64 | } 65 | size -= 1; 66 | element.style.fontSize = size + '%'; 67 | } 68 | }; 69 | 70 | resizeFont(); 71 | window.addEventListener( 'resize', resizeFont ); 72 | } 73 | } 74 | ); 75 | -------------------------------------------------------------------------------- /assets/js/web-components/prpl-chart-bar.js: -------------------------------------------------------------------------------- 1 | /* global customElements, HTMLElement */ 2 | 3 | /** 4 | * Register the custom web component. 5 | */ 6 | customElements.define( 7 | 'prpl-chart-bar', 8 | class extends HTMLElement { 9 | constructor( data = [] ) { 10 | // Get parent class properties 11 | super(); 12 | 13 | if ( data.length === 0 ) { 14 | data = JSON.parse( this.getAttribute( 'data' ) ); 15 | } 16 | 17 | const labelsDivider = 18 | data.length > 6 ? parseInt( data.length / 6 ) : 1; 19 | 20 | let html = `
`; 21 | let i = 0; 22 | data.forEach( ( item ) => { 23 | html += `
`; 24 | html += `
`; 31 | // Only display up to 6 labels. 32 | html += ``; 33 | html += 34 | i % labelsDivider === 0 35 | ? `${ item.label }` 36 | : ``; 37 | html += ``; 38 | html += `
`; 39 | i++; 40 | } ); 41 | html += `
`; 42 | 43 | this.innerHTML = html; 44 | 45 | // Tweak labels styling to fix positioning when there are many items. 46 | if ( this.querySelectorAll( '.label.invisible' ).length > 0 ) { 47 | this.querySelectorAll( '.label-container' ).forEach( 48 | ( label ) => { 49 | const labelWidth = 50 | label.querySelector( '.label' ).offsetWidth; 51 | const labelElement = label.querySelector( '.label' ); 52 | labelElement.style.display = 'block'; 53 | labelElement.style.width = 0; 54 | const marginLeft = 55 | ( label.offsetWidth - labelWidth ) / 2; 56 | if ( labelElement.classList.contains( 'visible' ) ) { 57 | labelElement.style.marginLeft = `${ marginLeft }px`; 58 | } 59 | } 60 | ); 61 | // Reduce the gap between items to avoid overflows. 62 | this.querySelector( '.chart-bar' ).style.gap = 63 | parseInt( 64 | Math.max( 65 | this.querySelector( '.label' ).offsetWidth / 4, 66 | 1 67 | ) 68 | ) + 'px'; 69 | } 70 | } 71 | } 72 | ); 73 | -------------------------------------------------------------------------------- /assets/js/web-components/prpl-interactive-task.js: -------------------------------------------------------------------------------- 1 | /* global HTMLElement */ 2 | 3 | /** 4 | * Register the custom web component. 5 | */ 6 | // eslint-disable-next-line no-unused-vars 7 | class PrplInteractiveTask extends HTMLElement { 8 | // eslint-disable-next-line no-useless-constructor 9 | constructor() { 10 | // Get parent class properties 11 | super(); 12 | } 13 | 14 | /** 15 | * Runs when the component is added to the DOM. 16 | */ 17 | connectedCallback() { 18 | const popoverId = this.getAttribute( 'popover-id' ); 19 | 20 | // Add default event listeners. 21 | this.attachDefaultEventListeners(); 22 | 23 | // Allow child components to add event listeners when the popover is added to the DOM. 24 | this.popoverAddedToDOM(); 25 | 26 | // Add popover close event listener. 27 | const popover = document.getElementById( popoverId ); 28 | popover.addEventListener( 'beforetoggle', ( event ) => { 29 | if ( event.newState === 'open' ) { 30 | this.popoverOpening(); 31 | } 32 | 33 | if ( event.newState === 'closed' ) { 34 | this.popoverClosing(); 35 | } 36 | } ); 37 | } 38 | 39 | /** 40 | * Attach button event listeners. 41 | * Every button with a data-action attribute will be handled by the component. 42 | */ 43 | attachDefaultEventListeners() { 44 | // Add event listeners. 45 | this.querySelectorAll( 'button' ).forEach( ( buttonElement ) => { 46 | buttonElement.addEventListener( 'click', ( e ) => { 47 | const button = e.target.closest( 'button' ); 48 | const action = button?.dataset.action; 49 | if ( action && typeof this[ action ] === 'function' ) { 50 | this[ action ](); 51 | } 52 | } ); 53 | } ); 54 | } 55 | 56 | /** 57 | * Runs when the popover is added to the DOM. 58 | */ 59 | popoverAddedToDOM() {} 60 | 61 | /** 62 | * Runs when the popover is opening. 63 | */ 64 | popoverOpening() {} 65 | 66 | /** 67 | * Runs when the popover is closing. 68 | */ 69 | popoverClosing() {} 70 | 71 | /** 72 | * Complete the task. 73 | */ 74 | completeTask() { 75 | const providerId = this.getAttribute( 'provider-id' ); 76 | const components = document.querySelectorAll( 'prpl-suggested-task' ); 77 | 78 | components.forEach( ( component ) => { 79 | const liElement = component.querySelector( 'li' ); 80 | if ( liElement.dataset.taskId === providerId ) { 81 | // Close popover. 82 | document 83 | .getElementById( 'prpl-popover-' + providerId ) 84 | .hidePopover(); 85 | // Complete task. 86 | component.runTaskAction( liElement.dataset.taskId, 'complete' ); 87 | } 88 | } ); 89 | } 90 | 91 | /** 92 | * Close the popover. 93 | */ 94 | closePopover() { 95 | const popoverId = this.getAttribute( 'popover-id' ); 96 | const popover = document.getElementById( popoverId ); 97 | popover.hidePopover(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /classes/activities/class-content-helpers.php: -------------------------------------------------------------------------------- 1 | get_settings()->get( [ 'include_post_types' ], $default ), 30 | function ( $post_type ) { 31 | return $post_type && \post_type_exists( $post_type ) && \is_post_type_viewable( $post_type ); 32 | } 33 | ); 34 | return empty( $include_post_types ) ? $default : \array_values( $include_post_types ); 35 | } 36 | 37 | /** 38 | * Get Activity from WP_Post object. 39 | * 40 | * @param \WP_Post $post The post object. 41 | * @param string $activity_type The activity type. 42 | * 43 | * @return \Progress_Planner\Activities\Content 44 | */ 45 | public function get_activity_from_post( $post, $activity_type = 'publish' ) { 46 | $activity = new Activities_Content(); 47 | $activity->category = 'content'; 48 | $activity->type = $activity_type; 49 | $activity->date = \progress_planner()->get_utils__date()->get_datetime_from_mysql_date( $post->post_modified ); 50 | $activity->data_id = (string) $post->ID; 51 | $activity->user_id = (int) $post->post_author; 52 | return $activity; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /classes/activities/class-content.php: -------------------------------------------------------------------------------- 1 | 50, 30 | 'update' => 10, 31 | 'delete' => 5, 32 | ]; 33 | 34 | /** 35 | * Get WP_Post from the activity. 36 | * 37 | * @return \WP_Post|null 38 | */ 39 | public function get_post() { 40 | return \get_post( (int) $this->data_id ); 41 | } 42 | 43 | /** 44 | * Get the points for an activity. 45 | * 46 | * @param \DateTime $date The date for which we want to get the points of the activity. 47 | * 48 | * @return int 49 | */ 50 | public function get_points( $date ) { 51 | $date_ymd = $date->format( 'Ymd' ); 52 | if ( isset( $this->points[ $date_ymd ] ) ) { 53 | return $this->points[ $date_ymd ]; 54 | } 55 | 56 | // Get the number of days between the activity date and the given date. 57 | $days = absint( \progress_planner()->get_utils__date()->get_days_between_dates( $date, $this->date ) ); 58 | 59 | // Maximum range for awarded points is 30 days. 60 | if ( $days >= 30 ) { 61 | $this->points[ $date_ymd ] = 0; 62 | return $this->points[ $date_ymd ]; 63 | } 64 | 65 | // Get the points for the activity on the publish date. 66 | $this->points[ $date_ymd ] = $this->get_points_on_publish_date(); 67 | 68 | // Bail early if the post score is 0. 69 | if ( 0 === $this->points[ $date_ymd ] ) { 70 | return $this->points[ $date_ymd ]; 71 | } 72 | 73 | // Calculate the points based on the age of the activity. 74 | $this->points[ $date_ymd ] = ( $days < 7 ) 75 | ? round( $this->points[ $date_ymd ] ) // If the activity is new (less than 7 days old), award full points. 76 | : round( $this->points[ $date_ymd ] * max( 0, ( 1 - $days / 30 ) ) ); // Decay the points based on the age of the activity. 77 | 78 | return (int) $this->points[ $date_ymd ]; 79 | } 80 | 81 | /** 82 | * Get the points for an activity. 83 | * 84 | * @return int 85 | */ 86 | public function get_points_on_publish_date() { 87 | $points = self::$points_config['publish']; 88 | if ( isset( self::$points_config[ $this->type ] ) ) { 89 | $points = self::$points_config[ $this->type ]; 90 | } 91 | 92 | return $this->get_post() ? $points : 0; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /classes/activities/class-maintenance.php: -------------------------------------------------------------------------------- 1 | date = new \DateTime(); 45 | $this->user_id = \get_current_user_id(); 46 | 47 | $existing = \progress_planner()->get_activities__query()->query_activities( 48 | [ 49 | 'category' => $this->category, 50 | 'type' => $this->type, 51 | 'data_id' => $this->data_id, 52 | 'start_date' => $this->date, 53 | ], 54 | 'RAW' 55 | ); 56 | if ( ! empty( $existing ) ) { 57 | \progress_planner()->get_activities__query()->update_activity( $existing[0]->id, $this ); 58 | return; 59 | } 60 | \progress_planner()->get_activities__query()->insert_activity( $this ); 61 | \do_action( 'progress_planner_activity_saved', $this ); 62 | } 63 | 64 | /** 65 | * Get the points for an activity. 66 | * 67 | * @param \DateTime $date The date for which we want to get the points of the activity. 68 | * 69 | * @return int 70 | */ 71 | public function get_points( $date ) { 72 | $date_ymd = $date->format( 'Ymd' ); 73 | if ( isset( $this->points[ $date_ymd ] ) ) { 74 | return $this->points[ $date_ymd ]; 75 | } 76 | $this->points[ $date_ymd ] = self::$points_config; 77 | $days = abs( \progress_planner()->get_utils__date()->get_days_between_dates( $date, $this->date ) ); 78 | 79 | $this->points[ $date_ymd ] = ( $days < 7 ) ? $this->points[ $date_ymd ] : 0; 80 | 81 | return $this->points[ $date_ymd ]; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /classes/activities/class-suggested-task.php: -------------------------------------------------------------------------------- 1 | date ) { 38 | $this->date = new \DateTime(); 39 | } 40 | 41 | if ( ! $this->user_id ) { 42 | $this->user_id = \get_current_user_id(); 43 | } 44 | 45 | if ( $this->id ) { 46 | \progress_planner()->get_activities__query()->update_activity( $this->id, $this ); 47 | return; 48 | } 49 | 50 | \progress_planner()->get_activities__query()->insert_activity( $this ); 51 | \do_action( 'progress_planner_activity_saved', $this ); 52 | } 53 | 54 | /** 55 | * Get the points for an activity. 56 | * 57 | * @param \DateTime $date The date for which we want to get the points of the activity. 58 | * 59 | * @return int 60 | */ 61 | public function get_points( $date ) { 62 | $date_ymd = $date->format( 'Ymd' ); 63 | if ( isset( $this->points[ $date_ymd ] ) ) { 64 | return $this->points[ $date_ymd ]; 65 | } 66 | 67 | // Default points for a suggested task. 68 | $points = 1; 69 | $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $this->data_id ); 70 | 71 | if ( ! empty( $tasks ) && isset( $tasks[0]['provider_id'] ) ) { 72 | if ( 'user' === $tasks[0]['provider_id'] ) { 73 | $points = isset( $tasks[0]['points'] ) ? (int) $tasks[0]['points'] : 0; 74 | } else { 75 | $task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $tasks[0]['provider_id'] ); 76 | 77 | if ( $task_provider ) { 78 | // Create post task provider had a different points system, this is for backwards compatibility. 79 | $points = $task_provider instanceof Content_Create ? $task_provider->get_points( $this->data_id ) : $task_provider->get_points(); 80 | } 81 | } 82 | } 83 | $this->points[ $date_ymd ] = $points; 84 | 85 | return (int) $this->points[ $date_ymd ]; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /classes/admin/class-dashboard-widget-score.php: -------------------------------------------------------------------------------- 1 | get_admin__page()->enqueue_styles(); 41 | \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-gauge' ); 42 | 43 | $suggested_tasks_widget = \progress_planner()->get_admin__page()->get_widget( 'suggested-tasks' ); 44 | if ( $suggested_tasks_widget ) { 45 | $suggested_tasks_widget->enqueue_styles(); 46 | $suggested_tasks_widget->enqueue_scripts(); 47 | } 48 | 49 | \progress_planner()->get_admin__enqueue()->enqueue_style( "progress-planner/dashboard-widgets/{$this->id}" ); 50 | 51 | \progress_planner()->get_admin__enqueue()->enqueue_script( 'external-link-accessibility-helper' ); 52 | 53 | \progress_planner()->the_view( "dashboard-widgets/{$this->id}.php" ); 54 | } 55 | 56 | /** 57 | * Get the badge. 58 | * 59 | * @param string $category The category of the badge. 60 | * 61 | * @return array 62 | */ 63 | public function get_badge_details( $category = 'content' ) { 64 | static $cached = [ 65 | 'content' => false, 66 | 'maintenance' => false, 67 | ]; 68 | 69 | if ( $cached[ $category ] ) { 70 | return $cached[ $category ]; 71 | } 72 | 73 | // Get the badge to display. 74 | foreach ( \progress_planner()->get_badges()->get_badges( $category ) as $badge ) { 75 | $progress = $badge->get_progress(); 76 | if ( 100 > $progress['progress'] ) { 77 | break; 78 | } 79 | } 80 | 81 | if ( ! isset( $badge ) || ! isset( $progress ) ) { 82 | return []; 83 | } 84 | 85 | $result = [ 86 | 'progress' => $progress, 87 | 'badge' => $badge, 88 | 'color' => 'var(--prpl-color-accent-red)', 89 | 'background' => $badge->get_background(), 90 | ]; 91 | 92 | if ( $result['progress']['progress'] > 50 ) { 93 | $result['color'] = 'var(--prpl-color-accent-orange)'; 94 | } 95 | if ( $result['progress']['progress'] > 75 ) { 96 | $result['color'] = 'var(--prpl-color-accent-green)'; 97 | } 98 | 99 | $cached[ $category ] = $result; 100 | 101 | return $result; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /classes/admin/class-dashboard-widget-todo.php: -------------------------------------------------------------------------------- 1 | get_admin__page()->enqueue_styles(); 40 | 41 | $todo_widget = \progress_planner()->get_admin__page()->get_widget( 'todo' ); 42 | if ( $todo_widget ) { 43 | $todo_widget->enqueue_styles(); 44 | $todo_widget->enqueue_scripts(); 45 | } 46 | 47 | \progress_planner()->the_view( "dashboard-widgets/{$this->id}.php" ); 48 | } 49 | } 50 | // phpcs:enable Generic.Commenting.Todo 51 | -------------------------------------------------------------------------------- /classes/admin/class-dashboard-widget.php: -------------------------------------------------------------------------------- 1 | id}", 37 | $this->get_title(), 38 | [ $this, 'render_widget' ] 39 | ); 40 | } 41 | 42 | /** 43 | * Get the title of the widget. 44 | * 45 | * @return string 46 | */ 47 | abstract protected function get_title(); 48 | 49 | /** 50 | * Render the dashboard widget. 51 | * 52 | * @return void 53 | */ 54 | abstract public function render_widget(); 55 | } 56 | -------------------------------------------------------------------------------- /classes/admin/class-editor.php: -------------------------------------------------------------------------------- 1 | get_page_types()->get_page_types(); 35 | 36 | // Check if the page-type is set in the URL (user is coming from the Settings page). 37 | if ( isset( $_GET['prpl_page_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended 38 | $prpl_pt = sanitize_text_field( wp_unslash( $_GET['prpl_page_type'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended 39 | 40 | foreach ( $page_types as $page_type ) { 41 | if ( $page_type['slug'] === $prpl_pt ) { 42 | $prpl_preselected_page_type = $page_type['id']; 43 | break; 44 | } 45 | } 46 | } else { 47 | // Get the default page-type. 48 | $prpl_preselected_page_type = \progress_planner()->get_page_types()->get_default_page_type( (string) \get_post_type(), (int) \get_the_ID() ); 49 | } 50 | 51 | \progress_planner()->get_admin__enqueue()->enqueue_script( 52 | 'editor', 53 | [ 54 | 'name' => 'progressPlannerEditor', 55 | 'data' => [ 56 | 'lessons' => \progress_planner()->get_lessons()->get_items(), 57 | 'pageTypes' => $page_types, 58 | 'defaultPageType' => $prpl_preselected_page_type, 59 | ], 60 | ] 61 | ); 62 | 63 | \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/editor' ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /classes/admin/widgets/class-badge-streak.php: -------------------------------------------------------------------------------- 1 | get_badges()->get_badges( $context ); 36 | 37 | // Get the badge to display. 38 | foreach ( $badges as $badge ) { 39 | $progress = $badge->get_progress(); 40 | if ( 100 > $progress['progress'] ) { 41 | $result[ $context ] = $badge; 42 | break; 43 | } 44 | } 45 | 46 | return isset( $result[ $context ] ) ? $result[ $context ] : false; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /classes/admin/widgets/class-content-activity.php: -------------------------------------------------------------------------------- 1 | get_chart_args( $type, $color ), 33 | [ 34 | 'count_callback' => function ( $activities, $date = null ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed 35 | return count( $activities ); 36 | }, 37 | ] 38 | ); 39 | } 40 | 41 | /** 42 | * Get the chart args. 43 | * 44 | * @param string $type The type of activity. 45 | * @param string $color The color of the chart. 46 | * 47 | * @return array The chart args. 48 | */ 49 | public function get_chart_args( $type = 'publish', $color = '#534786' ) { 50 | return [ 51 | 'type' => 'line', 52 | 'items_callback' => function ( $start_date, $end_date ) use ( $type ) { 53 | return \progress_planner()->get_activities__query()->query_activities( 54 | [ 55 | 'category' => 'content', 56 | 'start_date' => $start_date, 57 | 'end_date' => $end_date, 58 | 'type' => $type, 59 | ] 60 | ); 61 | }, 62 | 'dates_params' => [ 63 | 'start_date' => \DateTime::createFromFormat( 'Y-m-d', \gmdate( 'Y-m-01' ) )->modify( $this->get_range() ), 64 | 'end_date' => new \DateTime(), 65 | 'frequency' => $this->get_frequency(), 66 | 'format' => 'M', 67 | ], 68 | 'filter_results' => [ $this, 'filter_activities' ], 69 | 'color' => function () use ( $color ) { 70 | return $color; 71 | }, 72 | ]; 73 | } 74 | 75 | /** 76 | * Callback to filter the activities. 77 | * 78 | * @param \Progress_Planner\Activities\Content[] $activities The activities array. 79 | * 80 | * @return \Progress_Planner\Activities\Content[] 81 | */ 82 | public function filter_activities( $activities ) { 83 | return array_filter( 84 | $activities, 85 | function ( $activity ) { 86 | $post = $activity->get_post(); 87 | return 'delete' === $activity->type || ( is_object( $post ) 88 | && \in_array( $post->post_type, \progress_planner()->get_activities__content_helpers()->get_post_types_names(), true ) ); 89 | } 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /classes/admin/widgets/class-latest-badge.php: -------------------------------------------------------------------------------- 1 | endpoint = \progress_planner()->get_remote_server_root_url() . '/wp-json/progress-planner-saas/v1/share-badge-image?badge='; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /classes/admin/widgets/class-todo.php: -------------------------------------------------------------------------------- 1 | ' . \esc_html__( 'Write down all the website maintenance tasks you want to get done!', 'progress-planner' ) . '

'; 29 | $this->the_todo_list(); 30 | } 31 | 32 | /** 33 | * The TODO list. 34 | * 35 | * @return void 36 | */ 37 | public function the_todo_list() { 38 | ?> 39 |
40 | 41 | 42 | 43 |
44 | 45 | 48 |
49 |
50 | 51 | 52 |
53 | get_admin__enqueue()->enqueue_script( 64 | 'widgets/todo', 65 | [ 66 | 'name' => 'progressPlannerTodo', 67 | 'data' => [ 68 | 'ajaxUrl' => \admin_url( 'admin-ajax.php' ), 69 | 'nonce' => \wp_create_nonce( 'progress_planner' ), 70 | 'tasks' => \progress_planner()->get_todo()->get_items(), 71 | ], 72 | ] 73 | ); 74 | } 75 | 76 | /** 77 | * Get the stylesheet dependencies. 78 | * 79 | * @return array 80 | */ 81 | public function get_stylesheet_dependencies() { 82 | // Register styles for the web-component. 83 | \wp_register_style( 84 | 'progress-planner-web-components-prpl-suggested-task', 85 | constant( 'PROGRESS_PLANNER_URL' ) . '/assets/css/web-components/prpl-suggested-task.css', 86 | [], 87 | \progress_planner()->get_file_version( constant( 'PROGRESS_PLANNER_DIR' ) . '/assets/css/web-components/prpl-suggested-task.css' ) 88 | ); 89 | 90 | return [ 91 | 'progress-planner-web-components-prpl-suggested-task', 92 | ]; 93 | } 94 | } 95 | // phpcs:enable Generic.Commenting.Todo 96 | -------------------------------------------------------------------------------- /classes/admin/widgets/class-whats-new.php: -------------------------------------------------------------------------------- 1 | get_utils__cache()->get( self::CACHE_KEY ); 38 | 39 | // Migrate old feed to new format. 40 | if ( is_array( $feed_data ) && ! isset( $feed_data['expires'] ) && ! isset( $feed_data['feed'] ) ) { 41 | $feed_data = [ 42 | 'feed' => $feed_data, 43 | 'expires' => \get_option( '_transient_timeout_' . Cache::CACHE_PREFIX . self::CACHE_KEY, 0 ), 44 | ]; 45 | } 46 | 47 | // Transient not set. 48 | if ( false === $feed_data ) { 49 | $feed_data = [ 50 | 'feed' => [], 51 | 'expires' => 0, 52 | ]; 53 | } 54 | 55 | // Transient expired, fetch new feed. 56 | if ( $feed_data['expires'] < time() ) { 57 | // Get the feed using the REST API. 58 | $response = \wp_remote_get( \progress_planner()->get_remote_server_root_url() . '/wp-json/wp/v2/posts/?per_page=2' ); 59 | 60 | if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { 61 | // If we cant fetch the feed, we will try again later. 62 | $feed_data['expires'] = time() + 5 * MINUTE_IN_SECONDS; 63 | } else { 64 | $feed = json_decode( \wp_remote_retrieve_body( $response ), true ); 65 | 66 | foreach ( $feed as $key => $post ) { 67 | // Get the featured media. 68 | $featured_media_id = $post['featured_media']; 69 | if ( $featured_media_id ) { 70 | $response = \wp_remote_get( \progress_planner()->get_remote_server_root_url() . '/wp-json/wp/v2/media/' . $featured_media_id ); 71 | if ( ! \is_wp_error( $response ) ) { 72 | $media = json_decode( \wp_remote_retrieve_body( $response ), true ); 73 | 74 | $post['featured_media'] = $media; 75 | } 76 | } 77 | $feed[ $key ] = $post; 78 | } 79 | 80 | $feed_data['feed'] = $feed; 81 | $feed_data['expires'] = time() + 1 * DAY_IN_SECONDS; 82 | } 83 | 84 | // Transient uses 'expires' key to determine if it's expired. 85 | \progress_planner()->get_utils__cache()->set( self::CACHE_KEY, $feed_data, 0 ); 86 | } 87 | 88 | return $feed_data['feed']; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /classes/admin/widgets/class-widget.php: -------------------------------------------------------------------------------- 1 | id; 37 | } 38 | 39 | /** 40 | * Get the widget range. 41 | * 42 | * @return string 43 | */ 44 | public function get_range() { 45 | // phpcs:ignore WordPress.Security.NonceVerification 46 | return isset( $_GET['range'] ) 47 | // phpcs:ignore WordPress.Security.NonceVerification 48 | ? \sanitize_text_field( \wp_unslash( $_GET['range'] ) ) 49 | : '-6 months'; 50 | } 51 | 52 | /** 53 | * Get the widget frequency. 54 | * 55 | * @return string 56 | */ 57 | public function get_frequency() { 58 | // phpcs:ignore WordPress.Security.NonceVerification 59 | return isset( $_GET['frequency'] ) 60 | // phpcs:ignore WordPress.Security.NonceVerification 61 | ? \sanitize_text_field( \wp_unslash( $_GET['frequency'] ) ) 62 | : 'monthly'; 63 | } 64 | 65 | /** 66 | * Render the widget. 67 | * 68 | * @return void 69 | */ 70 | public function render() { 71 | $this->enqueue_styles(); 72 | $this->enqueue_scripts(); 73 | ?> 74 |
75 |
76 | the_view( "page-widgets/{$this->id}.php" ); ?> 77 |
78 |
79 | get_admin__enqueue()->enqueue_style( "progress-planner/page-widgets/{$this->id}" ); 89 | } 90 | 91 | /** 92 | * Enqueue scripts. 93 | * 94 | * @return void 95 | */ 96 | public function enqueue_scripts() { 97 | \progress_planner()->get_admin__enqueue()->enqueue_script( 'widgets/' . $this->id ); 98 | } 99 | 100 | /** 101 | * Get the stylesheet dependencies. 102 | * 103 | * @return array 104 | */ 105 | public function get_stylesheet_dependencies() { 106 | return []; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /classes/badges/class-badge-content.php: -------------------------------------------------------------------------------- 1 | Goal::class, 35 | 'id' => 'weekly_activity', 36 | 'title' => \esc_html__( 'Weekly activity', 'progress-planner' ), 37 | 'description' => \esc_html__( 'Streak: The number of weeks this goal has been accomplished consistently.', 'progress-planner' ), 38 | 'status' => 'active', 39 | 'priority' => 'low', 40 | 'evaluate' => function ( $goal_object ) { 41 | return count( 42 | \progress_planner()->get_activities__query()->query_activities( 43 | [ 44 | 'start_date' => $goal_object->get_details()['start_date'], 45 | 'end_date' => $goal_object->get_details()['end_date'], 46 | ] 47 | ) 48 | ); 49 | }, 50 | ], 51 | [ 52 | 'frequency' => 'weekly', 53 | 'start_date' => \progress_planner()->get_activation_date(), 54 | 'end_date' => new \DateTime(), // Today. 55 | 'allowed_break' => 1, // Allow break in the streak for 1 week. 56 | ] 57 | ); 58 | } 59 | 60 | /** 61 | * Get the saved progress. 62 | * 63 | * @return array 64 | */ 65 | protected function get_saved() { 66 | $value = parent::get_saved(); 67 | 68 | if ( isset( $value['progress'] ) && 100 === $value['progress'] ) { 69 | return $value; 70 | } 71 | 72 | if ( isset( $value['date'] ) ) { 73 | $last_date = new \DateTime( $value['date'] ); 74 | $diff = $last_date->diff( new \DateTime() ); 75 | if ( $diff->days <= 2 ) { 76 | return $value; 77 | } 78 | } 79 | 80 | return []; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /classes/badges/class-badge.php: -------------------------------------------------------------------------------- 1 | id; 43 | } 44 | 45 | /** 46 | * Get the badge name. 47 | * 48 | * @return string 49 | */ 50 | abstract public function get_name(); 51 | 52 | /** 53 | * Get the badge description. 54 | * 55 | * @return string 56 | */ 57 | abstract public function get_description(); 58 | 59 | /** 60 | * Progress callback. 61 | * 62 | * @return array 63 | */ 64 | abstract public function progress_callback(); 65 | 66 | /** 67 | * Get the saved progress. 68 | * 69 | * @return array 70 | */ 71 | protected function get_saved() { 72 | return \progress_planner()->get_settings()->get( [ 'badges', $this->id ], [] ); 73 | } 74 | 75 | /** 76 | * Get the badge progress. 77 | * 78 | * @return array 79 | */ 80 | public function get_progress() { 81 | return $this->progress_callback(); 82 | } 83 | 84 | /** 85 | * Save the progress. 86 | * 87 | * @param array $progress The progress to save. 88 | * 89 | * @return void 90 | */ 91 | protected function save_progress( $progress ) { 92 | $progress['date'] = ( new \DateTime() )->format( 'Y-m-d H:i:s' ); 93 | \progress_planner()->get_settings()->set( [ 'badges', $this->id ], $progress ); 94 | } 95 | 96 | /** 97 | * Clear the saved progress. 98 | * 99 | * @return void 100 | */ 101 | public function clear_progress() { 102 | \progress_planner()->get_settings()->set( [ 'badges', $this->id ], [] ); 103 | } 104 | 105 | /** 106 | * Get the background color for the badge. 107 | * 108 | * @return string 109 | */ 110 | public function get_background() { 111 | return $this->background; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /classes/badges/content/class-content-curator.php: -------------------------------------------------------------------------------- 1 | get_saved(); 50 | 51 | // If we have a saved value, return it. 52 | if ( isset( $saved_progress['progress'] ) && isset( $saved_progress['remaining'] ) ) { 53 | return $saved_progress; 54 | } 55 | 56 | // Get the total number of posts. 57 | $total_posts_count = 0; 58 | foreach ( \progress_planner()->get_activities__content_helpers()->get_post_types_names() as $post_type ) { 59 | $total_posts_count += \wp_count_posts( $post_type )->publish; 60 | } 61 | 62 | $remaining = 20 - min( 20, $total_posts_count ); 63 | 64 | // If there are 20 existing posts, save the badge as complete and return. 65 | if ( 0 === $remaining ) { 66 | $this->save_progress( 67 | [ 68 | 'progress' => 100, 69 | 'remaining' => 0, 70 | ] 71 | ); 72 | 73 | return [ 74 | 'progress' => 100, 75 | 'remaining' => 0, 76 | ]; 77 | } 78 | 79 | // Get the new posts count. 80 | $new_count = count( 81 | \progress_planner()->get_activities__query()->query_activities( 82 | [ 83 | 'category' => 'content', 84 | 'type' => 'publish', 85 | 'start_date' => \progress_planner()->get_activation_date(), 86 | ] 87 | ) 88 | ); 89 | 90 | $remaining_new = 10 - min( 10, $new_count ); 91 | 92 | $final_percent = max( 93 | min( 100, floor( $total_posts_count / 2 ) ), 94 | min( 100, floor( $new_count * 10 ) ) 95 | ); 96 | $final_remaining = min( $remaining, $remaining_new ); 97 | 98 | $this->save_progress( 99 | [ 100 | 'progress' => $final_percent, 101 | 'remaining' => $final_remaining, 102 | ] 103 | ); 104 | 105 | return [ 106 | 'progress' => $final_percent, 107 | 'remaining' => $final_remaining, 108 | ]; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /classes/badges/content/class-purposeful-publisher.php: -------------------------------------------------------------------------------- 1 | get_saved(); 50 | 51 | // If we have a saved value, return it. 52 | if ( isset( $saved_progress['progress'] ) && isset( $saved_progress['remaining'] ) ) { 53 | return $saved_progress; 54 | } 55 | 56 | // Get the number of new posts published. 57 | $new_count = count( 58 | \progress_planner()->get_activities__query()->query_activities( 59 | [ 60 | 'category' => 'content', 61 | 'type' => 'publish', 62 | 'start_date' => \progress_planner()->get_activation_date(), 63 | ], 64 | ) 65 | ); 66 | 67 | $percent = min( 100, floor( 100 * $new_count / 50 ) ); 68 | $remaining = 50 - min( 50, $new_count ); 69 | 70 | $this->save_progress( 71 | [ 72 | 'progress' => $percent, 73 | 'remaining' => $remaining, 74 | ] 75 | ); 76 | 77 | return [ 78 | 'progress' => $percent, 79 | 'remaining' => $remaining, 80 | ]; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /classes/badges/content/class-revision-ranger.php: -------------------------------------------------------------------------------- 1 | get_saved(); 50 | 51 | // If we have a saved value, return it. 52 | if ( isset( $saved_progress['progress'] ) && isset( $saved_progress['remaining'] ) ) { 53 | return $saved_progress; 54 | } 55 | 56 | // Get the number of new posts published. 57 | $new_count = count( 58 | \progress_planner()->get_activities__query()->query_activities( 59 | [ 60 | 'category' => 'content', 61 | 'type' => 'publish', 62 | 'start_date' => \progress_planner()->get_activation_date(), 63 | ], 64 | ) 65 | ); 66 | 67 | $percent = min( 100, floor( 100 * $new_count / 30 ) ); 68 | $remaining = 30 - min( 30, $new_count ); 69 | 70 | $this->save_progress( 71 | [ 72 | 'progress' => $percent, 73 | 'remaining' => $remaining, 74 | ] 75 | ); 76 | 77 | return [ 78 | 'progress' => $percent, 79 | 'remaining' => $remaining, 80 | ]; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /classes/badges/maintenance/class-maintenance-maniac.php: -------------------------------------------------------------------------------- 1 | get_saved(); 50 | 51 | // If we have a saved value, return it. 52 | if ( isset( $saved_progress['progress'] ) && isset( $saved_progress['remaining'] ) ) { 53 | return $saved_progress; 54 | } 55 | 56 | $max_streak = $this->get_goal()->get_streak()['max_streak']; 57 | $percent = min( 100, floor( 100 * $max_streak / 26 ) ); 58 | $remaining = 26 - min( 26, $max_streak ); 59 | 60 | $this->save_progress( 61 | [ 62 | 'progress' => $percent, 63 | 'remaining' => $remaining, 64 | ] 65 | ); 66 | 67 | return [ 68 | 'progress' => $percent, 69 | 'remaining' => $remaining, 70 | ]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /classes/badges/maintenance/class-progress-padawan.php: -------------------------------------------------------------------------------- 1 | get_saved(); 50 | 51 | // If we have a saved value, return it. 52 | if ( isset( $saved_progress['progress'] ) && isset( $saved_progress['remaining'] ) ) { 53 | return $saved_progress; 54 | } 55 | 56 | $max_streak = $this->get_goal()->get_streak()['max_streak']; 57 | $percent = min( 100, floor( 100 * $max_streak / 6 ) ); 58 | $remaining = 6 - min( 6, $max_streak ); 59 | 60 | $this->save_progress( 61 | [ 62 | 'progress' => $percent, 63 | 'remaining' => $remaining, 64 | ] 65 | ); 66 | 67 | return [ 68 | 'progress' => $percent, 69 | 'remaining' => $remaining, 70 | ]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /classes/badges/maintenance/class-super-site-specialist.php: -------------------------------------------------------------------------------- 1 | get_saved(); 50 | 51 | // If we have a saved value, return it. 52 | if ( isset( $saved_progress['progress'] ) && isset( $saved_progress['remaining'] ) ) { 53 | return $saved_progress; 54 | } 55 | 56 | $max_streak = $this->get_goal()->get_streak()['max_streak']; 57 | $percent = min( 100, floor( 100 * $max_streak / 52 ) ); 58 | $remaining = 52 - min( 52, $max_streak ); 59 | 60 | $this->save_progress( 61 | [ 62 | 'progress' => $percent, 63 | 'remaining' => $remaining, 64 | ] 65 | ); 66 | 67 | return [ 68 | 'progress' => $percent, 69 | 'remaining' => $remaining, 70 | ]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /classes/class-lessons.php: -------------------------------------------------------------------------------- 1 | get_remote_api_items(); 31 | } 32 | 33 | /** 34 | * Get items from the remote API. 35 | * 36 | * @return array 37 | */ 38 | public function get_remote_api_items() { 39 | $url = \progress_planner()->get_remote_server_root_url() . '/wp-json/progress-planner-saas/v1/lessons'; 40 | $url = ( \progress_planner()->is_pro_site() ) 41 | ? \add_query_arg( 42 | [ 43 | 'site' => \get_site_url(), 44 | 'license_key' => \get_option( 'progress_planner_pro_license_key' ), 45 | ], 46 | $url 47 | ) 48 | : \add_query_arg( [ 'site' => \get_site_url() ], $url ); 49 | 50 | $cache_key = md5( $url ); 51 | 52 | $cached = \progress_planner()->get_utils__cache()->get( $cache_key ); 53 | if ( is_array( $cached ) ) { 54 | return $cached; 55 | } 56 | 57 | $response = \wp_remote_get( $url ); 58 | 59 | if ( \is_wp_error( $response ) ) { 60 | \progress_planner()->get_utils__cache()->set( $cache_key, [], 5 * MINUTE_IN_SECONDS ); 61 | return []; 62 | } 63 | 64 | if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { 65 | \progress_planner()->get_utils__cache()->set( $cache_key, [], 5 * MINUTE_IN_SECONDS ); 66 | return []; 67 | } 68 | 69 | $json = json_decode( \wp_remote_retrieve_body( $response ), true ); 70 | if ( ! is_array( $json ) ) { 71 | \progress_planner()->get_utils__cache()->set( $cache_key, [], 5 * MINUTE_IN_SECONDS ); 72 | return []; 73 | } 74 | 75 | \progress_planner()->get_utils__cache()->set( $cache_key, $json, WEEK_IN_SECONDS ); 76 | 77 | return $json; 78 | } 79 | 80 | /** 81 | * Get the lessons pagetypes. 82 | * 83 | * @return array 84 | */ 85 | public function get_lesson_pagetypes() { 86 | $lessons = $this->get_items(); 87 | $pagetypes = []; 88 | $show_on_front = \get_option( 'show_on_front' ); 89 | 90 | foreach ( $lessons as $lesson ) { 91 | // Remove the "homepage" lesson if the site doesn't show a static page as the frontpage. 92 | if ( 'posts' === $show_on_front && 'homepage' === $lesson['settings']['id'] ) { 93 | continue; 94 | } 95 | $pagetypes[] = [ 96 | 'label' => $lesson['name'], 97 | 'value' => $lesson['settings']['id'], 98 | ]; 99 | } 100 | return $pagetypes; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /classes/class-page-todos.php: -------------------------------------------------------------------------------- 1 | true ], 'objects' ); 30 | foreach ( $post_types as $post_type ) { 31 | if ( ! \post_type_supports( $post_type->name, 'custom-fields' ) ) { 32 | \add_post_type_support( $post_type->name, 'custom-fields' ); 33 | } 34 | \register_post_meta( 35 | $post_type->name, 36 | 'progress_planner_page_todos', 37 | [ 38 | 'type' => 'string', 39 | 'single' => true, 40 | 'show_in_rest' => true, 41 | 'sanitize_callback' => [ $this, 'sanitize_post_meta_progress_planner_page_todos' ], 42 | ] 43 | ); 44 | } 45 | } 46 | 47 | /** 48 | * Sanitize the `progress_planner_page_todos` meta. 49 | * 50 | * @param string $value The meta value. 51 | * 52 | * @return string 53 | */ 54 | public function sanitize_post_meta_progress_planner_page_todos( $value ) { 55 | $values = explode( ',', $value ); 56 | // Remove any empty values. 57 | $values = array_filter( $values ); 58 | // Remove any duplicates. 59 | $values = array_unique( $values ); 60 | // Trim all values. 61 | $values = array_map( 'trim', $values ); 62 | 63 | // Return the sanitized value. 64 | return \sanitize_text_field( implode( ',', $values ) ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /classes/rest/class-tasks.php: -------------------------------------------------------------------------------- 1 | /wp-json/progress-planner/v1/tasks 7 | * 8 | * @package Progress_Planner 9 | */ 10 | 11 | namespace Progress_Planner\Rest; 12 | 13 | /** 14 | * Rest_API_Tasks class. 15 | */ 16 | class Tasks { 17 | /** 18 | * Constructor. 19 | */ 20 | public function __construct() { 21 | \add_action( 'rest_api_init', [ $this, 'register_rest_endpoint' ] ); 22 | } 23 | 24 | /** 25 | * Register the REST-API endpoint. 26 | * 27 | * @return void 28 | */ 29 | public function register_rest_endpoint() { 30 | \register_rest_route( 31 | 'progress-planner/v1', 32 | '/tasks', 33 | [ 34 | [ 35 | 'methods' => 'GET', 36 | 'callback' => [ $this, 'get_tasks' ], 37 | 'permission_callback' => '__return_true', 38 | 'args' => [ 39 | 'token' => [ 40 | 'required' => true, 41 | 'validate_callback' => [ $this, 'validate_token' ], 42 | ], 43 | ], 44 | ], 45 | ] 46 | ); 47 | } 48 | 49 | /** 50 | * Permission callback. 51 | * 52 | * @param string $token The token. 53 | * 54 | * @return bool 55 | */ 56 | public function validate_token( $token ) { 57 | $token = str_replace( 'token/', '', $token ); 58 | 59 | if ( $token === \get_option( 'progress_planner_test_token', '' ) ) { 60 | return true; 61 | } 62 | 63 | if ( \progress_planner()->is_pro_site() && $token === \get_option( 'progress_planner_pro_license_key' ) ) { 64 | return true; 65 | } 66 | $license_key = \get_option( 'progress_planner_license_key', false ); 67 | if ( ! $license_key || 'no-license' === $license_key ) { 68 | return false; 69 | } 70 | 71 | return $token === $license_key; 72 | } 73 | 74 | /** 75 | * Get task recommendations. 76 | * 77 | * @return \WP_REST_Response The REST response object containing the recommendations. 78 | */ 79 | public function get_tasks() { 80 | 81 | $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); 82 | 83 | return new \WP_REST_Response( $tasks ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /classes/suggested-tasks/class-task.php: -------------------------------------------------------------------------------- 1 | data = $data; 28 | } 29 | 30 | /** 31 | * Get the task data. 32 | * 33 | * @return array 34 | */ 35 | public function get_data() { 36 | return $this->data; 37 | } 38 | 39 | /** 40 | * Set the task data. 41 | * 42 | * @param array $data The task data. 43 | * 44 | * @return void 45 | */ 46 | public function set_data( array $data ) { 47 | $this->data = $data; 48 | } 49 | 50 | /** 51 | * Get the provider ID. 52 | * 53 | * @return string 54 | */ 55 | public function get_provider_id() { 56 | return $this->data['provider_id'] ?? ''; 57 | } 58 | 59 | /** 60 | * Get the provider ID. 61 | * 62 | * @return string 63 | */ 64 | public function get_task_id() { 65 | return $this->data['task_id'] ?? ''; 66 | } 67 | 68 | /** 69 | * Get the provider ID. 70 | * 71 | * @return array 72 | */ 73 | public function get_task_details() { 74 | $task_provider_id = $this->get_provider_id(); 75 | $task_id = $this->get_task_id(); 76 | 77 | $task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $task_provider_id ); 78 | if ( ! $task_provider ) { 79 | return []; 80 | } 81 | 82 | return $task_provider->get_task_details( $task_id ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /classes/suggested-tasks/class-tasks-interface.php: -------------------------------------------------------------------------------- 1 | update_cache(); 43 | } 44 | } 45 | 46 | /** 47 | * Calculate the archive format count. 48 | * 49 | * @return int 50 | */ 51 | protected function calculate_data() { 52 | // Check if there are any posts that use a post format using get_posts and get only the IDs. 53 | // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_tax_query 54 | $args = [ 55 | 'posts_per_page' => 10, 56 | 'fields' => 'ids', 57 | 'tax_query' => [ 58 | [ 59 | 'taxonomy' => 'post_format', 60 | 'operator' => 'EXISTS', 61 | ], 62 | ], 63 | ]; 64 | // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_tax_query 65 | 66 | return count( get_posts( $args ) ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /classes/suggested-tasks/data-collector/class-base-data-collector.php: -------------------------------------------------------------------------------- 1 | get_cached_data( $this->get_data_key() ); 60 | if ( null !== $data ) { 61 | return $data; 62 | } 63 | 64 | // If no cache, calculate fresh value. 65 | $data = $this->calculate_data(); 66 | 67 | // Store in cache. 68 | $this->set_cached_data( static::DATA_KEY, $data ); 69 | 70 | return $data; 71 | } 72 | 73 | /** 74 | * Update the cache. 75 | * 76 | * @return void 77 | */ 78 | public function update_cache() { 79 | $this->set_cached_data( $this->get_data_key(), $this->calculate_data() ); 80 | } 81 | 82 | /** 83 | * Get the cached data. 84 | * 85 | * @param string $key The key. 86 | * 87 | * @return mixed 88 | */ 89 | protected function get_cached_data( string $key ) { 90 | $settings = \progress_planner()->get_settings(); 91 | $data = $settings->get( static::CACHE_KEY, [] ); 92 | return $data[ $key ] ?? null; 93 | } 94 | 95 | /** 96 | * Set the cached data. 97 | * 98 | * @param string $key The key. 99 | * @param mixed $value The value. 100 | * 101 | * @return void 102 | */ 103 | protected function set_cached_data( string $key, $value ) { 104 | $settings = \progress_planner()->get_settings(); 105 | $data = $settings->get( static::CACHE_KEY, [] ); 106 | $data[ $key ] = $value; 107 | $settings->set( static::CACHE_KEY, $data ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /classes/suggested-tasks/data-collector/class-hello-world.php: -------------------------------------------------------------------------------- 1 | update_cache(); 51 | } 52 | } 53 | 54 | /** 55 | * Query the hello world post. 56 | * 57 | * @return int 58 | */ 59 | protected function calculate_data() { 60 | $sample_post = get_page_by_path( __( 'hello-world' ), OBJECT, 'post' ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain 61 | if ( null === $sample_post ) { 62 | $query = new \WP_Query( 63 | [ 64 | 'post_type' => 'post', 65 | 'title' => __( 'Hello world!' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain 66 | 'post_status' => 'publish', 67 | 'posts_per_page' => 1, 68 | ] 69 | ); 70 | 71 | $sample_post = ! empty( $query->post ) ? $query->post : 0; 72 | } 73 | 74 | return ( is_object( $sample_post ) && is_a( $sample_post, \WP_Post::class ) ) ? $sample_post->ID : 0; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /classes/suggested-tasks/data-collector/class-inactive-plugins.php: -------------------------------------------------------------------------------- 1 | update_cache(); 41 | } 42 | 43 | /** 44 | * Calculate the inactive plugins count. 45 | * 46 | * @return int 47 | */ 48 | protected function calculate_data() { 49 | if ( ! function_exists( 'get_plugins' ) ) { 50 | require_once ABSPATH . 'wp-admin/includes/plugin.php'; // @phpstan-ignore requireOnce.fileNotFound 51 | } 52 | 53 | // Clear the plugins cache, so get_plugins() returns the latest plugins. 54 | wp_cache_delete( 'plugins', 'plugins' ); 55 | 56 | $plugins = get_plugins(); 57 | $plugins_active = 0; 58 | $plugins_total = 0; 59 | 60 | // Loop over the available plugins and check their versions and active state. 61 | foreach ( array_keys( $plugins ) as $plugin_path ) { 62 | ++$plugins_total; 63 | 64 | if ( is_plugin_active( $plugin_path ) ) { 65 | ++$plugins_active; 66 | } 67 | } 68 | 69 | $unused_plugins = 0; 70 | if ( ! is_multisite() && $plugins_total > $plugins_active ) { 71 | $unused_plugins = $plugins_total - $plugins_active; 72 | } 73 | 74 | return $unused_plugins; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /classes/suggested-tasks/data-collector/class-last-published-post.php: -------------------------------------------------------------------------------- 1 | include_post_types = \progress_planner()->get_settings()->get_post_types_names(); 48 | } 49 | 50 | /** 51 | * Update the cache when post status changes. 52 | * 53 | * @param string $new_status The new status. 54 | * @param string $old_status The old status. 55 | * @param \WP_Post $post The post. 56 | * 57 | * @return void 58 | */ 59 | public function update_last_published_post_cache( $new_status, $old_status, $post ) { 60 | if ( true === \in_array( get_post_type( $post ), $this->include_post_types, true ) && ( $new_status === 'publish' || $old_status === 'publish' ) ) { 61 | $this->update_cache(); 62 | } 63 | } 64 | 65 | /** 66 | * Query the hello world post. 67 | * 68 | * @return array 69 | */ 70 | protected function calculate_data() { 71 | 72 | // Default data. 73 | $data = [ 74 | 'post_id' => 0, 75 | 'long' => false, 76 | 'post_date' => '', 77 | ]; 78 | 79 | // Get the post that was created last. 80 | $last_created_posts = \get_posts( 81 | [ 82 | 'posts_per_page' => 1, 83 | 'post_status' => 'publish', 84 | 'orderby' => 'date', 85 | 'order' => 'DESC', 86 | 'post_type' => $this->include_post_types, 87 | ] 88 | ); 89 | 90 | if ( ! empty( $last_created_posts ) ) { 91 | $data = [ 92 | 'post_id' => $last_created_posts[0]->ID, 93 | 'post_date' => $last_created_posts[0]->post_date, 94 | ]; 95 | } 96 | 97 | return $data; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /classes/suggested-tasks/data-collector/class-post-author.php: -------------------------------------------------------------------------------- 1 | post_author !== $post_before->post_author ) { 45 | $this->update_cache(); 46 | } 47 | } 48 | 49 | /** 50 | * Update the cache when post status changes. 51 | * 52 | * @param string $new_status The new status. 53 | * @param string $old_status The old status. 54 | * @param \WP_Post $post The post. 55 | * 56 | * @return void 57 | */ 58 | public function update_post_author_cache( $new_status, $old_status, $post ) { 59 | if ( $new_status === 'publish' || $old_status === 'publish' ) { 60 | $this->update_cache(); 61 | } 62 | } 63 | 64 | /** 65 | * Calculate the unique author count. 66 | * 67 | * @return int 68 | */ 69 | protected function calculate_data() { 70 | global $wpdb; 71 | 72 | $author_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 73 | " 74 | SELECT DISTINCT post_author 75 | FROM {$wpdb->posts} 76 | WHERE post_status = 'publish' 77 | AND post_type = 'post' 78 | " 79 | ); 80 | 81 | return count( $author_ids ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /classes/suggested-tasks/data-collector/class-post-tag-count.php: -------------------------------------------------------------------------------- 1 | get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 43 | " 44 | SELECT COUNT( * ) as tag_count 45 | FROM {$wpdb->terms} AS t 46 | INNER JOIN {$wpdb->term_taxonomy} AS tt ON t.term_id = tt.term_id 47 | WHERE tt.taxonomy = 'post_tag' 48 | ", 49 | ); 50 | 51 | return ! empty( $result ) ? $result : 0; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /classes/suggested-tasks/data-collector/class-published-post-count.php: -------------------------------------------------------------------------------- 1 | update_cache(); 49 | } 50 | 51 | /** 52 | * Query the published post count. 53 | * 54 | * @return int 55 | */ 56 | protected function calculate_data() { 57 | $result = \wp_count_posts( 'post' )->publish; 58 | 59 | return ! empty( $result ) ? $result : 0; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /classes/suggested-tasks/data-collector/class-sample-page.php: -------------------------------------------------------------------------------- 1 | update_cache(); 51 | } 52 | } 53 | 54 | /** 55 | * Query the sample page. 56 | * 57 | * @return \WP_Post|int 58 | */ 59 | protected function calculate_data() { 60 | $sample_page = get_page_by_path( __( 'sample-page' ) ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain 61 | if ( null === $sample_page ) { 62 | $query = new \WP_Query( 63 | [ 64 | 'post_type' => 'page', 65 | 'title' => __( 'Sample Page' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain 66 | 'post_status' => 'publish', 67 | 'posts_per_page' => 1, 68 | ] 69 | ); 70 | 71 | $sample_page = ! empty( $query->post ) ? $query->post : 0; 72 | } 73 | 74 | return ( is_object( $sample_page ) && is_a( $sample_page, \WP_Post::class ) ) ? $sample_page->ID : 0; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /classes/suggested-tasks/data-collector/class-uncategorized-category.php: -------------------------------------------------------------------------------- 1 | update_cache(); 41 | } 42 | 43 | /** 44 | * Query the uncategorized category. 45 | * 46 | * @return int 47 | */ 48 | protected function calculate_data() { 49 | global $wpdb; 50 | $default_category_name = __( 'Uncategorized' ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain 51 | $default_category_slug = sanitize_title( _x( 'Uncategorized', 'Default category slug' ) ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain 52 | 53 | // Get the Uncategorized category by name or slug. 54 | $uncategorized_category = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 55 | $wpdb->prepare( 56 | "SELECT $wpdb->terms.term_id FROM {$wpdb->terms} 57 | LEFT JOIN {$wpdb->term_taxonomy} ON {$wpdb->terms}.term_id = {$wpdb->term_taxonomy}.term_id 58 | WHERE ({$wpdb->terms}.name = %s OR {$wpdb->terms}.slug = %s) 59 | AND {$wpdb->term_taxonomy}.taxonomy = 'category'", 60 | $default_category_name, 61 | $default_category_slug 62 | ) 63 | ); 64 | 65 | return $uncategorized_category ? $uncategorized_category : 0; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/class-blog-description.php: -------------------------------------------------------------------------------- 1 | url = \admin_url( 'options-general.php?pp-focus-el=' . $this->get_task_id() ); 34 | $this->link_setting = [ 35 | 'hook' => 'options-general.php', 36 | 'iconEl' => 'th:has(+td #tagline-description)', 37 | ]; 38 | } 39 | 40 | /** 41 | * Get the title. 42 | * 43 | * @return string 44 | */ 45 | public function get_title() { 46 | return \esc_html__( 'Set tagline', 'progress-planner' ); 47 | } 48 | 49 | /** 50 | * Get the description. 51 | * 52 | * @return string 53 | */ 54 | public function get_description() { 55 | return sprintf( 56 | /* translators: %s:tagline link */ 57 | \esc_html__( 'Set the %s to make your website look more professional.', 'progress-planner' ), 58 | '' . \esc_html__( 'tagline', 'progress-planner' ) . '' 59 | ); 60 | } 61 | 62 | /** 63 | * Check if the task should be added. 64 | * 65 | * @return bool 66 | */ 67 | public function should_add_task() { 68 | return '' === \get_bloginfo( 'description' ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/class-debug-display.php: -------------------------------------------------------------------------------- 1 | We recommend link. 46 | \esc_html__( '%1$s is enabled. This means that errors are shown to users. %2$s disabling it.', 'progress-planner' ), 47 | 'WP_DEBUG_DISPLAY', 48 | '' . \esc_html__( 'We recommend', 'progress-planner' ) . '' 49 | ); 50 | } 51 | 52 | /** 53 | * Check if the task should be added. 54 | * 55 | * @return bool 56 | */ 57 | public function should_add_task() { 58 | return defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/class-disable-comments.php: -------------------------------------------------------------------------------- 1 | url = \admin_url( 'options-discussion.php' ); 34 | $this->link_setting = [ 35 | 'hook' => 'options-discussion.php', 36 | 'iconEl' => 'label[for="default_comment_status"]', 37 | ]; 38 | } 39 | 40 | /** 41 | * Get the title. 42 | * 43 | * @return string 44 | */ 45 | public function get_title() { 46 | return \esc_html__( 'Disable comments', 'progress-planner' ); 47 | } 48 | 49 | /** 50 | * Get the title. 51 | * 52 | * @return string 53 | */ 54 | public function get_description() { 55 | return sprintf( 56 | \esc_html( 57 | // translators: %d is the number of approved comments, %s is the disabling them link. 58 | \_n( 59 | 'There is %1$d comment. If you don\'t need comments on your site, consider %2$s.', 60 | 'There are %1$d comments. If you don\'t need comments on your site, consider %2$s.', 61 | (int) \wp_count_comments()->approved, 62 | 'progress-planner' 63 | ) 64 | ), 65 | (int) \wp_count_comments()->approved, 66 | '' . \esc_html__( 'disabling them', 'progress-planner' ) . '', 67 | ); 68 | } 69 | 70 | /** 71 | * Check if the task condition is satisfied. 72 | * (bool) true means that the task condition is satisfied, meaning that we don't need to add the task or task was completed. 73 | * 74 | * @return bool 75 | */ 76 | public function should_add_task() { 77 | return 10 > \wp_count_comments()->approved && 'open' === \get_default_comment_status(); 78 | } 79 | 80 | /** 81 | * Check if the task is completed. 82 | * 83 | * @param string $task_id The task ID. 84 | * 85 | * @return bool 86 | */ 87 | public function is_task_completed( $task_id = '' ) { 88 | return 'open' !== \get_default_comment_status(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/class-hello-world.php: -------------------------------------------------------------------------------- 1 | data_collector = new Hello_World_Data_Collector(); 50 | 51 | $hello_world_post_id = $this->data_collector->collect(); 52 | 53 | if ( 0 !== $hello_world_post_id ) { 54 | $this->url = (string) \get_edit_post_link( $hello_world_post_id ); 55 | } 56 | } 57 | 58 | /** 59 | * Get the title. 60 | * 61 | * @return string 62 | */ 63 | public function get_title() { 64 | return \esc_html__( 'Delete the "Hello World!" post.', 'progress-planner' ); 65 | } 66 | 67 | /** 68 | * Get the description. 69 | * 70 | * @return string 71 | */ 72 | public function get_description() { 73 | return sprintf( 74 | /* translators: %s:Hello World! link */ 75 | \esc_html__( 'On install, WordPress creates a %s post. This post is not needed and should be deleted.', 'progress-planner' ), 76 | '' . \esc_html__( '"Hello World!"', 'progress-planner' ) . '' 77 | ); 78 | } 79 | 80 | /** 81 | * Check if the task condition is satisfied. 82 | * 83 | * @return bool 84 | */ 85 | public function should_add_task() { 86 | return 0 !== $this->data_collector->collect(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/class-interactive.php: -------------------------------------------------------------------------------- 1 | 38 |
39 | the_popover_content(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> 40 |
41 | url = \admin_url( 'options-permalink.php' ); 34 | 35 | $icon_el = 'label[for="permalink-input-month-name"], label[for="permalink-input-post-name"]'; 36 | 37 | // If the task is completed, we want to add icon element only to the selected option (not both). 38 | if ( $this->is_task_completed() ) { 39 | $permalink_structure = \get_option( 'permalink_structure' ); 40 | 41 | if ( '/%year%/%monthnum%/%postname%/' === $permalink_structure || '/index.php/%year%/%monthnum%/%postname%/' === $permalink_structure ) { 42 | $icon_el = 'label[for="permalink-input-month-name"]'; 43 | } 44 | 45 | if ( '/%postname%/' === $permalink_structure || '/index.php/%postname%/' === $permalink_structure ) { 46 | $icon_el = 'label[for="permalink-input-post-name"]'; 47 | } 48 | } 49 | 50 | $this->link_setting = [ 51 | 'hook' => 'options-permalink.php', 52 | 'iconEl' => $icon_el, 53 | ]; 54 | } 55 | 56 | /** 57 | * Get the title. 58 | * 59 | * @return string 60 | */ 61 | public function get_title() { 62 | return \esc_html__( 'Set permalink structure', 'progress-planner' ); 63 | } 64 | 65 | /** 66 | * Get the description. 67 | * 68 | * @return string 69 | */ 70 | public function get_description() { 71 | return sprintf( 72 | /* translators: %1$s We recommend link */ 73 | \esc_html__( 'On install, WordPress sets the permalink structure to a format that is not SEO-friendly. %1$s changing it.', 'progress-planner' ), 74 | '' . \esc_html__( 'We recommend', 'progress-planner' ) . '', 75 | ); 76 | } 77 | 78 | /** 79 | * Check if the task condition is satisfied. 80 | * (bool) true means that the task condition is satisfied, meaning that we don't need to add the task or task was completed. 81 | * 82 | * @return bool 83 | */ 84 | public function should_add_task() { 85 | $permalink_structure = \get_option( 'permalink_structure' ); 86 | return '/%year%/%monthnum%/%day%/%postname%/' === $permalink_structure || '/index.php/%year%/%monthnum%/%day%/%postname%/' === $permalink_structure; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/class-php-version.php: -------------------------------------------------------------------------------- 1 | We recommend link */ 46 | \esc_html__( 'Your site is running on PHP version %1$s. %2$s updating to PHP version 8.0 or higher.', 'progress-planner' ), 47 | phpversion(), 48 | '' . \esc_html__( 'We recommend', 'progress-planner' ) . '', 49 | ); 50 | } 51 | 52 | /** 53 | * Check if the task should be added. 54 | * 55 | * @return bool 56 | */ 57 | public function should_add_task() { 58 | return version_compare( phpversion(), '8.0', '<' ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/class-remove-inactive-plugins.php: -------------------------------------------------------------------------------- 1 | data_collector = new Inactive_Plugins_Data_Collector(); 43 | $this->url = \admin_url( 'plugins.php' ); 44 | } 45 | 46 | /** 47 | * Get the title. 48 | * 49 | * @return string 50 | */ 51 | public function get_title() { 52 | return \esc_html__( 'Remove inactive plugins', 'progress-planner' ); 53 | } 54 | 55 | /** 56 | * Get the description. 57 | * 58 | * @return string 59 | */ 60 | public function get_description() { 61 | return sprintf( 62 | /* translators: %1$s removing any plugins link */ 63 | \esc_html__( 'You have inactive plugins. Consider %1$s that are not activated to free up resources, and improve security.', 'progress-planner' ), 64 | '' . \esc_html__( 'removing any plugins', 'progress-planner' ) . '', 65 | ); 66 | } 67 | 68 | /** 69 | * Check if the task should be added. 70 | * 71 | * @return bool 72 | */ 73 | public function should_add_task() { 74 | return $this->data_collector->collect() > 0; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/class-rename-uncategorized-category.php: -------------------------------------------------------------------------------- 1 | data_collector = new Uncategorized_Category_Data_Collector(); 50 | $this->url = \admin_url( 'edit-tags.php?taxonomy=category&post_type=post' ); 51 | } 52 | 53 | /** 54 | * Get the title. 55 | * 56 | * @return string 57 | */ 58 | public function get_title() { 59 | return \esc_html__( 'Rename Uncategorized category', 'progress-planner' ); 60 | } 61 | 62 | /** 63 | * Get the description. 64 | * 65 | * @return string 66 | */ 67 | public function get_description() { 68 | return sprintf( 69 | /* translators: %1$s We recommend link */ 70 | \esc_html__( 'The Uncategorized category is used for posts that don\'t have a category. %1$s renaming it to something that fits your site better.', 'progress-planner' ), 71 | '' . \esc_html__( 'We recommend', 'progress-planner' ) . '', 72 | ); 73 | } 74 | 75 | /** 76 | * Check if the task should be added. 77 | * 78 | * @return bool 79 | */ 80 | public function should_add_task() { 81 | return 0 !== $this->data_collector->collect(); 82 | } 83 | 84 | /** 85 | * Update the Uncategorized category cache. 86 | * 87 | * @return void 88 | */ 89 | public function update_uncategorized_category_cache() { 90 | $this->data_collector->update_uncategorized_category_cache(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/class-sample-page.php: -------------------------------------------------------------------------------- 1 | data_collector = new Sample_Page_Data_Collector(); 50 | 51 | $sample_page_id = $this->data_collector->collect(); 52 | 53 | if ( 0 !== $sample_page_id ) { 54 | $this->url = (string) \get_edit_post_link( $sample_page_id ); 55 | } 56 | } 57 | 58 | /** 59 | * Get the title. 60 | * 61 | * @return string 62 | */ 63 | public function get_title() { 64 | return \esc_html__( 'Delete "Sample Page"', 'progress-planner' ); 65 | } 66 | 67 | /** 68 | * Get the description. 69 | * 70 | * @return string 71 | */ 72 | public function get_description() { 73 | return sprintf( 74 | /* translators: %s:Sample Page link */ 75 | \esc_html__( 'On install, WordPress creates a %s page. This page is not needed and should be deleted.', 'progress-planner' ), 76 | '' . \esc_html__( '"Sample Page"', 'progress-planner' ) . '' 77 | ); 78 | } 79 | 80 | /** 81 | * Check if the task should be added. 82 | * 83 | * @return bool 84 | */ 85 | public function should_add_task() { 86 | return 0 !== $this->data_collector->collect(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/class-search-engine-visibility.php: -------------------------------------------------------------------------------- 1 | url = \admin_url( 'options-reading.php' ); 34 | $this->link_setting = [ 35 | 'hook' => 'options-reading.php', 36 | 'iconEl' => 'label[for="blog_public"]', 37 | ]; 38 | } 39 | 40 | /** 41 | * Get the title. 42 | * 43 | * @return string 44 | */ 45 | public function get_title() { 46 | return \esc_html__( 'Allow your site to be indexed by search engines', 'progress-planner' ); 47 | } 48 | 49 | /** 50 | * Get the description. 51 | * 52 | * @return string 53 | */ 54 | public function get_description() { 55 | return sprintf( 56 | /* translators: %1$s allowing search engines link */ 57 | \esc_html__( 'Your site is not currently visible to search engines. Consider %1$s to index your site.', 'progress-planner' ), 58 | '' . \esc_html__( 'allowing search engines', 'progress-planner' ) . '', 59 | ); 60 | } 61 | 62 | /** 63 | * Check if the task should be added. 64 | * 65 | * @return bool 66 | */ 67 | public function should_add_task() { 68 | return 0 === (int) \get_option( 'blog_public' ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/class-settings-saved.php: -------------------------------------------------------------------------------- 1 | url = \admin_url( 'admin.php?page=progress-planner-settings' ); 41 | } 42 | 43 | /** 44 | * Get the title. 45 | * 46 | * @return string 47 | */ 48 | public function get_title() { 49 | return \esc_html__( 'Fill settings page', 'progress-planner' ); 50 | } 51 | 52 | /** 53 | * Get the description. 54 | * 55 | * @return string 56 | */ 57 | public function get_description() { 58 | return \esc_html__( 'Head over to the settings page and fill in the required information.', 'progress-planner' ); 59 | } 60 | 61 | /** 62 | * Check if the task should be added. 63 | * 64 | * @return bool 65 | */ 66 | public function should_add_task() { 67 | return false === \get_option( 'progress_planner_pro_license_key', false ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/class-site-icon.php: -------------------------------------------------------------------------------- 1 | url = \admin_url( 'options-general.php?pp-focus-el=' . $this->get_task_id() ); 34 | $this->link_setting = [ 35 | 'hook' => 'options-general.php', 36 | 'iconEl' => '.site-icon-section th', 37 | ]; 38 | } 39 | 40 | /** 41 | * Get the title. 42 | * 43 | * @return string 44 | */ 45 | public function get_title() { 46 | return \esc_html__( 'Set site icon', 'progress-planner' ); 47 | } 48 | 49 | /** 50 | * Get the description. 51 | * 52 | * @return string 53 | */ 54 | public function get_description() { 55 | return sprintf( 56 | /* translators: %s:site icon link */ 57 | \esc_html__( 'Set the %s to make your website look more professional.', 'progress-planner' ), 58 | '' . \esc_html__( 'site icon', 'progress-planner' ) . '' 59 | ); 60 | } 61 | 62 | /** 63 | * Check if the task should be added. 64 | * 65 | * @return bool 66 | */ 67 | public function should_add_task() { 68 | $site_icon = \get_option( 'site_icon' ); 69 | return '' === $site_icon || '0' === $site_icon; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/class-user.php: -------------------------------------------------------------------------------- 1 | get_settings()->get( 'tasks', [] ); 54 | foreach ( $saved_tasks as $task_data ) { 55 | if ( isset( $task_data['provider_id'] ) && self::PROVIDER_ID === $task_data['provider_id'] ) { 56 | $tasks[] = [ 57 | 'task_id' => $task_data['task_id'], 58 | 'provider_id' => $this->get_provider_id(), 59 | 'category' => $this->get_provider_category(), 60 | 'points' => 0, 61 | ]; 62 | } 63 | } 64 | 65 | return $tasks; 66 | } 67 | 68 | /** 69 | * Get the task details. 70 | * 71 | * @param string $task_id The task ID. 72 | * 73 | * @return array 74 | */ 75 | public function get_task_details( $task_id = '' ) { 76 | // Get the user tasks from the database. 77 | $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); 78 | 79 | foreach ( $tasks as $task ) { 80 | if ( $task['task_id'] !== $task_id ) { 81 | continue; 82 | } 83 | 84 | return wp_parse_args( 85 | $task, 86 | [ 87 | 'task_id' => '', 88 | 'title' => '', 89 | 'parent' => 0, 90 | 'provider_id' => 'user', 91 | 'category' => 'user', 92 | 'priority' => 'medium', 93 | 'points' => 0, 94 | 'url' => '', 95 | 'url_target' => '_self', 96 | 'description' => '', 97 | 'link_setting' => [], 98 | 'dismissable' => true, 99 | 'snoozable' => false, 100 | ] 101 | ); 102 | } 103 | 104 | return []; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php: -------------------------------------------------------------------------------- 1 | providers as $provider ) { 48 | 49 | // Add Ravi icon if the task is pending or is completed. 50 | if ( $provider->is_task_relevant() || \progress_planner()->get_suggested_tasks()->was_task_completed( $provider->get_task_id() ) ) { 51 | if ( method_exists( $provider, 'get_focus_tasks' ) ) { 52 | $focus_task = $provider->get_focus_tasks(); 53 | 54 | if ( $focus_task ) { 55 | $focus_tasks = array_merge( $focus_tasks, $focus_task ); 56 | } 57 | } 58 | } 59 | } 60 | 61 | // Enqueue the script. 62 | \progress_planner()->get_admin__enqueue()->enqueue_script( 63 | 'yoast-focus-element', 64 | [ 65 | 'name' => 'progressPlannerYoastFocusElement', 66 | 'data' => [ 67 | 'tasks' => $focus_tasks, 68 | 'base_url' => constant( 'PROGRESS_PLANNER_URL' ), 69 | ], 70 | ] 71 | ); 72 | 73 | // Enqueue the style. 74 | \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/focus-element' ); 75 | } 76 | /** 77 | * Add the providers. 78 | * 79 | * @param array $providers The providers. 80 | * @return array 81 | */ 82 | public function add_providers( $providers ) { 83 | 84 | $this->providers = [ 85 | new Archive_Author(), 86 | new Archive_Date(), 87 | new Archive_Format(), 88 | new Crawl_Settings_Feed_Global_Comments(), 89 | new Crawl_Settings_Feed_Authors(), 90 | new Crawl_Settings_Emoji_Scripts(), 91 | new Media_Pages(), 92 | new Organization_Logo(), 93 | new Fix_Orphaned_Content(), 94 | ]; 95 | 96 | // Yoast SEO Premium. 97 | if ( defined( 'WPSEO_PREMIUM_VERSION' ) ) { 98 | $this->providers[] = new Cornerstone_Workout(); 99 | $this->providers[] = new Orphaned_Content_Workout(); 100 | } 101 | 102 | return array_merge( 103 | $providers, 104 | $this->providers 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-emoji-scripts.php: -------------------------------------------------------------------------------- 1 | url = \admin_url( 'admin.php?page=wpseo_page_settings#/crawl-optimization#input-wpseo-remove_emoji_scripts' ); 27 | } 28 | 29 | /** 30 | * Get the title. 31 | * 32 | * @return string 33 | */ 34 | public function get_title() { 35 | return \esc_html__( 'Yoast SEO: remove emoji scripts', 'progress-planner' ); 36 | } 37 | 38 | /** 39 | * Get the description. 40 | * 41 | * @return string 42 | */ 43 | public function get_description() { 44 | return sprintf( 45 | /* translators: %s: "Read more" link. */ 46 | \esc_html__( 'Remove JavaScript used for converting emoji characters in older browsers. %s.', 'progress-planner' ), 47 | '' . \esc_html__( 'Read more', 'progress-planner' ) . '' 48 | ); 49 | } 50 | 51 | /** 52 | * Get the focus tasks. 53 | * 54 | * @return array 55 | */ 56 | public function get_focus_tasks() { 57 | return [ 58 | [ 59 | 'iconElement' => '.yst-toggle-field__header', 60 | 'valueElement' => [ 61 | 'elementSelector' => 'button[data-id="input-wpseo-remove_emoji_scripts"]', 62 | 'attributeName' => 'aria-checked', 63 | 'attributeValue' => 'true', 64 | 'operator' => '=', 65 | ], 66 | ], 67 | ]; 68 | } 69 | 70 | /** 71 | * Determine if the task should be added. 72 | * 73 | * @return bool 74 | */ 75 | public function should_add_task() { 76 | $yoast_options = \WPSEO_Options::get_instance()->get_all(); 77 | foreach ( [ 'remove_emoji_scripts' ] as $option ) { 78 | // If the crawl settings are already optimized, we don't need to add the task. 79 | if ( $yoast_options[ $option ] ) { 80 | return false; 81 | } 82 | } 83 | 84 | return true; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-global-comments.php: -------------------------------------------------------------------------------- 1 | url = \admin_url( 'admin.php?page=wpseo_page_settings#/crawl-optimization#input-wpseo-remove_feed_global_comments' ); 27 | } 28 | 29 | /** 30 | * Get the title. 31 | * 32 | * @return string 33 | */ 34 | public function get_title() { 35 | return \esc_html__( 'Yoast SEO: remove global comment feeds', 'progress-planner' ); 36 | } 37 | 38 | /** 39 | * Get the description. 40 | * 41 | * @return string 42 | */ 43 | public function get_description() { 44 | return sprintf( 45 | /* translators: %s: "Read more" link. */ 46 | \esc_html__( 'Remove URLs which provide an overview of recent comments on your site. %s.', 'progress-planner' ), 47 | '' . \esc_html__( 'Read more', 'progress-planner' ) . '' 48 | ); 49 | } 50 | 51 | /** 52 | * Get the focus tasks. 53 | * 54 | * @return array 55 | */ 56 | public function get_focus_tasks() { 57 | return [ 58 | [ 59 | 'iconElement' => '.yst-toggle-field__header', 60 | 'valueElement' => [ 61 | 'elementSelector' => 'button[data-id="input-wpseo-remove_feed_global_comments"]', 62 | 'attributeName' => 'aria-checked', 63 | 'attributeValue' => 'true', 64 | 'operator' => '=', 65 | ], 66 | ], 67 | ]; 68 | } 69 | 70 | /** 71 | * Determine if the task should be added. 72 | * 73 | * @return bool 74 | */ 75 | public function should_add_task() { 76 | $yoast_options = \WPSEO_Options::get_instance()->get_all(); 77 | foreach ( [ 'remove_feed_global_comments' ] as $option ) { 78 | // If the crawl settings are already optimized, we don't need to add the task. 79 | if ( $yoast_options[ $option ] ) { 80 | return false; 81 | } 82 | } 83 | 84 | return true; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/integrations/yoast/class-media-pages.php: -------------------------------------------------------------------------------- 1 | url = \admin_url( 'admin.php?page=wpseo_page_settings#/media-pages' ); 27 | } 28 | 29 | /** 30 | * Get the title. 31 | * 32 | * @return string 33 | */ 34 | public function get_title() { 35 | return \esc_html__( 'Yoast SEO: disable the media pages', 'progress-planner' ); 36 | } 37 | 38 | /** 39 | * Get the description. 40 | * 41 | * @return string 42 | */ 43 | public function get_description() { 44 | return sprintf( 45 | /* translators: %s: "Read more" link. */ 46 | \esc_html__( 'Yoast SEO can disable the media / attachment pages, which are the pages that show the media files. You really don\'t need them, except when you are displaying photos or art on your site through them. %s.', 'progress-planner' ), 47 | '' . \esc_html__( 'Read more', 'progress-planner' ) . '' 48 | ); 49 | } 50 | 51 | /** 52 | * Get the focus tasks. 53 | * 54 | * @return array 55 | */ 56 | public function get_focus_tasks() { 57 | return [ 58 | [ 59 | 'iconElement' => '.yst-toggle-field__header', 60 | 'valueElement' => [ 61 | 'elementSelector' => 'button[data-id="input-wpseo_titles-disable-attachment"]', 62 | 'attributeName' => 'aria-checked', 63 | 'attributeValue' => 'false', 64 | 'operator' => '=', 65 | ], 66 | ], 67 | ]; 68 | } 69 | 70 | /** 71 | * Determine if the task should be added. 72 | * 73 | * @return bool 74 | */ 75 | public function should_add_task() { 76 | // If the media pages are already disabled, we don't need to add the task. 77 | return YoastSEO()->helpers->options->get( 'disable-attachment' ) !== true; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /classes/suggested-tasks/providers/integrations/yoast/class-yoast-provider.php: -------------------------------------------------------------------------------- 1 | id = $id; 32 | return $popover; 33 | } 34 | 35 | /** 36 | * Render the triggering button. 37 | * 38 | * @param string $icon The dashicon to use. 39 | * @param string $content The content to use. 40 | * @return void 41 | */ 42 | public function render_button( $icon, $content ) { 43 | \progress_planner()->the_view( 44 | 'popovers/parts/icon.php', 45 | [ 46 | 'prpl_popover_id' => $this->id, 47 | 'prpl_popover_trigger_icon' => $icon, 48 | 'prpl_popover_trigger_content' => $content, 49 | ] 50 | ); 51 | } 52 | 53 | /** 54 | * Render the widget content. 55 | * 56 | * @return void 57 | */ 58 | public function render() { 59 | \progress_planner()->the_view( 60 | 'popovers/popover.php', 61 | [ 62 | 'prpl_popover_id' => $this->id, 63 | ] 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /classes/update/class-update-140.php: -------------------------------------------------------------------------------- 1 | rename_tasks_option(); 26 | $this->delete_word_count_option(); 27 | } 28 | 29 | /** 30 | * Rename the tasks option. 31 | * 32 | * @return void 33 | */ 34 | private function rename_tasks_option() { 35 | // Migrate the tasks option. 36 | $old_tasks = \progress_planner()->get_settings()->get( 'local_tasks', [] ); 37 | $new_tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); 38 | 39 | // Merge the tasks. 40 | // We use the task_id if it exists, otherwise we use the md5 hash of the task. 41 | // This is to ensure that we don't lose any tasks, and at the same time we don't have duplicate tasks. 42 | $tasks = []; 43 | foreach ( $new_tasks as $new_task ) { 44 | $tasks[ isset( $new_task['task_id'] ) ? $new_task['task_id'] : md5( maybe_serialize( $new_task ) ) ] = $new_task; 45 | } 46 | foreach ( $old_tasks as $old_task ) { 47 | $tasks[ isset( $old_task['task_id'] ) ? $old_task['task_id'] : md5( maybe_serialize( $old_task ) ) ] = $old_task; 48 | } 49 | 50 | // Set the tasks option. 51 | \progress_planner()->get_settings()->set( 'tasks', array_values( $tasks ) ); 52 | 53 | // Delete the old tasks option. 54 | \progress_planner()->get_settings()->delete( 'local_tasks' ); 55 | } 56 | 57 | /** 58 | * Delete the word count option. 59 | * 60 | * @return void 61 | */ 62 | private function delete_word_count_option() { 63 | \progress_planner()->get_settings()->delete( 'word_count' ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /classes/utils/class-cache.php: -------------------------------------------------------------------------------- 1 | query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 66 | $wpdb->prepare( 67 | "DELETE FROM $wpdb->options WHERE option_name LIKE %s", 68 | '_transient_' . self::CACHE_PREFIX . '%' 69 | ) 70 | ); 71 | $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 72 | $wpdb->prepare( 73 | "DELETE FROM $wpdb->options WHERE option_name LIKE %s", 74 | '_transient_timeout_' . self::CACHE_PREFIX . '%' 75 | ) 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /classes/utils/class-onboard.php: -------------------------------------------------------------------------------- 1 | \esc_html__( 'Invalid nonce.', 'progress-planner' ) ] ); 65 | } 66 | 67 | if ( ! isset( $_POST['key'] ) ) { 68 | \wp_send_json_error( [ 'message' => \esc_html__( 'Missing data.', 'progress-planner' ) ] ); 69 | } 70 | 71 | $license_key = \sanitize_text_field( wp_unslash( $_POST['key'] ) ); 72 | 73 | // False also if option value has not changed. 74 | if ( \update_option( 'progress_planner_license_key', $license_key, false ) ) { 75 | \wp_send_json_success( 76 | [ 77 | 'message' => \esc_html__( 'Onboarding data saved.', 'progress-planner' ), 78 | ] 79 | ); 80 | } 81 | \wp_send_json_error( [ 'message' => \esc_html__( 'Unable to save data.', 'progress-planner' ) ] ); 82 | } 83 | 84 | /** 85 | * Get the remote nonce URL. 86 | * 87 | * @return string 88 | */ 89 | public function get_remote_nonce_url() { 90 | return \progress_planner()->get_remote_server_root_url() . self::REMOTE_API_URL . 'get-nonce'; 91 | } 92 | 93 | /** 94 | * Get the onboarding remote URL. 95 | * 96 | * @return string 97 | */ 98 | public function get_remote_url() { 99 | return \progress_planner()->get_remote_server_root_url() . self::REMOTE_API_URL . 'onboard'; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /playwright.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig, devices } = require( '@playwright/test' ); 2 | 3 | module.exports = defineConfig( { 4 | testDir: './tests/e2e', 5 | timeout: 30000, 6 | forbidOnly: !! process.env.CI, 7 | retries: process.env.CI ? 2 : 0, 8 | reporter: 'html', 9 | globalSetup: './tests/e2e/auth.setup.js', 10 | globalTeardown: './tests/e2e/auth.setup.js', 11 | use: { 12 | baseURL: process.env.WORDPRESS_URL || 'http://localhost:8080', 13 | trace: 'on-first-retry', 14 | screenshot: 'only-on-failure', 15 | storageState: 'auth.json', 16 | }, 17 | projects: [ 18 | { 19 | name: 'sequential', 20 | use: { ...devices[ 'Desktop Chrome' ] }, 21 | testMatch: 'sequential.spec.js', 22 | fullyParallel: false, 23 | workers: 1, 24 | }, 25 | { 26 | name: 'parallel', 27 | use: { ...devices[ 'Desktop Chrome' ] }, 28 | testIgnore: [ 'sequential.spec.js', '**/sequential/**' ], 29 | fullyParallel: true, 30 | workers: 4, 31 | }, 32 | ], 33 | } ); 34 | -------------------------------------------------------------------------------- /progress-planner.php: -------------------------------------------------------------------------------- 1 | init(); 41 | } 42 | return $progress_planner; 43 | } 44 | 45 | progress_planner(); 46 | -------------------------------------------------------------------------------- /uninstall.php: -------------------------------------------------------------------------------- 1 | query( 41 | $wpdb->prepare( 42 | // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnsupportedIdentifierPlaceholder, WordPress.DB.DirectDatabaseQuery.SchemaChange 43 | 'DROP TABLE IF EXISTS %i', 44 | $wpdb->prefix . \Progress_Planner\Activities\Query::TABLE_NAME 45 | ) 46 | ); 47 | -------------------------------------------------------------------------------- /views/admin-page-settings.php: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |
16 | 25 |
26 |

27 | 28 | the_asset( 'images/icon_settings.svg' ); ?> 29 | 30 | 31 | 32 | 33 |

34 | 35 |
36 | the_view( 'page-settings/pages.php' ); ?> 37 | 38 |
39 | the_view( 'page-settings/post-types.php' ); ?> 40 | the_view( 'page-settings/settings.php' ); ?> 41 | the_view( 'page-settings/license.php' ); ?> 42 |
43 | 44 | 45 | 46 | 54 |
55 |
56 | -------------------------------------------------------------------------------- /views/admin-page.php: -------------------------------------------------------------------------------- 1 | is_privacy_policy_accepted(); 14 | $prpl_wrapper_class = ''; 15 | 16 | if ( ! $prpl_privacy_policy_accepted ) { 17 | $prpl_wrapper_class = 'prpl-pp-not-accepted'; 18 | } 19 | ?> 20 | 21 |
22 | 23 |

24 | the_view( 'admin-page-header.php' ); ?> 25 |
26 | get_admin__page()->get_widgets() as $prpl_admin_widget ) : ?> 27 | render(); ?> 28 | 29 |
30 | 31 | 40 | 41 | the_view( 'welcome.php' ); ?> 42 | 43 |
44 | 45 | -------------------------------------------------------------------------------- /views/dashboard-widgets/todo.php: -------------------------------------------------------------------------------- 1 | 9 |
10 | 11 |

12 |
13 | get_admin__widgets__todo()->the_todo_list(); 16 | -------------------------------------------------------------------------------- /views/page-settings/license.php: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 |
20 |

21 | 22 | the_asset( 'images/icon_key.svg' ); ?> 23 | 24 | 25 | 26 | 27 |

28 |
29 | 30 |

31 | Progress Planner Pro' 36 | ); 37 | ?> 38 |

39 | 40 | 43 |
44 | 50 | 51 | 52 | 53 | 54 | the_asset( 'images/icon_check_circle.svg' ); ?> 55 | 56 | 57 | 58 | the_asset( 'images/icon_exclamation_circle.svg' ); ?> 59 | 60 | 61 | 62 | 63 |
64 |
65 |
66 |
67 | -------------------------------------------------------------------------------- /views/page-settings/pages.php: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |
16 |

17 | 18 | the_asset( 'images/icon_pages.svg' ); ?> 19 | 20 | 21 | 22 | 23 |

24 |

25 | 26 |

27 |
28 | get_admin__page_settings()->get_settings() as $prpl_setting ) { 30 | \progress_planner()->the_view( "setting/{$prpl_setting['type']}.php", [ 'prpl_setting' => $prpl_setting ] ); 31 | } 32 | ?> 33 |
34 |
35 |
36 | -------------------------------------------------------------------------------- /views/page-settings/post-types.php: -------------------------------------------------------------------------------- 1 | get_settings()->get_post_types_names(); 14 | $prpl_post_types = \progress_planner()->get_settings()->get_public_post_types(); 15 | 16 | // Early exit if there are no public post types. 17 | if ( empty( $prpl_post_types ) ) { 18 | return; 19 | } 20 | 21 | // We use it in order to change grid layout when there are more than 5 valuable post types. 22 | $prpl_data_attributes = 5 < count( $prpl_post_types ) ? 'data-has-many-valuable-post-types' : ''; 23 | ?> 24 | 25 |
> 26 |
27 |

28 | 29 | the_asset( 'images/icon_copywriting.svg' ); ?> 30 | 31 | 32 | 33 | 34 |

35 |

36 | 37 |

38 |
39 | 40 | 49 | 50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /views/page-settings/settings.php: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 |
18 |

19 | 20 | the_asset( 'images/icon_user.svg' ); ?> 21 | 22 | 23 | 24 | 25 |

26 |
27 | 36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /views/page-widgets/challenge.php: -------------------------------------------------------------------------------- 1 | get_admin__widgets__challenge()->get_challenge(); 13 | ?> 14 |

15 | 16 | 17 | 18 | 19 |

20 | 21 |
22 | 23 |
24 | -------------------------------------------------------------------------------- /views/page-widgets/latest-badge.php: -------------------------------------------------------------------------------- 1 | get_badges()->get_latest_completed_badge(); 14 | 15 | ?> 16 |

17 | 18 |

19 | 20 |

21 | 22 |

23 | get_id(), 'monthly-' ) ) : ?> 24 | 25 | 26 | ' . \esc_html( $prpl_latest_badge->get_name() ) . '' 31 | ); 32 | ?> 33 | 34 |

35 | <?php echo \esc_attr( $prpl_latest_badge->get_name() ); ?> 42 | is_local_site() ) : ?> 43 | $prpl_latest_badge->get_id(), 48 | 'url' => \home_url(), 49 | ], 50 | \progress_planner()->get_remote_server_root_url() . '/wp-json/progress-planner-saas/v1/share-badge' 51 | ); 52 | ?> 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 |
61 | is_local_site() ) : ?> 62 | get_ui__popover()->the_popover( 'subscribe-form' )->render_button( 64 | '', 65 | \esc_html__( 'Subscribe to share your badge!', 'progress-planner' ) 66 | ); 67 | ?> 68 | 69 | 70 | -------------------------------------------------------------------------------- /views/page-widgets/suggested-tasks.php: -------------------------------------------------------------------------------- 1 | get_admin__widgets__suggested_tasks(); 15 | $prpl_badge = \progress_planner()->get_badges()->get_badge( Monthly::get_badge_id_from_date( new \DateTime() ) ); 16 | ?> 17 | 18 | 19 |
20 |

21 | 22 |

23 |

24 | 25 |

26 | 27 | 28 | 29 |

30 | 31 |
32 | 33 |

34 |
35 |
36 | 37 | 38 |

39 | 40 |

41 | 42 | 50 | 51 | 52 | 53 | 54 | 55 |
56 | 57 | 58 | get_score(); ?>pt 59 | 60 |
61 | 62 |
63 | 64 | 65 |
66 | 67 | the_view( 69 | 'page-widgets/parts/monthly-badges-2024.php', 70 | [ 71 | 'title_tag' => 'h2', 72 | ] 73 | ); 74 | ?> 75 | 76 | 77 | the_view( 79 | 'page-widgets/parts/monthly-badges.php', 80 | [ 81 | 'title_year' => 2025, 82 | ] 83 | ); 84 | ?> 85 | 86 | get_ui__popover()->the_popover( 'monthly-badges' )->render_button( 88 | '', 89 | \esc_html__( 'Show all my badges!', 'progress-planner' ) 90 | ); 91 | \progress_planner()->get_ui__popover()->the_popover( 'monthly-badges' )->render(); 92 | ?> 93 |
94 | -------------------------------------------------------------------------------- /views/page-widgets/todo.php: -------------------------------------------------------------------------------- 1 | 13 |

14 | 15 | 44,75px / lijn 3px 16 | 17 | 18 |

19 | 20 |

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | the_asset( 'images/icon_info.svg' ); ?> 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |

43 | get_admin__widgets__todo()->the_todo_list(); ?> 44 | -------------------------------------------------------------------------------- /views/page-widgets/whats-new.php: -------------------------------------------------------------------------------- 1 | get_admin__widgets__whats_new(); 13 | 14 | ?> 15 |

16 | 17 |

18 | 19 | 42 | 47 | -------------------------------------------------------------------------------- /views/popovers/badge-streak.php: -------------------------------------------------------------------------------- 1 | 13 |

14 |

15 | 16 |
17 |
18 |

19 |

20 |

21 |
22 | the_view( 'popovers/parts/badge-streak-badge.php', [ 'prpl_category' => 'maintenance' ] ); ?> 23 |
24 | the_view( 'popovers/parts/badge-streak-progressbar.php', [ 'prpl_context' => 'maintenance' ] ); ?> 25 |
26 | 27 |
28 |

29 |

30 |
31 | the_view( 'popovers/parts/badge-streak-badge.php', [ 'prpl_category' => 'content' ] ); ?> 32 |
33 | the_view( 'popovers/parts/badge-streak-progressbar.php', [ 'prpl_context' => 'maintenance' ] ); ?> 34 |
35 |
36 | 42 | -------------------------------------------------------------------------------- /views/popovers/monthly-badges.php: -------------------------------------------------------------------------------- 1 | 13 |

14 |
15 |
16 | 22 | * 23 | * This is an associative array where the key is the year and the elements are arrays of \Progress_Planner\Badges\Badge objects. 24 | */ 25 | $prpl_badges = \progress_planner()->get_badges()->get_badges( 'monthly' ); 26 | 27 | foreach ( $prpl_badges as $prpl_badges_year => $prpl_monthly_badges ) { 28 | \progress_planner()->the_view( 29 | 'page-widgets/parts/monthly-badges.php', 30 | [ 31 | 'css_class' => 'in-popover', 32 | 'badges_year' => $prpl_badges_year, 33 | ] 34 | ); 35 | } 36 | ?> 37 |
38 | 39 |
40 | __( 'Writing badges', 'progress-planner' ), 43 | 'maintenance' => __( 'Streak badges', 'progress-planner' ), 44 | ]; 45 | ?> 46 | $prpl_widget_title ) : ?> 47 |
48 |

49 | 50 |

51 |
52 | get_badges()->get_badges( $prpl_badge_group ); ?> 53 |
54 | 55 | get_progress(); 57 | $prpl_badge_completed = 100 === (int) $prpl_badge_progress['progress']; 58 | ?> 59 | 63 | 67 |

get_name() ); ?>

68 |
69 | 70 |
71 |
72 |
73 | 74 |
75 |
76 | -------------------------------------------------------------------------------- /views/popovers/parts/badge-streak-badge.php: -------------------------------------------------------------------------------- 1 | 13 | get_badges()->get_badges( $prpl_category ) as $prpl_badge ) : // @phpstan-ignore-line variable.undefined ?> 14 | get_progress(); ?> 15 | 19 |
20 | 24 | get_name() ); ?> 25 |
26 |

get_description() ); ?>

27 |
28 | 29 | -------------------------------------------------------------------------------- /views/popovers/parts/badge-streak-progressbar.php: -------------------------------------------------------------------------------- 1 | get_badges()->get_badges( $prpl_context ); // @phpstan-ignore-line variable.undefined 14 | 15 | if ( empty( $prpl_badges ) ) { 16 | return; 17 | } 18 | ?> 19 |
20 | 21 | 22 | 23 |
24 | 25 |
26 | get_progress(); ?> 27 | 28 | 29 | ✔️ 30 | 31 | ' . \esc_html( \number_format_i18n( $prpl_badge_progress['remaining'] ) ) . '' 52 | ) 53 | ?> 54 | 55 | 56 |
57 | 58 |
59 |
60 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /views/popovers/popover.php: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | 16 | the_view( 'popovers/' . $prpl_popover_id . '.php' ); ?> 17 | 18 | 19 | 27 |
28 | -------------------------------------------------------------------------------- /views/popovers/subscribe-form.php: -------------------------------------------------------------------------------- 1 | 15 | 16 |

17 | 18 |
19 |

20 | progressplanner.com' 25 | ) 26 | ?> 27 |

28 |
29 | 41 | 53 | 58 | 63 | 68 |
69 | 70 |
71 | -------------------------------------------------------------------------------- /views/popovers/upgrade-tasks.php: -------------------------------------------------------------------------------- 1 | the_view( 'popovers/parts/upgrade-tasks.php', [ 'context' => 'upgrade' ] ); 16 | -------------------------------------------------------------------------------- /views/setting/radio.php: -------------------------------------------------------------------------------- 1 | 13 |
14 | 15 |
16 | $prpl_option_label ) : ?> 17 | 30 | 31 |
32 |
33 | --------------------------------------------------------------------------------