diff --git a/composer.json b/composer.json index ea26daae66..0220e070b8 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ "symfony/polyfill-ctype": "^1.19", "symfony/polyfill-mbstring": "^1.19", "woocommerce/action-scheduler": "^3.6", - "ext-json": "*" + "ext-json": "*", + "stellarwp/arrays": "^1.2" }, "require-dev": { "fakerphp/faker": "^1.9", @@ -94,6 +95,7 @@ "classmap_prefix": "Give_Vendors_", "constant_prefix": "GIVE_VENDORS_", "packages": [ + "stellarwp/arrays", "stellarwp/validation", "stellarwp/field-conditions", "symfony/polyfill-ctype", diff --git a/composer.lock b/composer.lock index 7f1b489795..2f7e2465ce 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "035a59cb7f15cc6ca6964b692035eb4d", + "content-hash": "9c8a978f81e78e569032fd37c33704de", "packages": [ { "name": "composer/installers", @@ -373,6 +373,63 @@ }, "time": "2021-09-14T21:35:26+00:00" }, + { + "name": "stellarwp/arrays", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/stellarwp/arrays.git", + "reference": "fe0a4cec1a299c196cc89bdba18ac99dea6bf5a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stellarwp/arrays/zipball/fe0a4cec1a299c196cc89bdba18ac99dea6bf5a2", + "reference": "fe0a4cec1a299c196cc89bdba18ac99dea6bf5a2", + "shasum": "" + }, + "require-dev": { + "codeception/module-asserts": "^1.0", + "codeception/module-cli": "^1.0", + "codeception/module-db": "^1.0", + "codeception/module-filesystem": "^1.0", + "codeception/module-phpbrowser": "^1.0", + "codeception/module-rest": "^1.0", + "codeception/module-webdriver": "^1.0", + "codeception/util-universalframework": "^1.0", + "lucatume/wp-browser": "^3.0.14", + "phpunit/phpunit": "~6.0", + "saggre/phpdocumentor-markdown": "^0.1.3", + "symfony/event-dispatcher-contracts": "^2.5.1", + "symfony/string": "^5.4", + "szepeviktor/phpstan-wordpress": "^1.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "StellarWP\\Arrays\\": "src/Arrays/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0" + ], + "authors": [ + { + "name": "StellarWP", + "email": "dev@stellarwp.com" + }, + { + "name": "Matthew Batchelder", + "email": "matt.batchelder@stellarwp.com" + } + ], + "description": "A library for array manipulation.", + "support": { + "issues": "https://github.com/stellarwp/arrays/issues", + "source": "https://github.com/stellarwp/arrays/tree/1.2.2" + }, + "time": "2023-11-14T12:48:06+00:00" + }, { "name": "stellarwp/container-contract", "version": "1.0.4", diff --git a/give.php b/give.php index 2b3fb75711..34e55b7e8d 100644 --- a/give.php +++ b/give.php @@ -42,6 +42,7 @@ * - The GiveWP Team */ +use Give\Campaigns\Repositories\CampaignRepository; use Give\Container\Container; use Give\DonationForms\ServiceProvider as DonationFormsServiceProvider; use Give\DonationForms\V2\Repositories\DonationFormsRepository; @@ -132,6 +133,7 @@ * @property-read DonorRepositoryProxy $donors * @property-read SubscriptionRepository $subscriptions * @property-read DonationFormsRepository $donationForms + * @property-read CampaignRepository $campaigns * @property-read Profile $donorDashboard * @property-read TabsRegister $donorDashboardTabs * @property-read Give_Recurring_DB_Subscription_Meta $subscription_meta @@ -243,6 +245,7 @@ final class Give Give\FormTaxonomies\ServiceProvider::class, Give\DonationSpam\ServiceProvider::class, Give\Settings\ServiceProvider::class, + Give\Campaigns\ServiceProvider::class, Give\FeatureFlags\OptionBasedFormEditor\ServiceProvider::class, ]; diff --git a/includes/post-types.php b/includes/post-types.php index e0a0e3c313..60edcc2002 100644 --- a/includes/post-types.php +++ b/includes/post-types.php @@ -56,8 +56,8 @@ function give_setup_post_types() { 'name' => __( 'Donation Forms', 'give' ), 'singular_name' => __( 'Form', 'give' ), 'add_new' => __( 'Add Form', 'give' ), - 'add_new_item' => __('Add New Donation Form', 'give'), - 'edit_item' => __('Edit Donation Form', 'give'), + 'add_new_item' => __( 'Add New Campaign Form', 'give' ), + 'edit_item' => __( 'Edit Campaign Form', 'give' ), 'new_item' => __( 'New Form', 'give' ), 'all_items' => __( 'All Forms', 'give' ), 'view_item' => __( 'View Form', 'give' ), diff --git a/package-lock.json b/package-lock.json index 8a1a72f537..b9d906eb54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@fortawesome/free-regular-svg-icons": "^5.15.1", "@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/react-fontawesome": "^0.1.12", - "@givewp/design-system-foundation": "^1.1.0", + "@givewp/design-system-foundation": "^1.2.0", "@givewp/form-builder-library": "^1.7.0", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^2.9.10", @@ -23,6 +23,7 @@ "@stripe/react-stripe-js": "^2.1.0", "@stripe/stripe-js": "^1.52.0", "@svgr/webpack": "^6.5.1", + "@wordpress/api-fetch": "^7.8.0", "@wordpress/block-editor": "^11.2.0", "@wordpress/block-library": "^8.15.0", "@wordpress/components": "^23.2.0", @@ -35,6 +36,7 @@ "@wordpress/interface": "^5.11.0", "@wordpress/server-side-render": "^4.2.0", "accounting": "^0.4.1", + "ajv": "^7.0.4", "axios": "^0.21.2", "chart.js": "^2.9.3", "chartjs-plugin-crosshair": "^1.1.4", @@ -63,6 +65,7 @@ "react-a11y-dialog": "^6.1.5", "react-acceptjs": "^0.3.0", "react-ace": "^10.1.0", + "react-apexcharts": "^1.4.1", "react-aria-components": "^1.0.0-alpha.6", "react-beautiful-dnd": "^13.1.1", "react-csv": "^2.0.1", @@ -2321,6 +2324,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@eslint/eslintrc/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2354,6 +2373,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2544,9 +2569,9 @@ } }, "node_modules/@givewp/design-system-foundation": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@givewp/design-system-foundation/-/design-system-foundation-1.1.0.tgz", - "integrity": "sha512-SOAS98QQOytIGsyDX55y4TCS0DeKijjmOPnNaG0YbClTL2u7HFNthqRHk246BXZ0s6U+CUzqZQ8mf/+3NY4Z1g==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@givewp/design-system-foundation/-/design-system-foundation-1.2.0.tgz", + "integrity": "sha512-MjWKkpaU5NfpOmatsRkx7NH++NQcuvhdPWbq/Xs3c3BF6OI1AxwU4kTrrglv+WymIanZt3nz2jTq0gNAbNHMRA==" }, "node_modules/@givewp/form-builder-library": { "version": "1.7.0", @@ -3718,6 +3743,31 @@ } } }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3734,6 +3784,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/loader-utils": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", @@ -7703,18 +7759,74 @@ } }, "node_modules/@wordpress/api-fetch": { - "version": "6.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.52.0.tgz", - "integrity": "sha512-zLgpRT6iKdfQupF7hGYbixjqgkeU2taclEHbbQqP6ClLfG709I3kX6Ft+2wh6FaG8MhdVZkl0/E0DTROJ5lbyA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-7.8.0.tgz", + "integrity": "sha512-yQx/zoM9e1vNWHSJVPvvspqGap/JMwtnxAvMDqUVUEETXwwGqaBffJCxVyGOfPhx/3cIw2T88xVxz0dgZ76a1w==", "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "^4.55.0", - "@wordpress/url": "^3.56.0" + "@wordpress/i18n": "^5.8.0", + "@wordpress/url": "^4.8.0" }, "engines": { - "node": ">=12" + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/api-fetch/node_modules/@wordpress/hooks": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.8.0.tgz", + "integrity": "sha512-6CPXtkZOHg8Q9gFulbuB+V74yCaPK2E2nRMw2BXE1yNfIAItqMbUiC8zrNOamtLcg3ifsk1PPeJ2DX5mR7Wyug==", + "dependencies": { + "@babel/runtime": "^7.16.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/api-fetch/node_modules/@wordpress/i18n": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-5.8.0.tgz", + "integrity": "sha512-pPx8RPT69Kds8wygHGfkt+D2jxdyu2HIYw3yM+dj47rNW2rHtZFVoOr+QzwOJ4yoHRuN1zMhOfzHsC4WV+ARcg==", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/hooks": "^4.8.0", + "gettext-parser": "^1.3.1", + "memize": "^2.1.0", + "sprintf-js": "^1.1.1", + "tannin": "^1.2.0" + }, + "bin": { + "pot-to-php": "tools/pot-to-php.js" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/api-fetch/node_modules/@wordpress/url": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-4.8.0.tgz", + "integrity": "sha512-8Za/lrTTH3+Y5/shsqmDgQ493Sr1Do99tIyCu62Z2hm6KmP5KH6nHX+kInKtBamdW+fHTBFN56cZj5/AgByM8w==", + "dependencies": { + "@babel/runtime": "^7.16.0", + "remove-accents": "^0.5.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" } }, + "node_modules/@wordpress/api-fetch/node_modules/memize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/memize/-/memize-2.1.0.tgz", + "integrity": "sha512-yywVJy8ctVlN5lNPxsep5urnZ6TTclwPEyigM9M3Bi8vseJBOfqNrGWN/r8NzuIt3PovM323W04blJfGQfQSVg==" + }, + "node_modules/@wordpress/api-fetch/node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + }, "node_modules/@wordpress/autop": { "version": "3.39.0", "resolved": "https://registry.npmjs.org/@wordpress/autop/-/autop-3.39.0.tgz", @@ -7836,6 +7948,19 @@ "react-dom": "^18.0.0" } }, + "node_modules/@wordpress/block-editor/node_modules/@wordpress/api-fetch": { + "version": "6.55.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.55.0.tgz", + "integrity": "sha512-1HrCUsJdeRY5Y0IjplotINwqMRO81e7O7VhBScuKk7iOuDm/E1ioKv2uLGnPNWziYu+Zf025byxOqVzXDyM2gw==", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/i18n": "^4.58.0", + "@wordpress/url": "^3.59.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@wordpress/block-editor/node_modules/@wordpress/compose": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-6.16.0.tgz", @@ -7955,6 +8080,19 @@ "react-dom": "^18.0.0" } }, + "node_modules/@wordpress/block-library/node_modules/@wordpress/api-fetch": { + "version": "6.55.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.55.0.tgz", + "integrity": "sha512-1HrCUsJdeRY5Y0IjplotINwqMRO81e7O7VhBScuKk7iOuDm/E1ioKv2uLGnPNWziYu+Zf025byxOqVzXDyM2gw==", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/i18n": "^4.58.0", + "@wordpress/url": "^3.59.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@wordpress/block-library/node_modules/@wordpress/block-editor": { "version": "12.7.0", "resolved": "https://registry.npmjs.org/@wordpress/block-editor/-/block-editor-12.7.0.tgz", @@ -8557,6 +8695,19 @@ "react": "^18.0.0" } }, + "node_modules/@wordpress/core-data/node_modules/@wordpress/api-fetch": { + "version": "6.55.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.55.0.tgz", + "integrity": "sha512-1HrCUsJdeRY5Y0IjplotINwqMRO81e7O7VhBScuKk7iOuDm/E1ioKv2uLGnPNWziYu+Zf025byxOqVzXDyM2gw==", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/i18n": "^4.58.0", + "@wordpress/url": "^3.59.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@wordpress/core-data/node_modules/@wordpress/block-editor": { "version": "12.7.0", "resolved": "https://registry.npmjs.org/@wordpress/block-editor/-/block-editor-12.7.0.tgz", @@ -8976,6 +9127,20 @@ "@playwright/test": ">=1" } }, + "node_modules/@wordpress/e2e-test-utils-playwright/node_modules/@wordpress/api-fetch": { + "version": "6.55.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.55.0.tgz", + "integrity": "sha512-1HrCUsJdeRY5Y0IjplotINwqMRO81e7O7VhBScuKk7iOuDm/E1ioKv2uLGnPNWziYu+Zf025byxOqVzXDyM2gw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/i18n": "^4.58.0", + "@wordpress/url": "^3.59.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@wordpress/element": { "version": "5.34.0", "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-5.34.0.tgz", @@ -9143,9 +9308,9 @@ } }, "node_modules/@wordpress/hooks": { - "version": "3.57.0", - "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.57.0.tgz", - "integrity": "sha512-+RaPsTj80QNUw3RfiMhxIzaAuYPAvMByrpy97jmodrvhPM5wR9utj40DYIlAiBfMhwACh8NM+kY+UB08CKcmCQ==", + "version": "3.58.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.58.0.tgz", + "integrity": "sha512-9LB0ZHnZRQlORttux9t/xbAskF+dk2ujqzPGsVzc92mSKpQP3K2a5Wy74fUnInguB1vLUNHT6nrNdkVom5qX1Q==", "dependencies": { "@babel/runtime": "^7.16.0" }, @@ -9165,12 +9330,12 @@ } }, "node_modules/@wordpress/i18n": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.57.0.tgz", - "integrity": "sha512-VYWYHE+7NxnZvE9Swhhe4leQcn0jHNkzRAEV36TkfAL/MvrQYCRh71KLTvKhsilG96HUQdBwjH0VPLmYEmR3sg==", + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.58.0.tgz", + "integrity": "sha512-VfvS3BWv/RDjRKD6PscIcvYfWKnGJcI/DEqyDgUMhxCM6NRwoL478CsUKTiGJIymeyRodNRfprdcF086DpGKYw==", "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/hooks": "^3.57.0", + "@wordpress/hooks": "^3.58.0", "gettext-parser": "^1.3.1", "memize": "^2.1.0", "sprintf-js": "^1.1.1", @@ -10131,6 +10296,19 @@ "react-dom": "^18.0.0" } }, + "node_modules/@wordpress/reusable-blocks/node_modules/@wordpress/api-fetch": { + "version": "6.55.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.55.0.tgz", + "integrity": "sha512-1HrCUsJdeRY5Y0IjplotINwqMRO81e7O7VhBScuKk7iOuDm/E1ioKv2uLGnPNWziYu+Zf025byxOqVzXDyM2gw==", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/i18n": "^4.58.0", + "@wordpress/url": "^3.59.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@wordpress/reusable-blocks/node_modules/@wordpress/block-editor": { "version": "12.7.0", "resolved": "https://registry.npmjs.org/@wordpress/block-editor/-/block-editor-12.7.0.tgz", @@ -12368,6 +12546,19 @@ "react-dom": "^18.0.0" } }, + "node_modules/@wordpress/server-side-render/node_modules/@wordpress/api-fetch": { + "version": "6.55.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.55.0.tgz", + "integrity": "sha512-1HrCUsJdeRY5Y0IjplotINwqMRO81e7O7VhBScuKk7iOuDm/E1ioKv2uLGnPNWziYu+Zf025byxOqVzXDyM2gw==", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/i18n": "^4.58.0", + "@wordpress/url": "^3.59.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@wordpress/server-side-render/node_modules/@wordpress/components": { "version": "25.5.0", "resolved": "https://registry.npmjs.org/@wordpress/components/-/components-25.5.0.tgz", @@ -12622,9 +12813,9 @@ } }, "node_modules/@wordpress/url": { - "version": "3.56.0", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.56.0.tgz", - "integrity": "sha512-uW5cTftroxvYSoF2Wy/Rfc5eUuqANXSrqBu8axv1dmNLYbg+2Y8f/bzH1ZNLLtmkpD25QPOIstGjA8lsYRPuig==", + "version": "3.59.0", + "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.59.0.tgz", + "integrity": "sha512-GxvoMjYCav0w4CiX0i0h3qflrE/9rhLIZg5aPCQjbrBdwTxYR3Exfw0IJYcmVaTKXQOUU8fOxlDxULsbLmKe9w==", "dependencies": { "@babel/runtime": "^7.16.0", "remove-accents": "^0.5.0" @@ -12749,6 +12940,12 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "node_modules/@yr/monotone-cubic-spline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==", + "peer": true + }, "node_modules/a11y-dialog": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/a11y-dialog/-/a11y-dialog-7.4.0.tgz", @@ -12874,14 +13071,13 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.4.tgz", + "integrity": "sha512-xzzzaqgEQfmuhbhAoqjJ8T/1okb6gAzXn/eQRNpAN1AEUoHJTNF9xCDRTtf/s3SKldtZfa+RJeTs+BQq+eZ/sw==", "dependencies": { "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", "uri-js": "^4.2.2" }, "funding": { @@ -12931,21 +13127,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/alphanum-sort": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", @@ -13032,6 +13213,21 @@ "node": ">= 8" } }, + "node_modules/apexcharts": { + "version": "3.54.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.54.0.tgz", + "integrity": "sha512-ZgI/seScffjLpwNRX/gAhIkAhpCNWiTNsdICv7qxnF0xisI23XSsaENUKIcMlyP1rbe8ECgvybDnp7plZld89A==", + "peer": true, + "dependencies": { + "@yr/monotone-cubic-spline": "^1.0.3", + "svg.draggable.js": "^2.2.2", + "svg.easing.js": "^2.0.0", + "svg.filter.js": "^2.0.2", + "svg.pathmorphing.js": "^0.1.3", + "svg.resize.js": "^1.4.3", + "svg.select.js": "^3.0.1" + } + }, "node_modules/are-docs-informative": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", @@ -15421,12 +15617,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/copy-webpack-plugin/node_modules/schema-utils": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", @@ -17686,6 +17876,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -17836,6 +18042,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/eslint/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -23072,10 +23284,9 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -23308,6 +23519,31 @@ "integrity": "sha512-AZU7vQcy/4WFEuwnwsNsJnFwupIpbllH1++LXScN6uxT1Z4zPzdrWG97w4/I7eFKFTvfy/bHFStWjdBAg2Vjug==", "dev": true }, + "node_modules/laravel-mix/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/laravel-mix/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/laravel-mix/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -23676,6 +23912,12 @@ "node": ">=8" } }, + "node_modules/laravel-mix/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/laravel-mix/node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -25570,12 +25812,6 @@ "ajv": "^8.8.2" } }, - "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", @@ -26073,6 +26309,22 @@ "npm": ">=6.0.0" } }, + "node_modules/npm-package-json-lint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/npm-package-json-lint/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -26169,6 +26421,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/npm-package-json-lint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/npm-package-json-lint/node_modules/jsonc-parser": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", @@ -29128,7 +29386,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, "engines": { "node": ">=6" } @@ -29550,6 +29807,18 @@ "react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-apexcharts": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.4.1.tgz", + "integrity": "sha512-G14nVaD64Bnbgy8tYxkjuXEUp/7h30Q0U33xc3AwtGFijJB9nHqOt1a6eG0WBn055RgRg+NwqbKGtqPxy15d0Q==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "apexcharts": "^3.41.0", + "react": ">=0.13" + } + }, "node_modules/react-aria": { "version": "3.27.0", "resolved": "https://registry.npmjs.org/react-aria/-/react-aria-3.27.0.tgz", @@ -30802,7 +31071,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -31347,6 +31615,37 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/select": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", @@ -32700,6 +32999,97 @@ "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", "dev": true }, + "node_modules/svg.draggable.js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "peer": true, + "dependencies": { + "svg.js": "^2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.easing.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", + "peer": true, + "dependencies": { + "svg.js": ">=2.3.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.filter.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", + "peer": true, + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==", + "peer": true + }, + "node_modules/svg.pathmorphing.js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "peer": true, + "dependencies": { + "svg.js": "^2.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "peer": true, + "dependencies": { + "svg.js": "^2.6.5", + "svg.select.js": "^2.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js/node_modules/svg.select.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "peer": true, + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.select.js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "peer": true, + "dependencies": { + "svg.js": "^2.6.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/svgo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", @@ -32853,12 +33243,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/tannin": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tannin/-/tannin-1.2.0.tgz", @@ -32985,6 +33369,31 @@ } } }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/terser-webpack-plugin/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -33014,6 +33423,12 @@ "node": ">= 10.13.0" } }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -33807,7 +34222,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -33849,6 +34263,37 @@ } } }, + "node_modules/url-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/url-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/url-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/url-loader/node_modules/loader-utils": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", @@ -34482,12 +34927,6 @@ "ajv": "^8.8.2" } }, - "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/webpack-dev-middleware/node_modules/schema-utils": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", @@ -34594,12 +35033,6 @@ "ajv": "^8.8.2" } }, - "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/webpack-dev-server/node_modules/schema-utils": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", @@ -34801,6 +35234,37 @@ "node": ">=0.10.0" } }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/webpack/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -36880,6 +37344,18 @@ "strip-json-comments": "^3.1.1" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -36904,6 +37380,12 @@ "argparse": "^2.0.1" } }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -37049,9 +37531,9 @@ } }, "@givewp/design-system-foundation": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@givewp/design-system-foundation/-/design-system-foundation-1.1.0.tgz", - "integrity": "sha512-SOAS98QQOytIGsyDX55y4TCS0DeKijjmOPnNaG0YbClTL2u7HFNthqRHk246BXZ0s6U+CUzqZQ8mf/+3NY4Z1g==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@givewp/design-system-foundation/-/design-system-foundation-1.2.0.tgz", + "integrity": "sha512-MjWKkpaU5NfpOmatsRkx7NH++NQcuvhdPWbq/Xs3c3BF6OI1AxwU4kTrrglv+WymIanZt3nz2jTq0gNAbNHMRA==" }, "@givewp/form-builder-library": { "version": "1.7.0", @@ -37913,6 +38395,25 @@ "source-map": "^0.7.3" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -37923,6 +38424,12 @@ "path-exists": "^4.0.0" } }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "loader-utils": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", @@ -41115,13 +41622,55 @@ } }, "@wordpress/api-fetch": { - "version": "6.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.52.0.tgz", - "integrity": "sha512-zLgpRT6iKdfQupF7hGYbixjqgkeU2taclEHbbQqP6ClLfG709I3kX6Ft+2wh6FaG8MhdVZkl0/E0DTROJ5lbyA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-7.8.0.tgz", + "integrity": "sha512-yQx/zoM9e1vNWHSJVPvvspqGap/JMwtnxAvMDqUVUEETXwwGqaBffJCxVyGOfPhx/3cIw2T88xVxz0dgZ76a1w==", "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "^4.55.0", - "@wordpress/url": "^3.56.0" + "@wordpress/i18n": "^5.8.0", + "@wordpress/url": "^4.8.0" + }, + "dependencies": { + "@wordpress/hooks": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.8.0.tgz", + "integrity": "sha512-6CPXtkZOHg8Q9gFulbuB+V74yCaPK2E2nRMw2BXE1yNfIAItqMbUiC8zrNOamtLcg3ifsk1PPeJ2DX5mR7Wyug==", + "requires": { + "@babel/runtime": "^7.16.0" + } + }, + "@wordpress/i18n": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-5.8.0.tgz", + "integrity": "sha512-pPx8RPT69Kds8wygHGfkt+D2jxdyu2HIYw3yM+dj47rNW2rHtZFVoOr+QzwOJ4yoHRuN1zMhOfzHsC4WV+ARcg==", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/hooks": "^4.8.0", + "gettext-parser": "^1.3.1", + "memize": "^2.1.0", + "sprintf-js": "^1.1.1", + "tannin": "^1.2.0" + } + }, + "@wordpress/url": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-4.8.0.tgz", + "integrity": "sha512-8Za/lrTTH3+Y5/shsqmDgQ493Sr1Do99tIyCu62Z2hm6KmP5KH6nHX+kInKtBamdW+fHTBFN56cZj5/AgByM8w==", + "requires": { + "@babel/runtime": "^7.16.0", + "remove-accents": "^0.5.0" + } + }, + "memize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/memize/-/memize-2.1.0.tgz", + "integrity": "sha512-yywVJy8ctVlN5lNPxsep5urnZ6TTclwPEyigM9M3Bi8vseJBOfqNrGWN/r8NzuIt3PovM323W04blJfGQfQSVg==" + }, + "remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + } } }, "@wordpress/autop": { @@ -41224,6 +41773,16 @@ "traverse": "^0.6.6" }, "dependencies": { + "@wordpress/api-fetch": { + "version": "6.55.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.55.0.tgz", + "integrity": "sha512-1HrCUsJdeRY5Y0IjplotINwqMRO81e7O7VhBScuKk7iOuDm/E1ioKv2uLGnPNWziYu+Zf025byxOqVzXDyM2gw==", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/i18n": "^4.58.0", + "@wordpress/url": "^3.59.0" + } + }, "@wordpress/compose": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-6.16.0.tgz", @@ -41325,6 +41884,16 @@ "uuid": "^8.3.0" }, "dependencies": { + "@wordpress/api-fetch": { + "version": "6.55.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.55.0.tgz", + "integrity": "sha512-1HrCUsJdeRY5Y0IjplotINwqMRO81e7O7VhBScuKk7iOuDm/E1ioKv2uLGnPNWziYu+Zf025byxOqVzXDyM2gw==", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/i18n": "^4.58.0", + "@wordpress/url": "^3.59.0" + } + }, "@wordpress/block-editor": { "version": "12.7.0", "resolved": "https://registry.npmjs.org/@wordpress/block-editor/-/block-editor-12.7.0.tgz", @@ -41775,6 +42344,16 @@ "uuid": "^8.3.0" }, "dependencies": { + "@wordpress/api-fetch": { + "version": "6.55.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.55.0.tgz", + "integrity": "sha512-1HrCUsJdeRY5Y0IjplotINwqMRO81e7O7VhBScuKk7iOuDm/E1ioKv2uLGnPNWziYu+Zf025byxOqVzXDyM2gw==", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/i18n": "^4.58.0", + "@wordpress/url": "^3.59.0" + } + }, "@wordpress/block-editor": { "version": "12.7.0", "resolved": "https://registry.npmjs.org/@wordpress/block-editor/-/block-editor-12.7.0.tgz", @@ -42090,6 +42669,19 @@ "lighthouse": "^10.4.0", "mime": "^3.0.0", "web-vitals": "^3.5.0" + }, + "dependencies": { + "@wordpress/api-fetch": { + "version": "6.55.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.55.0.tgz", + "integrity": "sha512-1HrCUsJdeRY5Y0IjplotINwqMRO81e7O7VhBScuKk7iOuDm/E1ioKv2uLGnPNWziYu+Zf025byxOqVzXDyM2gw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/i18n": "^4.58.0", + "@wordpress/url": "^3.59.0" + } + } } }, "@wordpress/element": { @@ -42219,9 +42811,9 @@ } }, "@wordpress/hooks": { - "version": "3.57.0", - "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.57.0.tgz", - "integrity": "sha512-+RaPsTj80QNUw3RfiMhxIzaAuYPAvMByrpy97jmodrvhPM5wR9utj40DYIlAiBfMhwACh8NM+kY+UB08CKcmCQ==", + "version": "3.58.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.58.0.tgz", + "integrity": "sha512-9LB0ZHnZRQlORttux9t/xbAskF+dk2ujqzPGsVzc92mSKpQP3K2a5Wy74fUnInguB1vLUNHT6nrNdkVom5qX1Q==", "requires": { "@babel/runtime": "^7.16.0" } @@ -42235,12 +42827,12 @@ } }, "@wordpress/i18n": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.57.0.tgz", - "integrity": "sha512-VYWYHE+7NxnZvE9Swhhe4leQcn0jHNkzRAEV36TkfAL/MvrQYCRh71KLTvKhsilG96HUQdBwjH0VPLmYEmR3sg==", + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.58.0.tgz", + "integrity": "sha512-VfvS3BWv/RDjRKD6PscIcvYfWKnGJcI/DEqyDgUMhxCM6NRwoL478CsUKTiGJIymeyRodNRfprdcF086DpGKYw==", "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/hooks": "^3.57.0", + "@wordpress/hooks": "^3.58.0", "gettext-parser": "^1.3.1", "memize": "^2.1.0", "sprintf-js": "^1.1.1", @@ -42928,6 +43520,16 @@ "@wordpress/url": "^3.40.0" }, "dependencies": { + "@wordpress/api-fetch": { + "version": "6.55.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.55.0.tgz", + "integrity": "sha512-1HrCUsJdeRY5Y0IjplotINwqMRO81e7O7VhBScuKk7iOuDm/E1ioKv2uLGnPNWziYu+Zf025byxOqVzXDyM2gw==", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/i18n": "^4.58.0", + "@wordpress/url": "^3.59.0" + } + }, "@wordpress/block-editor": { "version": "12.7.0", "resolved": "https://registry.npmjs.org/@wordpress/block-editor/-/block-editor-12.7.0.tgz", @@ -44428,6 +45030,16 @@ "fast-deep-equal": "^3.1.3" }, "dependencies": { + "@wordpress/api-fetch": { + "version": "6.55.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.55.0.tgz", + "integrity": "sha512-1HrCUsJdeRY5Y0IjplotINwqMRO81e7O7VhBScuKk7iOuDm/E1ioKv2uLGnPNWziYu+Zf025byxOqVzXDyM2gw==", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/i18n": "^4.58.0", + "@wordpress/url": "^3.59.0" + } + }, "@wordpress/components": { "version": "25.5.0", "resolved": "https://registry.npmjs.org/@wordpress/components/-/components-25.5.0.tgz", @@ -44612,9 +45224,9 @@ } }, "@wordpress/url": { - "version": "3.56.0", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.56.0.tgz", - "integrity": "sha512-uW5cTftroxvYSoF2Wy/Rfc5eUuqANXSrqBu8axv1dmNLYbg+2Y8f/bzH1ZNLLtmkpD25QPOIstGjA8lsYRPuig==", + "version": "3.59.0", + "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.59.0.tgz", + "integrity": "sha512-GxvoMjYCav0w4CiX0i0h3qflrE/9rhLIZg5aPCQjbrBdwTxYR3Exfw0IJYcmVaTKXQOUU8fOxlDxULsbLmKe9w==", "requires": { "@babel/runtime": "^7.16.0", "remove-accents": "^0.5.0" @@ -44713,6 +45325,12 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "@yr/monotone-cubic-spline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==", + "peer": true + }, "a11y-dialog": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/a11y-dialog/-/a11y-dialog-7.4.0.tgz", @@ -44812,14 +45430,13 @@ } }, "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.4.tgz", + "integrity": "sha512-xzzzaqgEQfmuhbhAoqjJ8T/1okb6gAzXn/eQRNpAN1AEUoHJTNF9xCDRTtf/s3SKldtZfa+RJeTs+BQq+eZ/sw==", "requires": { "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, @@ -44850,22 +45467,9 @@ "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true } } }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} - }, "alphanum-sort": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", @@ -44924,6 +45528,21 @@ "picomatch": "^2.0.4" } }, + "apexcharts": { + "version": "3.54.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.54.0.tgz", + "integrity": "sha512-ZgI/seScffjLpwNRX/gAhIkAhpCNWiTNsdICv7qxnF0xisI23XSsaENUKIcMlyP1rbe8ECgvybDnp7plZld89A==", + "peer": true, + "requires": { + "@yr/monotone-cubic-spline": "^1.0.3", + "svg.draggable.js": "^2.2.2", + "svg.easing.js": "^2.0.0", + "svg.filter.js": "^2.0.2", + "svg.pathmorphing.js": "^0.1.3", + "svg.resize.js": "^1.4.3", + "svg.select.js": "^3.0.1" + } + }, "are-docs-informative": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", @@ -46778,12 +47397,6 @@ "slash": "^4.0.0" } }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "schema-utils": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", @@ -48182,6 +48795,18 @@ "text-table": "^0.2.0" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -48281,6 +48906,12 @@ "argparse": "^2.0.1" } }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -52395,10 +53026,9 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -52592,6 +53222,25 @@ "integrity": "sha512-AZU7vQcy/4WFEuwnwsNsJnFwupIpbllH1++LXScN6uxT1Z4zPzdrWG97w4/I7eFKFTvfy/bHFStWjdBAg2Vjug==", "dev": true }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -52852,6 +53501,12 @@ "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", "dev": true }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -54230,12 +54885,6 @@ "fast-deep-equal": "^3.1.3" } }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "schema-utils": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", @@ -54633,6 +55282,18 @@ "validate-npm-package-name": "^5.0.0" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -54694,6 +55355,12 @@ "argparse": "^2.0.1" } }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "jsonc-parser": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", @@ -56903,8 +57570,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "puppeteer": { "version": "21.0.3", @@ -57198,6 +57864,14 @@ "prop-types": "^15.7.2" } }, + "react-apexcharts": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.4.1.tgz", + "integrity": "sha512-G14nVaD64Bnbgy8tYxkjuXEUp/7h30Q0U33xc3AwtGFijJB9nHqOt1a6eG0WBn055RgRg+NwqbKGtqPxy15d0Q==", + "requires": { + "prop-types": "^15.8.1" + } + }, "react-aria": { "version": "3.27.0", "resolved": "https://registry.npmjs.org/react-aria/-/react-aria-3.27.0.tgz", @@ -58139,8 +58813,7 @@ "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" }, "require-main-filename": { "version": "2.0.0", @@ -58539,6 +59212,33 @@ "@types/json-schema": "^7.0.5", "ajv": "^6.12.4", "ajv-keywords": "^3.5.2" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } } }, "select": { @@ -59639,6 +60339,78 @@ "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", "dev": true }, + "svg.draggable.js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "peer": true, + "requires": { + "svg.js": "^2.0.1" + } + }, + "svg.easing.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", + "peer": true, + "requires": { + "svg.js": ">=2.3.x" + } + }, + "svg.filter.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", + "peer": true, + "requires": { + "svg.js": "^2.2.5" + } + }, + "svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==", + "peer": true + }, + "svg.pathmorphing.js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "peer": true, + "requires": { + "svg.js": "^2.4.0" + } + }, + "svg.resize.js": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "peer": true, + "requires": { + "svg.js": "^2.6.5", + "svg.select.js": "^2.1.2" + }, + "dependencies": { + "svg.select.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "peer": true, + "requires": { + "svg.js": "^2.2.5" + } + } + } + }, + "svg.select.js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "peer": true, + "requires": { + "svg.js": "^2.6.5" + } + }, "svgo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", @@ -59766,12 +60538,6 @@ "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true } } }, @@ -59875,6 +60641,25 @@ "terser": "^5.16.8" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -59898,6 +60683,12 @@ "supports-color": "^8.0.0" } }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -60486,7 +61277,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "requires": { "punycode": "^2.1.0" } @@ -60520,6 +61310,31 @@ "schema-utils": "^3.0.0" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "loader-utils": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", @@ -60864,6 +61679,31 @@ "webpack-sources": "^3.2.3" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -60995,12 +61835,6 @@ "fast-deep-equal": "^3.1.3" } }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "schema-utils": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", @@ -61074,12 +61908,6 @@ "fast-deep-equal": "^3.1.3" } }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "schema-utils": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", diff --git a/package.json b/package.json index de52cc064d..a5603cbd5f 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@fortawesome/free-regular-svg-icons": "^5.15.1", "@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/react-fontawesome": "^0.1.12", - "@givewp/design-system-foundation": "^1.1.0", + "@givewp/design-system-foundation": "^1.2.0", "@givewp/form-builder-library": "^1.7.0", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^2.9.10", @@ -78,6 +78,7 @@ "@stripe/react-stripe-js": "^2.1.0", "@stripe/stripe-js": "^1.52.0", "@svgr/webpack": "^6.5.1", + "@wordpress/api-fetch": "^7.8.0", "@wordpress/block-editor": "^11.2.0", "@wordpress/block-library": "^8.15.0", "@wordpress/components": "^23.2.0", @@ -90,6 +91,7 @@ "@wordpress/interface": "^5.11.0", "@wordpress/server-side-render": "^4.2.0", "accounting": "^0.4.1", + "ajv": "^7.0.4", "axios": "^0.21.2", "chart.js": "^2.9.3", "chartjs-plugin-crosshair": "^1.1.4", @@ -118,6 +120,7 @@ "react-a11y-dialog": "^6.1.5", "react-acceptjs": "^0.3.0", "react-ace": "^10.1.0", + "react-apexcharts": "^1.4.1", "react-aria-components": "^1.0.0-alpha.6", "react-beautiful-dnd": "^13.1.1", "react-csv": "^2.0.1", diff --git a/src/Campaigns/Actions/AddCampaignFormFromRequest.php b/src/Campaigns/Actions/AddCampaignFormFromRequest.php new file mode 100644 index 0000000000..9b0bccf0a1 --- /dev/null +++ b/src/Campaigns/Actions/AddCampaignFormFromRequest.php @@ -0,0 +1,39 @@ +addCampaignForm($campaign, $formId); + } + } + + /** + * @unreleased + * + * @throws Exception + */ + public function visualFormBuilder(DonationForm $donationForm) + { + if (isset($_GET['campaignId']) && $campaign = Campaign::find(absint($_GET['campaignId']))) { + give(CampaignRepository::class)->addCampaignForm($campaign, $donationForm->id); + } + } +} diff --git a/src/Campaigns/Actions/ConvertQueryDataToCampaign.php b/src/Campaigns/Actions/ConvertQueryDataToCampaign.php new file mode 100644 index 0000000000..a064898b87 --- /dev/null +++ b/src/Campaigns/Actions/ConvertQueryDataToCampaign.php @@ -0,0 +1,41 @@ + (int)$queryObject->id, + 'defaultFormId' => (int)$queryObject->defaultFormId, + 'type' => new CampaignType($queryObject->type), + 'enableCampaignPage' => (bool)$queryObject->enableCampaignPage, + 'title' => $queryObject->title, + 'shortDescription' => $queryObject->shortDescription, + 'longDescription' => $queryObject->longDescription, + 'logo' => $queryObject->logo, + 'image' => $queryObject->image, + 'primaryColor' => $queryObject->primaryColor, + 'secondaryColor' => $queryObject->secondaryColor, + 'goal' => (int)$queryObject->goal, + 'goalType' => new CampaignGoalType($queryObject->goalType), + 'startDate' => Temporal::toDateTime($queryObject->startDate), + 'endDate' => Temporal::toDateTime($queryObject->endDate), + 'status' => new CampaignStatus($queryObject->status), + 'createdAt' => Temporal::toDateTime($queryObject->createdAt), + ]); + } +} diff --git a/src/Campaigns/Actions/CreateDefaultCampaignForm.php b/src/Campaigns/Actions/CreateDefaultCampaignForm.php new file mode 100644 index 0000000000..238ad6d233 --- /dev/null +++ b/src/Campaigns/Actions/CreateDefaultCampaignForm.php @@ -0,0 +1,38 @@ + $campaign->title, + 'status' => DonationFormStatus::DRAFT(), + 'settings' => FormSettings::fromArray([ + 'enableDonationGoal' => true, + 'goalAmount' => $campaign->goal, + 'goalType' => $campaign->goalType->getValue(), + 'designId' => ClassicFormDesign::id(), + ]), + 'blocks' => (new GenerateDefaultDonationFormBlockCollection())(), + ]); + + give(CampaignRepository::class)->addCampaignForm($campaign, $defaultCampaignForm->id, true); + } +} diff --git a/src/Campaigns/Actions/DeleteCampaignPage.php b/src/Campaigns/Actions/DeleteCampaignPage.php new file mode 100644 index 0000000000..b807bdff63 --- /dev/null +++ b/src/Campaigns/Actions/DeleteCampaignPage.php @@ -0,0 +1,23 @@ +page() ?: CampaignPage::create([ + 'campaignId' => $campaign->id, + ]); + + wp_safe_redirect($page->getEditLinkUrl(), 303); + exit(); + } +} diff --git a/src/Campaigns/Actions/FormInheritsCampaignGoal.php b/src/Campaigns/Actions/FormInheritsCampaignGoal.php new file mode 100644 index 0000000000..ecb32750e5 --- /dev/null +++ b/src/Campaigns/Actions/FormInheritsCampaignGoal.php @@ -0,0 +1,33 @@ +settings->enableDonationGoal = true; + $donationForm->settings->goalAmount = $campaign->goal; + $donationForm->settings->goalType = new GoalType($campaign->goalType->getValue()); + } + } + } +} diff --git a/src/Campaigns/Actions/LoadCampaignDetailsAssets.php b/src/Campaigns/Actions/LoadCampaignDetailsAssets.php new file mode 100644 index 0000000000..9f70322c04 --- /dev/null +++ b/src/Campaigns/Actions/LoadCampaignDetailsAssets.php @@ -0,0 +1,48 @@ + admin_url(), + 'currency' => give_get_currency(), + 'isRecurringEnabled' => defined('GIVE_RECURRING_VERSION') ? GIVE_RECURRING_VERSION : null + ] + ); + + wp_enqueue_script($handleName); + wp_enqueue_style('givewp-design-system-foundation'); + wp_enqueue_style( + $handleName, + GIVE_PLUGIN_URL . 'build/campaignDetails.css', + /** @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-components/#usage */ + ['wp-components'], + $scriptAsset['version'] + ); + } +} diff --git a/src/Campaigns/Actions/LoadCampaignsListTableAssets.php b/src/Campaigns/Actions/LoadCampaignsListTableAssets.php new file mode 100644 index 0000000000..03f3345c2e --- /dev/null +++ b/src/Campaigns/Actions/LoadCampaignsListTableAssets.php @@ -0,0 +1,45 @@ + esc_url_raw(rest_url('give-api/v2/campaigns/list-table')), + 'apiNonce' => wp_create_nonce('wp_rest'), + 'table' => give(CampaignsListTable::class)->toArray(), + 'adminUrl' => admin_url(), + 'paymentMode' => give_is_test_mode(), + 'pluginUrl' => GIVE_PLUGIN_URL, + 'currency' => give_get_currency(), + 'isRecurringEnabled' => defined('GIVE_RECURRING_VERSION') ? GIVE_RECURRING_VERSION : null, + ] + ); + + wp_enqueue_script($handleName); + wp_enqueue_style('givewp-design-system-foundation'); + } +} diff --git a/src/Campaigns/Actions/RegisterCampaignBlocks.php b/src/Campaigns/Actions/RegisterCampaignBlocks.php new file mode 100644 index 0000000000..b211d354ca --- /dev/null +++ b/src/Campaigns/Actions/RegisterCampaignBlocks.php @@ -0,0 +1,49 @@ +enqueueBlocksAssets(); + } + + /** + * @unreleased + */ + private function enqueueBlocksAssets() + { + $handleName = 'givewp-campaign-blocks'; + $scriptAsset = ScriptAsset::get(GIVE_PLUGIN_DIR . 'build/campaignBlocks.asset.php'); + + wp_register_script( + $handleName, + GIVE_PLUGIN_URL . 'build/campaignBlocks.js', + $scriptAsset['dependencies'], + $scriptAsset['version'], + true + ); + + wp_enqueue_script($handleName); + wp_enqueue_style( + $handleName, + GIVE_PLUGIN_URL . 'build/campaignBlocks.css', + /** @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-components/#usage */ + ['wp-components'], + $scriptAsset['version'] + ); + } +} diff --git a/src/Campaigns/Actions/RegisterCampaignEntity.php b/src/Campaigns/Actions/RegisterCampaignEntity.php new file mode 100644 index 0000000000..67f8d98665 --- /dev/null +++ b/src/Campaigns/Actions/RegisterCampaignEntity.php @@ -0,0 +1,30 @@ + function ($object) { + return get_post_meta($object['id'], 'campaignId', true); + }, + 'update_callback' => function ($value, $object) { + return update_post_meta($object->ID, 'campaignId', (int) $value); + }, + 'schema' => [ + 'description' => 'Campaign ID', + 'type' => 'string', + 'context' => ['view', 'edit'], + ], + ] + ); + } +} diff --git a/src/Campaigns/Actions/RegisterCampaignPagePostType.php b/src/Campaigns/Actions/RegisterCampaignPagePostType.php new file mode 100644 index 0000000000..e8dd4f189f --- /dev/null +++ b/src/Campaigns/Actions/RegisterCampaignPagePostType.php @@ -0,0 +1,32 @@ + __('Campaign Page', 'give'), + 'public' => true, + 'show_ui' => true, + 'show_in_menu' => false, + 'show_in_rest' => true, + 'supports' => [ + 'editor' + ], + 'rewrite' => [ + 'slug' => 'campaign' + ], + 'template' => [ + // TODO: Add default blocks template. + ], + ] ); + } +} diff --git a/src/Campaigns/Blocks/CampaignTitleBlock/block.json b/src/Campaigns/Blocks/CampaignTitleBlock/block.json new file mode 100644 index 0000000000..336a744657 --- /dev/null +++ b/src/Campaigns/Blocks/CampaignTitleBlock/block.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "givewp/campaign-title-block", + "version": "1.0.0", + "title": "Campaign Title", + "category": "give", + "icon": "heading", + "description": "Displays the title of the campaign.", + "attributes": { + "campaignId": { + "type": "integer" + }, + "headingLevel": { + "type": "number", + "default": 1 + }, + "textAlign": { + "type": "string" + } + }, + "supports": { + "align": [ + "wide", + "full" + ], + "anchor": true, + "className": true, + "splitting": true, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true + }, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true, + "__experimentalLetterSpacing": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalWritingMode": true, + "__experimentalDefaultControls": { + "fontSize": true + } + } + }, + "textdomain": "give", + "render": "file:./render.php" +} diff --git a/src/Campaigns/Blocks/CampaignTitleBlock/edit.tsx b/src/Campaigns/Blocks/CampaignTitleBlock/edit.tsx new file mode 100644 index 0000000000..a2414fa832 --- /dev/null +++ b/src/Campaigns/Blocks/CampaignTitleBlock/edit.tsx @@ -0,0 +1,82 @@ +import { + AlignmentControl, + BlockControls, + HeadingLevelDropdown, + InspectorControls, + useBlockProps, +} from '@wordpress/block-editor'; +import {BlockEditProps} from '@wordpress/blocks'; +import {BaseControl, Icon, PanelBody, TextareaControl} from '@wordpress/components'; +import ServerSideRender from '@wordpress/server-side-render'; +import {CampaignSelector} from '../shared/components/CampaignSelector'; +import useCampaign from '../shared/hooks/useCampaign'; +import {__} from '@wordpress/i18n'; +import {useSelect} from '@wordpress/data'; +import {external} from '@wordpress/icons'; + +import './editor.scss'; + +export default function Edit({ + attributes, + setAttributes, +}: BlockEditProps<{ + campaignId: number; + headingLevel: string; + textAlign: string; +}>) { + const blockProps = useBlockProps(); + const {campaign, hasResolved} = useCampaign(attributes.campaignId); + + const adminBaseUrl = useSelect( + // @ts-ignore + (select) => select('core').getSite()?.url + '/wp-admin/edit.php?post_type=give_forms&page=give-campaigns', + [] + ); + + const editCampaignUrl = `${adminBaseUrl}&id=${attributes.campaignId}&tab=settings`; + + return ( +
+ + + + + {hasResolved && campaign && ( + + + + null} + help={ + + {__('Edit campaign title', 'give')} + + + } + /> + + + + )} + + + setAttributes({headingLevel: newLevel})} + /> + setAttributes({textAlign: nextAlign})} + /> + +
+ ); +} diff --git a/src/Campaigns/Blocks/CampaignTitleBlock/editor.scss b/src/Campaigns/Blocks/CampaignTitleBlock/editor.scss new file mode 100644 index 0000000000..031ddc43f7 --- /dev/null +++ b/src/Campaigns/Blocks/CampaignTitleBlock/editor.scss @@ -0,0 +1,13 @@ +.givewp-campaign-title-block { + &__edit-campaign-link { + display: inline-flex; + align-items: center; + gap: 0.125rem; + + svg { + fill: currentColor; + height: 1.25rem; + width: 1.25rem; + } + } +} diff --git a/src/Campaigns/Blocks/CampaignTitleBlock/index.ts b/src/Campaigns/Blocks/CampaignTitleBlock/index.ts new file mode 100644 index 0000000000..fd554ff7cd --- /dev/null +++ b/src/Campaigns/Blocks/CampaignTitleBlock/index.ts @@ -0,0 +1,12 @@ +import metadata from './block.json'; +import Edit from './edit'; +import initBlock from '../shared/utils/init-block'; + +const {name} = metadata; + +export {metadata, name}; +export const settings = { + edit: Edit, +}; + +export const init = () => initBlock({name, metadata, settings}); diff --git a/src/Campaigns/Blocks/CampaignTitleBlock/render.php b/src/Campaigns/Blocks/CampaignTitleBlock/render.php new file mode 100644 index 0000000000..ed639e2384 --- /dev/null +++ b/src/Campaigns/Blocks/CampaignTitleBlock/render.php @@ -0,0 +1,27 @@ +getById($attributes['campaignId']); + +if (! $campaign) { + return; +} + +$headingLevel = isset($attributes['headingLevel']) ? (int) $attributes['headingLevel'] : 1; +$headingTag = 'h' . min(6, max(1, $headingLevel)); + +$textAlignClass = isset($attributes['textAlign']) ? 'has-text-align-' . $attributes['textAlign'] : ''; +?> + +< $textAlignClass])); ?>> +title); ?> +> diff --git a/src/Campaigns/Blocks/blocks.ts b/src/Campaigns/Blocks/blocks.ts new file mode 100644 index 0000000000..a42f34ca1d --- /dev/null +++ b/src/Campaigns/Blocks/blocks.ts @@ -0,0 +1,9 @@ +import * as campaignTitleBlock from './CampaignTitleBlock'; + +const getAllBlocks = () => { + return [campaignTitleBlock]; +}; + +getAllBlocks().forEach((block) => { + block.init(); +}); diff --git a/src/Campaigns/Blocks/shared/components/CampaignDropdown.tsx b/src/Campaigns/Blocks/shared/components/CampaignDropdown.tsx new file mode 100644 index 0000000000..9ffc26dc9b --- /dev/null +++ b/src/Campaigns/Blocks/shared/components/CampaignDropdown.tsx @@ -0,0 +1,47 @@ +import {PanelBody, SelectControl} from '@wordpress/components'; +import {InspectorControls} from '@wordpress/block-editor'; +import useCampaigns from '../hooks/useCampaigns'; +import {__} from '@wordpress/i18n'; + +export default function CampaignDropdown({campaignId, setAttributes, placement = 'sidebar'}) { + const {campaigns, hasResolved} = useCampaigns(); + + const options = (() => { + if (!hasResolved) { + return [{label: __('Loading...', 'give'), value: ''}]; + } + + if (campaigns.length) { + const campaignOptions = campaigns.map((campaign) => ({ + label: campaign.title, + value: campaign.id.toString(), + })); + + return [{label: __('Select...', 'give'), value: ''}, ...campaignOptions]; + } + + return [{label: __('No campaigns found.', 'give'), value: ''}]; + })(); + + const dropdown = ( + setAttributes({campaignId: newValue ? parseInt(newValue) : null})} + /> + ); + + if (placement === 'sidebar') { + return ( + + + {dropdown} + + + ); + } + + return dropdown; +} diff --git a/src/Campaigns/Blocks/shared/components/CampaignSelector.tsx b/src/Campaigns/Blocks/shared/components/CampaignSelector.tsx new file mode 100644 index 0000000000..2350e93e0a --- /dev/null +++ b/src/Campaigns/Blocks/shared/components/CampaignSelector.tsx @@ -0,0 +1,28 @@ +import useCampaignId from '../hooks/useCampaignId'; +import CampaignDropdown from './CampaignDropdown'; + +export function CampaignSelector({attributes, setAttributes, children}) { + const campaignId = useCampaignId(attributes, setAttributes); + + return ( + <> + {!campaignId && !attributes?.campaignId && ( + + )} + + {!campaignId && ( + + )} + + {attributes?.campaignId && children} + + ); +} diff --git a/src/Campaigns/Blocks/shared/hooks/useCampaign.ts b/src/Campaigns/Blocks/shared/hooks/useCampaign.ts new file mode 100644 index 0000000000..a7cf2b57ea --- /dev/null +++ b/src/Campaigns/Blocks/shared/hooks/useCampaign.ts @@ -0,0 +1,11 @@ +import {useEntityRecord} from '@wordpress/core-data'; +import {Campaign} from '@givewp/campaigns/admin/components/types'; + +export default function useCampaign(campaignId: number) { + const data = useEntityRecord('givewp', 'campaign', campaignId); + + return { + campaign: data?.record as Campaign, + hasResolved: data?.hasResolved, + }; +} diff --git a/src/Campaigns/Blocks/shared/hooks/useCampaignId.ts b/src/Campaigns/Blocks/shared/hooks/useCampaignId.ts new file mode 100644 index 0000000000..d894aafacb --- /dev/null +++ b/src/Campaigns/Blocks/shared/hooks/useCampaignId.ts @@ -0,0 +1,20 @@ +import {useSelect} from '@wordpress/data'; + +export default function useCampaignId(attributes, setAttributes) { + const campaignIdFromContext = useSelect((select) => { + // @ts-ignore + const postType = select('core/editor').getCurrentPostType(); + + if (postType === 'give_campaign_page') { + // @ts-ignore + return select('core/editor').getEditedPostAttribute('campaignId'); + } + return null; + }, []); + + if (campaignIdFromContext && campaignIdFromContext !== attributes?.campaignId) { + setAttributes({campaignId: campaignIdFromContext}); + } + + return campaignIdFromContext; +} diff --git a/src/Campaigns/Blocks/shared/hooks/useCampaigns.ts b/src/Campaigns/Blocks/shared/hooks/useCampaigns.ts new file mode 100644 index 0000000000..3764ad139d --- /dev/null +++ b/src/Campaigns/Blocks/shared/hooks/useCampaigns.ts @@ -0,0 +1,11 @@ +import {useEntityRecords} from '@wordpress/core-data'; +import {Campaign} from '@givewp/campaigns/admin/components/types'; + +export default function useCampaigns() { + const data = useEntityRecords('givewp', 'campaign'); + + return { + campaigns: data?.records as Campaign[], + hasResolved: data?.hasResolved, + }; +} diff --git a/src/Campaigns/Blocks/shared/utils/init-block.ts b/src/Campaigns/Blocks/shared/utils/init-block.ts new file mode 100644 index 0000000000..9ae353ac8d --- /dev/null +++ b/src/Campaigns/Blocks/shared/utils/init-block.ts @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import {registerBlockType} from '@wordpress/blocks'; + +/** + * Function to register an individual block. + * + * @param {Object} block The block to be registered. + * + * @return {WPBlockType | undefined} The block, if it has been successfully registered; + * otherwise `undefined`. + */ +export default function initBlock(block) { + if (!block) { + return; + } + const {metadata, settings, name} = block; + return registerBlockType({name, ...metadata}, settings); +} diff --git a/src/Campaigns/CampaignDonationQuery.php b/src/Campaigns/CampaignDonationQuery.php new file mode 100644 index 0000000000..2596fc5495 --- /dev/null +++ b/src/Campaigns/CampaignDonationQuery.php @@ -0,0 +1,127 @@ +from('posts', 'donation'); + $this->where('post_type', 'give_payment'); + + // Include only valid statuses + $this->whereIn('donation.post_status', ['publish', 'give_subscription']); + + // Include only current payment "mode" + $this->joinDonationMeta(DonationMetaKeys::MODE, 'paymentMode'); + $this->where('paymentMode.meta_value', give_is_test_mode() ? 'test' : 'live'); + + // Include only forms associated with the Campaign. + $this->joinDonationMeta(DonationMetaKeys::CAMPAIGN_ID, 'campaignId'); + $this->where('campaignId.meta_value', $campaign->id); + } + + /** + * @unreleased + */ + public function between(DateTimeInterface $startDate, DateTimeInterface $endDate): self + { + $query = clone $this; + $query->joinDonationMeta('_give_completed_date', 'completed'); + $query->whereBetween( + 'completed.meta_value', + $startDate->format('Y-m-d H:i:s'), + $endDate->format('Y-m-d H:i:s') + ); + return $query; + } + + /** + * Returns a calculated sum of the intended amounts (without recovered fees) for the donations. + * + * @unreleased + * + * @return int|float + */ + public function sumIntendedAmount() + { + $query = clone $this; + $query->joinDonationMeta(DonationMetaKeys::AMOUNT, 'amount'); + $query->joinDonationMeta('_give_fee_donation_amount', 'intendedAmount'); + return $query->sum( + /** + * The intended amount meta and the amount meta could either be 0 or NULL. + * So we need to use the NULLIF function to treat the 0 values as NULL. + * Then we coalesce the values to select the first non-NULL value. + * @link https://github.com/impress-org/givewp/pull/7411 + */ + 'COALESCE(NULLIF(intendedAmount.meta_value,0), NULLIF(amount.meta_value,0), 0)' + ); + } + + /** + * @unreleased + */ + public function countDonations(): int + { + $query = clone $this; + return $query->count('donation.ID'); + } + + /** + * @unreleased + */ + public function countDonors(): int + { + $query = clone $this; + $query->joinDonationMeta(DonationMetaKeys::DONOR_ID, 'donorId'); + return $query->count('DISTINCT donorId.meta_value'); + } + + /** + * @unreleased + */ + public function getDonationsByDay(): array + { + $query = clone $this; + + $query->joinDonationMeta(DonationMetaKeys::AMOUNT, 'amount'); + $query->joinDonationMeta('_give_fee_donation_amount', 'intendedAmount'); + $query->select( + 'SUM(COALESCE(NULLIF(intendedAmount.meta_value,0), NULLIF(amount.meta_value,0), 0)) as amount' + ); + + $query->joinDonationMeta('_give_completed_date', 'completed'); + $query->select('DATE(completed.meta_value) as date'); + $query->groupBy('date'); + + return $query->getAll(); + } + + /** + * An opinionated join method for the donation meta table. + * @unreleased + */ + protected function joinDonationMeta($key, $alias): self + { + $this->join(function (JoinQueryBuilder $builder) use ($key, $alias) { + $builder + ->leftJoin('give_donationmeta', $alias) + ->on('donation.ID', $alias . '.donation_id') + ->andOn($alias . '.meta_key', $key, true); + }); + return $this; + } +} diff --git a/src/Campaigns/CampaignsAdminPage.php b/src/Campaigns/CampaignsAdminPage.php new file mode 100644 index 0000000000..9fdf667d23 --- /dev/null +++ b/src/Campaigns/CampaignsAdminPage.php @@ -0,0 +1,57 @@ +'; + } + + /** + * @unreleased + */ + public static function isShowingDetailsPage(): bool + { + return isset($_GET['id']) && isset($_GET['page']) && 'give-campaigns' == isset($_GET['page']); + } +} diff --git a/src/Campaigns/Controllers/CampaignRequestController.php b/src/Campaigns/Controllers/CampaignRequestController.php new file mode 100644 index 0000000000..e8fd8e30be --- /dev/null +++ b/src/Campaigns/Controllers/CampaignRequestController.php @@ -0,0 +1,195 @@ +get_param('id')); + + if ( ! $campaign) { + return new WP_Error('campaign_not_found', __('Campaign not found', 'give'), ['status' => 404]); + } + + return new WP_REST_Response( + array_merge($campaign->toArray(), [ + 'goalProgress' => $campaign->goalProgress(), + 'defaultFormTitle' => $campaign->defaultForm()->title + ]) + ); + } + + /** + * @unreleased + */ + public function getCampaigns(WP_REST_Request $request): WP_REST_Response + { + $page = $request->get_param('page'); + $perPage = $request->get_param('per_page'); + + $query = give(CampaignRepository::class)->prepareQuery(); + + $query + ->limit($perPage) + ->offset(($page - 1) * $perPage); + + $campaigns = $query->getAll() ?? []; + $totalCampaigns = empty($campaigns) ? 0 : Campaign::query()->count(); + $totalPages = (int)ceil($totalCampaigns / $perPage); + + $response = rest_ensure_response($campaigns); + $response->header('X-WP-Total', $totalCampaigns); + $response->header('X-WP-TotalPages', $totalPages); + + $base = add_query_arg( + map_deep($request->get_query_params(), function ($value) { + if (is_bool($value)) { + $value = $value ? 'true' : 'false'; + } + + return urlencode($value); + }), + rest_url(CampaignRoute::CAMPAIGNS) + ); + + if ($page > 1) { + $prevPage = $page - 1; + + if ($prevPage > $totalPages) { + $prevPage = $totalPages; + } + + $response->link_header('prev', add_query_arg('page', $prevPage, $base)); + } + + if ($totalPages > $page) { + $nextPage = $page + 1; + $response->link_header('next', add_query_arg('page', $nextPage, $base)); + } + + return $response; + } + + /** + * @unreleased + * + * @return WP_Error | WP_REST_Response + * + * @throws Exception + */ + public function updateCampaign(WP_REST_Request $request) + { + $campaign = Campaign::find($request->get_param('id')); + + if ( ! $campaign) { + return new WP_Error('campaign_not_found', __('Campaign not found', 'give'), ['status' => 404]); + } + + $statusMap = [ + 'archived' => CampaignStatus::ARCHIVED(), + 'draft' => CampaignStatus::DRAFT(), + 'active' => CampaignStatus::ACTIVE(), + ]; + + foreach ($request->get_params() as $key => $value) { + switch ($key) { + case 'id': + break; + case 'status': + $status = array_key_exists($value, $statusMap) + ? $statusMap[$value] + : CampaignStatus::DRAFT(); + + $campaign->status = $status; + + break; + case 'goal': + $campaign->goal = (int)$value; + break; + case 'goalType': + $campaign->goalType = new CampaignGoalType($value); + break; + case 'defaultFormId': + give(CampaignRepository::class)->updateDefaultCampaignForm($campaign, $request->get_param('defaultFormId')); + break; + default: + if ($campaign->hasProperty($key)) { + $campaign->$key = $value; + } + } + } + + if ($campaign->isDirty()) { + $campaign->save(); + } + + return new WP_REST_Response( + array_merge($campaign->toArray(), [ + 'defaultFormTitle' => $campaign->defaultForm()->title + ]) + ); + } + + /** + * @unreleased + * + * @throws Exception + */ + public function mergeCampaigns(WP_REST_Request $request): WP_REST_Response + { + $destinationCampaign = Campaign::find($request->get_param('id')); + $campaignsToMerge = Campaign::query()->whereIn('id', $request->get_param('campaignsToMergeIds'))->getAll(); + + $campaignsMerged = $destinationCampaign->merge(...$campaignsToMerge); + + return new WP_REST_Response($campaignsMerged); + } + + + /** + * @unreleased + * + * @throws Exception + */ + public function createCampaign(WP_REST_Request $request): WP_REST_Response + { + $campaign = Campaign::create([ + 'type' => CampaignType::CORE(), + 'title' => $request->get_param('title'), + 'shortDescription' => $request->get_param('shortDescription') ?? '', + 'longDescription' => '', + 'logo' => '', + 'image' => $request->get_param('image') ?? '', + 'primaryColor' => '', + 'secondaryColor' => '', + 'goal' => (int)$request->get_param('goal'), + 'goalType' => new CampaignGoalType($request->get_param('goalType')), + 'status' => CampaignStatus::DRAFT(), + 'startDate' => $request->get_param('startDateTime'), + 'endDate' => $request->get_param('endDateTime'), + ]); + + return new WP_REST_Response($campaign->toArray(), 201); + } +} diff --git a/src/Campaigns/DataTransferObjects/CampaignGoalData.php b/src/Campaigns/DataTransferObjects/CampaignGoalData.php new file mode 100644 index 0000000000..780de8c704 --- /dev/null +++ b/src/Campaigns/DataTransferObjects/CampaignGoalData.php @@ -0,0 +1,126 @@ +campaign = $campaign; + $this->actual = $this->getActual(); + $this->actualFormatted = $this->getActualFormatted(); + $this->percentage = $this->getPercentage(); + $this->goal = $campaign->goal; + $this->goalFormatted = $this->getGoalFormatted(); + } + + /** + * @unreleased + */ + private function getActual(): int + { + $query = new CampaignDonationQuery($this->campaign); + + switch ($this->campaign->goalType->getValue()) { + case GoalType::DONATIONS(): + return $query->countDonations(); + + case GoalType::DONORS(): + return $query->countDonors(); + + case GoalType::AMOUNT(): + default: + return $query->sumIntendedAmount(); + } + } + + /** + * @unreleased + */ + private function getPercentage(): float + { + $percentage = $this->campaign->goal + ? $this->actual / $this->campaign->goal + : 0; + return round($percentage * 100, 2); + } + + /** + * @unreleased + */ + private function getActualFormatted(): string + { + if ($this->campaign->goalType == GoalType::AMOUNT) { + return give_currency_filter(give_format_amount($this->actual)); + } + + return $this->actual; + } + + /** + * @unreleased + */ + private function getGoalFormatted(): string + { + if ($this->campaign->goalType == GoalType::AMOUNT) { + return give_currency_filter(give_format_amount($this->goal)); + } + + return $this->goal; + } + + /** + * @unreleased + */ + public function toArray(): array + { + return [ + 'actual' => $this->actual, + 'actualFormatted' => $this->actualFormatted, + 'percentage' => $this->percentage, + 'goal' => $this->goal, + 'goalFormatted' => $this->goalFormatted, + ]; + } +} diff --git a/src/Campaigns/Factories/CampaignFactory.php b/src/Campaigns/Factories/CampaignFactory.php new file mode 100644 index 0000000000..1cc64f9510 --- /dev/null +++ b/src/Campaigns/Factories/CampaignFactory.php @@ -0,0 +1,42 @@ + CampaignType::CORE(), + 'enableCampaignPage' => true, + 'defaultFormId' => 1, + 'title' => __('GiveWP Campaign', 'give'), + 'shortDescription' => __('Campaign short description', 'give'), + 'longDescription' => __('Campaign long description', 'give'), + 'goal' => 10000000, + 'goalType' => CampaignGoalType::AMOUNT(), + 'status' => CampaignStatus::ACTIVE(), + 'logo' => '', + 'image' => '', + 'primaryColor' => '#28C77B', + 'secondaryColor' => '#FFA200', + 'createdAt' => Temporal::withoutMicroseconds($currentDate), + 'startDate' => Temporal::withoutMicroseconds($currentDate), + 'endDate' => Temporal::withoutMicroseconds($currentDate->modify('+1 day')), + ]; + } +} diff --git a/src/Campaigns/ListTable/CampaignsListTable.php b/src/Campaigns/ListTable/CampaignsListTable.php new file mode 100644 index 0000000000..27ef0ea570 --- /dev/null +++ b/src/Campaigns/ListTable/CampaignsListTable.php @@ -0,0 +1,69 @@ +createdAt->format($format); + } +} diff --git a/src/Campaigns/ListTable/Columns/DescriptionColumn.php b/src/Campaigns/ListTable/Columns/DescriptionColumn.php new file mode 100644 index 0000000000..77bb6dcd91 --- /dev/null +++ b/src/Campaigns/ListTable/Columns/DescriptionColumn.php @@ -0,0 +1,38 @@ +shortDescription, true); + } +} diff --git a/src/Campaigns/ListTable/Columns/DonationsCountColumn.php b/src/Campaigns/ListTable/Columns/DonationsCountColumn.php new file mode 100644 index 0000000000..a06e0461bf --- /dev/null +++ b/src/Campaigns/ListTable/Columns/DonationsCountColumn.php @@ -0,0 +1,59 @@ +countDonations(); + + $label = $totalDonations > 0 + ? sprintf( + _n( + '%1$s donation', + '%1$s donations', + $totalDonations, + 'give' + ), + $totalDonations + ) : __('No donations', 'give'); + + + return sprintf( + '%s', + admin_url("edit.php?post_type=give_forms&page=give-payment-history&form_id=$model->id"), + __('Visit donations page', 'give'), + apply_filters("givewp_list_table_cell_value_{$this::getId()}_content", $label, $model, $this) + ); + } +} diff --git a/src/Campaigns/ListTable/Columns/EndDateColumn.php b/src/Campaigns/ListTable/Columns/EndDateColumn.php new file mode 100644 index 0000000000..eeaba73645 --- /dev/null +++ b/src/Campaigns/ListTable/Columns/EndDateColumn.php @@ -0,0 +1,42 @@ +endDate->format($format); + } +} diff --git a/src/Campaigns/ListTable/Columns/GoalColumn.php b/src/Campaigns/ListTable/Columns/GoalColumn.php new file mode 100644 index 0000000000..2fc68dd58d --- /dev/null +++ b/src/Campaigns/ListTable/Columns/GoalColumn.php @@ -0,0 +1,76 @@ + + + +
+ %3$s%4$s %5$s +
+ '; + + return sprintf( + $template, + $model->id, + $goalData->percentage, + $goalData->actualFormatted, + sprintf( + ' %s %s', + __('of', 'give'), + $goalData->goalFormatted + ), + sprintf( + '%3$s%4$s', + apply_filters('givewp_list_table_goal_progress_achieved_opacity', $goalData->percentage >= 100 ? 1 : 0), + GIVE_PLUGIN_URL . 'assets/dist/images/list-table/star-icon.svg', + __('Goal achieved icon', 'give'), + __('Goal achieved!', 'give') + ) + ); + } +} diff --git a/src/Campaigns/ListTable/Columns/IdColumn.php b/src/Campaigns/ListTable/Columns/IdColumn.php new file mode 100644 index 0000000000..1a5a616cc5 --- /dev/null +++ b/src/Campaigns/ListTable/Columns/IdColumn.php @@ -0,0 +1,40 @@ +id; + } +} diff --git a/src/Campaigns/ListTable/Columns/RevenueColumn.php b/src/Campaigns/ListTable/Columns/RevenueColumn.php new file mode 100644 index 0000000000..66476c1523 --- /dev/null +++ b/src/Campaigns/ListTable/Columns/RevenueColumn.php @@ -0,0 +1,50 @@ +sumIntendedAmount())); + + return sprintf( + '%s', + admin_url("edit.php?post_type=give_forms&page=give-reports&tab=forms&legacy=true&form-id=$model->id"), + __('Visit form reports page', 'give'), + apply_filters("givewp_list_table_cell_value_{$this::getId()}_content", + $revenue, $model, $this) + ); + } +} diff --git a/src/Campaigns/ListTable/Columns/StartDateColumn.php b/src/Campaigns/ListTable/Columns/StartDateColumn.php new file mode 100644 index 0000000000..3a420f5191 --- /dev/null +++ b/src/Campaigns/ListTable/Columns/StartDateColumn.php @@ -0,0 +1,42 @@ +startDate->format($format); + } +} diff --git a/src/Campaigns/ListTable/Columns/StatusColumn.php b/src/Campaigns/ListTable/Columns/StatusColumn.php new file mode 100644 index 0000000000..4430d9a9a6 --- /dev/null +++ b/src/Campaigns/ListTable/Columns/StatusColumn.php @@ -0,0 +1,68 @@ +status->getValue()) { + case 'active': + $statusLabel = __('Active', 'give'); + break; + case 'inactive': + $statusLabel = __('Inactive', 'give'); + break; + case 'archived': + $statusLabel = __('Archived', 'give'); + break; + case 'draft': + $statusLabel = __('Draft', 'give'); + break; + case 'pending': + $statusLabel = __('Pending', 'give'); + break; + case 'processing': + $statusLabel = __('Processing', 'give'); + break; + case 'failed': + $statusLabel = __('Failed', 'give'); + break; + default: + $statusLabel = __('Draft', 'give'); + } + + return sprintf( + '

%2$s

', + $model->status->getValue(), + $statusLabel + ); + } +} diff --git a/src/Campaigns/ListTable/Columns/TitleColumn.php b/src/Campaigns/ListTable/Columns/TitleColumn.php new file mode 100644 index 0000000000..09b513841e --- /dev/null +++ b/src/Campaigns/ListTable/Columns/TitleColumn.php @@ -0,0 +1,45 @@ +%s', + admin_url("edit.php?post_type=give_forms&page=give-campaigns&id=$model->id"), + __('Visit campaign page', 'give'), + $model->title + ); + } +} diff --git a/src/Campaigns/Migrations/Donations/AddCampaignId.php b/src/Campaigns/Migrations/Donations/AddCampaignId.php new file mode 100644 index 0000000000..26aa60a658 --- /dev/null +++ b/src/Campaigns/Migrations/Donations/AddCampaignId.php @@ -0,0 +1,92 @@ +select('campaign_id', 'form_id') + ->getAll(); + + foreach ($data as $relationship) { + $relationships[$relationship->campaign_id][] = $relationship->form_id; + } + + $donations = DB::table('posts') + ->select('ID') + ->attachMeta( + 'give_donationmeta', + 'ID', + 'donation_id', + [DonationMetaKeys::FORM_ID(), 'formId'] + ) + ->where('post_type', 'give_payment') + ->getAll(); + + $donationMeta = []; + + foreach ($donations as $donation) { + foreach ($relationships as $campaignId => $formIds) { + if (in_array($donation->formId, $formIds)) { + $donationMeta[] = [ + 'donation_id' => $donation->ID, + 'meta_key' => DonationMetaKeys::CAMPAIGN_ID, + 'meta_value' => $campaignId, + ]; + + break; + } + } + } + + if ( ! empty($donationMeta)) { + DB::table('give_donationmeta') + ->insert($donationMeta, ['%d', '%s', '%d']); + } + } catch (DatabaseQueryException $exception) { + throw new DatabaseMigrationException("An error occurred while adding campaign ID to the donation meta table", 0, $exception); + } + } +} diff --git a/src/Campaigns/Migrations/MigrateFormsToCampaignForms.php b/src/Campaigns/Migrations/MigrateFormsToCampaignForms.php new file mode 100644 index 0000000000..e7a4ed8b24 --- /dev/null +++ b/src/Campaigns/Migrations/MigrateFormsToCampaignForms.php @@ -0,0 +1,312 @@ +getAllFormsData()); + array_map([$this, 'addUpgradedV2FormToCampaign'], $this->getUpgradedV2FormsData()); + } catch (DatabaseQueryException $exception) { + DB::rollback(); + throw new DatabaseMigrationException('An error occurred while creating initial campaigns', 0, $exception); + } + }); + } + + /** + * @unreleased + */ + protected function getAllFormsData(): array + { + $query = DB::table('posts', 'forms')->distinct() + ->select( + ['forms.ID', 'id'], + ['forms.post_title', 'title'], + ['forms.post_status', 'status'], + ['forms.post_date', 'createdAt'] + ) + ->where('forms.post_type', 'give_forms'); + + $query->select(['formmeta.meta_value', 'settings']) + ->join(function (JoinQueryBuilder $builder) { + $builder + ->leftJoin('give_formmeta', 'formmeta') + ->on('formmeta.form_id', 'forms.ID')->joinRaw("AND formmeta.meta_key = 'formBuilderSettings'"); + }); + + // Exclude forms already associated with a campaign (ie Peer-to-peer). + $query->join(function (JoinQueryBuilder $builder) { + $builder + ->leftJoin('give_campaigns', 'campaigns') + ->on('campaigns.form_id', 'forms.ID'); + }) + ->whereIsNull('campaigns.id'); + + /** + * Exclude forms with an "auto-draft" status, which are WP revisions. + * + * @see https://wordpress.org/documentation/article/post-status/#auto-draft + */ + $query->where('forms.post_status', 'auto-draft', '!='); + + /** + * Excluded upgraded V2 forms as their corresponding V3 version will be used to create the campaign - later the V2 form will be added to the proper campaign as a non-default form through the addUpgradedV2FormToCampaign() method. + */ + $query->whereNotIn('forms.ID', function (QueryBuilder $builder) { + $builder + ->select('meta_value') + ->from('give_formmeta') + ->where('meta_key', 'migratedFormId'); + }); + + // Ensure campaigns will be displayed in the same order on the list table + $query->orderBy('forms.ID'); + + return $query->getAll(); + } + + /** + * @unreleased + * @return array [{formId, campaignId, migratedFormId}] + */ + protected function getUpgradedV2FormsData(): array + { + return DB::table('posts', 'forms') + ->select(['forms.ID', 'formId'], ['campaign_forms.campaign_id', 'campaignId']) + ->attachMeta('give_formmeta', 'ID', 'form_id', 'migratedFormId') + ->join(function (JoinQueryBuilder $builder) { + $builder + ->rightJoin('give_campaign_forms', 'campaign_forms') + ->on('campaign_forms.form_id', 'forms.ID'); + }) + ->where('forms.post_type', 'give_forms') + ->whereIsNotNull('give_formmeta_attach_meta_migratedFormId.meta_value') + ->getAll(); + } + + /** + * @unreleased + */ + public function createCampaignForForm($formData): void + { + $formId = $formData->id; + $formStatus = $formData->status; + $formTitle = $formData->title; + $formCreatedAt = $formData->createdAt; + $isV3Form = ! is_null($formData->settings); + $formSettings = $isV3Form ? json_decode($formData->settings) : $this->getV2FormSettings($formId); + + DB::table('give_campaigns') + ->insert([ + 'form_id' => $formId, + 'campaign_type' => 'core', + 'campaign_title' => $formTitle, + 'status' => $this->mapFormToCampaignStatus($formStatus), + 'short_desc' => $formSettings->formExcerpt, + 'long_desc' => $formSettings->description, + 'campaign_logo' => $formSettings->designSettingsLogoUrl, + 'campaign_image' => $formSettings->designSettingsImageUrl, + 'primary_color' => $formSettings->primaryColor, + 'secondary_color' => $formSettings->secondaryColor, + 'campaign_goal' => $formSettings->goalAmount, + 'goal_type' => $formSettings->goalType, + 'start_date' => $formSettings->goalStartDate, + 'end_date' => $formSettings->goalEndDate, + 'date_created' => $formCreatedAt, + ]); + + $campaignId = DB::last_insert_id(); + + $this->addCampaignFormRelationship($formId, $campaignId); + } + + /** + * @param $data + */ + protected function addUpgradedV2FormToCampaign($data): void + { + $this->addCampaignFormRelationship($data->migratedFormId, $data->campaignId); + } + + /** + * @unreleased + */ + protected function addCampaignFormRelationship($formId, $campaignId) + { + DB::table('give_campaign_forms') + ->insert([ + 'form_id' => $formId, + 'campaign_id' => $campaignId, + ]); + } + + /** + * @unreleased + */ + protected function mapFormToCampaignStatus(string $status): string + { + switch ($status) { + + case 'pending': + return 'pending'; + + case 'draft': + case 'upgraded': // Some V3 forms can have the 'upgraded' status after being migrated from a V2 form + return 'draft'; + + case 'trash': + return 'archived'; + + case 'publish': + case 'private': + return 'active'; + + default: // TODO: How do we handle an unknown form status? + return 'inactive'; + } + } + + /** + * @unreleased + */ + protected function getV2FormSettings(int $formId): stdClass + { + $template = give_get_meta($formId, '_give_form_template', true); + $templateSettings = give_get_meta($formId, "_give_{$template}_form_template_settings", true); + $templateSettings = is_array($templateSettings) ? $templateSettings : []; + + return (object)[ + 'formExcerpt' => get_the_excerpt($formId), + 'description' => $this->getV2FormDescription($templateSettings), + 'designSettingsLogoUrl' => '', + 'designSettingsImageUrl' => $this->getV2FormFeaturedImage($templateSettings, $formId), + 'primaryColor' => $this->getV2FormPrimaryColor($templateSettings), + 'secondaryColor' => '', + 'goalAmount' => $this->getV2FormGoalAmount($formId), + 'goalType' => $this->getV2FormGoalType($formId), + 'goalStartDate' => '', + 'goalEndDate' => '', + ]; + } + + /** + * @unreleased + */ + protected function getV2FormFeaturedImage(array $templateSettings, int $formId): string + { + if ( ! empty($templateSettings['introduction']['image'])) { + // Sequoia Template (Multi-Step) + $featuredImage = $templateSettings['introduction']['image']; + } elseif ( ! empty($templateSettings['visual_appearance']['header_background_image'])) { + // Classic Template - it doesn't use the featured image from the WP default setting as a fallback + $featuredImage = $templateSettings['visual_appearance']['header_background_image']; + } elseif ( ! isset($templateSettings['visual_appearance']['header_background_image'])) { + // Legacy Template or Sequoia Template without the ['introduction']['image'] setting + $featuredImage = get_the_post_thumbnail_url($formId, 'full'); + } else { + $featuredImage = ''; + } + + return $featuredImage; + } + + /** + * @unreleased + */ + protected function getV2FormDescription(array $templateSettings): string + { + if ( ! empty($templateSettings['introduction']['description'])) { + // Sequoia Template (Multi-Step) + $description = $templateSettings['introduction']['description']; + } elseif ( ! empty($templateSettings['visual_appearance']['description'])) { + // Classic Template + $description = $templateSettings['visual_appearance']['description']; + } else { + $description = ''; + } + + return $description; + } + + /** + * @unreleased + */ + protected function getV2FormPrimaryColor(array $templateSettings): string + { + if ( ! empty($templateSettings['introduction']['primary_color'])) { + // Sequoia Template (Multi-Step) + $primaryColor = $templateSettings['introduction']['primary_color']; + } elseif ( ! empty($templateSettings['visual_appearance']['primary_color'])) { + // Classic Template + $primaryColor = $templateSettings['visual_appearance']['primary_color']; + } else { + $primaryColor = ''; + } + + return $primaryColor; + } + + /** + * @unreleased + */ + protected function getV2FormGoalAmount(int $formId) + { + return give_get_form_goal($formId); + } + + /** + * @unreleased + */ + protected function getV2FormGoalType(int $formId): string + { + $onlyRecurringEnabled = filter_var(give_get_meta($formId, '_give_recurring_goal_format', true), + FILTER_VALIDATE_BOOLEAN); + + switch (give_get_form_goal_format($formId)) { + case 'donors': + return $onlyRecurringEnabled ? 'donorsFromSubscriptions' : 'donors'; + case 'donation': + return $onlyRecurringEnabled ? 'subscriptions' : 'donations'; + case 'amount': + case 'percentage': + default: + return $onlyRecurringEnabled ? 'amountFromSubscriptions' : 'amount'; + } + } +} diff --git a/src/Campaigns/Migrations/P2P/SetCampaignType.php b/src/Campaigns/Migrations/P2P/SetCampaignType.php new file mode 100644 index 0000000000..66f9d3d292 --- /dev/null +++ b/src/Campaigns/Migrations/P2P/SetCampaignType.php @@ -0,0 +1,58 @@ +where('campaign_type', '') + ->update([ + 'campaign_type' => CampaignType::PEER_TO_PEER + ]); + } catch (DatabaseQueryException $exception) { + throw new DatabaseMigrationException('An error occurred while updating the campaign type', 0, $exception); + } + } +} diff --git a/src/Campaigns/Migrations/RevenueTable/AddCampaignID.php b/src/Campaigns/Migrations/RevenueTable/AddCampaignID.php new file mode 100644 index 0000000000..a53d7c5586 --- /dev/null +++ b/src/Campaigns/Migrations/RevenueTable/AddCampaignID.php @@ -0,0 +1,51 @@ +give_revenue} ADD INDEX (form_id), ADD INDEX (campaign_id)"); + } catch (DatabaseQueryException $exception) { + throw new DatabaseMigrationException("An error occurred while updating the {$wpdb->give_revenue} table", 0, + $exception); + } + } +} diff --git a/src/Campaigns/Migrations/RevenueTable/AssociateDonationsToCampaign.php b/src/Campaigns/Migrations/RevenueTable/AssociateDonationsToCampaign.php new file mode 100644 index 0000000000..3c1bf2d7cc --- /dev/null +++ b/src/Campaigns/Migrations/RevenueTable/AssociateDonationsToCampaign.php @@ -0,0 +1,54 @@ +give_revenue} AS revenue JOIN {$wpdb->give_campaign_forms} forms ON revenue.form_id = forms.form_id SET revenue.campaign_id = forms.campaign_id"); + } catch (DatabaseQueryException $exception) { + throw new DatabaseMigrationException("An error occurred while updating the {$wpdb->give_revenue} table", 0, + $exception); + } + } +} diff --git a/src/Campaigns/Migrations/Tables/CreateCampaignFormsTable.php b/src/Campaigns/Migrations/Tables/CreateCampaignFormsTable.php new file mode 100644 index 0000000000..49c32034c2 --- /dev/null +++ b/src/Campaigns/Migrations/Tables/CreateCampaignFormsTable.php @@ -0,0 +1,65 @@ +give_campaign_forms; + $charset = DB::get_charset_collate(); + + $sql = "CREATE TABLE $table ( + campaign_id INT UNSIGNED NOT NULL, + form_id INT UNSIGNED NOT NULL, + KEY form_id (form_id), + KEY campaign_id (campaign_id), + PRIMARY KEY (campaign_id, form_id) + ) $charset"; + + try { + DB::delta($sql); + } catch (DatabaseQueryException $exception) { + throw new DatabaseMigrationException("An error occurred while creating the $table table", 0, $exception); + } + } +} diff --git a/src/Campaigns/Migrations/Tables/CreateCampaignsTable.php b/src/Campaigns/Migrations/Tables/CreateCampaignsTable.php new file mode 100644 index 0000000000..f837078d5a --- /dev/null +++ b/src/Campaigns/Migrations/Tables/CreateCampaignsTable.php @@ -0,0 +1,80 @@ +give_campaigns; + $charset = DB::get_charset_collate(); + + $sql = "CREATE TABLE $table ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + campaign_page_id INT UNSIGNED NULL, + form_id INT NOT NULL, + campaign_type VARCHAR(12) NOT NULL DEFAULT '', + enable_campaign_page BOOLEAN NOT NULL DEFAULT 1, + campaign_title TEXT NOT NULL, + campaign_url TEXT NOT NULL, + short_desc TEXT NOT NULL, + long_desc TEXT NOT NULL, + campaign_logo TEXT NOT NULL, + campaign_image TEXT NOT NULL, + primary_color VARCHAR(7) NOT NULL, + secondary_color VARCHAR(7) NOT NULL, + campaign_goal INT UNSIGNED NOT NULL, + goal_type VARCHAR(24) NOT NULL DEFAULT 'amount', + status VARCHAR(12) NOT NULL, + start_date DATETIME NULL, + end_date DATETIME NULL, + date_created DATETIME NOT NULL, + PRIMARY KEY (id) + ) $charset"; + + try { + DB::delta($sql); + } catch (DatabaseQueryException $exception) { + throw new DatabaseMigrationException("An error occurred while creating the $table table", 0, $exception); + } + } +} diff --git a/src/Campaigns/Models/Campaign.php b/src/Campaigns/Models/Campaign.php new file mode 100644 index 0000000000..23d086d901 --- /dev/null +++ b/src/Campaigns/Models/Campaign.php @@ -0,0 +1,202 @@ + 'int', + 'defaultFormId' => 'int', + 'type' => CampaignType::class, + 'enableCampaignPage' => ['bool', true], + 'title' => 'string', + 'shortDescription' => 'string', + 'longDescription' => 'string', + 'logo' => 'string', + 'image' => 'string', + 'primaryColor' => 'string', + 'secondaryColor' => 'string', + 'goal' => 'int', + 'goalType' => CampaignGoalType::class, + 'status' => CampaignStatus::class, + 'startDate' => DateTime::class, + 'endDate' => DateTime::class, + 'createdAt' => DateTime::class, + ]; + + /** + * @unreleased + */ + public function defaultForm(): ?DonationForm + { + return give(DonationFormsRepository::class)->getById($this->defaultFormId); + } + + /** + * @unreleased + */ + public function forms(): ModelQueryBuilder + { + return DonationForm::query() + ->join(function (JoinQueryBuilder $builder) { + $builder + ->leftJoin('give_campaign_forms', 'campaign_forms') + ->on('campaign_forms.form_id', 'id'); + }) + ->where('campaign_forms.campaign_id', $this->id); + } + + /** + * @unreleased + */ + public function page() + { + return give(CampaignPageRepository::class)->findByCampaignId($this->id); + } + + /** + * @unreleased + */ + public static function factory(): CampaignFactory + { + return new CampaignFactory(static::class); + } + + /** + * Find campaign by ID + * + * @unreleased + */ + public static function find($id): ?Campaign + { + return give(CampaignRepository::class)->getById($id); + } + + /** + * Find campaign by Form ID + * + * @unreleased + */ + public static function findByFormId(int $formId): ?Campaign + { + return give(CampaignRepository::class)->getByFormId($formId); + } + + /** + * @unreleased + * + * @throws Exception + */ + public static function create(array $attributes): Campaign + { + $campaign = new static($attributes); + + give(CampaignRepository::class)->insert($campaign); + + return $campaign; + } + + /** + * @unreleased + * + * @throws Exception|InvalidArgumentException + */ + public function save(): void + { + if ( ! $this->id) { + give(CampaignRepository::class)->insert($this); + } else { + give(CampaignRepository::class)->update($this); + } + } + + /** + * @unreleased + * + * @throws Exception + */ + public function delete(): bool + { + return give(CampaignRepository::class)->delete($this); + } + + /** + * @unreleased + * + * @throws Exception + */ + public function merge(Campaign ...$campaignsToMerge): bool + { + return give(CampaignRepository::class)->mergeCampaigns($this, ...$campaignsToMerge); + } + + public function goalProgress() + { + $query = new CampaignDonationQuery($this); + return $query->sumIntendedAmount(); + } + + /** + * @unreleased + * + * @return ModelQueryBuilder + */ + public static function query(): ModelQueryBuilder + { + return give(CampaignRepository::class)->prepareQuery(); + } + + /** + * @unreleased + * + * @param object $object + */ + public static function fromQueryBuilderObject($object): Campaign + { + return (new ConvertQueryDataToCampaign())($object); + } +} diff --git a/src/Campaigns/Models/CampaignPage.php b/src/Campaigns/Models/CampaignPage.php new file mode 100644 index 0000000000..0f4f0ef9c5 --- /dev/null +++ b/src/Campaigns/Models/CampaignPage.php @@ -0,0 +1,117 @@ + 'int', + 'campaignId' => 'int', + 'createdAt' => DateTime::class, + 'updatedAt' => DateTime::class, + ]; + + public $relationships = [ + 'campaign' => Relationship::BELONGS_TO, + ]; + + /** + * @unreleased + */ + public function getEditLinkUrl(): string + { + // By default, the URL is encoded for display purposes. + // Setting any other value prevents encoding the URL. + return get_edit_post_link($this->id, 'redirect'); + } + + /** + * @unreleased + */ + public function campaign() + { + return Campaign::find($this->campaignId); + } + + /** + * @unreleased + */ + public static function find($id) + { + return give(CampaignPageRepository::class) + ->prepareQuery() + ->where('ID', $id) + ->get(); + } + + /** + * @unreleased + */ + public static function create(array $attributes): CampaignPage + { + $campaignPage = new static($attributes); + + give(CampaignPageRepository::class)->insert($campaignPage); + + return $campaignPage; + } + + /** + * @unreleased + */ + public function save(): void + { + if (!$this->id) { + give(CampaignPageRepository::class)->insert($this); + } else { + give(CampaignPageRepository::class)->update($this); + } + } + + /** + * @unreleased + */ + public function delete(): bool + { + return give(CampaignPageRepository::class)->delete($this); + } + + /** + * @unreleased + * + * @return ModelQueryBuilder + */ + public static function query(): ModelQueryBuilder + { + return give(CampaignPageRepository::class)->prepareQuery(); + } + + /** + * @unreleased + */ + public static function fromQueryBuilderObject($object): CampaignPage + { + return new CampaignPage([ + 'id' => (int) $object->id, + 'campaignId' => (int) $object->campaignId, + 'createdAt' => Temporal::toDateTime($object->createdAt), + 'updatedAt' => Temporal::toDateTime($object->updatedAt), + ]); + } +} diff --git a/src/Campaigns/Repositories/CampaignPageRepository.php b/src/Campaigns/Repositories/CampaignPageRepository.php new file mode 100644 index 0000000000..f88b8b0beb --- /dev/null +++ b/src/Campaigns/Repositories/CampaignPageRepository.php @@ -0,0 +1,211 @@ +prepareQuery() + ->where('id', $id) + ->get(); + } + + /** + * @unreleased + */ + public function findByCampaignId(int $campaignId): ?CampaignPage + { + return $this->prepareQuery() + ->where('postmeta_attach_meta_campaignId.meta_value', $campaignId) + ->get(); + } + + /** + * @unreleased + */ + public function insert(CampaignPage $campaignPage): void + { + $this->validate($campaignPage); + + Hooks::doAction('givewp_campaign_page_creating', $campaignPage); + + $dateCreated = Temporal::withoutMicroseconds($campaignPage->createdAt ?: Temporal::getCurrentDateTime()); + $dateCreatedFormatted = Temporal::getFormattedDateTime($dateCreated); + $dateUpdated = $campaignPage->updatedAt ?? $dateCreated; + $dateUpdatedFormatted = Temporal::getFormattedDateTime($dateUpdated); + + DB::query('START TRANSACTION'); + + try { + DB::table('posts') + ->insert([ + 'post_date' => $dateCreatedFormatted, + 'post_date_gmt' => get_gmt_from_date($dateCreatedFormatted), + 'post_modified' => $dateUpdatedFormatted, + 'post_modified_gmt' => get_gmt_from_date($dateUpdatedFormatted), + 'post_status' => 'publish', // TODO: Update to value object + 'post_type' => 'give_campaign_page', + ]); + + $campaignPage->id = DB::last_insert_id();; + $campaignPage->createdAt = $dateCreated; + $campaignPage->updatedAt = $dateUpdated; + + DB::table('postmeta') + ->insert([ + 'post_id' => $campaignPage->id, + 'meta_key' => 'campaignId', + 'meta_value' => $campaignPage->campaignId, + ]); + + } catch (Exception $exception) { + DB::query('ROLLBACK'); + + Log::error('Failed creating a campaign page', [$campaignPage]); + + throw new $exception('Failed creating a campaign page'); + } + + DB::query('COMMIT'); + + Hooks::doAction('givewp_campaign_page_created', $campaignPage); + } + + /** + * @unreleased + */ + public function update(CampaignPage $campaignPage): void + { + $this->validate($campaignPage); + + Hooks::doAction('givewp_campaign_page_updating', $campaignPage); + + $now = Temporal::withoutMicroseconds(Temporal::getCurrentDateTime()); + $nowFormatted = Temporal::getFormattedDateTime($now); + + DB::query('START TRANSACTION'); + + try { + DB::table('posts') + ->where('ID', $campaignPage->id) + ->update([ + 'post_modified' => $nowFormatted, + 'post_modified_gmt' => get_gmt_from_date($nowFormatted), + 'post_status' => 'publish', // TODO: Update to value object + 'post_type' => 'give_campaign_page', + ]); + + $campaignPage->updatedAt = $now; + + DB::table('postmeta') + ->where('post_id', $campaignPage->id) + ->where('meta_key', 'campaignId') + ->update([ + 'meta_value' => $campaignPage->campaignId, + ]); + + } catch (Exception $exception) { + DB::query('ROLLBACK'); + + Log::error('Failed updating a campaign page', [$campaignPage]); + + throw new $exception('Failed updating a campaign page'); + } + + DB::query('COMMIT'); + + Hooks::doAction('givewp_campaign_page_updated', $campaignPage); + } + + /** + * @unreleased + */ + public function delete(CampaignPage $campaignPage): bool + { + DB::query('START TRANSACTION'); + + Hooks::doAction('givewp_campaign_page_deleting', $campaignPage); + + try { + DB::table('posts') + ->where('id', $campaignPage->id) + ->delete(); + + DB::table('postmeta') + ->where('post_id', $campaignPage->id) + ->delete(); + } catch (Exception $exception) { + DB::query('ROLLBACK'); + + Log::error('Failed deleting a campaign page', [$campaignPage]); + + throw new $exception('Failed deleting a campaign page'); + } + + DB::query('COMMIT'); + + Hooks::doAction('givewp_campaign_page_deleted', $campaignPage); + + return true; + } + + /** + * @unreleased + * + * @return ModelQueryBuilder + */ + public function prepareQuery(): ModelQueryBuilder + { + $builder = new ModelQueryBuilder(CampaignPage::class); + + return $builder->from('posts') + ->select( + ['ID', 'id'], + ['post_date', 'createdAt'], + ['post_modified', 'updatedAt'], + ['post_status', 'status'] + ) + ->attachMeta( + 'postmeta', + 'ID', + 'post_id', + 'campaignId' + ) + ->where('post_type', 'give_campaign_page'); + } + + /** + * @unreleased + */ + public function validate(CampaignPage $campaignPage) + { + foreach ($this->requiredProperties as $key) { + if (!isset($campaignPage->$key)) { + throw new InvalidArgumentException("'$key' is required."); + } + } + } +} diff --git a/src/Campaigns/Repositories/CampaignRepository.php b/src/Campaigns/Repositories/CampaignRepository.php new file mode 100644 index 0000000000..b2b814c059 --- /dev/null +++ b/src/Campaigns/Repositories/CampaignRepository.php @@ -0,0 +1,376 @@ +prepareQuery() + ->where('id', $id) + ->get(); + } + + /** + * @unreleased + * + * Get Campaign by Form ID using a lookup table + */ + public function getByFormId(int $formId) + { + return $this->queryByFormId($formId)->get(); + } + + /** + * @unreleased + * + * @param int $formId + * + * @return ModelQueryBuilder + */ + public function queryByFormId(int $formId): ModelQueryBuilder + { + return $this->prepareQuery() + ->leftJoin('give_campaign_forms', 'campaigns.id', 'forms.campaign_id', 'forms') + ->where('forms.form_id', $formId); + } + + /** + * @unreleased + * + * @throws Exception|InvalidArgumentException + */ + public function insert(Campaign $campaign): void + { + $this->validateProperties($campaign); + + Hooks::doAction('givewp_campaign_creating', $campaign); + + $dateCreated = Temporal::withoutMicroseconds($campaign->createdAt ?: Temporal::getCurrentDateTime()); + $dateCreatedFormatted = Temporal::getFormattedDateTime($dateCreated); + $startDateFormatted = Temporal::getFormattedDateTime($campaign->startDate); + $endDateFormatted = Temporal::getFormattedDateTime($campaign->endDate); + + DB::query('START TRANSACTION'); + + try { + DB::table('give_campaigns') + ->insert([ + 'campaign_type' => $campaign->type->getValue(), + 'enable_campaign_page' => $campaign->enableCampaignPage, + 'campaign_title' => $campaign->title, + 'short_desc' => $campaign->shortDescription, + 'long_desc' => $campaign->longDescription, + 'campaign_logo' => $campaign->logo, + 'campaign_image' => $campaign->image, + 'primary_color' => $campaign->primaryColor, + 'secondary_color' => $campaign->secondaryColor, + 'campaign_goal' => $campaign->goal, + 'goal_type' => $campaign->goalType->getValue(), + 'status' => $campaign->status->getValue(), + 'start_date' => $startDateFormatted, + 'end_date' => $endDateFormatted, + 'date_created' => $dateCreatedFormatted, + ]); + + $campaignId = DB::last_insert_id(); + + } catch (Exception $exception) { + DB::query('ROLLBACK'); + + Log::error('Failed creating a campaign', compact('campaign')); + + throw new $exception('Failed creating a campaign'); + } + + DB::query('COMMIT'); + + $campaign->id = $campaignId; + $campaign->createdAt = $dateCreated; + + Hooks::doAction('givewp_campaign_created', $campaign); + } + + /** + * @unreleased + * + * @throws Exception|InvalidArgumentException + */ + public function update(Campaign $campaign): void + { + $this->validateProperties($campaign); + + $startDateFormatted = Temporal::getFormattedDateTime($campaign->startDate); + $endDateFormatted = Temporal::getFormattedDateTime($campaign->endDate); + + Hooks::doAction('givewp_campaign_updating', $campaign); + + DB::query('START TRANSACTION'); + + try { + DB::table('give_campaigns') + ->where('id', $campaign->id) + ->update([ + 'campaign_type' => $campaign->type->getValue(), + 'enable_campaign_page' => $campaign->enableCampaignPage, + 'campaign_title' => $campaign->title, + 'short_desc' => $campaign->shortDescription, + 'long_desc' => $campaign->longDescription, + 'campaign_logo' => $campaign->logo, + 'campaign_image' => $campaign->image, + 'primary_color' => $campaign->primaryColor, + 'secondary_color' => $campaign->secondaryColor, + 'campaign_goal' => $campaign->goal, + 'goal_type' => $campaign->goalType->getValue(), + 'status' => $campaign->status->getValue(), + 'start_date' => $startDateFormatted, + 'end_date' => $endDateFormatted, + ]); + } catch (Exception $exception) { + DB::query('ROLLBACK'); + + Log::error('Failed updating a campaign', compact('campaign')); + + throw new $exception('Failed updating a campaign'); + } + + DB::query('COMMIT'); + + Hooks::doAction('givewp_campaign_updated', $campaign); + } + + /** + * @unreleased + * + * @throws Exception + */ + public function addCampaignForm(Campaign $campaign, int $donationFormId, bool $isDefault = false) + { + Hooks::doAction('givewp_campaign_form_relationship_creating', $campaign, $donationFormId, $isDefault); + + DB::query('START TRANSACTION'); + + try { + if ($isDefault) { + DB::table('give_campaigns') + ->where('id', $campaign->id) + ->update([ + 'form_id' => $donationFormId, + ]); + + $campaign->defaultFormId = $donationFormId; + } + + DB::table('give_campaign_forms') + ->insert([ + 'form_id' => $donationFormId, + 'campaign_id' => $campaign->id, + ]); + } catch (Exception $exception) { + DB::query('ROLLBACK'); + + Log::error('Failed creating a campaign form relationship', compact('campaign')); + + throw new $exception('Failed creating a campaign form relationship'); + } + + DB::query('COMMIT'); + + Hooks::doAction('givewp_campaign_form_relationship_created', $campaign, $donationFormId, $isDefault); + } + + /** + * @unreleased + * + * @throws Exception + */ + public function updateDefaultCampaignForm(Campaign $campaign, int $donationFormId) + { + Hooks::doAction('givewp_campaign_default_form_updating', $campaign, $donationFormId); + + DB::query('START TRANSACTION'); + + try { + DB::table('give_campaigns') + ->where('id', $campaign->id) + ->update([ + 'form_id' => $donationFormId + ]); + + $campaign->defaultFormId = $donationFormId; + } catch (Exception $exception) { + DB::query('ROLLBACK'); + + Log::error('Failed updating the campaign default form', compact('campaign')); + + throw new $exception('Failed updating the campaign default form'); + } + + DB::query('COMMIT'); + + Hooks::doAction('givewp_campaign_default_form_updated', $campaign, $donationFormId); + } + + /** + * @unreleased + * + * @throws Exception + */ + public function delete(Campaign $campaign): bool + { + DB::query('START TRANSACTION'); + + Hooks::doAction('givewp_campaign_deleting', $campaign); + + try { + DB::table('give_campaigns') + ->where('id', $campaign->id) + ->delete(); + + } catch (Exception $exception) { + DB::query('ROLLBACK'); + + Log::error('Failed deleting a campaign', compact('campaign')); + + throw new $exception('Failed deleting a campaign'); + } + + DB::query('COMMIT'); + + Hooks::doAction('givewp_campaign_deleted', $campaign); + + return true; + } + + /** + * @unreleased + * + * @throws Exception + */ + public function mergeCampaigns(Campaign $destinationCampaign, Campaign ...$campaignsToMerge): bool + { + // Make sure the destination campaign ID will not be included into $campaignsToMergeIds + $campaignsToMergeIds = array_column($campaignsToMerge, 'id'); + if ($key = array_search($destinationCampaign->id, $campaignsToMergeIds)) { + unset($campaignsToMergeIds[$key]); + } + + Hooks::doAction('givewp_campaigns_merging', $destinationCampaign, $campaignsToMergeIds); + + DB::query('START TRANSACTION'); + + try { + // Convert $campaignsToMergeIds to string to use it in the queries + $campaignsToMergeIdsString = implode(', ', $campaignsToMergeIds); + + // Migrate revenue entries from campaigns to merge to the destination campaign + DB::query( + DB::prepare("UPDATE " . DB::prefix('give_revenue') . " SET campaign_id = %d WHERE campaign_id IN ($campaignsToMergeIdsString)", + [ + $destinationCampaign->id, + ]) + ); + + // Migrate forms from campaigns to merge to the destination campaign + DB::query( + DB::prepare("UPDATE " . DB::prefix('give_campaign_forms') . " SET campaign_id = %d WHERE campaign_id IN ($campaignsToMergeIdsString)", + [ + $destinationCampaign->id, + ]) + ); + + // Delete campaigns to merge now that we already migrated the necessary data to the destination campaign + DB::query("DELETE FROM " . DB::prefix('give_campaigns') . " WHERE id IN ($campaignsToMergeIdsString)"); + } catch (Exception $exception) { + DB::query('ROLLBACK'); + + Log::error('Failed merging campaigns into destination campaign', [ + 'campaignsToMergeIds' => $campaignsToMergeIds, + 'destinationCampaign' => compact('destinationCampaign'), + ]); + + throw new $exception('Failed merging campaigns into destination campaign'); + } + + DB::query('COMMIT'); + + Hooks::doAction('givewp_campaigns_merged', $destinationCampaign, $campaignsToMergeIds); + + return true; + } + + /** + * @unreleased + */ + private function validateProperties(Campaign $campaign): void + { + foreach ($this->requiredProperties as $key) { + if ( ! isset($campaign->$key)) { + throw new InvalidArgumentException("'$key' is required."); + } + } + } + + /** + * @unreleased + * + * @return ModelQueryBuilder + */ + public function prepareQuery(): ModelQueryBuilder + { + $builder = new ModelQueryBuilder(Campaign::class); + + return $builder->from('give_campaigns', 'campaigns') + ->select( + 'id', + ['campaigns.form_id', 'defaultFormId'], // Prefix the `form_id` column to avoid conflicts with the `give_campaign_forms` table. + ['campaign_type', 'type'], + ['enable_campaign_page', 'enableCampaignPage'], + ['campaign_title', 'title'], + ['short_desc', 'shortDescription'], + ['long_desc', 'longDescription'], + ['campaign_logo', 'logo'], + ['campaign_image', 'image'], + ['primary_color', 'primaryColor'], + ['secondary_color', 'secondaryColor'], + ['campaign_goal', 'goal'], + ['goal_type', 'goalType'], + 'status', + ['start_date', 'startDate'], + ['end_date', 'endDate'], + ['date_created', 'createdAt'] + ) + // Exclude Peer to Peer campaign type until it is fully supported. + ->where('campaigns.campaign_type', CampaignType::PEER_TO_PEER, '!='); + } +} diff --git a/src/Campaigns/Routes/DeleteCampaignListTable.php b/src/Campaigns/Routes/DeleteCampaignListTable.php new file mode 100644 index 0000000000..616f095d80 --- /dev/null +++ b/src/Campaigns/Routes/DeleteCampaignListTable.php @@ -0,0 +1,116 @@ +endpoint, + [ + [ + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => [$this, 'handleRequest'], + 'permission_callback' => [$this, 'permissionsCheck'], + ], + 'args' => [ + 'ids' => [ + 'type' => 'string', + 'required' => true, + 'validate_callback' => function ($ids) { + foreach ($this->splitString($ids) as $id) { + if ( ! filter_var($id, FILTER_VALIDATE_INT)) { + return false; + } + } + + return true; + }, + ], + ], + ] + ); + } + + /** + * @unreleased + * + * @throws Exception + */ + public function handleRequest(WP_REST_Request $request): WP_Rest_Response + { + $ids = $this->splitString($request->get_param('ids')); + $errors = []; + $successes = []; + + foreach ($ids as $id) { + $campaignDeleted = give(CampaignRepository::class)->getById($id)->delete(); + $campaignDeleted ? $successes[] = $id : $errors[] = $id; + } + + return new WP_REST_Response(['errors' => $errors, 'successes' => $successes]); + } + + + /** + * Split string + * + * @unreleased + * + * @return string[] + */ + protected function splitString(string $ids): array + { + if (strpos($ids, ',')) { + return array_map('trim', explode(',', $ids)); + } + + return [trim($ids)]; + } + + /** + * @unreleased + * + * @return bool|WP_Error + */ + public function permissionsCheck() + { + return current_user_can('delete_posts') ?: new WP_Error( + 'rest_forbidden', + esc_html__("You don't have permission to delete Campaigns", 'give'), + ['status' => is_user_logged_in() ? 403 : 401] + ); + } +} diff --git a/src/Campaigns/Routes/GetCampaignRevenue.php b/src/Campaigns/Routes/GetCampaignRevenue.php new file mode 100644 index 0000000000..d21e5c9b9c --- /dev/null +++ b/src/Campaigns/Routes/GetCampaignRevenue.php @@ -0,0 +1,96 @@ + WP_REST_Server::READABLE, + 'callback' => [$this, 'handleRequest'], + 'permission_callback' => function () { + return current_user_can('manage_options'); + }, + ], + 'args' => [ + 'id' => [ + 'type' => 'integer', + 'required' => true, + 'sanitize_callback' => 'absint', + ], + ], + ] + ); + } + + /** + * @unreleased + * + * @throws Exception + */ + public function handleRequest($request): WP_REST_Response + { + + $campaign = Campaign::find($request->get_param('id')); + + $dates = $this->getDatesFromRange(new DateTime('-7 days'), new DateTime()); + + $query = new CampaignDonationQuery($campaign); + $query->between(new DateTime('-7 days'), new DateTime()); + $results = $query->getDonationsByDay(); + + foreach($results as $result) { + $dates[$result->date] = $result->amount; + } + + $data = []; + foreach($dates as $date => $amount) { + $data[] = [ + 'date' => $date, + 'amount' => $amount, + ]; + } + + return new WP_REST_Response($data, 200); + } + + public function getDatesFromRange(DateTimeInterface $startDate, DateTimeInterface $endDate): array + { + $period = new DatePeriod( + $startDate, + new DateInterval('P1D'), + $endDate + ); + + $dates = array_map(function($date) { + return $date->format('Y-m-d'); + }, iterator_to_array($period)); + + return array_fill_keys($dates, 0); + } +} diff --git a/src/Campaigns/Routes/GetCampaignStatistics.php b/src/Campaigns/Routes/GetCampaignStatistics.php new file mode 100644 index 0000000000..d94d1a069d --- /dev/null +++ b/src/Campaigns/Routes/GetCampaignStatistics.php @@ -0,0 +1,93 @@ + WP_REST_Server::READABLE, + 'callback' => [$this, 'handleRequest'], + 'permission_callback' => function () { + return current_user_can('manage_options'); + }, + ], + 'args' => [ + 'id' => [ + 'type' => 'integer', + 'required' => true, + 'sanitize_callback' => 'absint', + ], + 'rangeInDays' => [ + 'type' => 'integer', + 'required' => false, + 'sanitize_callback' => 'absint', + 'default' => 0, // Zero to mean "all time". + ], + ], + ] + ); + } + + /** + * @unreleased + * + * @throws Exception + */ + public function handleRequest($request): WP_REST_Response + { + $campaign = Campaign::find($request->get_param('id')); + + $query = new CampaignDonationQuery($campaign); + + if(!$request->get_param('rangeInDays')) { + return new WP_REST_Response([[ + 'amountRaised' => $query->sumIntendedAmount(), + 'donationCount' => $query->countDonations(), + 'donorCount' => $query->countDonors(), + ]]); + } + + $days = $request->get_param('rangeInDays'); + $date = new DateTimeImmutable('now', wp_timezone()); + $interval = DateInterval::createFromDateString("-$days days"); + $period = new DatePeriod($date, $interval, 1); + + return new WP_REST_Response(array_map(function($targetDate) use ($query, $interval) { + + $query = $query->between( + Temporal::withStartOfDay($targetDate->add($interval)), + Temporal::withEndOfDay($targetDate) + ); + + return [ + 'amountRaised' => $query->sumIntendedAmount(), + 'donationCount' => $query->countDonations(), + 'donorCount' => $query->countDonors(), + ]; + }, iterator_to_array($period) )); + } +} diff --git a/src/Campaigns/Routes/GetCampaignsListTable.php b/src/Campaigns/Routes/GetCampaignsListTable.php new file mode 100644 index 0000000000..e179ab3450 --- /dev/null +++ b/src/Campaigns/Routes/GetCampaignsListTable.php @@ -0,0 +1,192 @@ +endpoint, + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [$this, 'handleRequest'], + 'permission_callback' => [$this, 'permissionsCheck'], + ], + 'args' => [ + 'page' => [ + 'type' => 'integer', + 'default' => 1, + 'minimum' => 1, + ], + 'perPage' => [ + 'type' => 'integer', + 'default' => 30, + 'minimum' => 1, + ], + 'search' => [ + 'type' => 'string', + 'required' => false, + 'sanitize_callback' => 'sanitize_text_field', + ], + 'status' => [ + 'type' => 'string', + 'required' => false, + 'sanitize_callback' => 'sanitize_text_field', + ], + 'sortColumn' => [ + 'type' => 'string', + 'default' => 'id', + 'sanitize_callback' => 'sanitize_text_field', + ], + 'sortDirection' => [ + 'type' => 'string', + 'default' => 'asc', + 'enum' => ['asc', 'desc'], + ], + 'locale' => [ + 'type' => 'string', + 'required' => false, + 'default' => get_locale(), + ], + ], + ] + ); + } + + /** + * @unreleased + */ + public function handleRequest(WP_REST_Request $request): WP_REST_Response + { + $this->request = $request; + $this->listTable = give(CampaignsListTable::class); + + $campaigns = $this->getCampaigns(); + $campaignsCount = $this->getTotalCampaignsCount(); + $pageCount = (int)ceil($campaignsCount / $request->get_param('perPage')); + + $this->listTable->items($campaigns, $this->request->get_param('locale') ?? ''); + $items = $this->listTable->getItems(); + + + return new WP_REST_Response( + [ + 'items' => $items, + 'totalItems' => $campaignsCount, + 'totalPages' => $pageCount, + ] + ); + } + + /** + * @unreleased + */ + public function getCampaigns(): array + { + $page = $this->request->get_param('page'); + $perPage = $this->request->get_param('perPage'); + $sortColumns = $this->listTable->getSortColumnById($this->request->get_param('sortColumn') ?: 'id'); + $sortDirection = $this->request->get_param('sortDirection') ?: 'desc'; + + $query = give(CampaignRepository::class)->prepareQuery(); + $query = $this->getWhereConditions($query); + + foreach ($sortColumns as $sortColumn) { + $query->orderBy($sortColumn, $sortDirection); + } + + $query->limit($perPage) + ->offset(($page - 1) * $perPage); + + $campaigns = $query->getAll(); + + if ( ! $campaigns) { + return []; + } + + return $campaigns; + } + + /** + * @unreleased + */ + public function getTotalCampaignsCount(): int + { + $query = Campaign::query(); + $query = $this->getWhereConditions($query); + + return $query->count(); + } + + /** + * @unreleased + */ + private function getWhereConditions(QueryBuilder $query): QueryBuilder + { + $search = $this->request->get_param('search'); + $status = $this->request->get_param('status'); + + if ($search) { + if (ctype_digit($search)) { + $query->where('id', $search); + } else { + $query->whereLike('campaign_title', $search); + $query->orWhereLike('short_desc', $search); + } + } + + if ($status && 'any' !== $status) { + $query->where('status', $status); + } + + return $query; + } + + /** + * @unreleased + * + * @return bool|WP_Error + */ + public function permissionsCheck() + { + return current_user_can('edit_posts') ?: new WP_Error( + 'rest_forbidden', + esc_html__("You don't have permission to view Campaigns", 'give'), + ['status' => is_user_logged_in() ? 403 : 401] + ); + } +} diff --git a/src/Campaigns/Routes/RegisterCampaignRoutes.php b/src/Campaigns/Routes/RegisterCampaignRoutes.php new file mode 100644 index 0000000000..1628e902db --- /dev/null +++ b/src/Campaigns/Routes/RegisterCampaignRoutes.php @@ -0,0 +1,423 @@ +campaignRequestController = $campaignRequestController; + } + + /** + * @unreleased + */ + public function __invoke() + { + $this->registerGetCampaign(); + $this->registerUpdateCampaign(); + $this->registerGetCampaigns(); + $this->registerMergeCampaigns(); + $this->registerCreateCampaign(); + } + + /** + * Get Campaign route + * + * @unreleased + */ + public function registerGetCampaign() + { + register_rest_route( + CampaignRoute::NAMESPACE, + CampaignRoute::CAMPAIGN, + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => function (WP_REST_Request $request) { + return $this->campaignRequestController->getCampaign($request); + }, + 'permission_callback' => function () { + return current_user_can('manage_options'); + }, + ], + 'args' => [ + 'id' => [ + 'type' => 'integer', + 'required' => true, + ], + ] + ] + ); + } + + /** + * Get Campaigns route + * + * @unreleased + */ + public function registerGetCampaigns() + { + register_rest_route( + CampaignRoute::NAMESPACE, + CampaignRoute::CAMPAIGNS, + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => function (WP_REST_Request $request) { + return $this->campaignRequestController->getCampaigns($request); + }, + 'permission_callback' => '__return_true', + ], + 'args' => [ + 'page' => [ + 'type' => 'integer', + 'default' => 1, + 'minimum' => 1, + ], + 'per_page' => [ + 'type' => 'integer', + 'default' => 30, + 'minimum' => 1, + 'maximum' => 100, + ], + ], + ] + ); + } + + /** + * Update Campaign route + * + * @unreleased + */ + public function registerUpdateCampaign() + { + register_rest_route( + CampaignRoute::NAMESPACE, + CampaignRoute::CAMPAIGN, + [ + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => function (WP_REST_Request $request) { + return $this->campaignRequestController->updateCampaign($request); + }, + 'permission_callback' => function () { + return current_user_can('manage_options'); + }, + ], + 'args' => rest_get_endpoint_args_for_schema($this->getSchema(), WP_REST_Server::EDITABLE), + 'schema' => [$this, 'getSchema'], + ] + ); + } + + + /** + * Update Campaign route + * + * @unreleased + */ + public function registerMergeCampaigns() + { + register_rest_route( + CampaignRoute::NAMESPACE, + CampaignRoute::CAMPAIGN . '/merge', + [ + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => function (WP_REST_Request $request) { + return $this->campaignRequestController->mergeCampaigns($request); + }, + 'permission_callback' => function () { + return current_user_can('manage_options'); + }, + ], + 'args' => [ + 'id' => [ + 'type' => 'integer', + 'required' => true, + ], + 'campaignsToMergeIds' => [ + 'type' => 'array', + 'required' => true, + 'items' => [ + 'type' => 'integer', + ], + ], + ], + ] + ); + } + + + /** + * Create Campaign route + * + * @unreleased + */ + public function registerCreateCampaign() + { + register_rest_route( + CampaignRoute::NAMESPACE, + CampaignRoute::CAMPAIGNS, + [ + [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => function (WP_REST_Request $request) { + return $this->campaignRequestController->createCampaign($request); + }, + 'permission_callback' => function () { + return current_user_can('manage_options'); + }, + ], + 'args' => [ + 'title' => [ + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => 'sanitize_text_field', + ], + 'shortDescription' => [ + 'type' => 'string', + 'required' => false, + 'sanitize_callback' => 'sanitize_text_field', + ], + 'startDateTime' => [ + 'type' => 'string', + 'format' => 'date-time', // @link https://datatracker.ietf.org/doc/html/rfc3339#section-5.8 + 'required' => false, + 'validate_callback' => 'rest_parse_date', + 'sanitize_callback' => function ($value) { + return new DateTime($value); + }, + ], + 'endDateTime' => [ + 'type' => 'string', + 'format' => 'date-time', // @link https://datatracker.ietf.org/doc/html/rfc3339#section-5.8 + 'required' => false, + 'validate_callback' => 'rest_parse_date', + 'sanitize_callback' => function ($value) { + return new DateTime($value); + }, + ], + ], + ] + ); + } + + + /** + * @unreleased + */ + public function getSchema(): array + { + return [ + 'title' => 'campaign', + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'type' => 'integer', + 'description' => esc_html__('Campaign ID', 'give'), + ], + 'title' => [ + 'type' => 'string', + 'description' => esc_html__('Campaign title', 'give'), + 'minLength' => 3, + 'maxLength' => 128, + 'errorMessage' => esc_html__('Campaign title is required', 'give'), + ], + 'status' => [ + 'enum' => ['active', 'inactive', 'draft', 'pending', 'processing', 'failed', 'archived'], + 'description' => esc_html__('Campaign status', 'give'), + ], + 'shortDescription' => [ + 'type' => 'string', + 'description' => esc_html__('Campaign short description', 'give'), + ], + 'goal' => [ + 'type' => 'number', + 'minimum' => 1, + 'description' => esc_html__('Campaign goal', 'give'), + 'errorMessage' => esc_html__('Must be a number', 'give'), + ], + 'goalProgress' => [ + 'type' => 'number', + 'description' => esc_html__('Campaign goal progress', 'give'), + ], + 'goalType' => [ + 'enum' => [ + 'amount', + 'donations', + 'donors', + 'amountFromSubscriptions', + 'subscriptions', + 'donorsFromSubscriptions', + ], + 'description' => esc_html__('Campaign goal type', 'give'), + ], + 'enableCampaignPage' => [ + 'type' => 'boolean', + 'default' => true, + 'description' => esc_html__('Enable campaign page for your campaign.', 'give'), + ], + 'defaultFormId' => [ + 'type' => 'integer', + 'description' => esc_html__('Default campaign form ID', 'give'), + ], + ], + 'required' => ['id', 'title', 'goal', 'goalType'], + 'allOf' => [ + [ + 'if' => [ + 'properties' => [ + 'goalType' => [ + 'const' => 'amount', + ], + ], + ], + 'then' => [ + 'properties' => [ + 'goal' => [ + 'minimum' => 1, + 'type' => 'number' + ], + ], + 'errorMessage' => [ + 'properties' => [ + 'goal' => esc_html__('Goal amount must be greater than 0', 'give'), + ], + ], + ], + ], + [ + 'if' => [ + 'properties' => [ + 'goalType' => [ + 'const' => 'donations', + ], + ], + ], + 'then' => [ + 'properties' => [ + 'goal' => [ + 'minimum' => 1, + 'type' => 'number' + ], + ], + 'errorMessage' => [ + 'properties' => [ + 'goal' => esc_html__('Number of donations must be greater than 0', 'give'), + ], + ], + ], + ], + [ + 'if' => [ + 'properties' => [ + 'goalType' => [ + 'const' => 'donors', + ], + ], + ], + 'then' => [ + 'properties' => [ + 'goal' => [ + 'minimum' => 1, + 'type' => 'number' + ], + ], + 'errorMessage' => [ + 'properties' => [ + 'goal' => esc_html__('Number of donors must be greater than 0', 'give'), + ], + ], + ], + ], + [ + 'if' => [ + 'properties' => [ + 'goalType' => [ + 'const' => 'amountFromSubscriptions', + ], + ], + ], + 'then' => [ + 'properties' => [ + 'goal' => [ + 'minimum' => 1, + 'type' => 'number' + ], + ], + 'errorMessage' => [ + 'properties' => [ + 'goal' => esc_html__('Goal recurring amount must be greater than 0', 'give'), + ], + ], + ], + ], + [ + 'if' => [ + 'properties' => [ + 'goalType' => [ + 'const' => 'subscriptions', + ], + ], + ], + 'then' => [ + 'properties' => [ + 'goal' => [ + 'minimum' => 1, + 'type' => 'number' + ], + ], + 'errorMessage' => [ + 'properties' => [ + 'goal' => esc_html__('Number of recurring donations must be greater than 0', 'give'), + ], + ], + ], + ], + [ + 'if' => [ + 'properties' => [ + 'goalType' => [ + 'const' => 'donorsFromSubscriptions', + ], + ], + ], + 'then' => [ + 'properties' => [ + 'goal' => [ + 'minimum' => 1, + 'type' => 'number' + ], + ], + 'errorMessage' => [ + 'properties' => [ + 'goal' => esc_html__('Number of recurring donors must be greater than 0', 'give'), + ], + ], + ], + ], + ], + ]; + } +} diff --git a/src/Campaigns/ServiceProvider.php b/src/Campaigns/ServiceProvider.php new file mode 100644 index 0000000000..c6c7f6282c --- /dev/null +++ b/src/Campaigns/ServiceProvider.php @@ -0,0 +1,161 @@ +singleton('campaigns', CampaignRepository::class); + $this->registerTableNames(); + } + + /** + * @unreleased + * @inheritDoc + */ + public function boot(): void + { + $this->registerMenus(); + $this->registerActions(); + $this->setupCampaignPages(); + $this->registerMigrations(); + $this->registerRoutes(); + $this->registerCampaignEntity(); + $this->registerCampaignBlocks(); + $this->setupCampaignForms(); + } + + /** + * @unreleased + */ + private function registerRoutes() + { + Hooks::addAction('rest_api_init', Routes\RegisterCampaignRoutes::class); + Hooks::addAction('rest_api_init', Routes\GetCampaignsListTable::class, 'registerRoute'); + Hooks::addAction('rest_api_init', Routes\DeleteCampaignListTable::class, 'registerRoute'); + Hooks::addAction('rest_api_init', Routes\GetCampaignStatistics::class, 'registerRoute'); + Hooks::addAction('rest_api_init', Routes\GetCampaignRevenue::class, 'registerRoute'); + } + + /** + * @unreleased + */ + private function registerMigrations(): void + { + give(MigrationsRegister::class)->addMigrations( + [ + CreateCampaignsTable::class, + SetCampaignType::class, + CreateCampaignFormsTable::class, + MigrateFormsToCampaignForms::class, + RevenueTableAddCampaignID::class, + AssociateDonationsToCampaign::class, + AddIndexes::class, + DonationsAddCampaignId::class + ] + ); + } + + /** + * @unreleased + */ + private function registerTableNames(): void + { + global $wpdb; + + $wpdb->give_campaigns = $wpdb->prefix . 'give_campaigns'; + $wpdb->give_campaign_forms = $wpdb->prefix . 'give_campaign_forms'; + } + + /** + * @unreleased + */ + private function registerActions(): void + { + Hooks::addAction('givewp_campaign_deleted', DeleteCampaignPage::class); + Hooks::addAction('givewp_donation_form_creating', FormInheritsCampaignGoal::class); + } + + /** + * @unreleased + */ + private function registerMenus() + { + Hooks::addAction('admin_menu', CampaignsAdminPage::class, 'addCampaignsSubmenuPage', 999); + } + + private function setupCampaignPages() + { + Hooks::addAction('init', Actions\RegisterCampaignPagePostType::class); + Hooks::addAction('admin_action_edit_campaign_page', Actions\EditCampaignPageRedirect::class); + } + + /** + * @unreleased + */ + private function registerCampaignEntity() + { + Hooks::addAction('init', Actions\RegisterCampaignEntity::class); + } + + /** + * @unreleased + */ + private function setupCampaignForms() + { + if (CampaignsAdminPage::isShowingDetailsPage()) { + Hooks::addAction('admin_enqueue_scripts', DonationFormsAdminPage::class, 'loadScripts'); + } + + /** + * We implemented a feature to load the stats columns ("Goal", "Donations" and "Revenue") using an async approach, + * so we could prevent a long page load on websites with lots of forms. However, the campaign details page's current + * "Forms" tab still doesn't support it. Still, it's using the same Form List Table that active the async approach by + * default, so the line below is necessary to disable it while we still don't have support for async loading on this screen. + * + * @see https://github.com/impress-org/givewp/pull/7483 + */ + if ( ! defined('GIVE_IS_ALL_STATS_COLUMNS_ASYNC_ON_ADMIN_FORM_LIST_VIEWS')) { + define('GIVE_IS_ALL_STATS_COLUMNS_ASYNC_ON_ADMIN_FORM_LIST_VIEWS', false); + } + + Hooks::addAction('save_post_give_forms', AddCampaignFormFromRequest::class, 'optionBasedFormEditor', 10, 3); + Hooks::addAction('givewp_donation_form_created', AddCampaignFormFromRequest::class, 'visualFormBuilder'); + Hooks::addAction('givewp_campaign_created', CreateDefaultCampaignForm::class); + } + + /** + * @unreleased + */ + private function registerCampaignBlocks() + { + Hooks::addAction('rest_api_init', Actions\RegisterCampaignIdRestField::class); + Hooks::addAction('init', Actions\RegisterCampaignBlocks::class); + } +} diff --git a/src/Campaigns/ValueObjects/CampaignGoalType.php b/src/Campaigns/ValueObjects/CampaignGoalType.php new file mode 100644 index 0000000000..469647b5ab --- /dev/null +++ b/src/Campaigns/ValueObjects/CampaignGoalType.php @@ -0,0 +1,31 @@ +[0-9]+)'; + const CAMPAIGNS = 'campaigns'; +} diff --git a/src/Campaigns/ValueObjects/CampaignStatus.php b/src/Campaigns/ValueObjects/CampaignStatus.php new file mode 100644 index 0000000000..6ffa5516b1 --- /dev/null +++ b/src/Campaigns/ValueObjects/CampaignStatus.php @@ -0,0 +1,36 @@ +); +} diff --git a/src/Campaigns/resources/admin/campaigns-list-table.tsx b/src/Campaigns/resources/admin/campaigns-list-table.tsx new file mode 100644 index 0000000000..d99f8b1c98 --- /dev/null +++ b/src/Campaigns/resources/admin/campaigns-list-table.tsx @@ -0,0 +1,6 @@ +import {createRoot} from 'react-dom/client'; +import CampaignsListTable from './components/CampaignsListTable'; + +const container = document.getElementById('give-admin-campaigns-root'); +const root = createRoot(container!); +root.render(); diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/CampaignDetailsPage.module.scss b/src/Campaigns/resources/admin/components/CampaignDetailsPage/CampaignDetailsPage.module.scss new file mode 100644 index 0000000000..a020eaccef --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/CampaignDetailsPage.module.scss @@ -0,0 +1,585 @@ +.container { + margin-top: 3rem; + padding: 3rem; +} + +:global { + :root { + --give-primary-color: #69b868; + } + + .post-type-give_forms #wpbody { + box-sizing: border-box; + background-color: #f9fafb; + min-height: calc(100vh - 32px); + + & > a { + text-decoration: underline; + } + } + + .post-type-give_forms #wpbody-content { + box-sizing: border-box; + } + + .post-type-give_forms #wpbody::after { + all: revert; + } + + .give-visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; + } + + #wpcontent { + padding: 0; + } +} + +.page { + box-sizing: border-box; + color: #333; + font-family: Inter, system-ui, sans-serif; + font-size: 1rem; + + *, + ::before, + ::after { + box-sizing: inherit; + } +} + +.pageHeader { + background-color: var(--givewp-shades-white); + padding-block: 1rem; + padding-inline: 1.5rem; +} + +.flexContainer { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-top: var(--givewp-spacing-2); + + & > * { + flex-shrink: 0; + } +} + +.flexRow { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + column-gap: var(--givewp-spacing-2); + margin-top: auto; +} + +.flexRow:not(:first-child) { + flex: 1; + justify-content: flex-end; +} + +.justifyContentEnd { + flex: 1; + justify-content: flex-end; +} + +.breadcrumb { + display: flex; + align-items: center; + gap: 0.25rem; + line-height: 1.125rem; + font-size: 0.75rem; + + & > span { + font-weight: bold; + color: var(--givewp-neutral-900); + } + + & > a { + text-decoration: none; + color: var(--givewp-neutral-500); + font-weight: 400; + } + + & > a:hover { + text-decoration: underline; + } +} + +.pageTitle { + color: var(--givewp-neutral-900); + margin: 0; + font-size: 1.5rem; + font-weight: 700; + line-height: 2.25rem; +} + +select[name="campaignId"] { + display: none; +} + +.tabs { + display: flex; + gap: 0.25rem; + background-color: #fff; + border-bottom: 0.0625rem solid #dbdbdb; + + &:not(.fullWidth) { + padding: 0 var(--givewp-spacing-6); + } + + &.fullWidth { + padding-left: var(--givewp-spacing-6); + } +} + +.tabs [data-reach-tab], +.tabs [role='tab'] { + position: relative; + appearance: none; + padding: var(--givewp-spacing-2) var(--givewp-spacing-4); + border: 0; + background-color: transparent; + color: var(--givewp-neutral-700); + font-size: 0.8rem; + line-height: 1.5rem; + text-align: center; + cursor: pointer; + box-sizing: border-box +} + +@media screen and (min-width: 48rem) { + .tabs [data-reach-tab], + .tabs [role='tab'] { + font-size: 1rem; + } +} + +.tabs [data-reach-tab]::after, +.tabs [role='tab']:after { + content: ''; + display: block; + position: absolute; + top: calc(100% - 0.1875em); + left: 0; + right: 0; + height: 0.1875rem; + background-color: transparent; + transition: background-color 100ms ease-in-out; +} + +.tabs [role='tab']:hover, +.tabs [data-reach-tab]:hover { + font-weight: 500; + color: var(--givewp-neutral-900); + background-color: var(--givewp-neutral-50); +} + +.tabs [data-reach-tab][aria-selected='true'], +.tabs [role='tab'][aria-selected='true'] { + font-weight: 600; + color: var(--givewp-neutral-900); +} + +// set width for each tab to maintain size on hover. +.tabs [data-reach-tab]:first-child, +.tabs [role='tab']:first-child { + width: 106px; +} + +.tabs [data-reach-tab]:nth-child(2), +.tabs [role='tab']:nth-child(2) { + width: 95px; +} + +.tabs [data-reach-tab]:last-child, +.tabs [role='tab']:last-child { + width: 79px; +} + +.tabs [data-reach-tab][aria-selected='true']::after, +.tabs [role='tab'][aria-selected='true']::after { + background-color: #66bb6a; +} + +.archiveDialogContent { + font-size: 16px; + font-weight: 500; + line-height: 1.5; + color: var(--givewp-neutral-700); +} + +.archiveDialogButtons { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: var(--givewp-spacing-2); + margin-top: var(--givewp-spacing-6); + + button { + cursor: pointer; + display: flex; + flex: 1; + justify-content: center; + border-radius: 4px; + font-size: 1rem; + font-weight: 500; + line-height: 1.5; + padding: var(--givewp-spacing-3) var(--givewp-spacing-6); + border: none; + } + + .cancelButton { + border: 1px solid var(--givewp-neutral-300); + background-color: var(--givewp-shades-white); + color: var(--givewp-neutral-900); + + &:hover { + background-color: var(--givewp-neutral-50); + } + } + + .confirmButton { + border: 1px solid var(--givewp-red-500); + background-color: var(--givewp-red-500); + color: var(--givewp-shades-white); + + &:hover { + border: 1px solid var(--givewp-red-400); + background-color: var(--givewp-red-400); + } + } +} + +.pageContent { + /*padding: 0 var(--givewp-spacing-6) var(--givewp-spacing-6);*/ + + &:not(.fullWidth) { + padding: var(--givewp-spacing-4) var(--givewp-spacing-6); + } + + section { + margin-bottom: var(--givewp-spacing-14); + position: relative; + + h2 { + font-size: 1.125rem; + line-height: 1.56; + margin: 0 0 var(--givewp-spacing-2); + } + } + + .sections { + display: flex; + flex-direction: column; + gap: var(--givewp-spacing-6); + + .section { + display: flex; + align-content: flex-start; + flex-direction: row; + flex-wrap: wrap; + background-color: var(--givewp-shades-white); + padding: var(--givewp-spacing-9) var(--givewp-spacing-6); + border-radius: var(--givewp-spacing-2); + + @media (max-width: 1100px) { + flex-direction: column; + } + + .leftColumn { + flex: 1; + padding-right: var(--givewp-spacing-6); + + @media (max-width: 1100px) { + width: 100%; + margin-bottom: var(--givewp-spacing-4); + } + } + + .rightColumn { + flex: 2; + } + + .sectionTitle { + font-size: 18px; + font-weight: 600; + line-height: 1.56; + color: var(--givewp-neutral-900); + padding: 0; + margin-bottom: var(--givewp-spacing-1); + } + + .sectionDescription { + font-size: 16px; + line-height: 1.5; + color: var(--givewp-neutral-500); + } + + .sectionSubtitle { + font-size: 1rem; + font-weight: 500; + line-height: 1.5; + color: var(--givewp-neutral-700); + } + + .sectionField { + position: relative; + + display: flex; + flex-direction: column; + gap: var(--givewp-spacing-1); + margin-bottom: var(--givewp-spacing-10); + + input, + textarea, + select, + .editor, + .upload{ + margin-top: var(--givewp-spacing-1); + color: var(--givewp-neutral-900); + font-weight: 500; + } + + input, select { + padding: var(--givewp-spacing-2) var(--givewp-spacing-4); + } + + input:focus, select:focus { + border-color: rgb(34, 113, 177); + outline: none; + box-shadow: rgb(34, 113, 177) 0px 0px 0px 1px; + } + } + + .sectionField:last-child { + margin: 0; + } + + .sectionFieldDescription { + font-size: 14px; + line-height: 1.43; + color: var(--givewp-neutral-500); + } + + .errorMsg { + font-size: 14px; + padding: var(--givewp-spacing-2) 0; + color: #ff0000; + } + + select { + max-width: 100%; + } + + input, + select, + textarea { + font-size: 1rem; + line-height: 2; + display: block; + width: 100%; + border: 1px solid #9ca0af; + border-radius: var(--givewp-spacing-1); + padding: var(--givewp-spacing-2); + } + } + } + + .toggle { + margin-top: var(--givewp-spacing-3); + + span:is(:global(.components-form-toggle)) { + height: 24px; + } + + & > div { + margin-bottom: 0; + } + + span:is(:global(.components-form-toggle .components-form-toggle__track)) { + width: 48px; + height: 24px; + border-radius: 133.3px; + } + + span:is(:global(.components-form-toggle .components-form-toggle__thumb)) { + width: 1rem; + height: 1rem; + top: 4px; + left: 4px; + } + + span:is(:global(.components-form-toggle.is-checked .components-form-toggle__thumb)) { + left: 12px; + } + + span:is(:global(.components-form-toggle.is-checked .components-form-toggle__track)) { + background-color: #007cba; + } + + span:is(:global(.components-form-toggle .components-form-toggle__input:focus+.components-form-toggle__track)) { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) #fff, 0 0 0 calc(var(--wp-admin-border-width-focus) * 2) #007cba; + } + + label { + font-family: Inter, system-ui, sans-serif; + font-size: 1rem; + font-weight: 500; + line-height: 1.5; + color: #1f2937; + padding: var(--givewp-spacing-2) 0; + margin-left: 0.5rem; + } + + p { + font-family: Inter, system-ui, sans-serif; + font-size: .875rem; + color: #4b5563; + margin-top: -0.1rem; + margin-left: 1.5rem; + } + } + + .warningNotice { + display: flex; + gap: 0.3rem; + margin-top: var(--givewp-spacing-2); + padding: 0 0.5rem 0 0.5rem; + background-color: #fffaf2; + border-radius: 4px; + border: 1px solid var(--givewp-orange-400); + border-left-width: 4px; + font-size: 0.875rem; + font-weight: 500; + color: #1a0f00; + + svg { + margin: 0.8rem 0.3rem; + height: 1.25rem; + width: 1.25rem; + } + } +} + +.loadingContainer { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + + .loadingContainerContent { + display: flex; + background-color: #fff; + padding: var(--givewp-spacing-6); + border-radius: var(--givewp-spacing-2); + margin-bottom: var(--givewp-spacing-4); + align-items: center; + flex-direction: column; + } + + .loadingContainerContentText { + padding: var(--givewp-spacing-4); + font-size: 1rem; + } +} + +:global(#give-admin-campaigns-root) { + + + .editCampaignPageButton, + .updateCampaignButton { + display: flex; + align-content: center; + border-radius: var(--givewp-rounded-4); + font-size: 0.875rem; + font-weight: 500; + line-height: 1.5; + text-align: center; + padding: var(--givewp-spacing-2) var(--givewp-spacing-4); + + svg { + margin: 3px 0 0 8px !important; + } + } + + .editCampaignPageButton { + color: #060c1a; + background-color: var(--givewp-neutral-100); + border-color: var(--givewp-neutral-100); + + &:hover { + background-color: var(--givewp-neutral-200); + border-color: var(--givewp-neutral-200); + } + } + + .campaignButtonDots { + background-color: var(--givewp-neutral-100); + border-color: var(--givewp-neutral-100); + border-radius: var(--givewp-rounded-4); + line-height: 0; + padding: var(--givewp-spacing-2); + + &:hover, &:active, &:focus { + background-color:var(--givewp-neutral-200); + border-color: var(--givewp-neutral-200); + } + } + + .campaignButtonDotsActive { + background-color: var(--givewp-neutral-200); + border-color: var(--givewp-neutral-200); + } + + .contextMenu { + position: absolute; + display: flex; + flex-direction: column; + gap: var(--givewp-spacing-1); + z-index: 9999; + padding: var(--givewp-spacing-1); + top: 50px; + width: 203px; + border-radius: 4px; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + border: solid 1px var(--givewp-neutral-50); + background-color: var(--givewp-shades-white); + + .contextMenuItem { + text-decoration: none; + gap: 4px; + display: flex; + align-items: center; + padding: var(--givewp-spacing-2); + font-size: .875rem; + font-weight: 500; + line-height: 1.43; + color: var(--givewp-neutral-700); + + + &:hover { + background-color: #f3f4f6; + } + } + + .archive { + color: var(--givewp-red-500); + } + + .draft { + font-weight: bold; + } + } +} diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/ArchiveCampaignDialog.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/ArchiveCampaignDialog.tsx new file mode 100644 index 0000000000..492b023fcb --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/ArchiveCampaignDialog.tsx @@ -0,0 +1,53 @@ +import {__} from '@wordpress/i18n' +import ModalDialog from '@givewp/components/AdminUI/ModalDialog'; +import {ErrorIcon} from '../../Icons'; +import styles from '../CampaignDetailsPage.module.scss' + +/** + * @unreleased + */ +export default ({ + isOpen, + title, + handleClose, + handleConfirm, + className, +}: { + isOpen: boolean; + handleClose: () => void; + handleConfirm: () => void; + title: string; + className?: string; +}) => { + return ( + } + isOpen={isOpen} + showHeader={true} + handleClose={handleClose} + title={title} + wrapperClassName={className} + > + <> +
+ {__('Are you sure you want to archive your campaign? All forms associated with this campaign will be inaccessible to donors.', 'give')} +
+
+ + +
+ +
+ ); +} diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats/index.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats/index.tsx new file mode 100644 index 0000000000..1de9b3d5b8 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats/index.tsx @@ -0,0 +1,175 @@ +import {__} from '@wordpress/i18n'; +import {useEffect, useState} from "react"; +import RevenueChart from "../RevenueChart"; +import GoalProgressChart from "../GoalProgressChart"; +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; +import HeaderText from '../HeaderText'; +import HeaderSubText from '../HeaderSubText'; +import DefaultFormWidget from "../DefaultForm"; +import {GiveCampaignDetails} from "@givewp/campaigns/admin/components/CampaignDetailsPage/types"; +import {useCampaignEntityRecord} from '@givewp/campaigns/utils'; + +import styles from "./styles.module.scss" + +const campaignId = new URLSearchParams(window.location.search).get('id'); + +declare const window: { + GiveCampaignDetails: GiveCampaignDetails; +} & Window; + +const pluck = (array: any[], property: string) => array.map(element => element[property]) + +const filterOptions = [ + { label: __('Today', 'give'), value: 1, description: __('from today', 'give') }, + { label: __('Last 7 days', 'give'), value: 7, description: __('from the last 7 days', 'give') }, + { label: __('Last 30 days', 'give'), value: 30, description: __('from the last 30 days', 'give') }, + { label: __('Last 90 days', 'give'), value: 90, description: __('from the last 90 days', 'give') }, + { label: __('All-time', 'give'), value: 0, description: __('total for all-time', 'give') }, +] + +const currency = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', +}) + +const CampaignStats = () => { + + const [dayRange, setDayRange] = useState(null); + const [stats, setStats] = useState([]); + const {campaign} = useCampaignEntityRecord(); + + useEffect(() => { + onDayRangeChange(0) + }, []) + + const onDayRangeChange = async (days: number) => { + setDayRange(days) + + apiFetch({path: addQueryArgs( '/give-api/v2/campaigns/' + campaignId +'/statistics', {rangeInDays: days} ) } ) + .then(setStats); + } + + const widgetDescription = filterOptions.find(option => option.value === dayRange)?.description + + return ( + <> + +
+ + + + +
+ + +
+
+ + ) +} + +const FooterText = ({children}) => { + return ( +
+ {children} +
+ ) +} + +const DisplayText = ({children}) => { + return ( +
+ {children} +
+ ) +} + +const StatWidget = ({label, values, description, formatter = null}) => { + return ( +
+
+ {label} +
+
+ + {formatter?.format(values[0]) ?? values[0]} + + {!! values[1] && ( + + )} +
+
+ {description} +
+
+ ) +} + +const PercentChangePill = ({value, comparison}) => { + + const change = Math.round(100 * ((value - comparison) / comparison)) ?? 0 + + const [color, backgroundColor, symbol] = change == 0 + ? ['#060c1a', '#f2f2f2', '⯈'] + : change > 0 + ? ['#2d802f', '#f2fff3', '⯅'] + : ['#e35f45', '#fff4f2', '⯆'] + + return ( + + {symbol} {Math.abs(change)}% + + ) + +} + + +const RevenueWidget = () => { + return ( +
+
+ {__('Revenue', 'give')} + {__('Show your revenue over time', 'give')} +
+ +
+ ); +} + +const GoalProgressWidget = () => { + + const {campaign} = useCampaignEntityRecord(); + + return ( +
+
+ {__('Goal progress', 'give')} + {__('Show your campaign performance', 'give')} +
+ +
+ ) +} + +const DateRangeFilters = ({options, onSelect, selected}) => { + return ( +
+ {options.map((option, index) => ( + + ))} +
+ ) +} + + +export default CampaignStats; diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats/styles.module.scss b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats/styles.module.scss new file mode 100644 index 0000000000..623b30cd74 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats/styles.module.scss @@ -0,0 +1,137 @@ +.dateRangeFilter { + padding: var(--givewp-spacing-2); + display: flex; + flex-direction: row; + justify-content: flex-end; + margin-bottom: var(--givewp-spacing-2); + background-color: var(--givewp-neutral-50); + border-radius: var(--givewp-rounded-8); +} + +.dateRangeFilter button { + padding: 0.5rem 1rem; + background-color: transparent; + color: var(--givewp-neutral-900); + font-size: 0.875rem; + font-weight: 500; + line-height: 1.25rem; + border: 0; + cursor: pointer; + + &:hover { + background-color: var(--givewp-neutral-100); + } + + &:first-child { + border-radius: var(--givewp-rounded-4) 0 0 var(--givewp-rounded-4); + } + + &:last-child { + border-radius: 0 var(--givewp-rounded-4) var(--givewp-rounded-4) 0; + } +} + +.dateRangeFilter .selectedDateRange { + background-color: var(--givewp-shades-white); + font-weight: 600; + + &:hover { + background-color: var(--givewp-shades-white); + } +} + +.statWidget { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: var(--givewp-spacing-2); + padding: var(--givewp-spacing-6); + background-color: var(--givewp-shades-white); + color: var(--givewp-neutral-700); + font-weight: 600; + border-radius: var(--givewp-rounded-8); + grid-column: span 1; +} + +.statWidget header, .statWidget footer { + flex: 1; +} + +.statWidget footer{ + font-size: 12px; + font-weight: 400; + line-height: 18px; + color: var(--givewp-neutral-700); +} + +.statWidgetAmount { + flex: 1; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: baseline; +} + +.statWidgetDisplay { + font-size: 2.25rem; + font-weight: 600; + line-height: 44px; + color: var(--givewp-neutral-900); +} + +.revenueWidget { + flex: 2; + background-color: var(--givewp-shades-white); + padding: 20px; + border-radius: var(--givewp-rounded-8); + grid-column: span 2; +} + +.percentChangePill { + padding: 0.5rem; + font-size: .8rem; + border-radius: var(--givewp-rounded-16); +} + +.progressWidget { + flex: 1; + background-color: var(--givewp-shades-white); + padding: 20px; + border-radius: var(--givewp-rounded-8); +} + +.headerSpacing { + display: flex; + flex-direction: column; + gap: .25rem; +} + +.mainGrid{ + display: grid; + grid-template-columns: repeat(3, 1fr); + row-gap: var(--givewp-spacing-6); + column-gap: var(--givewp-spacing-4); +} + +.nestedGrid { + grid-column: span 1; + display: grid; + gap: var(--givewp-spacing-6); +} + +@media (max-width: 768px) { + .mainGrid { + grid-template-columns: 1fr; + gap: var(--givewp-spacing-3); + } + + .revenueWidget { + grid-column: span 1; + } + + .nestGrid { + grid-template-columns: 1fr; + gap: var(--givewp-spacing-3); + } +} \ No newline at end of file diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/DefaultForm/index.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/DefaultForm/index.tsx new file mode 100644 index 0000000000..85018e707b --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/DefaultForm/index.tsx @@ -0,0 +1,29 @@ +import {__} from "@wordpress/i18n"; +import HeaderText from '../HeaderText'; +import HeaderSubText from '../HeaderSubText'; + +import styles from "./styles.module.scss" + +/** + * @unreleased + */ +const DefaultFormWidget = ({defaultForm}: {defaultForm: string}) => { + return ( +
+
+
+ {__('Default campaign form', 'give')} + {__('Your campaign page and blocks will collect donations through this form by default.', 'give')} +
+ + {__('Edit', 'give')} + +
+
+ {defaultForm} +
+
+ ) +} + +export default DefaultFormWidget; diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/DefaultForm/styles.module.scss b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/DefaultForm/styles.module.scss new file mode 100644 index 0000000000..bcd283f09f --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/DefaultForm/styles.module.scss @@ -0,0 +1,46 @@ +.defaultForm { + flex: 1; + display: flex; + flex-direction: column; + gap: 1.5rem; + background-color: var(--givewp-shades-white); + padding: 1rem 1.5rem 1.5rem 1.5rem; + border-radius: var(--givewp-rounded-8); +} + +.description { + display: flex; + gap: 1rem; + align-items: flex-start; + justify-content: space-between; +} + +.headerSpacing { + display: flex; + flex-direction: column; + gap: .25rem; +} + +.edit { + font-size: 0.875rem; + color: var(--givewp-neutral-900); + font-weight: 500; + background-color: var(--givewp-neutral-100); + padding: .5rem 1rem; + border-radius: var(--givewp-rounded-4); + text-decoration: none; + + &:hover { + background-color: var(--givewp-neutral-200); + color: var(--givewp-neutral-900); + } +} + +.formName { + font-weight: 500; + background-color: var(--givewp-neutral-25); + padding: 0.75rem 1rem; + border-radius: var(--givewp-rounded-4); + border: solid 1px var(--givewp-neutral-100); + cursor: default; +} diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/GoalProgressChart/index.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/GoalProgressChart/index.tsx new file mode 100644 index 0000000000..7ecd274610 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/GoalProgressChart/index.tsx @@ -0,0 +1,67 @@ +import {__} from '@wordpress/i18n'; +import Chart from "react-apexcharts"; +import React from "react"; + +import styles from "./styles.module.scss" + +const currency = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', +}) + +const GoalProgressChart = ({ value, goal }) => { + const percentage: number = Math.abs((value / goal) * 100); + return ( +
+
+ +
+
+
{__('Goal', 'give')}
+
{currency.format(goal)}
+
{__('Amount raised', 'give')}
+
+
+ ) +} + +export default GoalProgressChart; diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/GoalProgressChart/styles.module.scss b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/GoalProgressChart/styles.module.scss new file mode 100644 index 0000000000..f6555196c6 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/GoalProgressChart/styles.module.scss @@ -0,0 +1,43 @@ +.goalProgressChart { + display: flex; + gap: 20px; + align-items: center; +} + +/** + * The size of the chart is relative to the container. + * To get close to the design, + * the size is balanced at flex 3/2 + * and the margins use negative values to control padding. + */ +.chartContainer { + flex: 3; + margin: 0 -50px; +} + +.goalDetails { + flex: 2; +} + +.goal { + font-size: 14px; + font-weight: 400; + line-height: 20px; + margin-bottom: 4px; + color: #1F2937; +} + +.amount { + color: #2D802F; + font-size: 18px; + font-weight: 600; + margin-bottom: 2px; + line-height: 28px; +} + +.goalType { + font-size: 12px; + font-weight: 400; + line-height: 18px; + color: #4b5563; +} diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/HeaderSubText.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/HeaderSubText.tsx new file mode 100644 index 0000000000..112b63a917 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/HeaderSubText.tsx @@ -0,0 +1,17 @@ +/** + * @unreleased + */ +const HeaderSubText = ({children}) => { + return ( +
+ {children} +
+ ) +} + +export default HeaderSubText; diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/HeaderText.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/HeaderText.tsx new file mode 100644 index 0000000000..fe969ed252 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/HeaderText.tsx @@ -0,0 +1,17 @@ +/** + * @unreleased + */ +const HeaderText = ({children}) => { + return ( +
+ {children} +
+ ) +} + +export default HeaderText; diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/Notices/ArchivedCampaignNotice.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/Notices/ArchivedCampaignNotice.tsx new file mode 100644 index 0000000000..a4064b9e9f --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/Notices/ArchivedCampaignNotice.tsx @@ -0,0 +1,16 @@ +import {__} from '@wordpress/i18n'; +import {TriangleIcon} from '@givewp/campaigns/admin/components/Icons'; + +export default ({handleClick}) => ( + <> + + + {__("Your campaign is currently archived. You can view the campaign details but won't be able to make any changes until it's moved out of archive.", 'give')} + + + handleClick()}> + {__('Move to draft', 'give')} + + + +) diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/Notices/DefaultFormNotice.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/Notices/DefaultFormNotice.tsx new file mode 100644 index 0000000000..51652e664f --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/Notices/DefaultFormNotice.tsx @@ -0,0 +1,19 @@ +import {__} from '@wordpress/i18n'; +import {CloseIcon} from "@givewp/campaigns/admin/components/Icons"; + +import styles from './styles.module.scss' + +export default ({handleClick}) => ( +
+
+ +
+

+ {__('Default campaign form', 'give')} +

+
+ {__('The default form will always appear at the top of this list. Your campaign page and blocks will collect donations through this form by default. You can change it at any time.', 'give')} +
+
+) + diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/Notices/styles.module.scss b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/Notices/styles.module.scss new file mode 100644 index 0000000000..a002b25593 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/Notices/styles.module.scss @@ -0,0 +1,37 @@ +.tooltip { + position: absolute; + top: 420px; // hacky but I can't think of any other way as the entire list table is wrapped in an element that has the overflow property set + left: 100px; + z-index: 9; + width: 377px; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + gap: 16px; + padding: 16px 24px 24px; + border-radius: 8px; + box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.15); + border: solid 2px #6b7280; + background-color: #fff; + + .close { + cursor: pointer; + position: absolute; + right: 13px; + top: 13px; + } + + h3 { + padding: 0; + margin: 0; + font-size: 16px; + color: #060c1a; + } + + .content { + font-size: 14px; + color: #1f2937; + font-weight: normal; + } +} diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/RevenueChart.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/RevenueChart.tsx new file mode 100644 index 0000000000..5beed0ab01 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/RevenueChart.tsx @@ -0,0 +1,91 @@ +import React, {useEffect, useState} from "react"; +import Chart from "react-apexcharts"; +import apiFetch from "@wordpress/api-fetch"; +import {addQueryArgs} from "@wordpress/url"; + +const campaignId = new URLSearchParams(window.location.search).get('id'); + +const RevenueChart = () => { + + const [max, setMax] = useState(0); + const [categories, setCategories] = useState([]); + const [series, setSeries] = useState([{name: "Revenue", data: []}]); + + useEffect(() => { + apiFetch({path: addQueryArgs( '/give-api/v2/campaigns/' + campaignId +'/revenue' ) } ) + .then((data: {date: string, amount: number}[]) => { + + setMax(Math.max(...data.map(item => item.amount)) * 1.1) + + setCategories(data.map(item => item.date)) + + setSeries([{ + name: "Revenue", + data: data.map(item => item.amount) + }]) + }); + }, []) + + const options = { + chart: { + id: "campaign-revenue", + zoom: { + enabled: false + }, + }, + xaxis: { + categories, + type: 'datetime' as "datetime" | "category" | "numeric", + }, + yaxis: { + max, + }, + stroke: { + color: ['#60a1e2'], + width: 1.5, + curve: 'smooth' as "straight" | "smooth" | "monotoneCubic" | "stepline" | "linestep" | ("straight" | "smooth" | "monotoneCubic" | "stepline" | "linestep")[], + lineCap: 'butt' as "butt" | "square" | "round", + }, + dataLabels: { + enabled: false, + }, + fill: { + type: 'gradient', + gradient: { + colorStops: [ + [ + { + offset: 0, + color: '#eee', + opacity: 1 + }, + { + offset: 0.6, + color: '#b7d4f2', + opacity: 50 + }, + { + offset: 100, + color: '#f0f7ff', + opacity: 1 + } + ], + ], + } + } + }; + + return ( + <> + + + ) +} + +export default RevenueChart diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/Forms.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/Forms.tsx new file mode 100644 index 0000000000..0c40d707d2 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/Forms.tsx @@ -0,0 +1,10 @@ +import DonationFormsListTable from '../../../../../../DonationForms/V2/resources/components/DonationFormsListTable'; +import {useCampaignEntityRecord} from '@givewp/campaigns/utils'; + +/** + * @unreleased + */ +export default () => { + const entity = useCampaignEntityRecord(); + return ; +}; diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/Overview.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/Overview.tsx new file mode 100644 index 0000000000..70dfbe159c --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/Overview.tsx @@ -0,0 +1,16 @@ +import CampaignStats from "../Components/CampaignStats"; + +/** + * @unreleased + */ +export default () => { + + + return ( +
+
+ +
+
+ ); +} diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/Settings.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/Settings.tsx new file mode 100644 index 0000000000..214b23eedc --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/Settings.tsx @@ -0,0 +1,232 @@ +import {__} from '@wordpress/i18n'; +import {useFormContext} from 'react-hook-form'; +import {Currency, Editor, Upload} from '../../Inputs'; +import {GiveCampaignDetails} from '../types'; +import styles from '../CampaignDetailsPage.module.scss'; +import {ToggleControl} from '@wordpress/components'; +import campaignPageImage from './images/campaign-page.svg'; +import {WarningIcon} from '@givewp/campaigns/admin/components/Icons'; + +declare const window: { + GiveCampaignDetails: GiveCampaignDetails; +} & Window; + +/** + * @unreleased + */ +export default () => { + const { + register, + watch, + setValue, + formState: {errors}, + } = useFormContext(); + + const [goalType, image, status, shortDescription, enableCampaignPage] = watch([ + 'goalType', + 'image', + 'status', + 'shortDescription', + 'enableCampaignPage', + ]); + const isDisabled = status === 'archived'; + + return ( +
+ + {/* Campaign Page */} +
+
+
{__('Campaign page', 'give')}
+
+ {__( + 'Set up a landing page for your campaign. The default campaign page has the campaign details, the campaign form, and donor wall.', + 'give' + )} +
+
+ +
+
+ {__('Enable +
+ { + setValue('enableCampaignPage', value, {shouldDirty: true}); + }} + /> +
+ + {!enableCampaignPage && ( +
+ +

+ {__( + 'This will affect the campaign blocks associated with this campaign. Ensure that no campaign blocks are being used on any page.', + 'give' + )} +

+
+ )} + + {errors.enableCampaignPage && ( +
{`${errors.enableCampaignPage.message}`}
+ )} +
+
+
+ + {/* Campaign Details */} +
+
+
{__('Campaign Details', 'give')}
+
+ {__('This includes the campaign title, description, and the cover of your campaign.', 'give')} +
+
+ +
+
+
{__("What's the title of your campaign?", 'give')}
+
+ {__("Give your campaign a title that tells donors what it's about.", 'give')} +
+ + + {errors.title &&
{`${errors.title.message}`}
} +
+ +
+
{__("What's your campaign about?", 'give')}
+
+ {__('Let your donors know the story behind your campaign.', 'give')} +
+ + {isDisabled ? ( + + ) : ( +
+ +
+ )} + + {errors.shortDescription && ( +
{`${errors.shortDescription.message}`}
+ )} +
+ +
+
+ {__('Add a cover image or video for your campaign.', 'give')} +
+
+ {__('Upload an image or video to represent and inspire your campaign.', 'give')} +
+
+ { + setValue('image', coverImageUrl, {shouldDirty: true}); + }} + reset={() => setValue('image', '', {shouldDirty: true})} + /> +
+ + {errors.title &&
{`${errors.title.message}`}
} +
+
+
+ + {/* Campaign Goal */} +
+
+
{__('Campaign Goal', 'give')}
+
+ {__('How would you like to set your goal?', 'give')} +
+
+ +
+
+
+ {__('Set the details of your campaign goal here.', 'give')} +
+ + +
{goalDescription(goalType)}
+ + {errors.goalType &&
{`${errors.goalType.message}`}
} +
+ +
+
{__('How much do you want to raise?', 'give')}
+
+ {__('Let us know the target amount you’re aiming for in your campaign.', 'give')} +
+ + {goalType === 'amount' || goalType === 'amountFromSubscriptions' ? ( + + ) : ( + + )} + + {errors.goal &&
{`${errors.goal.message}`}
} +
+
+
+
+ ); +}; + +const goalDescription = (type: string) => { + switch (type) { + case 'amount': + return __( + 'Your goal progress is measured by the total amount of funds raised eg. $500 of $1,000 raised.', + 'give' + ); + case 'donations': + return __('Your goal progress is measured by the number of donations. eg. 1 of 5 donations.', 'give'); + case 'donors': + return __( + 'Your goal progress is measured by the number of donors. eg. 10 of 50 donors have given.', + 'give' + ); + case 'amountFromSubscriptions': + return __('Only the first donation amount of a recurring donation is counted toward the goal.', 'give'); + case 'subscriptions': + return __('Only the first donation of a recurring donation is counted toward the goal.', 'give'); + case 'donorsFromSubscriptions': + return __('Only the donors that subscribed to a recurring donation are counted toward the goal.', 'give'); + default: + return null; + } +}; diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/definitions.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/definitions.tsx new file mode 100644 index 0000000000..d082e31bf3 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/definitions.tsx @@ -0,0 +1,26 @@ +import {CampaignDetailsTab} from '../types'; +import {__} from '@wordpress/i18n'; +import OverviewTab from './Overview'; +import SettingsTab from './Settings'; +import FormsTab from './Forms'; + +const campaignDetailsTabs: CampaignDetailsTab[] = [ + { + id: 'overview', + title: __('Overview', 'give'), + content: () => , + }, + { + id: 'settings', + title: __('Settings', 'give'), + content: () => , + }, + { + id: 'forms', + title: __('Forms', 'give'), + content: () => , + fullwidth: true, + }, +]; + +export default campaignDetailsTabs; diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/images/campaign-page.svg b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/images/campaign-page.svg new file mode 100644 index 0000000000..b9eaebbb88 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/images/campaign-page.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/index.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/index.tsx new file mode 100644 index 0000000000..e473ac7461 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/index.tsx @@ -0,0 +1,89 @@ +import {useEffect, useState} from '@wordpress/element'; +import {Tab, TabList, TabPanel, Tabs} from 'react-aria-components'; +import cx from 'classnames'; +import {CampaignDetailsTab} from '../types'; + +import styles from '../CampaignDetailsPage.module.scss'; +import tabsDefinitions from './definitions'; +import NotificationsPlaceholder from '../../Notifications'; + +const tabs: CampaignDetailsTab[] = tabsDefinitions; + +/** + * @unreleased + */ +export default () => { + const [activeTab, setActiveTab] = useState(tabs[0]); + + const getTabFromURL = () => { + const urlParams = new URLSearchParams(window.location.search); + const tabId = urlParams.get('tab') || activeTab.id; + return tabs.find((tab) => tab.id === tabId); + }; + + const handleTabNavigation = (tabId: string) => { + const newTab = tabs.find((tab) => tab.id === tabId); + + if (!newTab) { + return; + } + + const urlParams = new URLSearchParams(window.location.search); + urlParams.set('tab', newTab.id); + + window.history.pushState(null, activeTab.title, `${window.location.pathname}?${urlParams.toString()}`); + + setActiveTab(newTab); + }; + + const handleUrlTabParamOnFirstLoad = () => { + const urlParams = new URLSearchParams(window.location.search); + // Add the 'tab' parameter only if it's not in the URL yet + if (!urlParams.has('tab')) { + urlParams.set('tab', activeTab.id); + window.history.replaceState(null, activeTab.title, `${window.location.pathname}?${urlParams.toString()}`); + } else { + setActiveTab(getTabFromURL()); + } + }; + + useEffect(() => { + handleUrlTabParamOnFirstLoad(); + + const handlePopState = () => setActiveTab(getTabFromURL()); + + // Updates state based on URL when user navigates with "Back" or "Forward" buttons + window.addEventListener('popstate', handlePopState); + + // Cleanup listener on unmount + return () => { + window.removeEventListener('popstate', handlePopState); + }; + }, []); + + return ( + +
+ + {Object.values(tabs).map((tab) => ( + + {tab.title}{' '} + + ))} + +
+ +
+ + + +
+ {Object.values(tabs).map((tab) => ( + + + + ))} +
+ + ); +}; diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/index.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/index.tsx new file mode 100644 index 0000000000..123b6b673e --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/index.tsx @@ -0,0 +1,305 @@ +import {__} from '@wordpress/i18n'; +import {useEffect, useState} from '@wordpress/element'; +import {useDispatch} from '@wordpress/data'; +import apiFetch from '@wordpress/api-fetch'; +import {JSONSchemaType} from 'ajv'; +import {ajvResolver} from '@hookform/resolvers/ajv'; +import {GiveCampaignDetails} from './types'; +import {Campaign} from '../types'; +import {FormProvider, SubmitHandler, useForm} from 'react-hook-form'; +import {Spinner as GiveSpinner} from '@givewp/components'; +import {Spinner} from '@wordpress/components'; +import Tabs from './Tabs'; +import ArchiveCampaignDialog from './Components/ArchiveCampaignDialog'; +import {ArrowReverse, BreadcrumbSeparatorIcon, DotsIcons, TrashIcon, ViewIcon} from '../Icons'; +import ArchivedCampaignNotice from './Components/Notices/ArchivedCampaignNotice'; +import NotificationPlaceholder from '../Notifications'; +import cx from 'classnames'; +import {useCampaignEntityRecord} from '@givewp/campaigns/utils'; + +import styles from './CampaignDetailsPage.module.scss'; + +declare const window: { + GiveCampaignDetails: GiveCampaignDetails; +} & Window; + +interface Show { + contextMenu?: boolean; + confirmationModal?: boolean; +} + +const StatusBadge = ({status}: {status: string}) => { + const statusMap = { + active: __('Active', 'give'), + archived: __('Archived', 'give'), + draft: __('Draft', 'give'), + }; + + return ( +
+
+

{statusMap[status]}

+
+
+ ); +}; + +export default function CampaignsDetailsPage({campaignId}) { + const [resolver, setResolver] = useState({}); + const [isSaving, setIsSaving] = useState(null); + const [show, _setShowValue] = useState({ + contextMenu: false, + confirmationModal: false, + }); + + const dispatch = useDispatch('givewp/campaign-notifications'); + + const setShow = (data: Show) => { + _setShowValue((prevState) => { + return { + ...prevState, + ...data, + }; + }); + }; + + useEffect(() => { + apiFetch({ + path: `/give-api/v2/campaigns/${campaignId}`, + method: 'OPTIONS', + }).then(({schema}: {schema: JSONSchemaType}) => { + setResolver({ + resolver: ajvResolver(schema), + }); + }); + }, []); + + const { + campaign, + hasResolved, + save, + edit, + } = useCampaignEntityRecord(campaignId); + + const methods = useForm({ + mode: 'onBlur', + ...resolver, + }); + + const {formState, handleSubmit, reset, setValue, watch} = methods; + + const [enableCampaignPage] = watch(['enableCampaignPage']); + + // Set default values when campaign is loaded + useEffect(() => { + if (hasResolved) { + reset({...campaign}); + } + }, [hasResolved]); + + // Show campaign archived notice + useEffect(() => { + if (campaign?.status !== 'archived') { + return; + } + + dispatch.addNotice({ + id: 'update-archive-notice', + type: 'warning', + onDismiss: () => updateStatus('draft'), + content: (onDismiss: Function) => + }); + }, [campaign?.status]); + + const onSubmit: SubmitHandler = async (data) => { + if (formState.isDirty) { + setIsSaving(data.status); + + edit(data); + + try { + const response = await save(); + + setIsSaving(null); + reset(response); + + dispatch.addSnackbarNotice({ + id: `save-${data.status}`, + content: __('Campaign updated', 'give'), + }); + } catch (err) { + setIsSaving(null); + + dispatch.addSnackbarNotice({ + id: `save-error`, + type: 'error', + content: __('Campaign update failed', 'give'), + }); + } + } + }; + + const updateStatus = (status: 'archived' | 'draft') => { + setValue('status', status); + handleSubmit(async (data) => { + edit(data); + + try { + const response: Campaign = await save(); + + setShow({ + contextMenu: false, + confirmationModal: false, + }); + reset(response); + + dispatch.addSnackbarNotice({ + id: `update-${status}`, + content: getMessageByStatus(status), + }); + } catch (err) { + setShow({ + contextMenu: false, + confirmationModal: false, + }); + + dispatch.addSnackbarNotice({ + id: 'update-error', + type: 'error', + content: __('Something went wrong', 'give'), + }); + } + })(); + }; + + if (!hasResolved) { + return ( +
+
+ +
{__('Loading campaign...', 'give')}
+
+
+ ); + } + + return ( + +
+
+
+
+ + {__('Campaigns', 'give')} + + + {campaign.title} +
+
+
+

{campaign.title}

+ +
+ +
+ {enableCampaignPage && ( + + {__('Edit campaign page', 'give')} + + )} + + + + + {!isSaving && show.contextMenu && ( + + )} +
+
+
+ + setShow({confirmationModal: false, contextMenu: false})} + handleConfirm={() => updateStatus('archived')} + /> +
+
+ +
+ ); +} + +const getMessageByStatus = (status: string) => { + switch (status) { + case 'archived': + return __('Campaign is moved to archive', 'give'); + case 'active': + return __('Campaign is now active', 'give'); + case 'draft': + return __('Campaign is moved to draft', 'give'); + } + + return null; +}; diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/types.ts b/src/Campaigns/resources/admin/components/CampaignDetailsPage/types.ts new file mode 100644 index 0000000000..a0e3f24082 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/types.ts @@ -0,0 +1,20 @@ +import {FC} from 'react'; + +export interface GiveCampaignDetails { + adminUrl: string; + currency: string; + isRecurringEnabled: boolean; + defaultForm: string; +} + +export type CampaignFormOption = { + id: number; + title: string; +}; + +export type CampaignDetailsTab = { + id: string; + title: string; + content: FC; + fullwidth?: boolean; +}; diff --git a/src/Campaigns/resources/admin/components/CampaignFormModal/CampaignFormModal.module.scss b/src/Campaigns/resources/admin/components/CampaignFormModal/CampaignFormModal.module.scss new file mode 100644 index 0000000000..cebb5faf88 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignFormModal/CampaignFormModal.module.scss @@ -0,0 +1,115 @@ +.campaignForm { + .submitButton { + border-radius: 0; + font-size: 0.875rem; + font-weight: 600; + line-height: 1.43; + padding: var(--givewp-spacing-2) var(--givewp-spacing-4); + text-align: center; + } + + input, + label { + font-size: 1rem; + } +} + +.fieldRequired { + color: var(--givewp-red-500); +} + +.goalType { + + .goalTypeOption { + margin: 0.5rem 0 0.5rem 0; + display: flex; + flex-direction: row; + align-items: center; + gap: 1rem; + + cursor: pointer; + border: 1px solid #9CA0AF; + border-radius: 8px; + padding: 0.75rem 1.5rem 0.75rem 1.5rem; + } + + .goalTypeOptionIcon { + + line-height: 0.875rem; + margin-top: 0.175rem; + + svg { + width: 1.5rem; + height: 1.5rem; + } + } + + .goalTypeOptionText { + + label, + span { + cursor: pointer; + } + + > label { + font-size: 0.875rem !important; + } + + > span { + font-size: 0.75rem !important; + margin-top: 0.2rem; + display: none; + line-height: 1rem !important; + } + + /* Hide the radio button input */ + input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; + } + } + + .goalTypeOptionSelected { + background-color: #374151; + + .goalTypeOptionIcon { + + svg path:not([fill]) { + stroke: #F9FAFB; + } + + svg path:not([stroke]) { + fill: #F9FAFB; + } + } + + .goalTypeOptionText { + label, + span { + color: #F9FAFB !important; + } + + span { + display: inline-block; + } + } + } +} + +.button:is(:global(.button)) { + border-radius: var(--givewp-rounded-8) +} + +.previousButton:is(:global(.button)) { + background-color: transparent; + border: solid 1px #9ca0af; + color: #060c1a; + + &:hover { + border: solid 1px #9ca0af; + color: #060c1a; + } +} diff --git a/src/Campaigns/resources/admin/components/CampaignFormModal/GoalTypeIcons.tsx b/src/Campaigns/resources/admin/components/CampaignFormModal/GoalTypeIcons.tsx new file mode 100644 index 0000000000..0ccc01eb34 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignFormModal/GoalTypeIcons.tsx @@ -0,0 +1,95 @@ +export const AmountIcon = () => ( + + + +); + +export const DonationsIcon = () => ( + + + + +); + +export const DonorsIcon = () => ( + + + +); + +export const AmountFromSubscriptionsIcon = () => ( + + + + + +); + +export const SubscriptionsIcon = () => ( + + + + +); + +export const DonorsFromSubscriptionsIcon = () => ( + + + + + +); diff --git a/src/Campaigns/resources/admin/components/CampaignFormModal/index.tsx b/src/Campaigns/resources/admin/components/CampaignFormModal/index.tsx new file mode 100644 index 0000000000..0f197198b6 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignFormModal/index.tsx @@ -0,0 +1,446 @@ +import {FormProvider, SubmitHandler, useForm} from 'react-hook-form'; +import {__} from '@wordpress/i18n'; +import styles from './CampaignFormModal.module.scss'; +import FormModal from '../FormModal'; +import CampaignsApi from '../api'; +import { + CampaignFormInputs, + CampaignModalProps, + GoalInputAttributes, + GoalTypeOption as GoalTypeOptionType, +} from './types'; +import {useRef, useState} from 'react'; +import {Currency, Upload} from '../Inputs'; +import { + AmountFromSubscriptionsIcon, + AmountIcon, + DonationsIcon, + DonorsFromSubscriptionsIcon, + DonorsIcon, + SubscriptionsIcon, +} from './GoalTypeIcons'; +import {getGiveCampaignsListTableWindowData} from '../CampaignsListTable'; + +const {currency, isRecurringEnabled} = getGiveCampaignsListTableWindowData(); + +/** + * Get the next sharp hour + * + * @unreleased + */ +const getNextSharpHour = (hoursToAdd: number) => { + const date = new Date(); + date.setHours(date.getHours() + hoursToAdd, 0, 0, 0); + + return date; +}; + +/** + * Format a given date to be used in datetime inputs + * + * @unreleased + */ +const getDateString = (date: Date) => { + const offsetInMilliseconds = date.getTimezoneOffset() * 60 * 1000; + const dateWithOffset = new Date(date.getTime() - offsetInMilliseconds); + + return removeTimezoneFromDateISOString(dateWithOffset.toISOString()); +}; + +/** + * Remove timezone from date string + * + * @unreleased + */ +const removeTimezoneFromDateISOString = (date: string) => { + return date.slice(0, -5); +}; + +/** + * @unreleased + */ +const getGoalTypeIcon = (type: string) => { + switch (type) { + case 'amount': + return ; + case 'donations': + return ; + case 'donors': + return ; + case 'amountFromSubscriptions': + return ; + case 'subscriptions': + return ; + case 'donorsFromSubscriptions': + return ; + } +}; + +/** + * Goal Type Option component + * + * @unreleased + */ +const GoalTypeOption = ({type, label, description, selected, register}: GoalTypeOptionType) => { + const divRef = useRef(null); + const labelRef = useRef(null); + + const handleDivClick = () => { + labelRef.current.click(); + }; + + return ( +
+
{getGoalTypeIcon(type)}
+
+ + {description} +
+
+ ); +}; + +/** + * Campaign Form Modal component + * + * @unreleased + */ +export default function CampaignFormModal({isOpen, handleClose, apiSettings, title, campaign}: CampaignModalProps) { + const API = new CampaignsApi(apiSettings); + const [step, setStep] = useState(1); + + const methods = useForm({ + defaultValues: { + title: campaign?.title ?? '', + shortDescription: campaign?.shortDescription ?? '', + image: campaign?.image ?? '', + goalType: campaign?.goalType ?? '', + goal: campaign?.goal ?? null, + startDateTime: getDateString( + campaign?.startDateTime?.date ? new Date(campaign?.startDateTime?.date) : getNextSharpHour(1) + ), + endDateTime: getDateString( + campaign?.endDateTime?.date ? new Date(campaign?.endDateTime?.date) : getNextSharpHour(2) + ), + }, + }); + + const { + register, + handleSubmit, + formState: {errors, isDirty, isSubmitting}, + setValue, + watch, + trigger, + } = methods; + + const image = watch('image'); + const selectedGoalType = watch('goalType'); + const goal = watch('goal'); + + const getFormModalTitle = () => { + switch (step) { + case 1: + return __('Tell us about your fundraising cause', 'give'); + case 2: + return __('Set up your campaign goal', 'give'); + } + + return null; + }; + + const goalInputAttributes: {[selectedGoalType: string]: GoalInputAttributes} = { + amount: { + label: __('How much do you want to raise?', 'give'), + description: __('Set the target amount your campaign should raise.', 'give'), + placeholder: __('eg. $2,000', 'give'), + }, + donations: { + label: __('How many donations do you need?', 'give'), + description: __('Set the target number of donations your campaign should bring in.', 'give'), + placeholder: __('eg. 100 donations', 'give'), + }, + donors: { + label: __('How many donors do you need?', 'give'), + description: __('Set the target number of donors your campaign should bring in.', 'give'), + placeholder: __('eg. 100 donors', 'give'), + }, + amountFromSubscriptions: { + label: __('How much do you want to raise?', 'give'), + description: __( + 'Set the target recurring amount your campaign should raise. One-time donations do not count.', + 'give' + ), + placeholder: __('eg. $2,000', 'give'), + }, + subscriptions: { + label: __('How many recurring donations do you need?', 'give'), + description: __( + 'Set the target number of recurring donations your campaign should bring in. One-time donations do not count.', + 'give' + ), + placeholder: __('eg. 100 subscriptions', 'give'), + }, + donorsFromSubscriptions: { + label: __('How many recurring donors do you need?', 'give'), + description: __( + 'Set the target number of recurring donors your campaign should bring in. One-time donations do not count.', + 'give' + ), + placeholder: __('eg. 100 subscribers', 'give'), + }, + }; + + const requiredAsterisk = *; + + const validateTitle = async () => { + return await trigger('title'); + }; + + const onSubmit: SubmitHandler = async (inputs, event) => { + event.preventDefault(); + + if (step !== 2) { + return; + } + + try { + inputs.startDateTime = getDateString(new Date(inputs.startDateTime)); + inputs.endDateTime = getDateString(new Date(inputs.endDateTime)); + + const endpoint = campaign?.id ? `/campaign/${campaign.id}` : ''; + const response = await API.fetchWithArgs(endpoint, inputs, 'POST'); + + handleClose(response); + } catch (error) { + console.error('Error submitting campaign campaign', error); + } + }; + + return ( + + + {step === 1 && ( + <> +
+ + {__("Give your campaign a title that tells donors what it's about.", 'give')} + + {errors.title && ( +
+

{errors.title.message}

+
+ )} +
+
+ + {__('Let your donors know the story behind your campaign.', 'give')} +