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}
+ >
+ setIsOpen(false)}
+ type="button"
+ >
+
+
+ {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.
+
+
+
+
+ setModalIsOpen(!modalIsOpen)}>
+ Toggle Modal
+
+
+
+ );
+};
+
+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) },
+ ]}
+ />
+
+
+
+
+
+
+
+ );
+};