diff --git a/package.json b/package.json
index ec9d96dba..8c41fff95 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"lazysizes": "^5.3.2",
"lodash": "^4.17.11",
"moment": "^2.29.4",
+ "postcode": "^5.1.0",
"prop-types": "^15.8.1",
"pure-react-carousel": "1.30.1",
"react": "^17.0.2",
diff --git a/src/components/Atoms/TextInputWithDropdown/TextInputWithDropdown.style.js b/src/components/Atoms/TextInputWithDropdown/TextInputWithDropdown.style.js
index 92cd0614f..fa84833bc 100644
--- a/src/components/Atoms/TextInputWithDropdown/TextInputWithDropdown.style.js
+++ b/src/components/Atoms/TextInputWithDropdown/TextInputWithDropdown.style.js
@@ -19,7 +19,6 @@ const Dropdown = styled.div`
border: 1px solid;
margin-top: -1px;
width: 100%;
-
@media ${({ theme }) => theme.allBreakpoints('M')} {
max-width: 500px;
}
diff --git a/src/components/Molecules/Lookup/Lookup.js b/src/components/Molecules/Lookup/Lookup.js
index d39d957a2..bc2278e4c 100644
--- a/src/components/Molecules/Lookup/Lookup.js
+++ b/src/components/Molecules/Lookup/Lookup.js
@@ -5,41 +5,34 @@ import TextInputWithDropdown from '../../Atoms/TextInputWithDropdown/TextInputWi
import spacing from '../../../theme/shared/spacing';
import ButtonWithStates from '../../Atoms/ButtonWithStates/ButtonWithStates';
-const StyledButton = styled(ButtonWithStates)`${({ theme }) => css`
- color: ${theme.color('grey_dark')};
- border: 2px solid ${theme.color('grey_dark')};
- background-color: ${theme.color('white')};
- padding-left: ${spacing('lg')};
- padding-right: ${spacing('lg')};
-
+const Button = styled(ButtonWithStates)`${({
+ theme,
+ buttonTextColour,
+ buttonBgColour,
+ buttonTextHoverColour,
+ buttonHoverBgColour
+}) => css`
+ margin-top: ${spacing('m')};
+ color: ${buttonTextColour || theme.color('white')};
+ background-color: ${buttonBgColour || theme.color('red')};
&:hover {
- color: ${theme.color('grey_dark')};
- background-color: ${theme.color('white')};
+ color: ${buttonTextHoverColour || theme.color('white')};
+ background-color: ${buttonHoverBgColour || theme.color('red_dark')};
}
+ padding: 0 ${spacing('lg')};
`}`;
const KEY_CODE_ENTER = 13;
/**
- * A simple lookup component
- *
- * The `lookupHandler` should be an async function which is called when a lookup is triggered
- * (either by hitting enter or clicking the button)
- *
- * It will receive the current search term and should:
- * - take care of any validation on the search term
- * - perform the actual lookup request
- * - return an array of options (or an empty array if none were found)
- * - only throw errors with user-friendly messages
- *
- * Any errors thrown will be caught and the message will be displayed to the user.
- *
- * The `onSelect` function will receive the chosen option.
- *
* @param name
* @param label
* @param placeholder
* @param buttonText
+ * @param buttonTextColour
+ * @param buttonBgColour
+ * @param buttonHoverBgColour
+ * @param buttonHoverTextColour
* @param lookupHandler
* @param mapOptionToString
* @param onSelect
@@ -54,6 +47,10 @@ const Lookup = ({
label,
placeholder,
buttonText,
+ buttonTextColour,
+ buttonBgColour,
+ buttonHoverBgColour,
+ buttonHoverTextColour,
lookupHandler,
mapOptionToString,
onSelect,
@@ -115,15 +112,19 @@ const Lookup = ({
errorMsg={errorMessage}
dropdownInstruction={dropdownInstruction}
/>
- handler()}
loading={isSearching}
disabled={isSearching}
loadingText="Searching"
+ buttonTextColour={buttonTextColour}
+ buttonBgColour={buttonBgColour}
+ buttonHoverBgColour={buttonHoverBgColour}
+ buttonHoverTextColour={buttonHoverTextColour}
>
{buttonText}
-
+
);
};
@@ -133,6 +134,10 @@ Lookup.propTypes = {
label: PropTypes.string.isRequired,
placeholder: PropTypes.string.isRequired,
buttonText: PropTypes.string.isRequired,
+ buttonTextColour: PropTypes.string,
+ buttonBgColour: PropTypes.string,
+ buttonHoverBgColour: PropTypes.string,
+ buttonHoverTextColour: PropTypes.string,
lookupHandler: PropTypes.func.isRequired,
mapOptionToString: PropTypes.func.isRequired,
onSelect: PropTypes.func.isRequired,
@@ -142,7 +147,11 @@ Lookup.propTypes = {
Lookup.defaultProps = {
noResultsMessage: 'Sorry, could not find any results for your search',
- dropdownInstruction: ''
+ dropdownInstruction: '',
+ buttonTextColour: 'white',
+ buttonBgColour: '#E52630',
+ buttonHoverTextColour: 'white',
+ buttonHoverBgColour: '#890B11'
};
export default Lookup;
diff --git a/src/components/Molecules/Lookup/Lookup.md b/src/components/Molecules/Lookup/Lookup.md
new file mode 100644
index 000000000..c66b34469
--- /dev/null
+++ b/src/components/Molecules/Lookup/Lookup.md
@@ -0,0 +1,64 @@
+# Lookup
+This is a simple basic vanilla component and is rendered here mainly for dev purposes.
+You may would be best served using one of these more substantial components in projects / production:
+- PostcodeLookup
+- SchoolLookup
+- SimpleSchoolLookup
+
+The `lookupHandler` should be an async function which is called when a lookup is triggered (either by hitting enter or clicking the button).
+
+It will receive the current search term and should:
+- take care of any validation on the search term
+- perform the actual lookup request
+- return an array of options (or an empty array if none were found)
+- only throw errors with user-friendly messages
+
+Any errors thrown will be caught and the message will be displayed to the user.
+
+The `onSelect` function will receive the chosen option.
+
+```js
+import Lookup from './Lookup';
+
+const schoolFetcher = async query => {
+ if (!query || !query.trim()) {
+ throw new Error('Please enter a search query');
+ }
+
+ if (query.length < 2) {
+ throw new Error('Please enter at least two characters');
+ }
+
+ try {
+ const res = await axios.get(
+ 'https://lookups.sls.comicrelief.com/schools/lookup',
+ { timeout: 10000, params: { query } }
+ );
+ return res.data.data.schools;
+ } catch (error) {
+ // if (typeof Sentry !== 'undefined') {
+ // Sentry.captureException(error);
+ // }
+ throw new Error('Sorry, something unexpected went wrong. Please try again or enter your school manually');
+ }
+};
+
+const schoolToString = school => `${school.name}, ${school.post_code}`;
+
+ alert(JSON.stringify(address, null, 2))}
+ // buttonTextColour='black'
+ // buttonBgColour='#f04257'
+ // buttonHoverTextColour='white'
+ // buttonHoverBgColour='orange'
+/>
+```
+
diff --git a/src/components/Molecules/PostcodeLookup/PostcodeLookup.js b/src/components/Molecules/PostcodeLookup/PostcodeLookup.js
new file mode 100644
index 000000000..17b8955b1
--- /dev/null
+++ b/src/components/Molecules/PostcodeLookup/PostcodeLookup.js
@@ -0,0 +1,100 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import Lookup from '../Lookup/Lookup';
+import AddressInputs from './utils/AddressInputs';
+import { addressToString, addressFetcher } from './utils/PostcodeFunctions';
+import spacing from '../../../theme/shared/spacing';
+
+// ${({ theme }) => theme.linkStyles('standard')};
+const AddressManually = styled.button`
+ margin: ${spacing('md')} 0 0 ${spacing('sm')};
+ background: inherit;
+ border: none;
+ font-size: 1rem;
+ &:hover, &:active {
+ text-decoration: underline;
+ }
+`;
+
+export default function PostcodeLookup({
+ label,
+ name,
+ placeholder,
+ buttonText,
+ noResultsMessage,
+ reportError,
+ dropdownInstruction,
+ buttonColour
+}) {
+ const [showFields, setShowFields] = useState(false);
+
+ // Address field state
+ const [addressFields, setAddressFields] = useState({
+ line1: '',
+ line2: '',
+ line3: '',
+ posttown: ''
+ });
+
+ // Function to update address fields
+ const handleAddressSelect = selectedAddress => {
+ setShowFields(true);
+ setAddressFields({
+ line1: selectedAddress.Line1 || '',
+ line2: selectedAddress.Line2 || '',
+ line3: selectedAddress.Line3 || '',
+ posttown: selectedAddress.posttown || ''
+ });
+ };
+
+ const handleManualClick = event => {
+ event.preventDefault();
+ setShowFields(true);
+ };
+
+ return (
+ <>
+ addressFetcher(postcode, reportError)}
+ onSelect={handleAddressSelect}
+ dropdownInstruction={dropdownInstruction}
+ noResultsMessage={noResultsMessage}
+ buttonColour={buttonColour}
+ />
+
+ Or enter your address manually
+
+ {showFields && }
+ >
+ );
+}
+
+PostcodeLookup.propTypes = {
+ name: PropTypes.string,
+ label: PropTypes.string,
+ placeholder: PropTypes.string,
+ buttonText: PropTypes.string,
+ noResultsMessage: PropTypes.string,
+ dropdownInstruction: PropTypes.string,
+ reportError: PropTypes.oneOfType([
+ PropTypes.func
+ ]),
+ buttonColour: PropTypes.string
+};
+
+PostcodeLookup.defaultProps = {
+ name: 'postcode_lookup',
+ label: 'Find address by postcode',
+ placeholder: 'Enter postcode...',
+ buttonText: 'Find address',
+ noResultsMessage: 'Sorry, could not find any addresses for that postcode',
+ dropdownInstruction: 'Please select an organisation from the list below',
+ reportError: undefined,
+ buttonColour: '#f04257'
+};
diff --git a/src/components/Molecules/PostcodeLookup/PostcodeLookup.md b/src/components/Molecules/PostcodeLookup/PostcodeLookup.md
new file mode 100644
index 000000000..32b276faa
--- /dev/null
+++ b/src/components/Molecules/PostcodeLookup/PostcodeLookup.md
@@ -0,0 +1,24 @@
+```js
+import PostcodeLookup from './PostcodeLookup';
+
+// This is just an example of how a parent component might handle fetch errors within the component.
+const [enterManually, setEnterManually] = React.useState(false);
+
+const fetchErrorHandler = () => {
+ setEnterManually(true);
+}
+
+enterManually
+ ? 'Sorry, there appears to be a problem. Please enter your details manually.'
+ : (
+
+ )
+```
diff --git a/src/components/Molecules/PostcodeLookup/PostcodeLookup.test.js b/src/components/Molecules/PostcodeLookup/PostcodeLookup.test.js
new file mode 100644
index 000000000..b66caed72
--- /dev/null
+++ b/src/components/Molecules/PostcodeLookup/PostcodeLookup.test.js
@@ -0,0 +1,12 @@
+import React from 'react';
+import 'jest-styled-components';
+
+import renderWithTheme from '../../../hoc/shallowWithTheme';
+import PostcodeLookup from './PostcodeLookup';
+
+it('renders correctly', () => {
+ const tree = renderWithTheme(
+ alert(JSON.stringify(address, null, 2))} />
+ ).toJSON();
+ expect(tree).toMatchSnapshot()
+});
diff --git a/src/components/Molecules/PostcodeLookup/__snapshots__/PostcodeLookup.test.js.snap b/src/components/Molecules/PostcodeLookup/__snapshots__/PostcodeLookup.test.js.snap
new file mode 100644
index 000000000..5c3a815f1
--- /dev/null
+++ b/src/components/Molecules/PostcodeLookup/__snapshots__/PostcodeLookup.test.js.snap
@@ -0,0 +1,231 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+Array [
+ .c3 {
+ font-size: 1rem;
+ line-height: 1rem;
+ text-transform: inherit;
+ font-weight: bold;
+ line-height: normal;
+ font-family: 'Montserrat',Helvetica,Arial,sans-serif;
+}
+
+.c7 {
+ display: -webkit-inline-box;
+ display: -webkit-inline-flex;
+ display: -ms-inline-flexbox;
+ display: inline-flex;
+ position: relative;
+ padding: 0.5rem 1.25rem;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ font-weight: 700;
+ font-size: 1rem;
+ border-radius: 2rem;
+ -webkit-transition: all 0.3s;
+ transition: all 0.3s;
+ height: 3.125rem;
+ width: 100%;
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ border: none;
+ cursor: pointer;
+ background-color: #E52630;
+ color: #FFFFFF;
+}
+
+.c7 > a {
+ -webkit-text-decoration: none;
+ text-decoration: none;
+}
+
+.c7:hover {
+ background-color: #890B11;
+ color: #FFFFFF;
+}
+
+.c2 {
+ position: relative;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: column;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ color: #5C5C5E;
+ width: 100%;
+}
+
+.c4 {
+ margin-bottom: 0.5rem;
+}
+
+.c6 {
+ position: relative;
+ box-sizing: border-box;
+ width: 100%;
+ height: 48px;
+ padding: 1rem 1.5rem;
+ background-color: #F4F3F5;
+ border: 1px solid;
+ border-color: #E1E2E3;
+ box-shadow: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ color: #000000;
+ border-radius: 0.5rem;
+ font-size: inherit;
+ z-index: 2;
+ font-family: 'Montserrat',Helvetica,Arial,sans-serif;
+}
+
+.c6:focus {
+ border: 1px solid #666;
+}
+
+.c5 {
+ position: relative;
+ font-size: 1.25rem;
+}
+
+.c0 {
+ position: relative;
+}
+
+.c8:disabled {
+ cursor: not-allowed;
+ opacity: 0.75;
+}
+
+.c9 {
+ margin-top: 1.5rem;
+ color: white;
+ background-color: #E52630;
+ padding: 0 3rem;
+}
+
+.c9:hover {
+ color: #FFFFFF;
+ background-color: #890B11;
+}
+
+.c1 {
+ margin-bottom: 1rem;
+}
+
+@media (min-width:740px) {
+ .c7 {
+ width: auto;
+ }
+}
+
+@media (min-width:1024px) {
+ .c7 {
+ width: auto;
+ padding: 1rem 2rem;
+ margin: 0 auto 2rem;
+ }
+}
+
+@media (min-width:740px) {
+ .c6 {
+ max-width: 290px;
+ }
+}
+
+,
+ .c0 {
+ margin: 1rem 0 0 0.5rem;
+ background: inherit;
+ border: none;
+ font-size: 1rem;
+}
+
+.c0:hover,
+.c0:active {
+ -webkit-text-decoration: underline;
+ text-decoration: underline;
+}
+
+@media (min-width:740px) {
+
+}
+
+@media (min-width:1024px) {
+
+}
+
+@media (min-width:740px) {
+
+}
+
+,
+]
+`;
diff --git a/src/components/Molecules/PostcodeLookup/utils/AddressInputs.js b/src/components/Molecules/PostcodeLookup/utils/AddressInputs.js
new file mode 100644
index 000000000..7b3c35bf2
--- /dev/null
+++ b/src/components/Molecules/PostcodeLookup/utils/AddressInputs.js
@@ -0,0 +1,29 @@
+import styled from 'styled-components';
+import React from 'react';
+import PropTypes from 'prop-types';
+import Input from '../../../Atoms/Input/Input';
+import spacing from '../../../../theme/shared/spacing';
+
+const Wrapper = styled.div`
+ margin-top: ${spacing('md')};
+`;
+
+export default function AddressInputs({ addressFields }) {
+ return (
+
+
+
+
+
+
+ );
+}
+
+AddressInputs.propTypes = {
+ addressFields: PropTypes.shape({
+ line1: PropTypes.string,
+ line2: PropTypes.string,
+ line3: PropTypes.string,
+ posttown: PropTypes.string
+ }).isRequired
+};
diff --git a/src/components/Molecules/PostcodeLookup/utils/PostcodeFunctions.js b/src/components/Molecules/PostcodeLookup/utils/PostcodeFunctions.js
new file mode 100644
index 000000000..a4f901199
--- /dev/null
+++ b/src/components/Molecules/PostcodeLookup/utils/PostcodeFunctions.js
@@ -0,0 +1,53 @@
+import { isValid, toNormalised } from 'postcode';
+
+const validatePostcode = postcode => {
+ const trimmed = typeof postcode === 'string' ? postcode.trim() : '';
+ return isValid(trimmed) && toNormalised(trimmed);
+};
+
+// report Error - Sentry function passed as a prop
+const getAddresses = async (postcode, reportError) => {
+ const url = `https://lookups-staging.sls.comicrelief.com/postcode/lookup?query=${postcode}`;
+ const options = {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ timeout: 10000
+ };
+
+ try {
+ const response = await fetch(url, options);
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+ const data = await response.json();
+ return data.addresses || [];
+ } catch (error) {
+ // Report the error to Sentry if available, or handle it locally
+ if (reportError) {
+ reportError(error);
+ }
+ throw new Error('Sorry, something unexpected went wrong. Please try again or enter your address manually');
+ }
+};
+
+const addressToString = address => [address.Line1, address.Line2, address.Line3, address.posttown]
+ .filter(line => line)
+ .join(', ');
+
+const addressFetcher = async (postcode, reportError) => {
+ const valid = validatePostcode(postcode);
+ if (!valid) {
+ throw new Error('Please provide a valid postcode');
+ }
+ try {
+ return await getAddresses(valid, reportError);
+ } catch (error) {
+ /* eslint-disable-next-line */
+ console.error('Error fetching addresses:', error);
+ return [];
+ }
+};
+
+export { addressToString, addressFetcher };
diff --git a/src/index.js b/src/index.js
index b9eb40aaa..dd805bcdf 100644
--- a/src/index.js
+++ b/src/index.js
@@ -60,6 +60,7 @@ export { default as Chip } from './components/Molecules/Chip/Chip';
export { default as Descriptor } from './components/Molecules/Descriptor/Descriptor';
export { default as Lookup } from './components/Molecules/Lookup/Lookup';
export { default as SimpleSchoolLookup } from './components/Molecules/SimpleSchoolLookup/SimpleSchoolLookup';
+export { default as PostcodeLookup } from './components/Molecules/PostcodeLookup/PostcodeLookup';
/* Organisms */
export {
diff --git a/yarn.lock b/yarn.lock
index 93ad96698..a70678b6d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11097,6 +11097,11 @@ posix-character-classes@^0.1.0:
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
integrity sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==
+postcode@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/postcode/-/postcode-5.1.0.tgz#ef30a2a4028fd20255fb0aceccf4c70baab8df5b"
+ integrity sha512-rjwIdlQ8UvdOnUVZRCZQA54PmQJeoMBDQb4RsW5z3MVp/u7Gcet1vsjVy/p0+YX+R7cmgU9DEJf0zSx5mWqxAA==
+
postcss-attribute-case-insensitive@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz#d93e46b504589e94ac7277b0463226c68041a880"