From 0eb9bce3d9ab4c822ee84f6020089aae92a9861b Mon Sep 17 00:00:00 2001 From: "Kyle B. Johnson" Date: Wed, 2 Oct 2024 15:20:55 -0400 Subject: [PATCH 1/3] feature: Add campaign overview stats --- package-lock.json | 241 +++++++++++++++++ package.json | 2 + .../Actions/LoadCampaignDetailsAssets.php | 2 + ...atistics.php => GetCampaignStatistics.php} | 16 +- src/Campaigns/ServiceProvider.php | 2 +- .../Components/CampaignStats.tsx | 253 ++++++++++++++++++ .../Components/GoalProgressChart.tsx | 42 +++ .../Components/RevenueChart.tsx | 74 +++++ .../CampaignDetailsPage/Tabs/Overview.tsx | 3 +- .../components/CampaignDetailsPage/types.ts | 2 + .../components/CampaignFormModal/index.tsx | 4 +- .../Routes/CampaignOverviewStatisticsTest.php | 6 +- 12 files changed, 630 insertions(+), 17 deletions(-) rename src/Campaigns/Routes/{CampaignOverviewStatistics.php => GetCampaignStatistics.php} (85%) create mode 100644 src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats.tsx create mode 100644 src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/GoalProgressChart.tsx create mode 100644 src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/RevenueChart.tsx 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/Actions/LoadCampaignDetailsAssets.php b/src/Campaigns/Actions/LoadCampaignDetailsAssets.php index ec5aa827e5..fa1cb70d33 100644 --- a/src/Campaigns/Actions/LoadCampaignDetailsAssets.php +++ b/src/Campaigns/Actions/LoadCampaignDetailsAssets.php @@ -31,6 +31,8 @@ public function __invoke() [ 'adminUrl' => admin_url(), 'currency' => give_get_currency(), + 'apiRoot' => esc_url_raw(rest_url('give-api/v2/campaigns')), + 'apiNonce' => wp_create_nonce('wp_rest'), ] ); 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 2ecf4d8fa6..b50cddda23 100644 --- a/src/Campaigns/ServiceProvider.php +++ b/src/Campaigns/ServiceProvider.php @@ -47,7 +47,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..4c74345c66 --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats.tsx @@ -0,0 +1,253 @@ +import {__} from '@wordpress/i18n'; +import {useEffect, useState} from "react"; +import RevenueChart from "./RevenueChart"; +import GoalProgressChart from "./GoalProgressChart"; +import CampaignsApi from "../../api"; +import {GiveCampaignDetails} from "../types"; + +declare const window: { + GiveCampaignDetails: GiveCampaignDetails; +} & Window; + +const API = new CampaignsApi(window.GiveCampaignDetails); + +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) + + const response = await API.fetchWithArgs(`/${campaignId}/statistics`, { + rangeInDays: days, + }, 'GET') + setStats(response) + } + + const widgetDescription = filterOptions.find(option => option.value === dayRange)?.description + + return ( + <> + + + + + + + + + + + + + + ) +} + +const HeaderText = ({children}) => { + return ( +
+ {children} +
+ ) +} + +const HeaderSubText = ({children}) => { + return ( +
+ {children} +
+ ) +} + +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 + {__('Show your revenue over time')} +
+ +
+ ); +} + +const GoalProgressWidget = () => { + return ( +
+ {__('Goal Progress')} + {__('Show your campaign performance')} + +
+ ) +} + +const DateRangeFilters = ({options, onSelect, selected}) => { + return ( +
+ {options.map((option, index) => ( + + ))} +
+ ) +} + +const Row = ({children}) => ( +
+ {children} +
+) + +export default CampaignStats; diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/GoalProgressChart.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/GoalProgressChart.tsx new file mode 100644 index 0000000000..792a30f76e --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/GoalProgressChart.tsx @@ -0,0 +1,42 @@ +import {__} from '@wordpress/i18n'; +import { + CircularProgressbarWithChildren, + buildStyles +} from "react-circular-progressbar"; +import 'react-circular-progressbar/dist/styles.css'; + +const GoalProgressChart = ({ value, goal }) => { + const percentage: number = Math.abs((value / goal) * 100); + return ( + <> +
+
+ + {percentage}% + {value} + +
+
+
{__('Goal')}
+
{goal}
+
{__('Amount raised')}
+
+
+ + ) +} + +export default GoalProgressChart; 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..cb3ff2820d --- /dev/null +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/RevenueChart.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import Chart from "react-apexcharts"; + +const RevenueChart = () => { + + const options = { + chart: { + id: "campaign-revenue", + zoom: { + enabled: false + }, + }, + xaxis: { + categories: ['Aug 06', 'Aug 07', 'Aug 08', 'Aug 09'] + }, + yaxis: { + max: 200, + }, + 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 + } + ], + ], + } + } + }; + + const series = [ + { + name: "Revenue", + data: [0, 100, 50, 150] + } + ]; + + return ( + <> + + + ) +} + +export default RevenueChart diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/Overview.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/Overview.tsx index 34fa3b89aa..d774e3d519 100644 --- a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/Overview.tsx +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Tabs/Overview.tsx @@ -1,6 +1,7 @@ import {__} from '@wordpress/i18n'; import styles from '../CampaignDetailsPage.module.scss'; +import CampaignStats from "../Components/CampaignStats"; /** * @unreleased @@ -11,7 +12,7 @@ export default () => { return (
- Overview +
); diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/types.ts b/src/Campaigns/resources/admin/components/CampaignDetailsPage/types.ts index e398f86fb2..7eaa88b677 100644 --- a/src/Campaigns/resources/admin/components/CampaignDetailsPage/types.ts +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/types.ts @@ -16,6 +16,8 @@ export interface Campaign { export interface GiveCampaignDetails { adminUrl: string; currency: string; + apiRoot: string; + apiNonce: string; } export type CampaignDetailsTab = { diff --git a/src/Campaigns/resources/admin/components/CampaignFormModal/index.tsx b/src/Campaigns/resources/admin/components/CampaignFormModal/index.tsx index fd2b143ba8..5d5b1a4b15 100644 --- a/src/Campaigns/resources/admin/components/CampaignFormModal/index.tsx +++ b/src/Campaigns/resources/admin/components/CampaignFormModal/index.tsx @@ -3,7 +3,7 @@ import {__} from '@wordpress/i18n'; import styles from './CampaignFormModal.module.scss'; import FormModal from '../FormModal'; import CampaignsApi from '../api'; -import {CampaignFormInputs, CampaignModalProps, GoalInputAttributes, GoalTypeOption} from './types'; +import {CampaignFormInputs, CampaignModalProps, GoalInputAttributes, GoalTypeOption as GoalTypeOptionType} from './types'; import {useEffect, useRef, useState} from 'react'; import UploadMedia from '../UploadMedia'; import {AmountIcon, DonationsIcon, DonorsIcon} from './GoalTypeIcons'; @@ -60,7 +60,7 @@ const getGoalTypeIcon = (type: string) => { * * @unreleased */ -const GoalTypeOption = ({type, label, description, selected, register}: GoalTypeOption) => { +const GoalTypeOption = ({type, label, description, selected, register}: GoalTypeOptionType) => { const divRef = useRef(null); const labelRef = useRef(null); diff --git a/tests/Unit/Campaigns/Routes/CampaignOverviewStatisticsTest.php b/tests/Unit/Campaigns/Routes/CampaignOverviewStatisticsTest.php index 90234b0339..adaf264e9c 100644 --- a/tests/Unit/Campaigns/Routes/CampaignOverviewStatisticsTest.php +++ b/tests/Unit/Campaigns/Routes/CampaignOverviewStatisticsTest.php @@ -4,7 +4,7 @@ use DateTime; use Give\Campaigns\Models\Campaign; -use Give\Campaigns\Routes\CampaignOverviewStatistics; +use Give\Campaigns\Routes\GetCampaignStatistics; use Give\DonationForms\Models\DonationForm; use Give\Donations\Models\Donation; use Give\Donations\ValueObjects\DonationStatus; @@ -59,7 +59,7 @@ public function testReturnsAllTimeDonationsStatistics() $request = new WP_REST_Request('GET', '/give-api/v2/campaign-overview-statistics'); $request->set_param('campaignId', $campaign->id); - $route = new CampaignOverviewStatistics; + $route = new GetCampaignStatistics; $response = $route->handleRequest($request); $this->assertEquals(3, $response->data[0]['donorCount']); @@ -106,7 +106,7 @@ public function testReturnsPeriodStatisticsWithPreviousPeriod() $request->set_param('campaignId', $campaign->id); $request->set_param('rangeInDays', 30); - $route = new CampaignOverviewStatistics; + $route = new GetCampaignStatistics; $response = $route->handleRequest($request); $this->assertEquals(2, $response->data[0]['donorCount']); From d820bb761077e3039e67a773cc26b37bf9419502 Mon Sep 17 00:00:00 2001 From: "Kyle B. Johnson" Date: Fri, 4 Oct 2024 11:53:54 -0400 Subject: [PATCH 2/3] tests: Update test name and parameters --- ...tatisticsTest.php => GetCampaignStatisticsTest.php} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename tests/Unit/Campaigns/Routes/{CampaignOverviewStatisticsTest.php => GetCampaignStatisticsTest.php} (92%) diff --git a/tests/Unit/Campaigns/Routes/CampaignOverviewStatisticsTest.php b/tests/Unit/Campaigns/Routes/GetCampaignStatisticsTest.php similarity index 92% rename from tests/Unit/Campaigns/Routes/CampaignOverviewStatisticsTest.php rename to tests/Unit/Campaigns/Routes/GetCampaignStatisticsTest.php index adaf264e9c..a73049c397 100644 --- a/tests/Unit/Campaigns/Routes/CampaignOverviewStatisticsTest.php +++ b/tests/Unit/Campaigns/Routes/GetCampaignStatisticsTest.php @@ -17,7 +17,7 @@ /** * @unreleased */ -final class CampaignOverviewStatisticsTest extends TestCase +final class GetCampaignStatisticsTest extends TestCase { use RefreshDatabase; @@ -56,8 +56,8 @@ public function testReturnsAllTimeDonationsStatistics() ]); give_update_meta($donation3->id, '_give_completed_date', $donation3->createdAt->format('Y-m-d H:i:s')); - $request = new WP_REST_Request('GET', '/give-api/v2/campaign-overview-statistics'); - $request->set_param('campaignId', $campaign->id); + $request = new WP_REST_Request('GET', "/give-api/v2/campaigns/$campaign->id/statistics"); + $request->set_param('id', $campaign->id); $route = new GetCampaignStatistics; $response = $route->handleRequest($request); @@ -102,8 +102,8 @@ public function testReturnsPeriodStatisticsWithPreviousPeriod() ]); give_update_meta($donation3->id, '_give_completed_date', $donation3->createdAt->format('Y-m-d H:i:s')); - $request = new WP_REST_Request('GET', '/give-api/v2/campaign-overview-statistics'); - $request->set_param('campaignId', $campaign->id); + $request = new WP_REST_Request('GET', "/give-api/v2/campaigns/$campaign->id/statistics"); + $request->set_param('id', $campaign->id); $request->set_param('rangeInDays', 30); $route = new GetCampaignStatistics; From 70cb37ec97df275091bf7afb5856c3d8e4e95f12 Mon Sep 17 00:00:00 2001 From: "Kyle B. Johnson" Date: Fri, 4 Oct 2024 12:37:51 -0400 Subject: [PATCH 3/3] refactor: Replace fetch with API Fetch Package --- .../Actions/LoadCampaignDetailsAssets.php | 2 -- .../Components/CampaignStats.tsx | 16 ++++------------ .../components/CampaignDetailsPage/types.ts | 2 -- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/Campaigns/Actions/LoadCampaignDetailsAssets.php b/src/Campaigns/Actions/LoadCampaignDetailsAssets.php index fa1cb70d33..ec5aa827e5 100644 --- a/src/Campaigns/Actions/LoadCampaignDetailsAssets.php +++ b/src/Campaigns/Actions/LoadCampaignDetailsAssets.php @@ -31,8 +31,6 @@ public function __invoke() [ 'adminUrl' => admin_url(), 'currency' => give_get_currency(), - 'apiRoot' => esc_url_raw(rest_url('give-api/v2/campaigns')), - 'apiNonce' => wp_create_nonce('wp_rest'), ] ); diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats.tsx index 4c74345c66..e9b605f5c7 100644 --- a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats.tsx +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats.tsx @@ -2,14 +2,8 @@ import {__} from '@wordpress/i18n'; import {useEffect, useState} from "react"; import RevenueChart from "./RevenueChart"; import GoalProgressChart from "./GoalProgressChart"; -import CampaignsApi from "../../api"; -import {GiveCampaignDetails} from "../types"; - -declare const window: { - GiveCampaignDetails: GiveCampaignDetails; -} & Window; - -const API = new CampaignsApi(window.GiveCampaignDetails); +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; const campaignId = new URLSearchParams(window.location.search).get('id'); @@ -40,10 +34,8 @@ const CampaignStats = () => { const onDayRangeChange = async (days: number) => { setDayRange(days) - const response = await API.fetchWithArgs(`/${campaignId}/statistics`, { - rangeInDays: days, - }, 'GET') - setStats(response) + apiFetch({path: addQueryArgs( '/give-api/v2/campaigns/' + campaignId +'/statistics', {rangeInDays: days} ) } ) + .then(setStats); } const widgetDescription = filterOptions.find(option => option.value === dayRange)?.description diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/types.ts b/src/Campaigns/resources/admin/components/CampaignDetailsPage/types.ts index 7eaa88b677..e398f86fb2 100644 --- a/src/Campaigns/resources/admin/components/CampaignDetailsPage/types.ts +++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/types.ts @@ -16,8 +16,6 @@ export interface Campaign { export interface GiveCampaignDetails { adminUrl: string; currency: string; - apiRoot: string; - apiNonce: string; } export type CampaignDetailsTab = {