@comicrelief/component-library 6.9.1 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/cypress/integration/components/Organisms/EmailSignUp.spec.js +47 -132
  2. package/dist/components/Molecules/Box/Box.js +6 -6
  3. package/dist/components/Molecules/Card/Card.js +5 -5
  4. package/dist/components/Organisms/EmailSignUp/EmailSignUp.md +8 -123
  5. package/dist/components/Organisms/EmailSignUp/EmailSignUp.style.js +46 -29
  6. package/dist/components/Organisms/EmailSignUp/EmailSignUp.test.js +24 -69
  7. package/dist/components/Organisms/EmailSignUp/EmailSignUpForm.js +92 -0
  8. package/dist/components/Organisms/EmailSignUp/_Confetti.js +116 -0
  9. package/dist/components/Organisms/EmailSignUp/_EmailSignUp.js +107 -0
  10. package/dist/components/Organisms/EmailSignUp/_EmailSignUpConfig.js +51 -0
  11. package/dist/components/Organisms/EmailSignUp/_TextInput.js +51 -0
  12. package/dist/components/Organisms/EmailSignUp/__snapshots__/EmailSignUp.test.js.snap +249 -406
  13. package/dist/components/Organisms/Header/Header.md +1 -13
  14. package/dist/components/Organisms/Membership/Membership.test.js +1 -1
  15. package/dist/index.js +14 -10
  16. package/package.json +2 -1
  17. package/src/components/Molecules/Box/Box.js +6 -6
  18. package/src/components/Molecules/Card/Card.js +5 -5
  19. package/src/components/Organisms/EmailSignUp/EmailSignUp.md +8 -123
  20. package/src/components/Organisms/EmailSignUp/EmailSignUp.style.js +33 -13
  21. package/src/components/Organisms/EmailSignUp/EmailSignUp.test.js +35 -69
  22. package/src/components/Organisms/EmailSignUp/EmailSignUpForm.js +60 -0
  23. package/src/components/Organisms/EmailSignUp/_Confetti.js +106 -0
  24. package/src/components/Organisms/EmailSignUp/_EmailSignUp.js +138 -0
  25. package/src/components/Organisms/EmailSignUp/_EmailSignUpConfig.js +54 -0
  26. package/src/components/Organisms/EmailSignUp/_TextInput.js +45 -0
  27. package/src/components/Organisms/EmailSignUp/__snapshots__/EmailSignUp.test.js.snap +249 -406
  28. package/src/components/Organisms/Header/Header.md +1 -13
  29. package/src/components/Organisms/Membership/Membership.test.js +33 -33
  30. package/src/index.js +10 -4
  31. package/cypress/integration/components/Molecules/HeaderEsuWithIcon.spec.js +0 -69
  32. package/dist/components/Molecules/HeaderEsuWithIcon/HeaderEsuWithIcon.js +0 -136
  33. package/dist/components/Molecules/HeaderEsuWithIcon/HeaderEsuWithIcon.md +0 -47
  34. package/dist/components/Molecules/HeaderEsuWithIcon/HeaderEsuWithIcon.style.js +0 -52
  35. package/dist/components/Molecules/HeaderEsuWithIcon/HeaderEsuWithIcon.test.js +0 -99
  36. package/dist/components/Molecules/HeaderEsuWithIcon/__snapshots__/HeaderEsuWithIcon.test.js.snap +0 -1211
  37. package/dist/components/Molecules/HeaderEsuWithIcon/assets/HeaderIcons.js +0 -25
  38. package/dist/components/Molecules/HeaderEsuWithIcon/assets/icon--close.svg +0 -5
  39. package/dist/components/Molecules/HeaderEsuWithIcon/assets/icon--email.svg +0 -5
  40. package/dist/components/Organisms/EmailSignUp/EmailSignUp.js +0 -182
  41. package/src/components/Molecules/HeaderEsuWithIcon/HeaderEsuWithIcon.js +0 -135
  42. package/src/components/Molecules/HeaderEsuWithIcon/HeaderEsuWithIcon.md +0 -47
  43. package/src/components/Molecules/HeaderEsuWithIcon/HeaderEsuWithIcon.style.js +0 -60
  44. package/src/components/Molecules/HeaderEsuWithIcon/HeaderEsuWithIcon.test.js +0 -103
  45. package/src/components/Molecules/HeaderEsuWithIcon/__snapshots__/HeaderEsuWithIcon.test.js.snap +0 -1211
  46. package/src/components/Molecules/HeaderEsuWithIcon/assets/HeaderIcons.js +0 -15
  47. package/src/components/Molecules/HeaderEsuWithIcon/assets/icon--close.svg +0 -5
  48. package/src/components/Molecules/HeaderEsuWithIcon/assets/icon--email.svg +0 -5
  49. package/src/components/Organisms/EmailSignUp/EmailSignUp.js +0 -197
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import { useForm, FormProvider } from 'react-hook-form';
3
+ import { yupResolver } from '@hookform/resolvers/yup';
4
+ import RichText from '../../Atoms/RichText/RichText';
5
+ import {
6
+ EmailSignUp,
7
+ buildEsuValidationSchema,
8
+ ESU_FIELDS
9
+ } from './_EmailSignUp';
10
+
11
+ const EmailSignUpForm = () => {
12
+ const validationSchema = buildEsuValidationSchema({});
13
+ const formMethods = useForm({
14
+ mode: 'onBlur',
15
+ resolver: yupResolver(validationSchema)
16
+ });
17
+ const { handleSubmit, trigger } = formMethods;
18
+
19
+ async function handleSubscribe(data) {
20
+ const valid = await trigger([
21
+ ESU_FIELDS.EMAIL,
22
+ ESU_FIELDS.FIRST_NAME,
23
+ ESU_FIELDS.LAST_NAME
24
+ ]);
25
+ if (valid) {
26
+ console.log(data);
27
+ }
28
+ }
29
+ const title = 'Stay in the know!';
30
+ const topCopy = (
31
+ <RichText
32
+ markup={"<p>Get regular email updates and info on what we're up to!</p>"}
33
+ />
34
+ );
35
+ const privacyCopy = (
36
+ <RichText
37
+ markup={
38
+ '<p>Our <a class="link link--white inline" href="/privacy-notice">Privacy Policy</a> describes how we handle and protect your information.<br><br>If you are under 18, please make sure you have your parents’ permission before providing us with any personal details.</p>'
39
+ }
40
+ />
41
+ );
42
+ const successCopy = (
43
+ <RichText markup="<p>Thanks! Your first email will be with you shortly</p>" />
44
+ );
45
+ return (
46
+ <FormProvider {...formMethods}>
47
+ <form onSubmit={handleSubmit(handleSubscribe)} noValidate>
48
+ <EmailSignUp
49
+ id="default"
50
+ title={title}
51
+ topCopy={topCopy}
52
+ successCopy={successCopy}
53
+ privacyCopy={privacyCopy}
54
+ formContext={formMethods}
55
+ />
56
+ </form>
57
+ </FormProvider>
58
+ );
59
+ };
60
+ export default EmailSignUpForm;
@@ -0,0 +1,106 @@
1
+ import React, {
2
+ useCallback, useEffect, useRef, useState
3
+ } from 'react';
4
+ import ReactCanvasConfetti from 'react-canvas-confetti';
5
+ import PropTypes from 'prop-types';
6
+
7
+ function randomInRange(min, max) {
8
+ return Math.random() * (max - min) + min;
9
+ }
10
+
11
+ const canvasStyles = {
12
+ position: 'fixed',
13
+ pointerEvents: 'none',
14
+ width: '100%',
15
+ height: '100%',
16
+ top: 0,
17
+ left: 0
18
+ };
19
+
20
+ function getAnimationSettings(originXA, originXB) {
21
+ return {
22
+ startVelocity: 30,
23
+ spread: 360,
24
+ ticks: 60,
25
+ zIndex: 0,
26
+ particleCount: 150,
27
+ origin: {
28
+ x: randomInRange(originXA, originXB),
29
+ y: Math.random() - 0.2
30
+ }
31
+ };
32
+ }
33
+ // TODO: Refactor this into an atom
34
+ export default function Confetti({ trigger, duration }) {
35
+ const refAnimationInstance = useRef(null);
36
+ const [intervalId, setIntervalId] = useState();
37
+
38
+ const getInstance = useCallback(instance => {
39
+ refAnimationInstance.current = instance;
40
+ }, []);
41
+
42
+ const nextTickAnimation = useCallback(() => {
43
+ if (refAnimationInstance.current) {
44
+ refAnimationInstance.current(getAnimationSettings(0.1, 0.3));
45
+ refAnimationInstance.current(getAnimationSettings(0.7, 0.9));
46
+ }
47
+ }, []);
48
+
49
+ const startAnimation = useCallback(() => {
50
+ if (!intervalId) {
51
+ setIntervalId(setInterval(nextTickAnimation, 400));
52
+ }
53
+ }, [intervalId, nextTickAnimation]);
54
+
55
+ const pauseAnimation = useCallback(() => {
56
+ clearInterval(intervalId);
57
+ setIntervalId(null);
58
+ }, [intervalId]);
59
+
60
+ const stopAnimation = useCallback(() => {
61
+ clearInterval(intervalId);
62
+ setIntervalId(null);
63
+ if (refAnimationInstance.current) {
64
+ refAnimationInstance.current.reset();
65
+ }
66
+ }, [intervalId]);
67
+
68
+ // eslint-disable-next-line
69
+ useEffect(() => {
70
+ return () => {
71
+ clearInterval(intervalId);
72
+ };
73
+ }, [intervalId]);
74
+
75
+ useEffect(() => {
76
+ let timeOut;
77
+ if (trigger) {
78
+ startAnimation();
79
+ timeOut = setTimeout(() => {
80
+ // This gracefully ends the animation
81
+ pauseAnimation();
82
+ }, duration);
83
+ }
84
+ return () => {
85
+ if (timeOut) {
86
+ // this clears up the animation
87
+ stopAnimation();
88
+ }
89
+ }; // eslint-disable-next-line react-hooks/exhaustive-deps
90
+ }, [trigger, duration]);
91
+
92
+ return (
93
+ <>
94
+ <ReactCanvasConfetti refConfetti={getInstance} style={canvasStyles} />
95
+ </>
96
+ );
97
+ }
98
+
99
+ Confetti.defaultProps = {
100
+ duration: 3000
101
+ };
102
+
103
+ Confetti.propTypes = {
104
+ trigger: PropTypes.bool.isRequired,
105
+ duration: PropTypes.number
106
+ };
@@ -0,0 +1,138 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ ESUWrapper,
5
+ TopCopyWrapper,
6
+ FormInner,
7
+ PrivacyCopyWrapper,
8
+ InputField,
9
+ ButtonWrapper,
10
+ Title,
11
+ NameWrapper
12
+ } from './EmailSignUp.style';
13
+ import ButtonWithStates from '../../Atoms/ButtonWithStates/ButtonWithStates';
14
+
15
+ import Text from '../../Atoms/Text/Text';
16
+ import { buildEsuValidationSchema, ESU_FIELDS } from './_EmailSignUpConfig';
17
+ import ErrorText from '../../Atoms/ErrorText/ErrorText';
18
+ import Confetti from './_Confetti';
19
+
20
+ const EmailSignUp = ({
21
+ title,
22
+ topCopy,
23
+ successCopy,
24
+ privacyCopy,
25
+ backgroundColour,
26
+ buttonColour,
27
+ formContext,
28
+ columnLayout,
29
+ ...rest
30
+ }) => {
31
+ const {
32
+ formState: {
33
+ isValid,
34
+ isSubmitting,
35
+ isSubmitted,
36
+ isSubmitSuccessful,
37
+ errors
38
+ }
39
+ } = formContext;
40
+
41
+ return (
42
+ <ESUWrapper backgroundColour={backgroundColour} {...rest}>
43
+ <Title tag="h2" size="xxl" weight="400" family="Anton" uppercase>
44
+ {title}
45
+ </Title>
46
+ {!isSubmitted ? (
47
+ <TopCopyWrapper>
48
+ <Text>{topCopy}</Text>
49
+ </TopCopyWrapper>
50
+ ) : (
51
+ isSubmitSuccessful && (
52
+ <>
53
+ <Confetti trigger={isSubmitSuccessful} />
54
+ <TopCopyWrapper>
55
+ <Text>{successCopy}</Text>
56
+ </TopCopyWrapper>
57
+ </>
58
+ )
59
+ )}
60
+ {!isSubmitSuccessful && (
61
+ <FormInner>
62
+ <NameWrapper columnLayout={columnLayout}>
63
+ <InputField
64
+ fieldName={ESU_FIELDS.FIRST_NAME}
65
+ id="first-name"
66
+ type="text"
67
+ label="First Name"
68
+ placeholder="Enter your first name"
69
+ formContext={formContext}
70
+ />
71
+ <InputField
72
+ fieldName={ESU_FIELDS.LAST_NAME}
73
+ id="last-name"
74
+ type="text"
75
+ label="Last Name"
76
+ placeholder="Enter your last name"
77
+ formContext={formContext}
78
+ />
79
+ </NameWrapper>
80
+ <InputField
81
+ fieldName={ESU_FIELDS.EMAIL}
82
+ id="email"
83
+ type="email"
84
+ label="Email Address"
85
+ placeholder="example@youremail.com"
86
+ formContext={formContext}
87
+ />
88
+ <ButtonWrapper buttonColour={buttonColour}>
89
+ <ButtonWithStates
90
+ type="submit"
91
+ disabled={!isValid || isSubmitting}
92
+ loading={isSubmitting}
93
+ loadingText="Submitting..."
94
+ data-test="subscribe-button"
95
+ >
96
+ <Text>Subscribe</Text>
97
+ </ButtonWithStates>
98
+ </ButtonWrapper>
99
+ </FormInner>
100
+ )}
101
+ {isSubmitted && !isSubmitSuccessful && (
102
+ <>
103
+ {/*
104
+ Field errors will prevent submission,
105
+ so theoretically this should just be a single error set in the submission callback
106
+ with with RHF's `setError` method, but will neatly display multiple errors.
107
+ */}
108
+ {Object.values(errors).map(error => (
109
+ <ErrorText>{error.message}</ErrorText>
110
+ ))}
111
+ </>
112
+ )}
113
+
114
+ <PrivacyCopyWrapper>
115
+ <Text>{privacyCopy}</Text>
116
+ </PrivacyCopyWrapper>
117
+ </ESUWrapper>
118
+ );
119
+ };
120
+
121
+ EmailSignUp.propTypes = {
122
+ title: PropTypes.string.isRequired,
123
+ topCopy: PropTypes.node.isRequired,
124
+ successCopy: PropTypes.node.isRequired,
125
+ privacyCopy: PropTypes.node.isRequired,
126
+ backgroundColour: PropTypes.string,
127
+ buttonColour: PropTypes.string,
128
+ formContext: PropTypes.shape().isRequired,
129
+ columnLayout: PropTypes.bool
130
+ };
131
+
132
+ EmailSignUp.defaultProps = {
133
+ backgroundColour: 'deep_violet_dark',
134
+ buttonColour: 'red',
135
+ columnLayout: false
136
+ };
137
+
138
+ export { EmailSignUp, buildEsuValidationSchema, ESU_FIELDS };
@@ -0,0 +1,54 @@
1
+ import { merge } from 'lodash';
2
+ import * as yup from 'yup';
3
+
4
+ /**
5
+ * ESU_FIELDS
6
+ *
7
+ * Exposes an enum to consumer of the component, to accurately access the underlying field names.
8
+ * can be used in conjunction with RHF or buildEsuValidationSchema
9
+ * to customise form validation or behaviour, as the fields are handled within the CL
10
+ * we just make this read-only to prevent any external changes of this object.
11
+ */
12
+ const ESU_FIELDS = Object.freeze({
13
+ FIRST_NAME: 'firstName',
14
+ LAST_NAME: 'lastName',
15
+ EMAIL: 'email'
16
+ });
17
+
18
+ /**
19
+ * buildEsuValidationSchema
20
+ *
21
+ * Exposes a function that can be passed a partial or complete yup schema
22
+ * to extend or override the default buildEsuValidationSchema
23
+ *
24
+ * @param overrides {Object} - A yup schema object (or an empty object)
25
+ */
26
+ const buildEsuValidationSchema = overrides => {
27
+ const defaultSchema = yup.object({
28
+ [ESU_FIELDS.FIRST_NAME]: yup
29
+ .string()
30
+ .required('Please enter your first name')
31
+ .matches(
32
+ /^[A-Za-z][A-Za-z' -]*$/,
33
+ "This field only accepts letters and ' - and must start with a letter"
34
+ )
35
+ .max(25, 'Your first name must be between 1 and 25 characters'),
36
+ [ESU_FIELDS.LAST_NAME]: yup
37
+ .string()
38
+ .required('Please enter your last name')
39
+ .matches(
40
+ /^[A-Za-z][A-Za-z' -]*$/,
41
+ "This field only accepts letters and ' - and must start with a letter"
42
+ )
43
+ .max(50, 'Your first name must be between 1 and 50 characters'),
44
+ [ESU_FIELDS.EMAIL]: yup
45
+ .string()
46
+ .required('Please enter your email address')
47
+ .email('Please enter a valid email address')
48
+ .max(100, 'Your email address must be between 1 and 100 characters long')
49
+ });
50
+
51
+ return merge(defaultSchema, overrides);
52
+ };
53
+
54
+ export { ESU_FIELDS, buildEsuValidationSchema };
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import Input from '../../Atoms/Input/Input';
4
+
5
+ // TODO: This is a copy paste of the RHF friendly `TextInput` from Marketing Prefs.
6
+ // Perhaps it would be worthwhile refactoring this into a new `Atom` as a seperate PR.
7
+ const TextInput = ({
8
+ fieldName,
9
+ label,
10
+ optional,
11
+ fieldType,
12
+ formContext,
13
+ ...rest
14
+ }) => {
15
+ const { errors, register } = formContext;
16
+
17
+ const props = {
18
+ name: fieldName,
19
+ type: fieldType,
20
+ label,
21
+ placeholder: label,
22
+ errorMsg: errors && errors[fieldName] && errors[fieldName].message,
23
+ optional,
24
+ 'aria-required': !optional,
25
+ ...rest
26
+ };
27
+
28
+ return <Input {...props} ref={register} />;
29
+ };
30
+
31
+ TextInput.defaultProps = {
32
+ optional: null,
33
+ fieldType: 'text',
34
+ formContext: null
35
+ };
36
+
37
+ TextInput.propTypes = {
38
+ fieldName: PropTypes.string.isRequired,
39
+ label: PropTypes.string.isRequired,
40
+ optional: PropTypes.bool,
41
+ fieldType: PropTypes.string,
42
+ formContext: PropTypes.shape()
43
+ };
44
+
45
+ export default TextInput;