diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..c6f3cf495 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +# This file is for unifying the coding style for different editors and IDEs +# editorconfig.org + +# WordPress Coding Standards +# https://make.wordpress.org/core/handbook/coding-standards/ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab + +[*.yml] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[{*.txt,wp-config-sample.php}] +end_of_line = crlf diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..8123f4237 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +.editorconfig export-ignore +.gitattributes export-ignore +.github/* export-ignore +tests/* export-ignore +.gitignore export-ignore +composer.json export-ignore +composer.lock export-ignore +package.json export-ignore +package-lock.json export-ignore +.wordpress-org/* export-ignore +phpcs.xml.dist export-ignore +phpstan.neon.dist export-ignore +phpunit.xml.dist export-ignore +README.md export-ignore +.wordpress-org/* export-ignore diff --git a/.github/workflows/cs.yml b/.github/workflows/cs.yml new file mode 100644 index 000000000..55dca57ce --- /dev/null +++ b/.github/workflows/cs.yml @@ -0,0 +1,68 @@ +name: CS + +on: + # Run on all relevant pushes (except to main) and on all relevant pull requests. + push: + paths: + - '**.php' + - 'composer.json' + - 'composer.lock' + - '.phpcs.xml.dist' + - 'phpcs.xml.dist' + - '.github/workflows/cs.yml' + pull_request: + paths: + - '**.php' + - 'composer.json' + - 'composer.lock' + - '.phpcs.xml.dist' + - 'phpcs.xml.dist' + - '.github/workflows/cs.yml' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + checkcs: + name: 'Check code style' + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: none + tools: cs2pr + + # Validate the composer.json file. + # @link https://getcomposer.org/doc/03-cli.md#validate + - name: Validate Composer installation + run: composer validate --no-check-all + + # Install dependencies and handle caching in one go. + # @link https://github.com/marketplace/actions/install-composer-dependencies + - name: Install Composer dependencies + uses: ramsey/composer-install@v2 + with: + # Bust the cache at least once a month - output format: YYYY-MM. + custom-cache-suffix: $(date -u "+%Y-%m") + + # Check the codestyle of the files. + # The results of the CS check will be shown inline in the PR via the CS2PR tool. + # @link https://github.com/staabm/annotate-pull-request-from-checkstyle/ + - name: Check PHP code style + id: phpcs + run: composer check-cs -- --no-cache --report-full --report-checkstyle=./phpcs-report.xml + + - name: Show PHPCS results in PR + if: ${{ always() && steps.phpcs.outcome == 'failure' }} + run: cs2pr ./phpcs-report.xml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..b86737b32 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,18 @@ +name: "Deploy to WordPress.org" + +on: + push: + tags: + - "v*" + +jobs: + tag: + name: New tag + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@main + - name: WordPress Plugin Deploy + uses: 10up/action-wordpress-plugin-deploy@stable + env: + SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} + SVN_USERNAME: ${{ secrets.SVN_USERNAME }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..c46134e32 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,69 @@ +name: Lint + +on: + # Run on pushes to select branches and on all pull requests. + push: + branches: + - main + - develop + - 'release/[0-9]+.[0-9]+*' + - 'hotfix/[0-9]+.[0-9]+*' + pull_request: + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + + strategy: + matrix: + # Lint against the highest/lowest supported versions of each PHP major. + # And also do a run against "nightly" (the current dev version of PHP). + php_version: ['7.4', '8.0', '8.1', '8.2'] + + name: "Lint: PHP ${{ matrix.php_version }}" + + steps: + - name: Checkout code + uses: actions/checkout + + - name: Install PHP for the composer install + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + coverage: none + + # The lint stage doesn't use code style, so no need for WPCS or phpcompatibility. + - name: 'Composer: adjust dependencies - remove PHPCompatibility' + run: composer remove --no-update --dev phpcompatibility/phpcompatibility-wp --no-scripts --no-interaction + - name: 'Composer: adjust dependencies - remove WPCS' + run: composer remove --no-update --dev wp-coding-standards/wpcs --no-scripts --no-interaction + + # Install dependencies and handle caching in one go. + # @link https://github.com/marketplace/actions/install-composer-dependencies + - name: Install Composer dependencies + uses: ramsey/composer-install@v2 + with: + # Bust the cache at least once a month - output format: YYYY-MM-DD. + custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F") + + - name: Install PHP for the actual test + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php_version }} + ini-values: zend.assertions=1, error_reporting=-1, display_errors=On + coverage: none + tools: cs2pr + + - name: Lint against parse errors + run: composer lint -- --checkstyle | cs2pr + + # - name: Lint blueprint file + # run: composer lint-blueprint diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 000000000..bdf527464 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,36 @@ +name: Run PHPStan + +on: + # Run on pushes to select branches and on all pull requests. + push: + branches: + - main + - develop + - 'release/[0-9]+.[0-9]+*' + - 'hotfix/[0-9]+.[0-9]+*' + pull_request: + # Allow manually triggering the workflow. + workflow_dispatch: + +jobs: + phpstan: + name: Static Analysis + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 'latest' + coverage: none + tools: composer, cs2pr + + - name: Install PHP dependencies + uses: ramsey/composer-install@v2 + with: + composer-options: '--prefer-dist --no-scripts' + + - name: PHPStan + run: composer phpstan diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 000000000..45211585f --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,90 @@ +name: Test + +on: + # Run on pushes to select branches and on all pull requests. + push: + pull_request: + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + integration: + runs-on: ubuntu-latest + + strategy: + matrix: + include: + - php_version: '8.1' + wp_version: '6.2' + multisite: false + + - php_version: '8.1' + wp_version: 'latest' + multisite: false + + - php_version: '8.1' + wp_version: 'latest' + multisite: true + + - php_version: '8.2' + wp_version: 'latest' + multisite: true + + name: "Integration Test: PHP ${{ matrix.php_version }} | WP ${{ matrix.wp_version }}${{ matrix.multisite == true && ' (+ ms)' || '' }}" + + # Allow builds to fail on as-of-yet unreleased WordPress versions. + continue-on-error: ${{ matrix.wp_version == 'trunk' }} + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: false + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=10s --health-retries=10 + + steps: + - name: Checkout code + uses: actions/checkout + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php_version }} + ini-values: zend.assertions=1, error_reporting=-1, display_errors=On + coverage: none + + # Install dependencies and handle caching in one go. + # @link https://github.com/marketplace/actions/install-composer-dependencies + - name: "Composer: remove the PHP platform requirement" + run: composer config --unset platform.php + + - name: "Install Composer dependencies" + uses: ramsey/composer-install@v2 + with: + # Force a `composer update` run. + dependency-versions: "highest" + # But make it selective. + composer-options: "yoast/wp-test-utils --with-dependencies" + # Bust the cache at least once a month - output format: YYYY-MM-DD. + custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F") + + - name: Install WP + shell: bash + run: tests/bin/install-wp-tests.sh wordpress_tests root '' 127.0.0.1:3306 ${{ matrix.wp_version }} + + - name: Run unit tests - single site + run: composer test + + - name: Run unit tests - multisite + if: ${{ matrix.multisite == true }} + run: composer test + env: + WP_MULTISITE: 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..021d8b9bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor/ +._* +.phpunit.result.cache +node_modules diff --git a/.wordpress-org/banner-1544x500.png b/.wordpress-org/banner-1544x500.png new file mode 100644 index 000000000..4d63d5d38 Binary files /dev/null and b/.wordpress-org/banner-1544x500.png differ diff --git a/.wordpress-org/banner-772x250.png b/.wordpress-org/banner-772x250.png new file mode 100644 index 000000000..243a729c2 Binary files /dev/null and b/.wordpress-org/banner-772x250.png differ diff --git a/.wordpress-org/icon-128x128.png b/.wordpress-org/icon-128x128.png new file mode 100644 index 000000000..5a072bfa1 Binary files /dev/null and b/.wordpress-org/icon-128x128.png differ diff --git a/.wordpress-org/icon-256x256.png b/.wordpress-org/icon-256x256.png new file mode 100644 index 000000000..3d59b968c Binary files /dev/null and b/.wordpress-org/icon-256x256.png differ diff --git a/.wordpress-org/icon.svg b/.wordpress-org/icon.svg new file mode 100644 index 000000000..19990ef1f --- /dev/null +++ b/.wordpress-org/icon.svg @@ -0,0 +1,6 @@ + + diff --git a/.wordpress-org/screenshot-1.png b/.wordpress-org/screenshot-1.png new file mode 100644 index 000000000..48399edbb Binary files /dev/null and b/.wordpress-org/screenshot-1.png differ diff --git a/.wordpress-org/screenshot-2.png b/.wordpress-org/screenshot-2.png new file mode 100644 index 000000000..b83e68701 Binary files /dev/null and b/.wordpress-org/screenshot-2.png differ diff --git a/.wordpress-org/screenshot-3.png b/.wordpress-org/screenshot-3.png new file mode 100644 index 000000000..b5a77c67d Binary files /dev/null and b/.wordpress-org/screenshot-3.png differ diff --git a/README.md b/README.md index b269efbdd..07b130444 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,9 @@ # progress-planner Progress Planner helps you track your website statistics and rewards you with badges when you're doing well! + +## Branches on this repository + +We use a couple of branches in this repository to keep things clean: + +- `develop` contains the current state of development. +- `main` contains the current stable release. Releases here will be tagged as such. diff --git a/assets/css/admin.css b/assets/css/admin.css new file mode 100644 index 000000000..b13b3f911 --- /dev/null +++ b/assets/css/admin.css @@ -0,0 +1,1082 @@ +/*------------------------------------*\ + Set variables. +\*------------------------------------*/ +:root { + --prpl-gap: 32px; + --prpl-padding: 20px; + --prpl-column-min-width: 15rem; + --prpl-column-max-width: 30rem; + --prpl-max-columns: 4; + --prpl-border-radius: 8px; + --prpl-border-radius-big: calc(var(--prpl-border-radius) * 2); + + --prpl-container-max-width: calc(var(--prpl-column-max-width) * var(--prpl-max-columns) + var(--prpl-gap) * (var(--prpl-max-columns) - 1)); + + --prpl-color-gray-1: #e1e3e7; + --prpl-color-gray-2: #d1d5db; + --prpl-color-gray-3: #9ca3af; + --prpl-color-gray-4: #6b7280; + --prpl-color-gray-5: #4b5563; + --prpl-color-gray-6: #374151; + + --prpl-color-accent-red: #f43f5e; + --prpl-color-accent-orange: #faa310; + --prpl-color-accent-purple: #0d6b9e; + --prpl-color-accent-green: #14b8a6; + + --prpl-color-headings: #38296d; + --prpl-color-text: var(--prpl-color-gray-5); + --prpl-color-link: #1e40af; + + --prpl-color-notification-green: #16a34a; + --prpl-color-notification-red: #e73136; + + --prpl-background-orange: #fff9f0; + --prpl-background-purple: #f6f5fb; + --prpl-background-green: #f2faf9; + --prpl-background-red: #fff6f7; + --prpl-background-blue: #effbfe; + + --prpl-font-size-xs: 0.75rem; /* 12px */ + --prpl-font-size-small: 0.875rem; /* 14px */ + --prpl-font-size-base: 1rem; /* 16px */ + --prpl-font-size-lg: 1.125rem; /* 18px */ + --prpl-font-size-xl: 1.25rem; /* 20px */ + --prpl-font-size-2xl: 1.5rem; /* 24px */ + --prpl-font-size-3xl: 2rem; /* 32px */ + --prpl-font-size-4xl: 3rem; /* 48px */ + --prpl-font-size-5xl: 3.5rem; /* 56px */ + --prpl-font-size-6xl: 4.5rem; /* 72px */ +} + +/*------------------------------------*\ + Styles for the container of the page. +\*------------------------------------*/ +.prpl-wrap { + background: #fff; + border: 1px solid var(--prpl-color-gray-2); + border-radius: var(--prpl-border-radius); + padding: calc(var(--prpl-padding) * 2); + max-width: var(--prpl-container-max-width); + color: var(--prpl-color-text); + font-size: var(--prpl-font-size-base); + line-height: 1.4; + position: relative; +} + +/*------------------------------------*\ + Generic styles. +\*------------------------------------*/ +.prpl-wrap p { + font-size: var(--prpl-font-size-base); + color: var(--prpl-color-text); + margin: var(--prpl-padding) 0; +} + +.prpl-wrap h2:has(+ p) { + margin-bottom: 0; +} + +.prpl-wrap h2 + p { + margin-top: 0.5em; +} + +.prpl-wrap h1, +.prpl-wrap h2, +.prpl-wrap h3, +.prpl-wrap h4, +.prpl-wrap h5, +.prpl-wrap h6 { + color: var(--prpl-color-headings); +} + +.prpl-wrap a { + color: var(--prpl-color-link); +} + +.prpl-widget-title { + margin-top: 0; +} + +.prpl-widget-title:has(.prpl-info-icon) { + display: flex; + justify-content: space-between; +} + +/*------------------------------------*\ + Info buttons. +\*------------------------------------*/ +button.prpl-info-icon { + background: none; + border: none; + color: var(--prpl-color-gray-4); + cursor: pointer; + font-size: var(--prpl-font-size-xs); + padding: 0; +} + +/*------------------------------------*\ + Header & logo. +\*------------------------------------*/ +.prpl-header { + margin-bottom: 2em; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; +} + +.prpl-header-logo svg { + height: 100px; +} + +/*------------------------------------*\ + Charts container. +\*------------------------------------*/ +.prpl-chart-container { + position: relative; + height: 100%; + width: 100%; + max-height: 300px; +} + +/*------------------------------------*\ + Progress bar styles for the posts scanner. +\*------------------------------------*/ +#progress-planner-scan-progress progress { + width: 100%; + min-height: 1px; +} + +/*------------------------------------*\ + Layout for widgets. +\*------------------------------------*/ +.prpl-widgets-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(calc(var(--prpl-column-min-width) * 2), 1fr)); + grid-gap: var(--prpl-gap); +} + +.prpl-column-main { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(var(--prpl-column-min-width), 1fr)); + grid-gap: var(--prpl-gap); +} + +.prpl-column { + display: flex; + flex-direction: column; + gap: var(--prpl-gap); +} + +.prpl-column-main-primary .prpl-column { + display: flex; + flex-direction: column; +} + +.prpl-column-main-primary .prpl-column-two-col { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(var(--prpl-column-min-width), 1fr)); + grid-gap: var(--prpl-gap); +} + +/*------------------------------------*\ + Widgets with 2 columns. +\*------------------------------------*/ +.two-col { + display: flex; + flex-wrap: wrap; + gap: var(--prpl-gap); +} + +.two-col:has(.counter-big-wrapper) { + gap: var(--prpl-padding); +} + +.two-col > * { + flex: 1; + min-width: 12em; + max-width: 100%; +} + +.two-col.narrow > * { + min-width: 7em; +} + +/*------------------------------------*\ + Generic styles for individual widgets. +\*------------------------------------*/ +.prpl-widget-wrapper { + border: 1px solid var(--prpl-color-gray-2); + border-radius: var(--prpl-border-radius); + padding: var(--prpl-padding); + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; +} + +/*------------------------------------*\ + The big counters in widgets. +\*------------------------------------*/ +.prpl-wrap .counter-big-wrapper { + background-color: var(--prpl-background-purple); + padding: var(--prpl-padding); + border-radius: var(--prpl-border-radius-big); + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + align-content: center; + justify-content: center; +} + +.prpl-wrap .counter-big-number { + font-size: var(--prpl-font-size-5xl); + line-height: 1; + font-weight: 600; +} + +.prpl-wrap .counter-big-text { + font-size: var(--prpl-font-size-2xl); +} + +/*------------------------------------*\ + Generic styles for the graph wrappers. +\*------------------------------------*/ +.prpl-graph-wrapper { + position: relative; + height: 100%; +} + +/*------------------------------------*\ + The wrapper for widgets that have a + big counter at the top, and content at the bottom. +\*------------------------------------*/ +.prpl-top-counter-bottom-content { + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.prpl-top-counter-bottom-content .counter-big-wrapper { + display: flex; + flex-direction: column; + justify-content: center; +} + +/*------------------------------------*\ + Percent display for badges. +\*------------------------------------*/ +.prpl-badge-wrapper .progress-percent { + font-size: var(--prpl-font-size-3xl); + line-height: 1; + font-weight: 600; + display: block; + text-align: center; + padding-top: calc(var(--prpl-padding) / 2); +} + +/*------------------------------------*\ + Activity-score widget. +\*------------------------------------*/ +.prpl-widget-wrapper.prpl-website-activity-score .two-col > *:first-child { + flex: 1.4; +} + +.prpl-widget-wrapper.prpl-website-activity-score ul { + margin-top: 10px; +} + +.prpl-widget-wrapper.prpl-website-activity-score ul li { + margin-left: 10px; + line-height: 1.2em; +} + +.prpl-widget-wrapper.prpl-website-activity-score .prpl-icon { + display: inline-block; + text-align: center; + width: .75em; + font-size: 1.25em; + margin-right: 5px; + vertical-align: top; +} + +/* .prpl-widget-wrapper.prpl-website-activity-score .prpl-icon.prpl-green { + color: var(--prpl-color-notification-green); + +} + +.prpl-widget-wrapper.prpl-website-activity-score .prpl-icon.prpl-red { + color: var(--prpl-color-notification-red); +} */ + +.prpl-gauge-number { + font-size: var(--prpl-font-size-6xl); + top: -1em; + display: block; + padding-top: 50%; + font-weight: 600; + text-align: center; + position: absolute; + color: var(--prpl-color-gray-5); + width: 100%; + line-height: 1.2; +} + +.prpl-activities-gauge-container { + padding: var(--prpl-padding); + background: var(--prpl-background-orange); + border-radius: var(--prpl-border-radius); + aspect-ratio: 2 / 1; + overflow: hidden; + position: relative; + margin-bottom: var(--prpl-padding); +} + +.prpl-activities-gauge-container .prpl-gauge-0, .prpl-gauge-100 { + font-size: var(--prpl-font-size-small); + position: absolute; + top: 50%; + color: var(--prpl-color-gray-5); +} + +.prpl-activities-gauge-container .prpl-gauge-0 { + left: 10px; +} + +.prpl-activities-gauge-container .prpl-gauge-100 { + right: 3px; +} + +/*------------------------------------*\ + Activity scores +\*------------------------------------*/ +.prpl-widget-wrapper.prpl-activity-scores .prpl-graph-wrapper { + max-height: 300px; +} + +/*------------------------------------*\ + Badges progress widget. +\*------------------------------------*/ +.prpl-widget-wrapper.prpl-badges-progress .progress-label { + display: inline-block; +} + +.prpl-widget-wrapper.prpl-badges-progress .progress-wrapper { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: calc(var(--prpl-gap) / 2); + background-color: var(--prpl-background-blue); + padding: calc(var(--prpl-padding) / 2); + border-radius: var(--prpl-border-radius); + margin-bottom: var(--prpl-padding); +} + +.prpl-widget-wrapper.prpl-badges-progress .progress-wrapper .prpl-badge { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + min-width: 0; +} + +.prpl-widget-wrapper.prpl-badges-progress .progress-wrapper p { + margin: 0; + font-size: var(--prpl-font-size-xs); + text-align: center; + line-height: 1.2; +} + +.prpl-widget-wrapper.prpl-badges-progress .prpl-badge { + /* Change this number to adjust the rate of growth of the badges size. */ + --multiplier-default: 1.125; + --multiplier: var(--multiplier-default); +} + +.prpl-widget-wrapper.prpl-badges-progress .prpl-badge + .prpl-badge { + --multiplier: calc(var(--multiplier-default) * var(--multiplier-default)); +} + +.prpl-widget-wrapper.prpl-badges-progress .prpl-badge + .prpl-badge + .prpl-badge { + --multiplier: calc(var(--multiplier-default) * var(--multiplier-default) * var(--multiplier-default)); +} + +.prpl-widget-wrapper.prpl-badges-progress .progress-wrapper svg { + width: calc(100% * var(--multiplier)); + margin-left: calc(100% * (1 - var(--multiplier)) / 2) +} + +.prpl-widget-wrapper.prpl-badges-progress .progress-wrapper + .progress-wrapper { + background-color: var(--prpl-background-orange); +} + +.prpl-widget-wrapper.prpl-badges-progress .progress-wrapper + .progress-wrapper.badge-group-maintenance { + background-color: var(--prpl-background-red); +} + +.prpl-widget-wrapper.prpl-badges-progress .prpl-widget-content { + margin-bottom: 1em; +} + +/*------------------------------------*\ + Published content widget. +\*------------------------------------*/ +.prpl-widget-wrapper.prpl-published-content .two-col { + align-items: flex-start; +} + +.prpl-widget-wrapper.prpl-published-content table { + width: 100%; + margin-bottom: 1em; +} + +.prpl-widget-wrapper.prpl-published-content th, +.prpl-widget-wrapper.prpl-published-content td { + border: none; +} + +.prpl-widget-wrapper.prpl-published-content th { + text-align: start; + border-bottom: 1px solid var(--prpl-color-gray-3); +} + +.prpl-widget-wrapper.prpl-published-content th:not(:first-child), +.prpl-widget-wrapper.prpl-published-content td:not(:first-child) { + padding: 0.5em; + text-align: center; +} + +.prpl-widget-wrapper.prpl-published-content tr:nth-child(even) { + background-color: #f9fafb; +} + +.prpl-widget-wrapper.prpl-published-content tr:last-child td { + border-bottom: none; +} + +/*------------------------------------*\ + Individual badge widgets. +\*------------------------------------*/ +.prpl-badges-columns-wrapper { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(calc(var(--prpl-column-min-width) / 2), 1fr)); + grid-gap: var(--prpl-padding); +} + +.prpl-badge-gauge, +.prpl-activities-gauge { + --background: var(--prpl-background-blue); + --cutout: 57%; + --max: 270deg; + --start: -135deg; + --color: var(--prpl-color-accent-orange); + width: 100%; + aspect-ratio: 1 / 1; + border-radius: 100%; + position: relative; + background: + radial-gradient(var(--background) 0 var(--cutout), transparent var(--cutout) 100%), + conic-gradient(from var(--start), var(--color) calc(var(--max) * var(--value)), var(--prpl-color-gray-1) calc(var(--max) * var(--value)) var(--max), transparent var(--max)); + text-align: center; +} + +.prpl-badge-gauge svg { + aspect-ratio: 1.15; + filter: drop-shadow(0 0 1px rgb(0 0 0 / 0.4)); +} + +.prpl-badge-wrapper { + background: var(--prpl-background-blue); + padding: var(--prpl-padding); + border-radius: calc(var(--prpl-border-radius) * 2); + position: relative; + overflow: hidden; +} + +.prpl-badge-wrapper * { + z-index: 1; +} + +.prpl-badge-watermark { + display: block; + width: 150%; + height: 150%; + background-image: url(data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIGFyaWEtaGlkZGVuPSJ0cnVlIiBmb2N1c2FibGU9ImZhbHNlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNjggNTAwIj48cGF0aCBmaWxsPSIjMzgyOTZkIiBkPSJNMjE3LjQ2IDE3Mi45YzMuMjEuMTIgNS45NyAxLjc0IDcuNzMgNC4xNS0xLjg3LTEwLjI0LTEwLjY0LTE4LjE3LTIxLjQ4LTE4LjU2LTEyLjUyLS40NS0yMy4wMyA5LjMzLTIzLjQ4IDIxLjg1LS40NSAxMi41MiA5LjMzIDIzLjAzIDIxLjg1IDIzLjQ4IDkuNC4zNCAxNy42Ny01LjEgMjEuNC0xMy4xMy0xLjgzIDEuNTEtNC4xOCAyLjQyLTYuNzQgMi4zMy01LjU1LS4yLTkuODktNC44Ni05LjY5LTEwLjQxLjItNS41NSA0Ljg2LTkuODkgMTAuNDEtOS42OVpNMjQxLjUxIDMwNS44NGMuNTggMS45MiAxLjEzIDMuODYgMS43MyA1Ljc3IDE0LjA0IDQ0Ljk3IDMzLjk0IDg4Ljc1IDU2LjQyIDEyNC4yN2w2Ny43NS0xMzAuMDRoLTEyNS45Wk0yOTcuOTYgMjA1Ljk3YzEyLjEyLTQuNSAyMy41NC03LjE4IDMzLjY0LTguOTYtMjIuNTEtMjIuMjctNjEuMjQtMjcuMDYtNjEuNDctMjcuMDkgMS4yNyA2LjE3LjU4IDE1LjgtMi40NCAyNi40Ni0zLjMgMTEuNjYtOS4zOCAyNC41NC0xOC43IDM1LjQ4LTMuNDUgNC4wNi03LjM2IDcuODMtMTEuNzMgMTEuMTloLjA3di0uMDFjLjE2LjYyLjM4IDEuMi41OCAxLjc5IDIuNzQgOC4yNyA4LjYxIDEzLjc0IDE0LjkzIDE3LjE0IDYuNDggMy40OSAxMy4zNyA0LjgzIDE3LjY4IDQuODMgNi40IDAgMTEuODgtMy43OSAxNC40My05LjIyLjk3LTIuMDYgMS41NS00LjMzIDEuNTUtNi43NiAwLTMuODUtMS40Mi03LjM0LTMuNjktMTAuMS0xLjkyLTIuMzMtNC40Ni00LjA4LTcuMzktNS4wM2w0NC44Mi04LjY1Yy02LjYzLTYuMTItMTQuNzItMTEuNTktMjIuNzMtMTYuMjMtMS45Ny0xLjE0LTEuNjktNC4wNS40NS00Ljg0WiIvPjxwYXRoIGZpbGw9IiNmYWEzMTAiIGQ9Ik0yODEuMzcgNDU4LjM3Yy0yNS43OS0zOC44NC00OC42OC04OC4wNC02NC40NS0xMzguNTQtMS40NS00LjYzLTIuODMtOS4zMS00LjE3LTEzLjk5LTEuMTItMy45NC0yLjIyLTcuODgtMy4yNS0xMS44LTIuMDktNy45Mi05LjI4LTEzLjQ2LTE3LjQ4LTEzLjQ2aC0yNy45NWMtOC4yIDAtMTUuMzkgNS41My0xNy40OCAxMy40NS0yLjI4IDguNjUtNC43OCAxNy4zMi03LjQyIDI1Ljc5LTE1Ljc3IDUwLjUtMzguNjUgOTkuNy02NC40NSAxMzguNTQtNC4wMSA2LjAzLTEuNzggMTEuNjMtLjY0IDEzLjc2IDIuNCA0LjQ3IDYuODYgNy4xNCAxMS45NCA3LjE0aDY2LjAxbDMuOTcgNi45MmM0LjU0IDcuOSAxMi45OSAxMi44MSAyMi4wNSAxMi44MXMxNy41MS00LjkxIDIyLjA2LTEyLjgxbDMuOTgtNi45Mmg2NmMzLjIyIDAgNi4xOS0xLjA4IDguNTUtMy4wMiAxLjM1LTEuMTEgMi41MS0yLjQ5IDMuMzgtNC4xMy41Ny0xLjA3IDEuNDItMy4wMiAxLjYxLTUuNDYuMTktMi40MS0uMjYtNS4zMS0yLjI1LTguMzFaIi8+PHBhdGggZmlsbD0iIzM4Mjk2ZCIgZD0iTTI5NS43IDc2LjA2Yy03LjU0LTEyLjA1LTMyLjM4IDEtNTkuNTQgMi44Ni0xNS4wNCAxLjAzLTM3LjA1LTExMC42My03MS43Ny01Ni45OS0zOS41NiA2MS4xLTc5LjEyLTQ0LjY4LTg4LjY2LTE1LjgzLTIxLjExIDQzLjI3IDI1LjE1IDg0LjYxIDI1LjE1IDg0LjYxcy0xMi44NCA3LjkyLTIwLjYzIDEzLjkzYy01LjQ3IDQuMTctMTAuODIgOC42NS0xNi4wMyAxMy41MS0yMC40NSAxOS4wMy0zNi4wNCA0MC4zMi00Ni43NyA2My44NkM2LjcyIDIwNS41NSAxLjExIDIyOS41OS42MiAyNTQuMTVjLS40OSAyNC41NiA0LjAxIDQ5LjEgMTMuNTQgNzMuNjMgOS41MiAyNC41MyAyNC4xNyA0Ny40MiA0My45NSA2OC42OCA0LjAyIDQuMzIgOC4xMiA4LjQxIDEyLjMxIDEyLjMgNC4xLTYuMzEgNy45Ny0xMi43NCAxMS42NC0xOS4yNiA0LjM5LTcuOCA4LjUtMTUuNzIgMTIuMjUtMjMuNzgtLjMzLS4zNS0uNjYtLjY5LS45OS0xLjAzLS4xNy0uMTgtLjM0LS4zNS0uNTEtLjUzLTE1LjUzLTE2LjY5LTI3LjE3LTM0LjU5LTM0LjkzLTUzLjcyLTcuNzctMTkuMTMtMTEuNS0zOC4yNS0xMS4yLTU3LjM2LjI5LTE5LjEgNC40Ny0zNy42OCAxMi41My01NS43MiA4LjA2LTE4LjA1IDIwLjAyLTM0LjQ1IDM1LjktNDkuMjIgMTMuOTktMTMuMDIgMjguODQtMjIuODMgNDQuNTUtMjkuNDEgMTUuNy02LjU5IDMxLjYzLTkuOTggNDcuNzYtMTAuMTggOS4wNS0uMTEgMTkuMTEgMS4xNSAyOS41MSA0LjUgMTAuMzIgNC4yNyAxOS4yMiA5LjQ0IDI2LjYzIDE1LjM1IDEwLjE5IDguMTMgMTcuNjEgMTcuNjUgMjIuMjIgMjguMSAxLjkxIDQuMzIgMy4zNyA4LjggNC4zMiAxMy40MSAxNi4yNy0yOC4yNyAzNi43NS03NS45NiAyNS41Ny05My44M1oiLz48L3N2Zz4=); + position: absolute; + top: -20%; + left: -10%; + opacity: 0.04; + z-index: 0; + filter: grayscale(80%); + background-repeat: no-repeat; +} + +/*------------------------------------*\ + Streak badge widget. +\*------------------------------------*/ +.prpl-widget-wrapper.prpl-badge-streak .prpl-badge-wrapper { + background: var(--prpl-background-red); +} + +.prpl-widget-wrapper.prpl-badge-streak .prpl-badge-wrapper .prpl-badge-gauge { + --background: var(--prpl-background-red); +} + +/*------------------------------------*\ + Personal record widget. +\*------------------------------------*/ +.prpl-widget-wrapper.prpl-personal-record-content .counter-big-wrapper { + background-color: var(--prpl-background-green); +} + +/*------------------------------------*\ + What's new widget. +\*------------------------------------*/ +.prpl-widget-wrapper.prpl-whats-new ul { + margin: 0; +} + +.prpl-widget-wrapper.prpl-whats-new ul p { + margin: 0; +} + +.prpl-widget-wrapper.prpl-whats-new li > a { + color: var(--prpl-color-gray-6); + text-decoration: none; +} + +.prpl-widget-wrapper.prpl-whats-new li > a h3 { + margin-top: 0; + font-size: 1.15em; + font-weight: 600; +} + +.prpl-widget-wrapper.prpl-whats-new li > a h3::after { + content: url('data:image/svg+xml,<%3Fxml version="1.0" encoding="UTF-8"%3F>'); + margin-left: 0.25em; + width: .75em; + height: .75em; + display: inline-block; +} + +.prpl-widget-wrapper.prpl-whats-new li img { + width: 100%; +} + +.prpl-blog-post-image { + width: 100%; + min-height: 120px; + aspect-ratio: 16 / 9; + background-size: cover; + margin-bottom: 1em; + border-radius: var(--prpl-border-radius); + box-shadow: 5px 5px 5px var(--prpl-color-gray-2); + border: 1px solid var(--prpl-color-gray-2); +} + +/*------------------------------------*\ + Latest badge widget. +\*------------------------------------*/ + +.prpl-widget-wrapper.prpl-latest-badge img { + border: 1px solid var(--prpl-color-gray-2); + border-radius: var(--prpl-border-radius); +} + +/*------------------------------------*\ + Plugins widget. +\*------------------------------------*/ + +/* .prpl-widget-wrapper.prpl-plugins:has(.pending-updates) { + border-color: var(--prpl-color-notification-red); + border-width: 2px; + background-color: var(--prpl-background-orange); +} + +.prpl-widget-wrapper.prpl-plugins:has(.pending-updates) .counter-big-wrapper { + background-color: var(--prpl-background-orange); +} + +.prpl-widget-wrapper.prpl-plugins:has(.pending-updates) .accent { + color: var(--prpl-color-notification-red); + font-weight: 600; +} */ + +.prpl-widget-wrapper.prpl-plugins .counter-big-wrapper { + background-color: var(--prpl-background-green); +} + +/*------------------------------------*\ + Welcome widget. +\*------------------------------------*/ +.prpl-widget-wrapper.prpl-welcome { + padding: 0; + margin-bottom: var(--prpl-gap); + overflow: hidden; +} + +.prpl-welcome .welcome-header { + background: var(--prpl-color-accent-orange); + display: flex; + justify-content: space-between; + align-items: center; +} + +.prpl-welcome .welcome-header h1 { + font-size: var(--prpl-font-size-3xl); + padding: var(--prpl-padding) calc(var(--prpl-gap) * 1.5); + font-weight: 600; +} + +.welcome-header .welcome-header-icon { + background: var(--prpl-background-orange); + background: linear-gradient(105deg, var(--prpl-color-accent-orange) 25%, var(--prpl-background-orange) 25%); + padding: var(--prpl-padding); + padding-left: 100px; + padding-right: calc(var(--prpl-gap) * 1.5); +} + +.welcome-header .welcome-header-icon svg { + height: 100px; +} + +.prpl-welcome .welcome-subheader { + display: flex; + justify-content: space-around; + gap: var(--prpl-padding); +} + +.prpl-welcome .welcome-subheader > div { + padding: var(--prpl-padding); + text-align: center; + color: var(--prpl-color-gray-3); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.prpl-welcome .welcome-subheader .icon { + --font-size: var(--prpl-font-size-4xl); + font-size: var(--font-size); + width: var(--font-size); + height: var(--font-size); +} + +.prpl-widget-wrapper.prpl-welcome .inner-content { + padding: calc(var(--prpl-gap) * 1.5); + padding-bottom: 0; + margin-bottom: calc(var(--prpl-gap) * 1.5); + overflow: hidden; + display: grid; + grid-template-columns: 1fr 1px 1fr; + grid-gap: calc(var(--prpl-gap) / 2); /* halve it because we have a separator */ +} + +.prpl-widget-wrapper.prpl-welcome .inner-content .separator { + background: var(--prpl-color-gray-1); + display: block; + height: 100%; +} + +/*------------------------------------*\ + Onboarding form. +\*------------------------------------*/ +#prpl-onboarding-form label, +#prpl-onboarding-submit-grid-wrapper { + display: grid; + grid-template-columns: 1fr 3fr; + margin-bottom: 0.5em; + gap: var(--prpl-padding); +} + +#prpl-onboarding-form label >span:has(input[type="checkbox"]) { + display: flex; + align-items: baseline; +} + +#prpl-onboarding-form input[type="submit"] { + display: block; + margin: 1em 0; + padding: 0.5em 1em; + font-size: var(--prpl-font-size-base); + background: var(--prpl-color-accent-red); + box-shadow: none; + border: none; + border-radius: var(--prpl-border-radius); +} + +#prpl-onboarding-form input[type="submit"]:hover, +#prpl-onboarding-form input[type="submit"]:focus { + text-decoration: underline; + box-shadow: 3px 3px 10px var(--prpl-color-accent-red); +} + +.prpl-form-notice { + background: var(--prpl-background-orange); + border: 1px solid var(--prpl-color-accent-orange); + border-radius: var(--prpl-border-radius); + padding: var(--prpl-padding); + margin-bottom: var(--prpl-padding); +} + +/*------------------------------------*\ + Popovers generic styles. +\*------------------------------------*/ +.prpl-popover { + background: #fff; + border: 1px solid var(--prpl-color-gray-3); + border-radius: var(--prpl-border-radius); + padding: var(--prpl-padding); + font-weight: 400; + max-height: 82vh; +} + +.prpl-popover p { + font-weight: 400; +} + +::backdrop { + background: rgba(0, 0, 0, 0.5); +} +/*------------------------------------*\ + Popups close button. +\*------------------------------------*/ +.prpl-popover-close { + position: absolute; + top: 0; + right: 0; + padding: 0.5em; + cursor: pointer; + background: none; + border: none; + color: var(--prpl-color-gray-4); +} + +.prpl-popover-close:hover, +.prpl-popover-close:focus { + color: var(--prpl-color-gray-6); +} + +/*------------------------------------*\ + Badges popover. +\*------------------------------------*/ +#popover-badges-content, +#popover-badges-maintenance { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-gap: var(--prpl-padding); +} + +#popover-badges-content .inner, +#popover-badges-maintenance .inner { + border-radius: var(--prpl-border-radius-big); + padding: var(--prpl-padding); + font-size: var(--prpl-font-size-small); + text-align: center; +} + +#popover-badges-content .inner { + background: var(--prpl-background-red); +} + +#popover-badges-maintenance .inner { + background: var(--prpl-background-blue); +} + +.badges-popover-progress-total { + display: block; + width: 100%; + height: 20px; + background: var(--prpl-color-gray-1); +} + +.badges-popover-progress-total > span { + display: block; + height: 100%; + background: var(--prpl-color-accent-red); +} + +#prpl-popover-badges-details .indicators-maintenance { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-gap: var(--prpl-padding); +} + +#prpl-popover-badges-details .indicators-maintenance .indicator { + text-align: center; + line-height: 1.2; + margin-top: 5px; +} + +#prpl-popover-badges-details .indicators-maintenance .indicator .number { + font-size: var(--prpl-font-size-2xl); + font-weight: 500; + display: block; +} + +#prpl-popover-badges-details .prpl-widgets-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(var(--prpl-column-min-width), 1fr)); + grid-gap: var(--prpl-gap); +} + +.string-freeze-explain { + max-width: 42em; +} + +/*------------------------------------*\ + Mobile styles. +\*------------------------------------*/ +@media all and (max-width:610px) { + :root { + --prpl-gap: 16px; + --prpl-padding: 10px; + --prpl-column-min-width: 0; + } + + .prpl-column-main, + .prpl-column-main-primary .prpl-column-two-col { + grid-template-columns: 1fr; + } + + .prpl-welcome .welcome-header { + flex-wrap: wrap; + } + + .prpl-welcome .welcome-subheader { + display: none; + } + + .prpl-widget-wrapper.prpl-welcome .inner-content, + #prpl-onboarding-form label, + #prpl-onboarding-submit-grid-wrapper { + display: flex; + flex-direction: column; + } + + .prpl-graph-wrapper { + max-width: calc(100vw - 4 * var(--prpl-padding) - 4 * var(--prpl-gap)); + } +} + +/*------------------------------------*\ + TODO list styles. +\*------------------------------------*/ + +.prpl-widget-wrapper.prpl-todo { + padding-left: 0; +} + +.prpl-widget-wrapper.prpl-todo > * { + padding-left: var(--prpl-padding); +} + +#create-todo-item { + display: flex; + align-items: center; + flex-direction: row-reverse; + gap: 1em; +} + +#create-todo-item button { + padding: 0; + border: 1.5px solid; + border-radius: 50%; + background: none; + box-shadow: none; + color: var(--prpl-color-gray-3); + display: flex; + align-items: center; + justify-content: center; + padding: 0.2em; +} + +#create-todo-item button .dashicons { + font-size: 0.825em; + width: 1em; + height: 1em; +} + +#new-todo-content { + flex: 1; + min-width: 0; +} + +#todo-list { + list-style: none; + padding: 0; + max-height:30em; + overflow-y: auto; + margin: 0 0 .5em -5px; +} + +#todo-list li { + position: relative; + display: flex; + align-items: center; +} + +#todo-list li:not(:focus-within):has(:checked) .content { + opacity: 0.5; + text-decoration: line-through; +} + +#todo-list li .content { + padding: 0 .5em; + width: 100%; + display:border-box; + border-bottom: 1.5px solid transparent; +} + +#todo-list li:focus-within .content { + outline: none; + border-bottom: 1.5px solid var(--prpl-color-gray-3); +} + +#todo-list li input { + margin: 0 5px; +} + +#todo-list li .trash { + opacity: 0; + padding: 0; + border: 0; + background: none; + color: var(--prpl-color-gray-3); + cursor: pointer; + box-shadow: none; + transition: all 0.1s; +} + +#todo-list li:hover .trash, +#todo-list li:focus-within .trash { + opacity: 1; +} + +#todo-list li .trash:hover { + color: var(--prpl-color-accent-red); +} + +.prpl-todo-drag-handle { + width: var(--prpl-padding); + display: flex; + opacity: 0; +} + +#todo-list li:hover .prpl-todo-drag-handle, +#todo-list li:focus-within .prpl-todo-drag-handle { + opacity: 1; +} + +#todo-list li input[type=checkbox]:checked::before { + content: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%3Cpath%20d%3D%27M14.83%204.89l1.34.94-5.81%208.38H9.02L5.78%209.67l1.34-1.25%202.57%202.4z%27%20fill%3D%27%2316a34a%27%2F%3E%3C%2Fsvg%3E"); +} + +/*------------------------------------*\ + Dashboard widget styles. +\*------------------------------------*/ +#progress_planner_dashboard_widget_score .prpl-dashboard-widget { + display: grid; + grid-template-columns: 55% 1px 30%; + grid-gap: calc(var(--prpl-gap) / 2); +} + +#progress_planner_dashboard_widget_score .prpl-badge-wrapper { + background: none; + display: grid; + grid-template-columns: 1fr max-content; + grid-gap: 1em; + align-items: center; + border: none; + padding: 0 0 1em 0; +} + +#progress_planner_dashboard_widget_score .prpl-gauge-0, .prpl-gauge-100 { + font-size: var(--prpl-font-size-small); + position: absolute; + top: 50%; + color: var(--prpl-color-gray-5); +} + +#progress_planner_dashboard_widget_score .prpl-gauge-0 { + left: 6px; +} + +#progress_planner_dashboard_widget_score .prpl-gauge-100 { + right: 0px; +} + +#progress_planner_dashboard_widget_score .prpl-badge-wrapper .progress-percent { + font-size: 28px; + font-weight: 600; + padding-top: 0; + color: var(--prpl-color-gray-5); +} + +#progress_planner_dashboard_widget_score h3 { + font-weight: 500; +} + +#progress_planner_dashboard_widget_score .grid-separator { + background: #c3c4c7; /* same color as the one WP-Core uses */ + width: 0.5px; + height: 100%; +} + +#progress_planner_dashboard_widget_score .prpl-badge-gauge { + width: 64px; +} + +#progress_planner_dashboard_widget_score .prpl-dashboard-widget-latest-activities { + margin-top: 1em; + padding-top: 1em; + border-top: 1px solid #c3c4c7; /* same color as the one WP-Core uses */ +} + +#progress_planner_dashboard_widget_score .prpl-dashboard-widget-latest-activities li { + display: flex; + justify-content: space-between; +} + +#progress_planner_dashboard_widget_score .prpl-dashboard-widget-footer { + margin-top: 1em; + padding-top: 1em; + border-top: 1px solid #c3c4c7; /* same color as the one WP-Core uses */ + font-size: var(--prpl-font-size-base); + display: flex; + gap: 1em; + align-items: center; +} + +#progress_planner_dashboard_widget_score .prpl-activities-gauge-container { + background-color: #ffffff; +} + +#progress_planner_dashboard_widget_score .prpl-activities-gauge-container-container p { + text-align: center; + font-size: var(--prpl-font-size-base); + color: var(--prpl-color-gray-5); + margin-top: -15px; +} + +/*------------------------------------*\ + Progress Planner TODO Dashboard widget styles. +\*------------------------------------*/ +#progress_planner_dashboard_widget_todo #create-todo-item { + padding: 0 16px; +} + +#prpl-dashboard-widget-todo-header { + display: flex; + gap: 1em; + align-items: center; + margin-bottom: 1em; + padding: 0 16px; +} + +#prpl-dashboard-widget-todo-header p, #todo-list li { + font-size: 14px; +} diff --git a/assets/images/badges/streak_badge1.svg b/assets/images/badges/streak_badge1.svg new file mode 100644 index 000000000..10e8e1da9 --- /dev/null +++ b/assets/images/badges/streak_badge1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/badges/streak_badge1_gray.svg b/assets/images/badges/streak_badge1_gray.svg new file mode 100644 index 000000000..d94287ce0 --- /dev/null +++ b/assets/images/badges/streak_badge1_gray.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/badges/streak_badge2.svg b/assets/images/badges/streak_badge2.svg new file mode 100644 index 000000000..0bc6abe98 --- /dev/null +++ b/assets/images/badges/streak_badge2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/badges/streak_badge2_gray.svg b/assets/images/badges/streak_badge2_gray.svg new file mode 100644 index 000000000..f8d27daae --- /dev/null +++ b/assets/images/badges/streak_badge2_gray.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/badges/streak_badge3.svg b/assets/images/badges/streak_badge3.svg new file mode 100644 index 000000000..54117a7dc --- /dev/null +++ b/assets/images/badges/streak_badge3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/badges/streak_badge3_gray.svg b/assets/images/badges/streak_badge3_gray.svg new file mode 100644 index 000000000..1e8f6d07a --- /dev/null +++ b/assets/images/badges/streak_badge3_gray.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/badges/writing_badge1.svg b/assets/images/badges/writing_badge1.svg new file mode 100644 index 000000000..70923969c --- /dev/null +++ b/assets/images/badges/writing_badge1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/badges/writing_badge1_gray.svg b/assets/images/badges/writing_badge1_gray.svg new file mode 100644 index 000000000..fc6a82b2a --- /dev/null +++ b/assets/images/badges/writing_badge1_gray.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/badges/writing_badge2.svg b/assets/images/badges/writing_badge2.svg new file mode 100644 index 000000000..1286d8121 --- /dev/null +++ b/assets/images/badges/writing_badge2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/badges/writing_badge2_gray.svg b/assets/images/badges/writing_badge2_gray.svg new file mode 100644 index 000000000..56df11ee9 --- /dev/null +++ b/assets/images/badges/writing_badge2_gray.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/badges/writing_badge3.svg b/assets/images/badges/writing_badge3.svg new file mode 100644 index 000000000..70154a956 --- /dev/null +++ b/assets/images/badges/writing_badge3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/badges/writing_badge3_gray.svg b/assets/images/badges/writing_badge3_gray.svg new file mode 100644 index 000000000..395530e03 --- /dev/null +++ b/assets/images/badges/writing_badge3_gray.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/icon_progress_planner.svg b/assets/images/icon_progress_planner.svg new file mode 100644 index 000000000..cf3762711 --- /dev/null +++ b/assets/images/icon_progress_planner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/logo_progress_planner.svg b/assets/images/logo_progress_planner.svg new file mode 100644 index 000000000..1869ddfe2 --- /dev/null +++ b/assets/images/logo_progress_planner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/js/ajax-request.js b/assets/js/ajax-request.js new file mode 100644 index 000000000..2e1f790c9 --- /dev/null +++ b/assets/js/ajax-request.js @@ -0,0 +1,55 @@ +/* global XMLHttpRequest */ + +/** + * A helper to make AJAX requests. + * + * @param {Object} params The callback parameters. + * @param {string} params.url The URL to send the request to. + * @param {Object} params.data The data to send with the request. + * @param {Function} params.successAction The callback to run on success. + * @param {Function} params.failAction The callback to run on failure. + */ +// eslint-disable-next-line no-unused-vars +const progressPlannerAjaxRequest = ( { + url, + data, + successAction, + failAction, +} ) => { + const http = new XMLHttpRequest(); + http.open( 'POST', url, true ); + http.onreadystatechange = () => { + const defaultCallback = ( response ) => { + // eslint-disable-next-line no-console + console.info( response ); + }; + + let response; + try { + response = JSON.parse( http.response ); + } catch ( e ) { + if ( http.readyState === 4 && http.status !== 200 ) { + // eslint-disable-next-line no-console + console.warn( http, e ); + return http.response; + } + } + if ( http.readyState === 4 && http.status === 200 ) { + return successAction + ? successAction( response ) + : defaultCallback( response ); + } + return failAction + ? failAction( response ) + : defaultCallback( response ); + }; + + const dataForm = new FormData(); + + // eslint-disable-next-line prefer-const + for ( let [ key, value ] of Object.entries( data ) ) { + dataForm.append( key, value ); + } + + http.send( dataForm ); +}; diff --git a/assets/js/header-filters.js b/assets/js/header-filters.js new file mode 100644 index 000000000..7eb1bdf92 --- /dev/null +++ b/assets/js/header-filters.js @@ -0,0 +1,19 @@ +// Handle changes to the range dropdown. +document + .getElementById( 'prpl-select-range' ) + .addEventListener( 'change', function () { + const range = this.value; + const url = new URL( window.location.href ); + url.searchParams.set( 'range', range ); + window.location.href = url.href; + } ); + +// Handle changes to the frequency dropdown. +document + .getElementById( 'prpl-select-frequency' ) + .addEventListener( 'change', function () { + const frequency = this.value; + const url = new URL( window.location.href ); + url.searchParams.set( 'frequency', frequency ); + window.location.href = url.href; + } ); diff --git a/assets/js/onboard.js b/assets/js/onboard.js new file mode 100644 index 000000000..4215efa6f --- /dev/null +++ b/assets/js/onboard.js @@ -0,0 +1,98 @@ +/* global progressPlanner, progressPlannerAjaxRequest, progressPlannerTriggerScan */ + +/** + * Make a request to save the license key. + * + * @param {string} licenseKey The license key. + */ +const progressPlannerSaveLicenseKey = ( licenseKey ) => { + // eslint-disable-next-line no-console + console.log( 'License key: ' + licenseKey ); + progressPlannerAjaxRequest( { + url: progressPlanner.ajaxUrl, + data: { + action: 'progress_planner_save_onboard_data', + _ajax_nonce: progressPlanner.nonce, + key: licenseKey, + }, + } ); +}; + +/** + * Make the AJAX request. + * + * @param {Object} data The data to send with the request. + */ +const progressPlannerAjaxAPIRequest = ( data ) => { + progressPlannerAjaxRequest( { + url: progressPlanner.onboardAPIUrl, + data, + successAction: ( response ) => { + // Show link to reset password. + document.getElementById( + 'prpl-password-reset-link' + ).style.display = 'block'; + document.getElementById( 'prpl-password-reset-link' ).href = + response.password_reset_url; + + // Hide the form. + document.getElementById( 'prpl-onboarding-form' ).style.display = + 'none'; + + // Make a local request to save the response data. + progressPlannerSaveLicenseKey( response.license_key ); + + // Start scanning posts. + progressPlannerTriggerScan(); + }, + failAction: ( response ) => { + // eslint-disable-next-line no-console + console.warn( response ); + }, + } ); +}; + +/** + * Make the AJAX request. + * + * Make a request to get the nonce. + * Once the nonce is received, make a request to the API. + * + * @param {Object} data The data to send with the request. + */ +const progressPlannerOnboardCall = ( data ) => { + progressPlannerAjaxRequest( { + url: progressPlanner.onboardNonceURL, + data, + successAction: ( response ) => { + if ( 'ok' === response.status ) { + // Add the nonce to our data object. + data.nonce = response.nonce; + + // Make the request to the API. + progressPlannerAjaxAPIRequest( data ); + } + }, + } ); +}; + +if ( document.getElementById( 'prpl-onboarding-form' ) ) { + document + .getElementById( 'prpl-onboarding-form' ) + .addEventListener( 'submit', function ( event ) { + event.preventDefault(); + document.querySelector( + '#prpl-onboarding-form input[type="submit"]' + ).disabled = true; + const inputs = this.querySelectorAll( 'input' ); + + // Build the data object. + const data = {}; + inputs.forEach( ( input ) => { + if ( input.name ) { + data[ input.name ] = input.value; + } + } ); + progressPlannerOnboardCall( data ); + } ); +} diff --git a/assets/js/scan-posts.js b/assets/js/scan-posts.js new file mode 100644 index 000000000..501822152 --- /dev/null +++ b/assets/js/scan-posts.js @@ -0,0 +1,88 @@ +/* global progressPlanner, progressPlannerAjaxRequest */ + +// eslint-disable-next-line no-unused-vars +const progressPlannerTriggerScan = () => { + document.getElementById( 'progress-planner-scan-progress' ).style.display = + 'block'; + + /** + * The action to run on a successful AJAX request. + * This function should update the UI and re-trigger the scan if necessary. + * + * @param {Object} response The response from the server. + * The response should contain a `progress` property. + */ + const successAction = ( response ) => { + const progressBar = document.querySelector( + '#progress-planner-scan-progress progress' + ); + // Update the progressbar. + if ( response.data.progress > progressBar.value ) { + progressBar.value = response.data.progress; + } + + // eslint-disable-next-line no-console + console.info( + `Progress: ${ response.data.progress }%, (${ response.data.lastScanned }/${ response.data.lastPage })` + ); + + // Refresh the page when scan has finished. + if ( response.data.progress >= 100 ) { + document.getElementById( + 'progress-planner-scan-progress' + ).style.display = 'none'; + window.location.href = window.location.href.replace( '&content-scan', '' ); + return; + } + + progressPlannerTriggerScan(); + }; + + const failAction = ( response ) => { + // If the window.progressPlannerFailScanCount is not defined, set it to 1. + if ( ! window.progressPlannerFailScanCount ) { + window.progressPlannerFailScanCount = 1; + } else { + window.progressPlannerFailScanCount++; + } + + // If the scan has failed more than 10 times, stop retrying. + if ( window.progressPlannerFailScanCount > 10 ) { + return; + } + + console.warn( 'Failed to scan posts. Retrying...' ); // eslint-disable-line no-console + console.log( response ); // eslint-disable-line no-console + // Retry after 200ms. + setTimeout( progressPlannerTriggerScan, 200 ); + }; + + /** + * The AJAX request to run. + */ + progressPlannerAjaxRequest( { + url: progressPlanner.ajaxUrl, + data: { + action: 'progress_planner_scan_posts', + _ajax_nonce: progressPlanner.nonce, + }, + successAction, + failAction, + } ); +}; + +if ( document.getElementById( 'prpl-scan-button' ) ) { + document.getElementById( 'prpl-scan-button' ).addEventListener( 'click', ( event ) => { + event.preventDefault(); + document.getElementById( 'prpl-scan-button' ).disabled = true; + progressPlannerAjaxRequest( { + url: progressPlanner.ajaxUrl, + data: { + action: 'progress_planner_reset_posts_data', + _ajax_nonce: progressPlanner.nonce, + }, + successAction: progressPlannerTriggerScan, + failAction: progressPlannerTriggerScan, + } ); + } ); +} diff --git a/assets/js/todo.js b/assets/js/todo.js new file mode 100644 index 000000000..39f311b68 --- /dev/null +++ b/assets/js/todo.js @@ -0,0 +1,128 @@ +/* global progressPlannerTodo, jQuery */ + +jQuery( document ).ready( function () { + const saveTodoList = () => { + const todoList = []; + + jQuery( '#todo-list li' ).each( function () { + todoList.push( { + content: jQuery( this ).find( '.content' ).text(), + done: jQuery( this ) + .find( 'input[type="checkbox"]' ) + .prop( 'checked' ), + } ); + } ); + + // Save the todo list to the database + jQuery.post( progressPlannerTodo.ajaxUrl, { + action: 'progress_planner_save_todo_list', + todo_list: todoList, + nonce: progressPlannerTodo.nonce, + } ); + }; + + // Initialize the sortable. + const initSortable = () => { + jQuery( '#todo-list' ).sortable( { + axis: 'y', + handle: '.prpl-todo-drag-handle', + update() { + // Add a 'data-order' attribute to each todo item + // based on its position in the list + jQuery( '#todo-list li' ).each( function ( index ) { + jQuery( this ).attr( 'data-order', index ); + } ); + }, + stop: () => { + saveTodoList(); + }, + } ); + }; + + /** + * Inject a todo item into the DOM. + * + * @param {string} content The content of the todo item. + * @param {boolean} done Whether the todo item is done. + * @param {boolean} addToStart Whether to add the todo item to the start of the list. + * @param {boolean} save Whether to save the todo list to the database. + */ + const injectTodoItem = ( content, done, addToStart, save ) => { + const todoItemElement = jQuery( '
' ).html( ` + + + + + ${ content } + + ` ); + + if ( addToStart ) { + jQuery( '#todo-list' ).prepend( todoItemElement ); + } else { + jQuery( '#todo-list' ).append( todoItemElement ); + } + + if ( save ) { + saveTodoList(); + } + }; + + // Inject the existing todo list items into the DOM + progressPlannerTodo.listItems.forEach( ( todoItem, index, array ) => { + jQuery( '#todo-list' ).append( + injectTodoItem( todoItem.content, todoItem.done, false, false ) + ); + + // If this is the last item in the array, initialize the sortable + if ( index === array.length - 1 ) { + initSortable(); + } + } ); + + // When the '#create-todo-item' form is submitted, + // add a new todo item to the list + jQuery( '#create-todo-item' ).submit( function ( event ) { + event.preventDefault(); + injectTodoItem( + jQuery( '#new-todo-content' ).val(), + false, // Not done. + true, // Add to start. + true // Save. + ); + + jQuery( '#new-todo-content' ).val( '' ); + } ); + + // When an item is marked as done, move it to the end of the list. + // When an item is marked as not done, move it to the start of the list. + jQuery( '#todo-list' ).on( 'change', 'input[type="checkbox"]', function () { + const todoItem = jQuery( this ).closest( 'li' ); + const todoItemContent = todoItem.find( '.content' ).text(); + const todoItemDone = jQuery( this ).prop( 'checked' ); + + todoItem.remove(); + injectTodoItem( + todoItemContent, + todoItemDone, + ! todoItemDone, + true // Save. + ); + } ); + + // When an item's contenteditable element is edited, + // save the new content to the database + jQuery( '#todo-list' ).on( 'input', '.content', function () { + saveTodoList(); + } ); + + // When the trash button is clicked, remove the todo item from the list + jQuery( '#todo-list' ).on( 'click', '.trash', function () { + jQuery( this ).closest( 'li' ).remove(); + saveTodoList(); + } ); +} ); diff --git a/assets/js/vendor/chart.min.js b/assets/js/vendor/chart.min.js new file mode 100644 index 000000000..79f59d7c7 --- /dev/null +++ b/assets/js/vendor/chart.min.js @@ -0,0 +1,20 @@ +/** + * Skipped minification because the original files appears to be already minified. + * Original file: /npm/chart.js@4.4.2/dist/chart.umd.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +/*! + * Chart.js v4.4.2 + * https://www.chartjs.org + * (c) 2024 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Go},get Decimation(){return Qo},get Filler(){return ma},get Legend(){return ya},get SubTitle(){return ka},get Title(){return Ma},get Tooltip(){return Ba}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;a+ +
++ +
++ ' . \esc_html( $latest_badge['name'] ) . '' + ); + ?> +
+ + + personal_record_callback(); + ?> ++ | + | + |
---|---|---|
labels->name ); ?> | ++ | + |
+ +
+ ++ +
+