Skip to content

Commit bbb9b28

Browse files
committed
feat: matches contact form validation to backend
- updates contact form validation rules - updates error text - stores error messages in error.fieldname.message to reduce duplication - updates form-error styles to enable longer messages
1 parent 3a59d74 commit bbb9b28

File tree

3 files changed

+102
-69
lines changed

3 files changed

+102
-69
lines changed

public/locales/en/common.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,27 @@
1717
"contactDetails": "Contact Details",
1818
"firstName": "First Name",
1919
"firstNameRequired": "First Name is required",
20+
"firstNameInvalid": "First name is invalid. Only these special characters are allowed: space ' . -",
2021
"lastName": "Last Name",
2122
"lastNameRequired": "Last Name is required",
22-
"atLeast3LettersRequired": "At least 3 letters required",
23+
"lastNameInvalid": "Last name is invalid. Only these special characters are allowed: space ' -",
24+
"max50Chars": "Should be 50 characters or less",
2325
"address": "Address",
2426
"addressRequired": "Address is required",
27+
"addressInvalid": "Address is invalid. Only these special characters are allowed: space . , # - /",
2528
"city": "City",
2629
"cityRequired": "City is required",
30+
"cityInvalid": "City is invalid. Only letters and these special characters are allowed: space . , ( ) -",
2731
"zipCode": "Zip Code",
28-
"zipCodeAlphaNumValidation": "ZipCode is invalid",
32+
"zipCodeRequired": "Zip Code is required",
33+
"zipCodeInvalid": "Zip Code is invalid",
2934
"country": "Country",
3035
"countryRequired": "Country is required",
3136
"welcomePackageConsent": "Yes, please send me the Welcome Package (only EU countries).",
3237
"isACompanyDonation": "This donation is made by a company",
3338
"companyName": "Company Name",
3439
"companyRequired": "Company Name is required",
40+
"companyNameInvalid": "Company name is invalid. Only these special characters are allowed: space . , ' & ( ) ! -",
3541
"companyUneditableHelpText": "Edit your profile if you need to update company name",
3642
"askPublishName": "Show my full name on the Plant-for-the-Planet platform & leaderboards.",
3743
"nameAlreadyPublished": "Your full name will appear in leaderboard as your Plant-for-the-Planet profile is currently public.",
@@ -210,4 +216,4 @@
210216
"top_project_standards_fulfilled": "The project inspection revealed that this project fulfilled at least 12 of the 19 Top Project <2>standards.</2>",
211217
"standardsLink": "https://www.plant-for-the-planet.org/standards/",
212218
"enterValidEmail": "Please enter a valid email"
213-
}
219+
}

src/Donations/Components/ContactsForm.tsx

Lines changed: 92 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { AddressCandidate, GeocodeSuggestion } from "src/Common/Types/arcgis";
1919
import GiftIcon from "public/assets/icons/GiftIcon";
2020
import { euCountries } from "src/Utils/countryUtils";
2121
import { isEmailValid } from "src/Utils/isEmailValid";
22+
// import { DevTool } from "@hookform/devtools";
2223

