├── .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.
21 | itemRadiosWrapperEl.querySelector(
22 | '.prpl-select-page'
23 | ).style.visibility = 'hidden';
24 | }
25 |
26 | // Show only select and edit button.
27 | if ( 'yes' === value ) {
28 | // Show 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 |
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 | : `${ item.label } `;
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 |
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 |
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 |
26 |
27 |
28 | the_asset( 'images/icon_settings.svg' ); ?>
29 |
30 |
31 |
32 |
33 |
34 |
35 |
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 |
13 | get_admin__widgets__todo()->the_todo_list();
16 |
--------------------------------------------------------------------------------
/views/page-settings/license.php:
--------------------------------------------------------------------------------
1 |
17 |
18 |
67 |
--------------------------------------------------------------------------------
/views/page-settings/pages.php:
--------------------------------------------------------------------------------
1 |
13 |
14 |
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 |
53 |
--------------------------------------------------------------------------------
/views/page-settings/settings.php:
--------------------------------------------------------------------------------
1 |
15 |
16 |
39 |
--------------------------------------------------------------------------------
/views/page-widgets/challenge.php:
--------------------------------------------------------------------------------
1 | get_admin__widgets__challenge()->get_challenge();
13 | ?>
14 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/views/page-widgets/latest-badge.php:
--------------------------------------------------------------------------------
1 | get_badges()->get_latest_completed_badge();
14 |
15 | ?>
16 |
19 |
20 |
21 |
22 |
23 | get_id(), 'monthly-' ) ) : ?>
24 |
25 |
26 | ' . \esc_html( $prpl_latest_badge->get_name() ) . ''
31 | );
32 | ?>
33 |
34 |
35 |
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 |
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 |
36 |
37 |
38 |
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 |
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 |
18 |
19 |
20 | get_blog_feed() as $prpl_blog_post ) : ?>
21 |
26 |
27 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
47 |
--------------------------------------------------------------------------------
/views/popovers/badge-streak.php:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
36 |
42 |
--------------------------------------------------------------------------------
/views/popovers/monthly-badges.php:
--------------------------------------------------------------------------------
1 |
13 |
14 |
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 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/views/popovers/popover.php:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 | the_view( 'popovers/' . $prpl_popover_id . '.php' ); ?>
17 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/views/popovers/subscribe-form.php:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
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 |
18 |
24 | data-page=""
25 |
26 |
27 | >
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------