diff --git a/package-lock.json b/package-lock.json
index d7932472a8..8aa620f73a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -65,8 +65,10 @@
"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-circular-progressbar": "^2.1.0",
"react-csv": "^2.0.1",
"react-currency-input-field": "^3.6.10",
"react-datepicker": "^4.16.0",
@@ -12939,6 +12941,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",
@@ -13206,6 +13214,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",
@@ -29785,6 +29808,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",
@@ -29924,6 +29959,14 @@
}
}
},
+ "node_modules/react-circular-progressbar": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.1.0.tgz",
+ "integrity": "sha512-xp4THTrod4aLpGy68FX/k1Q3nzrfHUjUe5v6FsdwXBl3YVMwgeXYQKDrku7n/D6qsJA9CuunarAboC2xCiKs1g==",
+ "peerDependencies": {
+ "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/react-colorful": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
@@ -32965,6 +33008,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",
@@ -45200,6 +45334,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",
@@ -45397,6 +45537,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",
@@ -57718,6 +57873,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",
@@ -57829,6 +57992,12 @@
}
}
},
+ "react-circular-progressbar": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.1.0.tgz",
+ "integrity": "sha512-xp4THTrod4aLpGy68FX/k1Q3nzrfHUjUe5v6FsdwXBl3YVMwgeXYQKDrku7n/D6qsJA9CuunarAboC2xCiKs1g==",
+ "requires": {}
+ },
"react-colorful": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
@@ -60185,6 +60354,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",
diff --git a/package.json b/package.json
index 6de678c5cf..e79a7f5ffe 100644
--- a/package.json
+++ b/package.json
@@ -120,8 +120,10 @@
"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-circular-progressbar": "^2.1.0",
"react-csv": "^2.0.1",
"react-currency-input-field": "^3.6.10",
"react-datepicker": "^4.16.0",
diff --git a/src/Campaigns/Routes/CampaignOverviewStatistics.php b/src/Campaigns/Routes/GetCampaignStatistics.php
similarity index 85%
rename from src/Campaigns/Routes/CampaignOverviewStatistics.php
rename to src/Campaigns/Routes/GetCampaignStatistics.php
index 9101dc4ebb..d94d1a069d 100644
--- a/src/Campaigns/Routes/CampaignOverviewStatistics.php
+++ b/src/Campaigns/Routes/GetCampaignStatistics.php
@@ -9,6 +9,7 @@
use Give\API\RestRoute;
use Give\Campaigns\CampaignDonationQuery;
use Give\Campaigns\Models\Campaign;
+use Give\Campaigns\ValueObjects\CampaignRoute;
use Give\Framework\Support\Facades\DateTime\Temporal;
use WP_REST_Response;
use WP_REST_Server;
@@ -16,19 +17,16 @@
/**
* @unreleased
*/
-class CampaignOverviewStatistics implements RestRoute
+class GetCampaignStatistics implements RestRoute
{
- /** @var string */
- protected $endpoint = 'campaign-overview-statistics';
-
/**
* @unreleased
*/
public function registerRoute()
{
register_rest_route(
- 'give-api/v2',
- $this->endpoint,
+ CampaignRoute::NAMESPACE,
+ CampaignRoute::CAMPAIGN . '/statistics',
[
[
'methods' => WP_REST_Server::READABLE,
@@ -38,18 +36,16 @@ public function registerRoute()
},
],
'args' => [
- 'campaignId' => [
+ 'id' => [
'type' => 'integer',
'required' => true,
'sanitize_callback' => 'absint',
- 'validate_callback' => 'is_numeric',
],
'rangeInDays' => [
'type' => 'integer',
'required' => false,
'sanitize_callback' => 'absint',
'default' => 0, // Zero to mean "all time".
- 'validate_callback' => 'is_numeric',
],
],
]
@@ -63,7 +59,7 @@ public function registerRoute()
*/
public function handleRequest($request): WP_REST_Response
{
- $campaign = Campaign::find($request->get_param('campaignId'));
+ $campaign = Campaign::find($request->get_param('id'));
$query = new CampaignDonationQuery($campaign);
diff --git a/src/Campaigns/ServiceProvider.php b/src/Campaigns/ServiceProvider.php
index 7f0330b86a..f638768d06 100644
--- a/src/Campaigns/ServiceProvider.php
+++ b/src/Campaigns/ServiceProvider.php
@@ -51,7 +51,7 @@ 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\CampaignOverviewStatistics::class, 'registerRoute');
+ Hooks::addAction('rest_api_init', Routes\GetCampaignStatistics::class, 'registerRoute');
}
/**
diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats.tsx
new file mode 100644
index 0000000000..e9b605f5c7
--- /dev/null
+++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats.tsx
@@ -0,0 +1,245 @@
+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';
+
+const campaignId = new URLSearchParams(window.location.search).get('id');
+
+const pluck = (array: any[], property: string) => array.map(element => element[property])
+
+const filterOptions = [
+ { label: __('Today'), value: 1, description: __('from today') },
+ { label: __('Last 7 days'), value: 7, description: __('from the last 7 days') },
+ { label: __('Last 30 days'), value: 30, description: __('from the last 30 days') },
+ { label: __('Last 90 days'), value: 90, description: __('from the last 90 days') },
+ { label: __('All-time'), value: 0, description: __('total for all-time') },
+]
+
+const currency = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+})
+
+const CampaignStats = () => {
+
+ const [dayRange, setDayRange] = useState(null);
+ const [stats, setStats] = useState([]);
+
+ 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 (
+ <>
+