2324
interface FormData extends ContactDetails {
2425
isPackageWanted: boolean;
@@ -81,7 +82,7 @@ function ContactsForm(): ReactElement {
8182
setValue,
8283
formState: { errors },
8384
} = useForm<FormData>({
84-
mode: "all",
85+
mode: "onTouched",
8586
defaultValues: contactDetails,
8687
});
8788

@@ -227,49 +228,61 @@ function ContactsForm(): ReactElement {
227228
<Controller
228229
name="firstname"
229230
control={control}
230-
rules={{ required: true, minLength: 1 }}
231-
render={({ field: { onChange, value } }) => (
231+
rules={{
232+
required: t("firstNameRequired"),
233+
maxLength: {
234+
value: 50,
235+
message: t("max50Chars"),
236+
},
237+
pattern: {
238+
value: /^[\p{L}\p{N}\sß.'-]+$/u,
239+
message: t("firstNameInvalid"),
240+
},
241+
}}
242+
render={({ field: { onChange, value, onBlur } }) => (
232243
<MaterialTextField
233244
onChange={onChange}
245+
onBlur={onBlur}
234246
value={value}
235247
label={t("firstName")}
236248
variant="outlined"
237249
data-test-id="test-firstName"
238250
/>
239251
)}
240252
/>
241-
{errors.firstname && errors.firstname.type === "required" && (
242-
<span className={"form-errors"}>{t("firstNameRequired")}</span>
243-
)}
244-
{errors.firstname && errors.firstname.type === "minLength" && (
245-
<span className={"form-errors"}>
246-
{t("atLeast3LettersRequired")}
247-
</span>
253+
{errors.firstname !== undefined && (
254+
<div className={"form-errors"}>{errors.firstname.message}</div>
248255
)}
249256
</div>
250257
<div style={{ width: "20px" }} />
251258
<div className={"form-field mt-20 flex-1"}>
252259
<Controller
253260
name="lastname"
254261
control={control}
255-
rules={{ required: true, minLength: 1 }}
256-
render={({ field: { onChange, value } }) => (
262+
rules={{
263+
required: t("lastNameRequired"),
264+
maxLength: {
265+
value: 50,
266+
message: t("max50Chars"),
267+
},
268+
pattern: {
269+
value: /^[\p{L}\p{N}\sß'-]+$/u,
270+
message: t("lastNameInvalid"),
271+
},
272+
}}
273+
render={({ field: { onChange, value, onBlur } }) => (
257274
<MaterialTextField
258275
onChange={onChange}
276+
onBlur={onBlur}
259277
value={value}
260278
label={t("lastName")}
261279
variant="outlined"
262280
data-test-id="test-lastName"
263281
/>
264282
)}
265283
/>
266-
{errors.lastname && errors.lastname.type === "required" && (
267-
<span className={"form-errors"}>{t("lastNameRequired")}</span>
268-
)}
269-
{errors.lastname && errors.lastname.type === "minLength" && (
270-
<span className={"form-errors"}>
271-
{t("atLeast3LettersRequired")}
272-
</span>
284+
{errors.lastname !== undefined && (
285+
<div className={"form-errors"}>{errors.lastname.message}</div>
273286
)}
274287
</div>
275288
</div>
@@ -279,18 +292,18 @@ function ContactsForm(): ReactElement {
279292
name="email"
280293
control={control}
281294
rules={{
282-
required: {
283-
value: true,
284-
message: t("emailRequired"),
285-
},
295+
required: t("emailRequired"),
286296
validate: {
287297
emailInvalid: (value) =>
288-
value.length === 0 || isEmailValid(value),
298+
value.length === 0 ||
299+
isEmailValid(value) ||
300+
t("enterValidEmail"),
289301
},
290302
}}
291-
render={({ field: { onChange, value } }) => (
303+
render={({ field: { onChange, value, onBlur } }) => (
292304
<MaterialTextField
293305
onChange={onChange}
306+
onBlur={onBlur}
294307
value={value}
295308
label={t("email")}
296309
variant="outlined"
@@ -299,23 +312,22 @@ function ContactsForm(): ReactElement {
299312
/>
300313
)}
301314
/>
302-
{errors.email && (
303-
<span className={"form-errors"}>
304-
{errors.email.type === "required"
305-
? t("emailRequired")
306-
: t("enterValidEmail")}
307-
</span>
315+
{errors.email !== undefined && (
316+
<div className={"form-errors"}>{errors.email.message}</div>
308317
)}
309-
{/* {errors.email && errors.email.type === "validate" && (
310-
<span className={"form-errors"}>{t("useSameEmail")}</span>
311-
)} */}
312318
</div>
313319

314320
<div className={"form-field mt-30"} style={{ position: "relative" }}>
315321
<Controller
316322
name="address"
317323
control={control}
318-
rules={{ required: true }}
324+
rules={{
325+
required: t("addressRequired"),
326+
pattern: {
327+
value: /^[\p{L}\p{N}\sß.,#-/]+$/u,
328+
message: t("addressInvalid"),
329+
},
330+
}}
319331
render={({ field: { onChange, onBlur, value } }) => (
320332
<MaterialTextField
321333
onChange={(event) => {
@@ -352,8 +364,8 @@ function ContactsForm(): ReactElement {
352364
</div>
353365
)
354366
: null}
355-
{errors.address && (
356-
<span className={"form-errors"}>{t("addressRequired")}</span>
367+
{errors.address !== undefined && (
368+
<div className={"form-errors"}>{errors.address.message}</div>
357369
)}
358370
</div>
359371

@@ -363,20 +375,28 @@ function ContactsForm(): ReactElement {
363375
name="city"
364376
control={control}
365377
rules={{
366-
required: true,
378+
required: {
379+
value: true,
380+
message: t("cityRequired"),
381+
},
382+
pattern: {
383+
value: /^[\p{L}\sß.,()-]+$/u,
384+
message: t("cityInvalid"),
385+
},
367386
}}
368-
render={({ field: { onChange, value } }) => (
387+
render={({ field: { onChange, value, onBlur } }) => (
369388
<MaterialTextField
370389
onChange={onChange}
390+
onBlur={onBlur}
371391
value={value}
372392
label={t("city")}
373393
variant="outlined"
374394
data-test-id="test-city"
375395
/>
376396
)}
377397
/>
378-
{errors.city && (
379-
<span className={"form-errors"}>{t("cityRequired")}</span>
398+
{errors.city !== undefined && (
399+
<div className={"form-errors"}>{errors.city.message}</div>
380400
)}
381401
</div>
382402
<div style={{ width: "20px" }} />
@@ -386,12 +406,16 @@ function ContactsForm(): ReactElement {
386406
name="zipCode"
387407
control={control}
388408
rules={{
389-
required: true,
390-
pattern: postalRegex,
409+
required: t("zipCodeRequired"),
410+
pattern: {
411+
value: postalRegex as RegExp,
412+
message: t("zipCodeInvalid"),
413+
},
391414
}}
392-
render={({ field: { onChange, value } }) => (
415+
render={({ field: { onChange, value, onBlur } }) => (
393416
<MaterialTextField
394417
onChange={onChange}
418+
onBlur={onBlur}
395419
value={value}
396420
label={t("zipCode")}
397421
variant="outlined"
@@ -400,24 +424,19 @@ function ContactsForm(): ReactElement {
400424
)}
401425
/>
402426
)}
403-
{errors.zipCode && (
404-
<span className={"form-errors"}>
405-
{t("zipCodeAlphaNumValidation")}
406-
</span>
427+
{errors.zipCode !== undefined && (
428+
<div className={"form-errors"}>{errors.zipCode.message}</div>
407429
)}
408430
</div>
409431
</div>
410432

411433
<div className={"form-field mt-30"}>
412434
<Controller
435+
name="country"
413436
control={control}
414437
rules={{
415-
required: {
416-
value: true,
417-
message: t("countryRequired"),
418-
},
438+
required: t("countryRequired"),
419439
}}
420-
name="country"
421440
defaultValue={
422441
contactDetails.country ? contactDetails.country : country
423442
}
@@ -431,9 +450,8 @@ function ContactsForm(): ReactElement {
431450
/>
432451
)}
433452
/>
434-
435453
{errors.country && (
436-
<span className={"form-errors"}>{t("countryRequired")}</span>
454+
<div className={"form-errors"}>{errors.country.message}</div>
437455
)}
438456
</div>
439457

@@ -446,18 +464,19 @@ function ContactsForm(): ReactElement {
446464
<Controller
447465
name="tin"
448466
control={control}
449-
rules={{ required: true }}
450-
render={({ field: { onChange, value } }) => (
467+
rules={{ required: t("tinRequired") }}
468+
render={({ field: { onChange, value, onBlur } }) => (
451469
<MaterialTextField
452470
onChange={onChange}
471+
onBlur={onBlur}
453472
value={value}
454473
label={t("taxIdentificationNumber")}
455474
variant="outlined"
456475
/>
457476
)}
458477
/>
459-
{errors.tin && errors.tin.type !== "validate" && (
460-
<span className={"form-errors"}>{t("tinRequired")}</span>
478+
{errors.tin !== undefined && (
479+
<div className={"form-errors"}>{errors.tin.message}</div>
461480
)}
462481
</div>
463482
) : (
@@ -518,10 +537,17 @@ function ContactsForm(): ReactElement {
518537
<Controller
519538
name="companyname"
520539
control={control}
521-
rules={{ required: true }}
522-
render={({ field: { onChange, value } }) => (
540+
rules={{
541+
required: t("companyRequired"),
542+
pattern: {
543+
value: /^[\p{L}\p{N}\sß.,'&()!-]+$/u,
544+
message: t("firstNameInvalid"),
545+
},
546+
}}
547+
render={({ field: { onChange, value, onBlur } }) => (
523548
<MaterialTextField
524549
onChange={onChange}
550+
onBlur={onBlur}
525551
value={value}
526552
label={t("companyName")}
527553
variant="outlined"
@@ -533,8 +559,10 @@ function ContactsForm(): ReactElement {
533559
/>
534560
)}
535561
/>
536-
{errors.companyname && (
537-
<span className={"form-errors"}>{t("companyRequired")}</span>
562+
{errors.companyname !== undefined && (
563+
<div className={"form-errors"}>
564+
{errors.companyname.message}
565+
</div>
538566
)}
539567
</div>
540568
) : null}
@@ -571,6 +599,7 @@ function ContactsForm(): ReactElement {
571599
</button>
572600
)}
573601
</form>
602+
{/* <DevTool control={control} /> */}
574603
</div>
575604
</div>
576605
);

styles/globals.scss

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,11 @@ button {
165165
.form-field {
166166
position: relative;
167167
.form-errors {
168-
position: absolute;
169-
bottom: -20px;
170-
left: 0px;
171168
font-family: $primaryFontFamily;
172169
font-size: 12px;
173170
color: $dangerColor;
174171
margin-left: 6px;
172+
margin-bottom: -1.43em;
175173
}
176174
}
177175

0 commit comments

Comments
 (0)