diff --git a/jest/loadershim.js b/jest/loadershim.js index d08bba40..bbefc068 100644 --- a/jest/loadershim.js +++ b/jest/loadershim.js @@ -2,3 +2,9 @@ global.___loader = { enqueue: jest.fn(), }; + +// Used to allow react-modal to work in tests. +// https://github.com/facebook/react/issues/11565#issuecomment-427547413 +jest.mock("react-dom", () => ({ + createPortal: (node) => node, +})); diff --git a/package-lock.json b/package-lock.json index a49625e5..8d161e84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16332,6 +16332,11 @@ } } }, + "exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=" + }, "exif-parser": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", @@ -29645,6 +29650,17 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-modal": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.11.2.tgz", + "integrity": "sha512-o8gvvCOFaG1T7W6JUvsYjRjMVToLZgLIsi5kdhFIQCtHxDkA47LznX62j+l6YQkpXDbvQegsDyxe/+JJsFQN7w==", + "requires": { + "exenv": "^1.2.0", + "prop-types": "^15.5.10", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + } + }, "react-popper": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz", diff --git a/package.json b/package.json index 23b3278c..d7d71281 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "pure-react-carousel": "^1.27.6", "react": "^16.13.1", "react-burger-menu": "^2.9.0", - "react-helmet": "^6.1.0" + "react-helmet": "^6.1.0", + "react-modal": "^3.11.2" }, "devDependencies": { "@babel/core": "^7.11.6", diff --git a/src/components/grid-aware/Modal/Modal.js b/src/components/grid-aware/Modal/Modal.js new file mode 100644 index 00000000..08be31fb --- /dev/null +++ b/src/components/grid-aware/Modal/Modal.js @@ -0,0 +1,40 @@ +import PropTypes from "prop-types"; +import React from "react"; +import ReactModal from "react-modal"; + +import s from "./Modal.module.css"; +import closeIcon from "./close-icon.svg"; + +const Modal = ({ children, isOpen, setIsOpen, ariaHideApp, contentLabel }) => ( + setIsOpen(false)} + ariaHideApp={ariaHideApp} + contentLabel={contentLabel} + > + + {children} + +); + +Modal.propTypes = { + children: PropTypes.node.isRequired, + isOpen: PropTypes.bool.isRequired, + setIsOpen: PropTypes.func.isRequired, + ariaHideApp: PropTypes.bool, + contentLabel: PropTypes.string.isRequired, +}; + +Modal.defaultProps = { + ariaHideApp: true, +}; + +export default Modal; diff --git a/src/components/grid-aware/Modal/Modal.module.css b/src/components/grid-aware/Modal/Modal.module.css new file mode 100644 index 00000000..600551a5 --- /dev/null +++ b/src/components/grid-aware/Modal/Modal.module.css @@ -0,0 +1,45 @@ +.overlay { + background-color: rgba(0, 0, 0, 0.5); + bottom: 0; + left: 0; + overflow: auto; + position: fixed; + right: 0; + top: 0; +} + +.content { + background: var(--color-white); + border-radius: 16px; + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + margin: 80px auto 40px; + max-width: 850px; + outline: none; + padding: 50px; + position: relative; +} + +.closeButton { + background: none; + border: 0; + cursor: pointer; + display: inline-block; + padding: 0; + position: absolute; + right: 30px; + top: 30px; +} + +@media (--tablet-and-down) { + .content { + margin-left: 20px; + margin-right: 20px; + padding: 35px 25px; + } + + .closeButton { + right: 25px; + top: 25px; + } +} diff --git a/src/components/grid-aware/Modal/Modal.stories.js b/src/components/grid-aware/Modal/Modal.stories.js new file mode 100644 index 00000000..5c3f23bd --- /dev/null +++ b/src/components/grid-aware/Modal/Modal.stories.js @@ -0,0 +1,47 @@ +import React, { useState } from "react"; + +import Modal from "./Modal"; + +export default { + title: "Grid-Aware/Modal", + component: Modal, +}; + +const Template = () => { + const [modalIsOpen, setModalIsOpen] = useState(true); + return ( +
+ +
+

Work with Us

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim + ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum. +

