primaryColor ?? '#0b72d9'),
+ esc_attr($campaign->secondaryColor ?? '#27ae60')
);
?>
{
+ const {campaign, hasResolved} = useCampaign(campaignId);
+
+ if (!hasResolved || !campaignId) {
+ return null;
+ }
+
+ return
;
+}
+
+/**
+ * @unreleased
+ */
+const nodeList = document.querySelectorAll('[data-givewp-campaign-goal]');
+
+if (nodeList) {
+ const containers = Array.from(nodeList);
+
+ containers.map((container: any) => {
+ return render(
, container);
+ });
+}
diff --git a/src/Campaigns/Blocks/CampaignGoal/app/index.tsx b/src/Campaigns/Blocks/CampaignGoal/app/index.tsx
new file mode 100644
index 0000000000..37fa61bdcd
--- /dev/null
+++ b/src/Campaigns/Blocks/CampaignGoal/app/index.tsx
@@ -0,0 +1,34 @@
+import {__} from '@wordpress/i18n';
+import {Campaign} from '@givewp/campaigns/admin/components/types';
+import {getGoalDescription, getGoalFormattedValue} from '../utils';
+
+import './styles.scss';
+
+export default ({campaign}: { campaign: Campaign }) => {
+ return (
+
+
+
+ {getGoalDescription(campaign.goalType)}
+
+ {getGoalFormattedValue(campaign.goalType, campaign.goalStats.actual)}
+
+
+
+ {__('Our goal', 'give')}
+
+ {getGoalFormattedValue(campaign.goalType, campaign.goal)}
+
+
+
+
+
+ );
+}
diff --git a/src/Campaigns/Blocks/CampaignGoal/app/styles.scss b/src/Campaigns/Blocks/CampaignGoal/app/styles.scss
new file mode 100644
index 0000000000..b98f4b8b2f
--- /dev/null
+++ b/src/Campaigns/Blocks/CampaignGoal/app/styles.scss
@@ -0,0 +1,51 @@
+.givewp-campaign-goal {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+
+ &__container {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+
+ &-item {
+ display: flex;
+ flex-direction: column;
+ gap: 0.2rem;
+
+ span {
+ text-transform: uppercase;
+ font-size: 12px;
+ line-height: 18px;
+ color: #4b5563;
+ }
+
+ strong {
+ font-size: 20px;
+ line-height: 32px;
+ color: #060c1a;
+ }
+ }
+ }
+
+ &__progress-bar {
+ display: flex;
+
+ &-container {
+ display: flex;
+ height: 8px;
+ flex-grow: 1;
+ border-radius: 14px;
+ box-shadow: inset 0 1px 4px 0 rgba(0, 0, 0, 0.09);
+ background-color: #f2f2f2;
+ }
+
+ &-progress {
+ display: flex;
+ height: 8px;
+ border-radius: 14px;
+ box-shadow: inset 0 1px 4px 0 rgba(0, 0, 0, 0.09);
+ background-color: #2d802f;
+ }
+ }
+}
diff --git a/src/Campaigns/Blocks/CampaignGoal/block.json b/src/Campaigns/Blocks/CampaignGoal/block.json
new file mode 100644
index 0000000000..0145bccb61
--- /dev/null
+++ b/src/Campaigns/Blocks/CampaignGoal/block.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://json.schemastore.org/block.json",
+ "apiVersion": 2,
+ "name": "givewp/campaign-goal",
+ "version": "1.0.0",
+ "title": "Campaign Goal",
+ "category": "give",
+ "description": "Displays the goal of the campaign.",
+ "supports": {
+ "align": [
+ "wide",
+ "full"
+ ]
+ },
+ "attributes": {
+ "campaignId": {
+ "type": "number"
+ }
+ },
+ "textdomain": "give",
+ "viewScript": "file:../../../../build/campaignGoalBlockApp.js",
+ "style": "file:../../../../build/campaignGoalBlockApp.css",
+ "render": "file:./render.php"
+}
diff --git a/src/Campaigns/Blocks/CampaignGoal/edit.tsx b/src/Campaigns/Blocks/CampaignGoal/edit.tsx
new file mode 100644
index 0000000000..ed200bbb18
--- /dev/null
+++ b/src/Campaigns/Blocks/CampaignGoal/edit.tsx
@@ -0,0 +1,53 @@
+import {__} from '@wordpress/i18n';
+import {useSelect} from '@wordpress/data';
+import {InspectorControls, useBlockProps} from '@wordpress/block-editor';
+import {BlockEditProps} from '@wordpress/blocks';
+import {ExternalLink, PanelBody, TextControl} from '@wordpress/components';
+import useCampaign from '../shared/hooks/useCampaign';
+import CampaignGoalApp from './app/index';
+import {CampaignSelector} from '../shared/components/CampaignSelector';
+import {getGoalDescription} from './utils';
+
+/**
+ * @unreleased
+ */
+export default function Edit({attributes, setAttributes}: BlockEditProps<{
+ campaignId: number;
+ goalType: string;
+}>) {
+ const {campaign, hasResolved} = useCampaign(attributes.campaignId);
+
+ const blockProps = useBlockProps();
+
+ const adminBaseUrl = useSelect(
+ // @ts-ignore
+ (select) => select('core').getSite()?.url + '/wp-admin/edit.php?post_type=give_forms&page=give-campaigns',
+ []
+ );
+
+ if (!hasResolved) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ {campaign?.id && (
+
+
+
+
+ {__('Edit campaign goal settings', 'give')}
+
+
+
+ )}
+
+ );
+}
diff --git a/src/Campaigns/Blocks/CampaignGoal/icon.jsx b/src/Campaigns/Blocks/CampaignGoal/icon.jsx
new file mode 100644
index 0000000000..fd153a0135
--- /dev/null
+++ b/src/Campaigns/Blocks/CampaignGoal/icon.jsx
@@ -0,0 +1,11 @@
+export default () => (
+
+)
diff --git a/src/Campaigns/Blocks/CampaignGoal/index.tsx b/src/Campaigns/Blocks/CampaignGoal/index.tsx
new file mode 100644
index 0000000000..034f5fc8a5
--- /dev/null
+++ b/src/Campaigns/Blocks/CampaignGoal/index.tsx
@@ -0,0 +1,15 @@
+import edit from './edit';
+import icon from './icon';
+import schema from './block.json';
+
+/**
+ * @unreleased
+ */
+export default {
+ schema,
+ settings: {
+ icon,
+ edit
+ }
+};
+
diff --git a/src/Campaigns/Blocks/CampaignGoal/render.php b/src/Campaigns/Blocks/CampaignGoal/render.php
new file mode 100644
index 0000000000..7b507bdf08
--- /dev/null
+++ b/src/Campaigns/Blocks/CampaignGoal/render.php
@@ -0,0 +1,20 @@
+getById($attributes['campaignId'])
+) {
+ return;
+}
+
+?>
+
+
diff --git a/src/Campaigns/Blocks/CampaignGoal/utils.ts b/src/Campaigns/Blocks/CampaignGoal/utils.ts
new file mode 100644
index 0000000000..8a81a38016
--- /dev/null
+++ b/src/Campaigns/Blocks/CampaignGoal/utils.ts
@@ -0,0 +1,35 @@
+import {__} from '@wordpress/i18n';
+import {getCampaignOptionsWindowData, amountFormatter} from '@givewp/campaigns/utils';
+
+
+export const getGoalDescription = (goalType: string) => {
+ switch (goalType) {
+ case 'amount':
+ return __('Amount raised', 'give');
+ case 'donations':
+ return __('Number of donations', 'give');
+ case 'donors':
+ return __('Number of donors', 'give');
+ case 'amountFromSubscriptions':
+ return __('Recurring amount raised', 'give');
+ case 'subscriptions':
+ return __('Number of recurring donations', 'give');
+ case 'donorsFromSubscriptions':
+ return __('Number of recurring donors', 'give');
+ }
+}
+
+
+export const getGoalFormattedValue = (goalType: string, value: number) => {
+ switch (goalType) {
+ case 'amount':
+ case 'amountFromSubscriptions':
+ const {currency} = getCampaignOptionsWindowData()
+ const currencyFormatter = amountFormatter(currency);
+
+ return currencyFormatter.format(value);
+
+ default:
+ return value;
+ }
+}
diff --git a/src/Campaigns/Blocks/CampaignStats/Icon.tsx b/src/Campaigns/Blocks/CampaignStats/Icon.tsx
new file mode 100644
index 0000000000..b8e68daa9f
--- /dev/null
+++ b/src/Campaigns/Blocks/CampaignStats/Icon.tsx
@@ -0,0 +1,20 @@
+import {Path, SVG} from '@wordpress/components';
+
+export function StatsIcon() {
+ return (
+
+ );
+}
diff --git a/src/Campaigns/Blocks/CampaignStats/app.tsx b/src/Campaigns/Blocks/CampaignStats/app.tsx
new file mode 100644
index 0000000000..e39aaa19eb
--- /dev/null
+++ b/src/Campaigns/Blocks/CampaignStats/app.tsx
@@ -0,0 +1 @@
+import './styles.scss';
diff --git a/src/Campaigns/Blocks/CampaignStats/block.json b/src/Campaigns/Blocks/CampaignStats/block.json
new file mode 100644
index 0000000000..ee52e1e2bd
--- /dev/null
+++ b/src/Campaigns/Blocks/CampaignStats/block.json
@@ -0,0 +1,71 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "givewp/campaign-stats-block",
+ "version": "1.0.0",
+ "title": "Campaign Statistics",
+ "category": "give",
+ "description": "Displays the campaign’s statistics.",
+ "attributes": {
+ "campaignId": {
+ "type": "integer"
+ },
+ "statistic": {
+ "type": "string",
+ "enum": [
+ "top-donation",
+ "average-donation"
+ ],
+ "default": "top-donation"
+ }
+ },
+ "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",
+ "viewScript": "file:../../../../build/campaignStatsApp.js",
+ "style": "file:../../../../build/campaignStatsBlockApp.css"
+}
diff --git a/src/Campaigns/Blocks/CampaignStats/edit.tsx b/src/Campaigns/Blocks/CampaignStats/edit.tsx
new file mode 100644
index 0000000000..785230cd50
--- /dev/null
+++ b/src/Campaigns/Blocks/CampaignStats/edit.tsx
@@ -0,0 +1,51 @@
+import {InspectorControls, useBlockProps} from '@wordpress/block-editor';
+import {__} from '@wordpress/i18n';
+import {BlockEditProps} from '@wordpress/blocks';
+import {PanelBody, SelectControl} from '@wordpress/components';
+import {CampaignSelector} from '../shared/components/CampaignSelector';
+import ServerSideRender from '@wordpress/server-side-render';
+import useCampaign from '../shared/hooks/useCampaign';
+
+import './styles.scss';
+
+type statisticType = 'top-donation' | 'average-donation';
+
+export default function Edit({
+ attributes,
+ setAttributes,
+}: BlockEditProps<{
+ campaignId: number;
+ statistic: statisticType;
+}>) {
+ const blockProps = useBlockProps();
+ const {campaign, hasResolved} = useCampaign(attributes?.campaignId);
+
+ const statsHelpText = attributes.statistic === 'top-donation'
+ ? __('Displays the top donation of the selected campaign.', 'give')
+ : __('Displays the average donation of the selected campaign.', 'give');
+
+ return (
+
+
+
+
+
+ {hasResolved && campaign?.id && (
+
+
+ setAttributes({statistic: value})}
+ />
+
+
+ )}
+
+ );
+}
diff --git a/src/Campaigns/Blocks/CampaignStats/index.tsx b/src/Campaigns/Blocks/CampaignStats/index.tsx
new file mode 100644
index 0000000000..1e035d8a69
--- /dev/null
+++ b/src/Campaigns/Blocks/CampaignStats/index.tsx
@@ -0,0 +1,16 @@
+import schema from './block.json';
+import Edit from './edit';
+import {StatsIcon} from './Icon';
+
+/**
+ * @unreleased
+ */
+const settings = {
+ icon:
,
+ edit: Edit,
+};
+
+export default {
+ schema,
+ settings,
+};
diff --git a/src/Campaigns/Blocks/CampaignStats/render.php b/src/Campaigns/Blocks/CampaignStats/render.php
new file mode 100644
index 0000000000..82cd23a08a
--- /dev/null
+++ b/src/Campaigns/Blocks/CampaignStats/render.php
@@ -0,0 +1,43 @@
+getById($attributes['campaignId'])
+) {
+ return;
+}
+
+$query = (new CampaignDonationQuery($campaign))
+ ->select(
+ $attributes['statistic'] === 'top-donation'
+ ? 'MAX(amountMeta.meta_value) as amount'
+ : 'AVG(amountMeta.meta_value) as amount'
+ )
+ ->joinDonationMeta(DonationMetaKeys::AMOUNT, 'amountMeta');
+
+$donationStat = $query->get();
+
+$amount = $donationStat && $donationStat->amount
+ ? Money::fromDecimal($donationStat->amount, give_get_currency())
+ : Money::fromDecimal(0, give_get_currency());
+
+$title = $attributes['statistic'] === 'top-donation' ? __('Top Donation', 'give') : __('Average Donation', 'give');
+?>
+
+
+
+ formatToLocale()) ?>
+
diff --git a/src/Campaigns/Blocks/CampaignStats/styles.scss b/src/Campaigns/Blocks/CampaignStats/styles.scss
new file mode 100644
index 0000000000..575b3afbda
--- /dev/null
+++ b/src/Campaigns/Blocks/CampaignStats/styles.scss
@@ -0,0 +1,23 @@
+.givewp-campaign-stats-block {
+
+ span {
+ display: block;
+ margin-bottom: 2px;
+ height: 18px;
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 1.5;
+ letter-spacing: 0.48px;
+ text-align: left;
+ color: var(--givewp-neutral-500);
+ }
+
+ strong {
+ height: 32px;
+ font-size: 20px;
+ font-weight: 600;
+ line-height: 1.6;
+ letter-spacing: normal;
+ color: var(--givewp-neutral-900);
+ }
+}
diff --git a/src/Campaigns/Blocks/DonateButton/render.php b/src/Campaigns/Blocks/DonateButton/render.php
index c202651817..63f92ff37d 100644
--- a/src/Campaigns/Blocks/DonateButton/render.php
+++ b/src/Campaigns/Blocks/DonateButton/render.php
@@ -16,6 +16,11 @@
return;
}
+$blockInlineStyles = sprintf(
+ '--givewp-primary-color: %s;',
+ esc_attr($campaign->primaryColor ?? '#0b72d9')
+);
+
$params = [
'formId' => $attributes['useDefaultForm']
? $campaign->defaultFormId
@@ -23,5 +28,13 @@
'openFormButton' => $attributes['buttonText'],
'formFormat' => 'modal',
];
+?>
-echo (new BlockRenderController())->render($params);
+
'givewp-campaign-donate-button-block'])); ?>
+ style="">
+ render($params); ?>
+
diff --git a/src/Campaigns/Blocks/blocks.ts b/src/Campaigns/Blocks/blocks.ts
index 59ae9980bc..ada033fe2a 100644
--- a/src/Campaigns/Blocks/blocks.ts
+++ b/src/Campaigns/Blocks/blocks.ts
@@ -11,10 +11,20 @@ import campaignDonateButton from './DonateButton';
import campaignDonations from './CampaignDonations';
import campaignDonors from './CampaignDonors';
import campaignTitle from './CampaignTitle';
+import campaignGoal from './CampaignGoal';
+import campaignStats from './CampaignStats';
export const getAllBlocks = () => {
- return [campaignCover, campaignDonateButton, campaignDonations, campaignDonors, campaignTitle];
-};
+ return [
+ campaignCover,
+ campaignDonateButton,
+ campaignDonations,
+ campaignDonors,
+ campaignTitle,
+ campaignGoal,
+ campaignStats
+ ];
+}
getAllBlocks().forEach((block) => {
if (!getBlockType(block.schema.name)) {
diff --git a/src/Campaigns/Blocks/shared/hooks/useCampaign.ts b/src/Campaigns/Blocks/shared/hooks/useCampaign.ts
index a7cf2b57ea..3d0ed8f3ad 100644
--- a/src/Campaigns/Blocks/shared/hooks/useCampaign.ts
+++ b/src/Campaigns/Blocks/shared/hooks/useCampaign.ts
@@ -2,10 +2,12 @@ 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);
+ const campaignData = useEntityRecord('givewp', 'campaign', campaignId);
return {
- campaign: data?.record as Campaign,
- hasResolved: data?.hasResolved,
+ campaign: {
+ ...campaignData?.record as Campaign
+ },
+ hasResolved: campaignData?.hasResolved,
};
}
diff --git a/src/Campaigns/CampaignsAdminPage.php b/src/Campaigns/CampaignsAdminPage.php
index 9fdf667d23..cf6be15b93 100644
--- a/src/Campaigns/CampaignsAdminPage.php
+++ b/src/Campaigns/CampaignsAdminPage.php
@@ -39,7 +39,7 @@ public function renderCampaignsPage()
wp_die(__('Campaign not found', 'give'), 404);
}
- give(LoadCampaignDetailsAssets::class)($campaign);
+ give(LoadCampaignDetailsAssets::class)();
} else {
give(LoadCampaignsListTableAssets::class)();
}
@@ -52,6 +52,6 @@ public function renderCampaignsPage()
*/
public static function isShowingDetailsPage(): bool
{
- return isset($_GET['id']) && isset($_GET['page']) && 'give-campaigns' == isset($_GET['page']);
+ return isset($_GET['id'], $_GET['page']) && 'give-campaigns' === $_GET['page'];
}
}
diff --git a/src/Campaigns/Controllers/CampaignRequestController.php b/src/Campaigns/Controllers/CampaignRequestController.php
index 65dcb05738..836b259a12 100644
--- a/src/Campaigns/Controllers/CampaignRequestController.php
+++ b/src/Campaigns/Controllers/CampaignRequestController.php
@@ -3,7 +3,6 @@
namespace Give\Campaigns\Controllers;
use Exception;
-use Give\Campaigns\CampaignDonationQuery;
use Give\Campaigns\Models\Campaign;
use Give\Campaigns\Repositories\CampaignRepository;
use Give\Campaigns\ValueObjects\CampaignGoalType;
@@ -34,7 +33,7 @@ public function getCampaign(WP_REST_Request $request)
return new WP_REST_Response(
array_merge($campaign->toArray(), [
- 'goalProgress' => $campaign->goalProgress(),
+ 'goalStats' => $campaign->getGoalStats(),
'defaultFormTitle' => $campaign->defaultForm()->title
])
);
@@ -60,7 +59,9 @@ public function getCampaigns(WP_REST_Request $request): WP_REST_Response
// todo: remove - temporary solution
$campaigns = array_map(function ($campaign) {
- return $campaign->toArray();
+ return array_merge($campaign->toArray(), [
+ 'goalStats' => $campaign->getGoalStats(),
+ ]);
}, $campaigns);
$response = rest_ensure_response($campaigns);
@@ -186,8 +187,8 @@ public function createCampaign(WP_REST_Request $request): WP_REST_Response
'longDescription' => '',
'logo' => '',
'image' => $request->get_param('image') ?? '',
- 'primaryColor' => '',
- 'secondaryColor' => '',
+ 'primaryColor' => '#0b72d9',
+ 'secondaryColor' => '#27ae60',
'goal' => (int)$request->get_param('goal'),
'goalType' => new CampaignGoalType($request->get_param('goalType')),
'status' => CampaignStatus::DRAFT(),
diff --git a/src/Campaigns/Models/Campaign.php b/src/Campaigns/Models/Campaign.php
index 23d086d901..f0f8e5ef5c 100644
--- a/src/Campaigns/Models/Campaign.php
+++ b/src/Campaigns/Models/Campaign.php
@@ -5,7 +5,7 @@
use DateTime;
use Exception;
use Give\Campaigns\Actions\ConvertQueryDataToCampaign;
-use Give\Campaigns\CampaignDonationQuery;
+use Give\Campaigns\DataTransferObjects\CampaignGoalData;
use Give\Campaigns\Factories\CampaignFactory;
use Give\Campaigns\Repositories\CampaignPageRepository;
use Give\Campaigns\Repositories\CampaignRepository;
@@ -25,6 +25,7 @@
* @unreleased
*
* @property int $id
+ * @property int $pageId
* @property int $defaultFormId
* @property CampaignType $type
* @property bool $enableCampaignPage
@@ -50,6 +51,7 @@ class Campaign extends Model implements ModelCrud, ModelHasFactory
*/
protected $properties = [
'id' => 'int',
+ 'pageId' => 'int',
'defaultFormId' => 'int',
'type' => CampaignType::class,
'enableCampaignPage' => ['bool', true],
@@ -174,10 +176,9 @@ public function merge(Campaign ...$campaignsToMerge): bool
return give(CampaignRepository::class)->mergeCampaigns($this, ...$campaignsToMerge);
}
- public function goalProgress()
+ public function getGoalStats(): array
{
- $query = new CampaignDonationQuery($this);
- return $query->sumIntendedAmount();
+ return (new CampaignGoalData($this))->toArray();
}
/**
diff --git a/src/Campaigns/Repositories/CampaignRepository.php b/src/Campaigns/Repositories/CampaignRepository.php
index b2b814c059..699695932a 100644
--- a/src/Campaigns/Repositories/CampaignRepository.php
+++ b/src/Campaigns/Repositories/CampaignRepository.php
@@ -143,6 +143,7 @@ public function update(Campaign $campaign): void
->update([
'campaign_type' => $campaign->type->getValue(),
'enable_campaign_page' => $campaign->enableCampaignPage,
+ 'campaign_page_id' => $campaign->pageId,
'campaign_title' => $campaign->title,
'short_desc' => $campaign->shortDescription,
'long_desc' => $campaign->longDescription,
@@ -355,6 +356,7 @@ public function prepareQuery(): ModelQueryBuilder
'id',
['campaigns.form_id', 'defaultFormId'], // Prefix the `form_id` column to avoid conflicts with the `give_campaign_forms` table.
['campaign_type', 'type'],
+ ['campaign_page_id', 'pageId'],
['enable_campaign_page', 'enableCampaignPage'],
['campaign_title', 'title'],
['short_desc', 'shortDescription'],
diff --git a/src/Campaigns/Routes/RegisterCampaignRoutes.php b/src/Campaigns/Routes/RegisterCampaignRoutes.php
index 1628e902db..4a366a40bc 100644
--- a/src/Campaigns/Routes/RegisterCampaignRoutes.php
+++ b/src/Campaigns/Routes/RegisterCampaignRoutes.php
@@ -252,6 +252,14 @@ public function getSchema(): array
'type' => 'string',
'description' => esc_html__('Campaign short description', 'give'),
],
+ 'primaryColor' => [
+ 'type' => 'string',
+ 'description' => esc_html__('Primary color for the campaign', 'give'),
+ ],
+ 'secondaryColor' => [
+ 'type' => 'string',
+ 'description' => esc_html__('Secondary color for the campaign', 'give'),
+ ],
'goal' => [
'type' => 'number',
'minimum' => 1,
diff --git a/src/Campaigns/ServiceProvider.php b/src/Campaigns/ServiceProvider.php
index e0eb6bca40..4310126a3c 100644
--- a/src/Campaigns/ServiceProvider.php
+++ b/src/Campaigns/ServiceProvider.php
@@ -3,9 +3,11 @@
namespace Give\Campaigns;
use Give\Campaigns\Actions\AddCampaignFormFromRequest;
+use Give\Campaigns\Actions\AssociateCampaignPageWithCampaign;
use Give\Campaigns\Actions\CreateDefaultCampaignForm;
use Give\Campaigns\Actions\DeleteCampaignPage;
use Give\Campaigns\Actions\FormInheritsCampaignGoal;
+use Give\Campaigns\Actions\LoadCampaignOptions;
use Give\Campaigns\Migrations\Donations\AddCampaignId as DonationsAddCampaignId;
use Give\Campaigns\Migrations\MigrateFormsToCampaignForms;
use Give\Campaigns\Migrations\P2P\SetCampaignType;
@@ -51,6 +53,7 @@ public function boot(): void
$this->registerCampaignEntity();
$this->registerCampaignBlocks();
$this->setupCampaignForms();
+ $this->loadCampaignOptions();
}
/**
@@ -102,6 +105,7 @@ private function registerActions(): void
{
Hooks::addAction('givewp_campaign_deleted', DeleteCampaignPage::class);
Hooks::addAction('givewp_donation_form_creating', FormInheritsCampaignGoal::class);
+ Hooks::addAction('givewp_campaign_page_created', AssociateCampaignPageWithCampaign::class);
}
/**
@@ -161,4 +165,12 @@ private function registerCampaignBlocks()
Hooks::addAction('rest_api_init', Actions\RegisterCampaignIdRestField::class);
Hooks::addAction('init', Actions\RegisterCampaignBlocks::class);
}
+
+ /**
+ * @unreleased
+ */
+ private function loadCampaignOptions()
+ {
+ Hooks::addAction('init', LoadCampaignOptions::class);
+ }
}
diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/CampaignDetailsPage.module.scss b/src/Campaigns/resources/admin/components/CampaignDetailsPage/CampaignDetailsPage.module.scss
index a020eaccef..149aa9dc6f 100644
--- a/src/Campaigns/resources/admin/components/CampaignDetailsPage/CampaignDetailsPage.module.scss
+++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/CampaignDetailsPage.module.scss
@@ -471,6 +471,10 @@ select[name="campaignId"] {
width: 1.25rem;
}
}
+
+ .colorControl {
+ margin-top: var(--givewp-spacing-3);
+ }
}
.loadingContainer {
diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats/index.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats/index.tsx
index 1de9b3d5b8..101915be04 100644
--- a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats/index.tsx
+++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/CampaignStats/index.tsx
@@ -3,36 +3,29 @@ 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 {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 {useCampaignEntityRecord, amountFormatter, getCampaignOptionsWindowData} 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 {currency} = getCampaignOptionsWindowData();
+const currencyFormatter = amountFormatter(currency);
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') },
+ {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);
@@ -46,7 +39,7 @@ const CampaignStats = () => {
const onDayRangeChange = async (days: number) => {
setDayRange(days)
- apiFetch({path: addQueryArgs( '/give-api/v2/campaigns/' + campaignId +'/statistics', {rangeInDays: days} ) } )
+ apiFetch({path: addQueryArgs('/give-api/v2/campaigns/' + campaignId + '/statistics', {rangeInDays: days})})
.then(setStats);
}
@@ -56,10 +49,13 @@ const CampaignStats = () => {
<>
-
-
-
-
+
+
+
+
@@ -93,9 +89,12 @@ const StatWidget = ({label, values, description, formatter = null}) => {
- {formatter?.format(values[0]) ?? values[0]}
+ {'undefined' !== typeof values[0]
+ ? formatter?.format(values[0]) ?? values[0]
+ :
+ }
- {!! values[1] && (
+ {!!values[1] && (
)}
@@ -150,7 +149,7 @@ const GoalProgressWidget = () => {
{__('Goal progress', 'give')}
{__('Show your campaign performance', 'give')}
-
+
)
}
diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/ColorControl/Icons/CheckIcon.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/ColorControl/Icons/CheckIcon.tsx
new file mode 100644
index 0000000000..19b3d353ed
--- /dev/null
+++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/ColorControl/Icons/CheckIcon.tsx
@@ -0,0 +1,24 @@
+export default function CheckIcon({refColor}) {
+ return (
+
+ );
+}
+
+function getContrastColor(hexColor){
+ hexColor = hexColor.replace(/^#/, '');
+
+ let r = parseInt(hexColor.substring(0, 2), 16);
+ let g = parseInt(hexColor.substring(2, 4), 16);
+ let b = parseInt(hexColor.substring(4, 6), 16);
+
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
+
+ return luminance > 0.5 ? '#000000' : '#FFFFFF';
+}
diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/ColorControl/Icons/EditIcon.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/ColorControl/Icons/EditIcon.tsx
new file mode 100644
index 0000000000..d254e1cd51
--- /dev/null
+++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/ColorControl/Icons/EditIcon.tsx
@@ -0,0 +1,12 @@
+export default function EditIcon() {
+ return (
+
+ );
+}
diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/ColorControl/index.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/ColorControl/index.tsx
new file mode 100644
index 0000000000..4020522af3
--- /dev/null
+++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/ColorControl/index.tsx
@@ -0,0 +1,100 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+import {useCallback, useState} from 'react';
+import {Controller, useFormContext} from 'react-hook-form';
+
+/**
+ * WordPress dependencies
+ */
+import {ColorIndicator, ColorPalette, Popover} from '@wordpress/components';
+import {__} from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import EditIcon from './Icons/EditIcon';
+import CheckIcon from './Icons/CheckIcon';
+import './styles.scss';
+
+interface ColorOption {
+ name: string;
+ slug: string;
+ color: string;
+}
+
+const defaultColors: ColorOption[] = [
+ {name: 'Blue', slug: 'blue', color: '#0b72d9'},
+ {name: 'Green', slug: 'green', color: '#27ae60'},
+ {name: 'Purple', slug: 'purple', color: '#19078c'},
+ {name: 'Orange', slug: 'orange', color: '#f29718'},
+ {name: 'Lavender', slug: 'lavender', color: '#9b51e0'},
+ {name: 'Terracotta', slug: 'terracotta', color: '#e26f56'},
+ {name: 'Red', slug: 'red', color: '#cc1818'},
+];
+
+/**
+ * @unreleased
+ */
+function ColorControl({name, disabled = false, className}: { name: string; disabled?: boolean; className?: string }) {
+ const [popoverIsVisible, setPopoverIsVisible] = useState
(false);
+ const {control} = useFormContext();
+
+ const toggleVisible = useCallback(() => {
+ setPopoverIsVisible((prev) => !prev);
+ }, []);
+
+ return (
+ (
+
+
+
+ {field.value && }
+
+
+ {!disabled && (
+
+
+ {popoverIsVisible && (
+
+
+
+ )}
+
+ )}
+
+ )}
+ />
+ );
+};
+
+export default ColorControl;
diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/ColorControl/styles.scss b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/ColorControl/styles.scss
new file mode 100644
index 0000000000..d9f49d43f2
--- /dev/null
+++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/ColorControl/styles.scss
@@ -0,0 +1,63 @@
+.givewp-color-control {
+ align-items: center;
+ display: flex;
+ gap: var(--givewp-spacing-4);
+
+ &__indicator {
+ align-items: center;
+ display: flex;
+ position: relative;
+
+ span {
+ border-radius: 0.5rem;
+ border: 0;
+ box-shadow: none;
+ height: 3rem;
+ width: 3rem;
+ }
+
+ svg {
+ left: 50%;
+ position: absolute;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ }
+ }
+
+ &__edit-button {
+ align-items: center;
+ background: none;
+ border-radius: 0.5rem;
+ border: solid 1px var(--givewp-neutral-300);
+ color: var(--givewp-neutral-900);
+ display: flex;
+ font-size: 1rem;
+ font-weight: 500;
+ gap: var(--givewp-spacing-2);
+ height: 3rem;
+ line-height: 1.5;
+ padding: var(--givewp-spacing-3) var(--givewp-spacing-6) var(--givewp-spacing-3) var(--givewp-spacing-4);
+
+ &:hover {
+ background: var(--givewp-neutral-50);
+ cursor: pointer;
+ }
+
+ &--active {
+ background: var(--givewp-neutral-700);
+ border: solid 1px var(--givewp-neutral-700);
+ color: var(--givewp-neutral-25);
+
+ &:hover {
+ background: var(--givewp-neutral-900);
+ }
+ }
+ }
+
+ &__popover-content {
+ .components-popover__content {
+ padding: var(--givewp-spacing-4);
+ width: auto;
+ }
+ }
+}
diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/DefaultForm/index.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/DefaultForm/index.tsx
index 85018e707b..47325399af 100644
--- a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/DefaultForm/index.tsx
+++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/DefaultForm/index.tsx
@@ -1,8 +1,8 @@
-import {__} from "@wordpress/i18n";
+import {__} from '@wordpress/i18n';
import HeaderText from '../HeaderText';
import HeaderSubText from '../HeaderSubText';
-import styles from "./styles.module.scss"
+import styles from './styles.module.scss';
/**
* @unreleased
@@ -15,9 +15,6 @@ const DefaultFormWidget = ({defaultForm}: {defaultForm: string}) => {
{__('Default campaign form', 'give')}
{__('Your campaign page and blocks will collect donations through this form by default.', 'give')}
-
- {__('Edit', 'give')}
-
{defaultForm}
diff --git a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/GoalProgressChart/index.tsx b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/GoalProgressChart/index.tsx
index 7ecd274610..cd4d479788 100644
--- a/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/GoalProgressChart/index.tsx
+++ b/src/Campaigns/resources/admin/components/CampaignDetailsPage/Components/GoalProgressChart/index.tsx
@@ -3,11 +3,10 @@ import Chart from "react-apexcharts";
import React from "react";
import styles from "./styles.module.scss"
+import {getCampaignOptionsWindowData, amountFormatter} from '@givewp/campaigns/utils';
-const currency = new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
-})
+const {currency} = getCampaignOptionsWindowData();
+const currencyFormatter = amountFormatter(currency);
const GoalProgressChart = ({ value, goal }) => {
const percentage: number = Math.abs((value / goal) * 100);
@@ -49,7 +48,7 @@ const GoalProgressChart = ({ value, goal }) => {
}
},
colors: ['#459948'],
- labels: [currency.format(value)],
+ labels: [currencyFormatter.format(value)],
}}
series={[percentage]}
type="radialBar"
@@ -57,7 +56,7 @@ const GoalProgressChart = ({ value, goal }) => {