PluginProbe ʕ •ᴥ•ʔ
GiveWP – Donation Plugin and Fundraising Platform / 4.3.2
GiveWP – Donation Plugin and Fundraising Platform v4.3.2
4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 2.3.2 2.30.0 2.31.0 2.31.1 2.32.0 2.33.0 2.33.1 2.33.2 2.33.3 2.33.4 2.33.5 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.5.0 2.5.1 2.5.10 2.5.11 2.5.12 2.5.13 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 2.5.8 2.5.9 2.6.0 2.6.1 2.6.2 2.6.3 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.8.0 2.8.1 2.9.0 2.9.1 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.10.0 3.11.0 3.12.0 3.12.1 3.12.2 3.12.3 3.13.0 3.14.0 3.14.1 3.14.2 3.15.0 3.15.1 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.16.5 3.17.0 3.17.1 3.17.2 3.18.0 3.19.0 3.19.1 3.19.2 3.19.3 3.19.4 3.2.0 3.2.1 3.2.2 3.20.0 3.21.0 3.21.1 3.22.0 3.22.1 3.22.2 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.6.0 3.6.1 3.6.2 3.7.0 3.8.0 3.9.0 4.0.0 4.1.0 4.1.1 4.10.0 4.10.1 4.11.0 4.12.0 4.13.0 4.13.1 4.13.2 4.14.0 4.14.1 4.14.2 4.14.3 4.14.4 4.14.5 4.14.6 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.1 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 trunk 1.9.0 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.2 2.11.3 2.12.0 2.12.1 2.12.2 2.12.3 2.13.0 2.13.1 2.13.2 2.13.3 2.13.4 2.14.0 2.15.0 2.16.0 2.16.1 2.17.0 2.17.1 2.17.3 2.18.0 2.18.1 2.19.1 2.19.2 2.19.3 2.19.4 2.19.5 2.19.6 2.19.7 2.19.8 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.20.0 2.20.1 2.20.2 2.21.0 2.21.1 2.21.2 2.21.3 2.21.4 2.22.0 2.22.1 2.22.2 2.22.3 2.23.0 2.23.1 2.23.2 2.24.0 2.24.1 2.24.2 2.25.0 2.25.1 2.25.2 2.25.3 2.26.0 2.27.0 2.27.1 2.27.2 2.27.3 2.28.0 2.29.0 2.29.1 2.29.2
give / src / Campaigns / resources / admin / components / CampaignFormModal / index.tsx
give / src / Campaigns / resources / admin / components / CampaignFormModal Last commit date
CampaignFormModal.module.scss 1 year ago GoalTypeIcons.tsx 1 year ago index.tsx 1 year ago types.ts 1 year ago
index.tsx
407 lines
1 import {FormProvider, SubmitHandler, useForm} from 'react-hook-form';
2 import {__, sprintf} from '@wordpress/i18n';
3 import styles from './CampaignFormModal.module.scss';
4 import FormModal from '../FormModal';
5 import CampaignsApi from '../api';
6 import {CampaignFormInputs, CampaignModalProps, GoalTypeOption as GoalTypeOptionType} from './types';
7 import {useRef, useState} from 'react';
8 import {Currency, Upload} from '../Inputs';
9 import {
10 AmountFromSubscriptionsIcon,
11 AmountIcon,
12 DonationsIcon,
13 DonorsFromSubscriptionsIcon,
14 DonorsIcon,
15 SubscriptionsIcon,
16 } from './GoalTypeIcons';
17 import {getGiveCampaignsListTableWindowData} from '../CampaignsListTable';
18 import {amountFormatter} from '@givewp/campaigns/utils';
19 import TextareaControl from '../CampaignDetailsPage/Components/TextareaControl';
20 import {CampaignGoalInputAttributes, isValidGoalType} from '../../constants/goalInputAttributes';
21
22 const {currency, isRecurringEnabled} = getGiveCampaignsListTableWindowData();
23 const currencyFormatter = amountFormatter(currency);
24
25 /**
26 * Get the next sharp hour
27 *
28 * @since 4.0.0
29 */
30 const getNextSharpHour = (hoursToAdd: number) => {
31 const date = new Date();
32 date.setHours(date.getHours() + hoursToAdd, 0, 0, 0);
33
34 return date;
35 };
36
37 /**
38 * Format a given date to be used in datetime inputs
39 *
40 * @since 4.0.0
41 */
42 const getDateString = (date: Date) => {
43 const offsetInMilliseconds = date.getTimezoneOffset() * 60 * 1000;
44 const dateWithOffset = new Date(date.getTime() - offsetInMilliseconds);
45
46 return removeTimezoneFromDateISOString(dateWithOffset.toISOString());
47 };
48
49 /**
50 * Remove timezone from date string
51 *
52 * @since 4.0.0
53 */
54 const removeTimezoneFromDateISOString = (date: string) => {
55 return date.slice(0, -5);
56 };
57
58 /**
59 * @since 4.0.0
60 */
61 const getGoalTypeIcon = (type: string) => {
62 switch (type) {
63 case 'amount':
64 return <AmountIcon />;
65 case 'donations':
66 return <DonationsIcon />;
67 case 'donors':
68 return <DonorsIcon />;
69 case 'amountFromSubscriptions':
70 return <AmountFromSubscriptionsIcon />;
71 case 'subscriptions':
72 return <SubscriptionsIcon />;
73 case 'donorsFromSubscriptions':
74 return <DonorsFromSubscriptionsIcon />;
75 }
76 };
77
78 /**
79 * Goal Type Option component
80 *
81 * @since 4.0.0
82 */
83 const GoalTypeOption = ({type, label, description, selected, register}: GoalTypeOptionType) => {
84 const divRef = useRef(null);
85 const labelRef = useRef(null);
86
87 const handleDivClick = () => {
88 labelRef.current.click();
89 };
90
91 return (
92 <div
93 className={`${styles.goalTypeOption} ${selected ? styles.goalTypeOptionSelected : ''}`}
94 ref={divRef}
95 onClick={handleDivClick}
96 >
97 <div className={styles.goalTypeOptionIcon}>{getGoalTypeIcon(type)}</div>
98 <div className={styles.goalTypeOptionText}>
99 <label ref={labelRef}>
100 <input type="radio" value={type} {...register('goalType')} />
101 {label}
102 </label>
103 <span>{description}</span>
104 </div>
105 </div>
106 );
107 };
108
109 /**
110 * Campaign Form Modal component
111 *
112 * @since 4.0.0
113 */
114 export default function CampaignFormModal({isOpen, handleClose, apiSettings, title, campaign}: CampaignModalProps) {
115 const API = new CampaignsApi(apiSettings);
116 const [step, setStep] = useState<number>(1);
117
118 const methods = useForm<CampaignFormInputs>({
119 defaultValues: {
120 title: campaign?.title ?? '',
121 shortDescription: campaign?.shortDescription ?? '',
122 image: campaign?.image ?? '',
123 goalType: campaign?.goalType ?? 'amount',
124 goal: campaign?.goal ?? null,
125 startDateTime: getDateString(
126 campaign?.startDateTime?.date ? new Date(campaign?.startDateTime?.date) : getNextSharpHour(1)
127 ),
128 endDateTime: campaign?.endDateTime?.date ? getDateString(new Date(campaign.startDateTime.date)) : '',
129 },
130 });
131
132 const {
133 register,
134 handleSubmit,
135 formState: {errors, isDirty, isSubmitting},
136 setValue,
137 watch,
138 trigger,
139 } = methods;
140
141 const image = watch('image');
142 const selectedGoalType = watch('goalType');
143 const goal = watch('goal');
144
145 const getFormModalTitle = () => {
146 switch (step) {
147 case 1:
148 return __('Tell us about your fundraising cause', 'give');
149 case 2:
150 return __('Set up your campaign goal', 'give');
151 }
152
153 return null;
154 };
155
156 const goalInputAttribute =
157 selectedGoalType && isValidGoalType(selectedGoalType)
158 ? new CampaignGoalInputAttributes(selectedGoalType, currency)
159 : new CampaignGoalInputAttributes('amount', currency);
160
161 const requiredAsterisk = <span className={`givewp-field-required ${styles.fieldRequired}`}>*</span>;
162
163 const validateTitle = async () => {
164 return await trigger('title');
165 };
166
167 const onSubmit: SubmitHandler<CampaignFormInputs> = async (inputs, event) => {
168 event.preventDefault();
169
170 if (step !== 2) {
171 return;
172 }
173
174 try {
175 inputs.startDateTime = getDateString(new Date(inputs.startDateTime));
176 inputs.endDateTime = inputs.endDateTime && getDateString(new Date(inputs.endDateTime));
177
178 const endpoint = campaign?.id ? `/campaign/${campaign.id}` : '';
179 const response = await API.fetchWithArgs(endpoint, inputs, 'POST');
180
181 handleClose(response);
182 } catch (error) {
183 console.error('Error submitting campaign campaign', error);
184 }
185 };
186
187 return (
188 <FormProvider {...methods}>
189 <FormModal
190 isOpen={isOpen}
191 handleClose={handleClose}
192 title={getFormModalTitle()}
193 handleSubmit={handleSubmit(onSubmit)}
194 errors={[]}
195 className={styles.campaignForm}
196 >
197 {step === 1 && (
198 <>
199 <div className="givewp-campaigns__form-row">
200 <label htmlFor="title">
201 {__("What's the title of your campaign?", 'give')} {requiredAsterisk}
202 </label>
203 <span>{__("Give your campaign a title that tells donors what it's about.", 'give')}</span>
204 <input
205 type="text"
206 {...register('title', {required: __('The campaign must have a title!', 'give')})}
207 aria-invalid={errors.title ? 'true' : 'false'}
208 placeholder={__('Eg. Holiday Food Drive', 'give')}
209 onBlur={validateTitle}
210 />
211 {errors.title && (
212 <div className={'givewp-campaigns__form-errors'}>
213 <p>{errors.title.message}</p>
214 </div>
215 )}
216 </div>
217 <div className="givewp-campaigns__form-row">
218 <label htmlFor="shortDescription">{__("What's your campaign about?", 'give')}</label>
219 <span>{__('Let your donors know the story behind your campaign.', 'give')}</span>
220 <TextareaControl
221 name="shortDescription"
222 rows={4}
223 maxLength={120}
224 placeholder={__('Brief description for your campaign.', 'give')}
225 />
226 </div>
227 <div className="givewp-campaigns__form-row">
228 <label htmlFor="image">{__('Add a cover image for your campaign.', 'give')}</label>
229 <span>{__('Upload an image to represent and inspire your campaign.', 'give')}</span>
230 <Upload
231 id="givewp-campaigns-upload-cover-image"
232 label={__('Cover', 'give')}
233 actionLabel={__('Select to upload', 'give')}
234 value={image}
235 onChange={(coverImageUrl, coverImageAlt) => {
236 setValue('image', coverImageUrl);
237 }}
238 reset={() => setValue('image', '')}
239 />
240 </div>
241 <button
242 type="submit"
243 onClick={async () => (await validateTitle()) && setStep(2)}
244 className={`button button-primary ${!isDirty ? 'disabled' : ''}`}
245 aria-disabled={!isDirty}
246 disabled={!isDirty}
247 >
248 {__('Continue', 'give')}
249 </button>
250 </>
251 )}
252 {step === 2 && (
253 <>
254 <div className="givewp-campaigns__form-row">
255 <label htmlFor="goalType">
256 {__('How would you like to set your goal?', 'give')} {requiredAsterisk}
257 </label>
258 <span>{__('Set the goal your fundraising efforts will work toward.', 'give')}</span>
259 <div className={styles.goalType}>
260 <GoalTypeOption
261 type={'amount'}
262 label={__('Amount raised', 'give')}
263 description={sprintf(
264 __(
265 'Your goal progress is measured by the total amount of funds raised eg. %s of %s raised.',
266 'give'
267 ),
268 currencyFormatter.format(500),
269 currencyFormatter.format(1000)
270 )}
271 selected={selectedGoalType === 'amount'}
272 register={register}
273 />
274 <GoalTypeOption
275 type={'donations'}
276 label={__('Number of donations', 'give')}
277 description={__(
278 'Your goal progress is measured by the number of donations. eg. 1 of 5 donations.',
279 'give'
280 )}
281 selected={selectedGoalType === 'donations'}
282 register={register}
283 />
284 <GoalTypeOption
285 type={'donors'}
286 label={__('Number of donors', 'give')}
287 description={__(
288 'Your goal progress is measured by the number of donors. eg. 10 of 50 donors have given.',
289 'give'
290 )}
291 selected={selectedGoalType === 'donors'}
292 register={register}
293 />
294 {isRecurringEnabled && (
295 <>
296 <GoalTypeOption
297 type={'amountFromSubscriptions'}
298 label={__('Recurring amount raised', 'give')}
299 description={__(
300 'Only the first donation amount of a recurring donation is counted toward the goal.',
301 'give'
302 )}
303 selected={selectedGoalType === 'amountFromSubscriptions'}
304 register={register}
305 />
306 <GoalTypeOption
307 type={'subscriptions'}
308 label={__('Number of recurring donations', 'give')}
309 description={__(
310 'Only the first donation of a recurring donation is counted toward the goal.',
311 'give'
312 )}
313 selected={selectedGoalType === 'subscriptions'}
314 register={register}
315 />
316 <GoalTypeOption
317 type={'donorsFromSubscriptions'}
318 label={__('Number of recurring donors', 'give')}
319 description={__(
320 'Only the donors that subscribed to a recurring donation are counted toward the goal.',
321 'give'
322 )}
323 selected={selectedGoalType === 'donorsFromSubscriptions'}
324 register={register}
325 />
326 </>
327 )}
328 </div>
329 {errors.goalType && (
330 <div className={'givewp-campaigns__form-errors'}>
331 <p>{errors.goalType.message}</p>
332 </div>
333 )}
334 </div>
335 {selectedGoalType && (
336 <div className="givewp-campaigns__form-row">
337 <label htmlFor="title">
338 {goalInputAttribute.getLabel()} {requiredAsterisk}
339 </label>
340 <span>{goalInputAttribute.getDescription()}</span>
341 {selectedGoalType === 'amount' || selectedGoalType === 'amountFromSubscriptions' ? (
342 <Currency
343 name="goal"
344 currency={currency}
345 placeholder={goalInputAttribute.getPlaceholder()}
346 />
347 ) : (
348 <input
349 type="number"
350 {...register('goal', {valueAsNumber: true})}
351 aria-invalid={errors.goal ? 'true' : 'false'}
352 placeholder={goalInputAttribute.getPlaceholder()}
353 />
354 )}
355 {errors.goal && (
356 <div className={'givewp-campaigns__form-errors'}>
357 <p>{errors.goal.message}</p>
358 </div>
359 )}
360 </div>
361 )}
362 {/*<div className="givewp-campaigns__form-row givewp-campaigns__form-row--half">
363 <div className="givewp-campaigns__form-column">
364 <label htmlFor="startDateTime">{__('Start date and time', 'give')}</label>
365 <input
366 type="datetime-local"
367 {...register('startDateTime', {
368 required: __('The campaign must have a start date!', 'give'),
369 })}
370 aria-invalid={errors.startDateTime ? 'true' : 'false'}
371 />
372 </div>
373 <div className="givewp-campaigns__form-column">
374 <label htmlFor="endDateTime">{__('End date and time', 'give')}</label>
375 <input type="datetime-local" {...register('endDateTime')} />
376 </div>
377 </div>*/}
378 <div
379 className="givewp-campaigns__form-row givewp-campaigns__form-row--half"
380 style={{marginBottom: 0}}
381 >
382 <button
383 type="button"
384 onClick={() => setStep(1)}
385 className={`button button-secondary ${styles.button} ${styles.previousButton}`}
386 >
387 {__('Previous', 'give')}
388 </button>
389
390 <button
391 type="submit"
392 className={`button button-primary ${styles.button} ${
393 !goal || isSubmitting ? 'disabled' : ''
394 }`}
395 aria-disabled={!goal || isSubmitting}
396 disabled={!goal || isSubmitting}
397 >
398 {__('Continue', 'give')}
399 </button>
400 </div>
401 </>
402 )}
403 </FormModal>
404 </FormProvider>
405 );
406 }
407