+
+
+
+ +
+
+ ); +}; + +export const DefaultModal = Template.bind({}); +DefaultModal.args = {}; diff --git a/src/components/grid-aware/Modal/close-icon.svg b/src/components/grid-aware/Modal/close-icon.svg new file mode 100644 index 00000000..7f0b0b0f --- /dev/null +++ b/src/components/grid-aware/Modal/close-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/grid-aware/Modal/index.js b/src/components/grid-aware/Modal/index.js new file mode 100644 index 00000000..09b91f72 --- /dev/null +++ b/src/components/grid-aware/Modal/index.js @@ -0,0 +1 @@ +export { default } from "./Modal"; diff --git a/src/components/layout.js b/src/components/layout.js index edef962f..760ec638 100644 --- a/src/components/layout.js +++ b/src/components/layout.js @@ -1,5 +1,6 @@ import PropTypes from "prop-types"; -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; +import ReactModal from "react-modal"; import "../stylesheets/global.css"; import { BurgerMenu, Navigation } from "./grid-aware/Navigation"; @@ -14,6 +15,9 @@ const Layout = ({ children }) => { const pageWrapperID = "page-wrapper"; const outerContainerID = "outer-container"; const [burgerMenuIsOpen, setBurgerMenuIsOpen] = useState(false); + useEffect(() => { + ReactModal.setAppElement(`#${outerContainerID}`); + }, []); return (
+ + +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ + +`, +}; + +const VolunteerSignupForm = () => ( +
+

Work With Us

+

+ Thank you for your interest in partnering with ShelterTech! Enter your + contact information below and we'll get back to you shortly. +

+
+
+); +export default VolunteerSignupForm; diff --git a/src/components/thirdparty/mailchimp/VolunteerSignupForm.module.css b/src/components/thirdparty/mailchimp/VolunteerSignupForm.module.css new file mode 100644 index 00000000..08b05dc3 --- /dev/null +++ b/src/components/thirdparty/mailchimp/VolunteerSignupForm.module.css @@ -0,0 +1,11 @@ +.title { + font: var(--font-headline); + margin-bottom: 20px; + margin-top: 0; +} + +.description { + font: var(--font-body-medium); + margin-bottom: 40px; + margin-top: 0; +} diff --git a/src/components/thirdparty/mailchimp/VolunteerSignupForm.stories.js b/src/components/thirdparty/mailchimp/VolunteerSignupForm.stories.js new file mode 100644 index 00000000..355f2fa1 --- /dev/null +++ b/src/components/thirdparty/mailchimp/VolunteerSignupForm.stories.js @@ -0,0 +1,27 @@ +import React from "react"; + +import VolunteerSignupForm from "./VolunteerSignupForm"; + +export default { + title: "Third Party/Mailchimp/VolunteerSignupForm", + component: VolunteerSignupForm, +}; + +const Template = () => ( +
+

+ Note: This is a real form, and submitting it will really subscribe you to + a Mailchimp list. +

+ +
+); + +export const Default = Template.bind({}); +Default.args = {}; diff --git a/src/pages/new/index.js b/src/pages/new/index.js index 53e08538..eff7f9a6 100644 --- a/src/pages/new/index.js +++ b/src/pages/new/index.js @@ -1,9 +1,10 @@ -import React from "react"; +import React, { useState } from "react"; import ArticleSpotlightCard from "../../components/grid-aware/ArticleSpotlightCard"; import articleSpotlightImage from "../../components/grid-aware/ArticleSpotlightCard/stories/background.png"; import BlockQuoteBlock from "../../components/grid-aware/BlockQuoteBlock/BlockQuoteBlock"; import HomePageLargeParagraph from "../../components/grid-aware/HomePageLargeParagraph"; +import Modal from "../../components/grid-aware/Modal"; import PartnersAndSponsorsBlock from "../../components/grid-aware/PartnersAndSponsorsBlock"; import benetechLogo from "../../components/grid-aware/PartnersAndSponsorsBlock/stories/benetech-logo.png"; import ciscoLogo from "../../components/grid-aware/PartnersAndSponsorsBlock/stories/cisco-logo.png"; @@ -27,168 +28,179 @@ import videoHeaderImage from "../../components/grid-aware/VideoHeader/stories/Vi import VideoSpotlightBlock from "../../components/grid-aware/VideoSpotlightBlock"; import videoSpotlightBlockImage from "../../components/grid-aware/VideoSpotlightBlock/stories/VideoSpotlightBlock.png"; import Layout from "../../components/layout"; +import VolunteerSignupForm from "../../components/thirdparty/mailchimp/VolunteerSignupForm"; -export default () => ( - - - - {} }, - ]} - /> - { + const [volunteerFormIsOpen, setVolunteerFormIsOpen] = useState(false); + return ( + + + + + + + - - - - - - -); + "Our programs are largely funded by donations from people who care about bridging the digital divide. Support ShelterTech today.", + }} + image1={{ + url: image1, + alt: "Two volunteers working on a laptop together at a datathon.", + }} + image2={{ + url: image2, + alt: "Team posing for a photo after a design workshop.", + }} + image3={{ + url: image3, + alt: "Multiple volunteers working at a datathon.", + }} + ctaTitle="Volunteer, donate, or reach out to our partnerships team" + ctaButtons={[ + { text: "Volunteer", internalLink: "/new/volunteer" }, + { text: "Donate", internalLink: "/new/donate" }, + { text: "Work with us", onClick: () => setVolunteerFormIsOpen(true) }, + ]} + /> + + + + + + + + ); +};