diff --git a/src/components/app/App.css b/src/components/app/App.css
index 27abc93a..667c5024 100644
--- a/src/components/app/App.css
+++ b/src/components/app/App.css
@@ -5,6 +5,8 @@
--main-primary-dark: #042c7a;
--main-cta-color: #4884f8;
--main-lightblue-color: #ebf8ff;
+ --main-grey: #656a6f;
+ --main-lightgrey: #d0d0d4;
--main-black: #232624;
}
@@ -69,10 +71,17 @@ a:hover {
background-color: var(--main-primary-dark);
}
+.lightgrey {
+ color: var(--main-lightgrey);
+}
.blue {
color: var(--main-primary-color);
}
+.grey {
+ color: var(--main-grey);
+}
+
.bg-lightblue {
background-color: var(--main-lightblue-color);
}
@@ -102,6 +111,14 @@ h1 {
font-size: 16px;
}
+.p6 {
+ font-size: 14px;
+}
+
+.p7 {
+ font-size: 13px;
+}
+
@media (min-width: 768px) {
h1 {
font-size: 48px;
@@ -132,6 +149,13 @@ h1 {
max-width: 90%;
}
+.w-33 {
+ max-width: 33%;
+}
+
+.top-2 {
+ top: 2em;
+}
.secondary-btn {
border: var(--main-primary-color) 1px solid !important;
color: var(--main-primary-color) !important;
diff --git a/src/components/app/App.tsx b/src/components/app/App.tsx
index 232504bf..f46495e1 100644
--- a/src/components/app/App.tsx
+++ b/src/components/app/App.tsx
@@ -10,9 +10,11 @@ import Footer from "./footer/Footer";
import HowItWorks from "./pages/HowItWorks";
import Ambassadors from "./pages/Ambassadors";
import GetInvolved from "./pages/GetInvolved";
-
+import StateMailingGuide from "./pages/StateMailingGuide";
import { HashRouter as Router, Route, Switch } from "react-router-dom";
import { trackPageOpen, load, page } from "../../utils/analytics";
+import { US_STATES } from "../../utils/us_states";
+import FacilityGuide from "./pages/FacilityGuide";
const App: React.FC = () => {
useEffect(() => {
@@ -32,6 +34,21 @@ const App: React.FC = () => {
+ {US_STATES.map((item) => (
+
+ ))}
+ {US_STATES.map((item) => (
+
+ ))}
diff --git a/src/components/app/cards/AuthorCard.css b/src/components/app/cards/AuthorCard.css
new file mode 100644
index 00000000..46702735
--- /dev/null
+++ b/src/components/app/cards/AuthorCard.css
@@ -0,0 +1,4 @@
+.author-img {
+ height: 60px;
+ width: 60px;
+}
diff --git a/src/components/app/cards/AuthorCard.tsx b/src/components/app/cards/AuthorCard.tsx
new file mode 100644
index 00000000..219296d0
--- /dev/null
+++ b/src/components/app/cards/AuthorCard.tsx
@@ -0,0 +1,20 @@
+import React from "react";
+import Image from "react-bootstrap/Image";
+import "./AuthorCard.css";
+
+const AuthorCard = () => {
+ return (
+
+
+
+ Written by Lara Schull
+ Updated July 9, 2020
+
+
+ );
+};
+export default AuthorCard;
diff --git a/src/components/app/cards/CtaCard.css b/src/components/app/cards/CtaCard.css
new file mode 100644
index 00000000..a916d0b6
--- /dev/null
+++ b/src/components/app/cards/CtaCard.css
@@ -0,0 +1,5 @@
+.cta-card-wrapper {
+ box-shadow: rgba(0, 0, 0, 0.1) 0px 5px 15px -5px;
+ padding: 3em;
+ width: 100%;
+}
diff --git a/src/components/app/cards/CtaCard.tsx b/src/components/app/cards/CtaCard.tsx
new file mode 100644
index 00000000..a839d085
--- /dev/null
+++ b/src/components/app/cards/CtaCard.tsx
@@ -0,0 +1,38 @@
+import React, { FunctionComponent } from "react";
+import { Home, Send, Heart } from "react-feather";
+import "./CtaCard.css";
+import Button from "react-bootstrap/Button";
+
+const CtaCard: FunctionComponent = () => {
+ return (
+
+
+ A nonprofit that helps you stay in touch with your loved one for free.
+
+
+
+
+
+ We've helped {process.env.REACT_APP_FAMILY_COUNT} families send
+ over 20,000 letters.
+
+
+
+
+
+ We serve all jails, facilities and detention centers.
+
+
+
+
+
+ Our award winning nonprofit's help is 100% free.
+
+
+
+ Get Started
+
+
+ );
+};
+export default CtaCard;
diff --git a/src/components/app/cards/JobCard.tsx b/src/components/app/cards/JobCard.tsx
index 856c6efb..dd7311b5 100644
--- a/src/components/app/cards/JobCard.tsx
+++ b/src/components/app/cards/JobCard.tsx
@@ -1,12 +1,6 @@
import React, { useState, useEffect } from "react";
import "./JobCard.css";
-interface JobProps {
- title: string;
- description: string;
- type: string;
-}
-
const JobCard = ({ title, description, type }) => {
const applyLink =
type === "volunteer"
diff --git a/src/components/app/cards/SummaryCard.css b/src/components/app/cards/SummaryCard.css
new file mode 100644
index 00000000..a1e7bf68
--- /dev/null
+++ b/src/components/app/cards/SummaryCard.css
@@ -0,0 +1,23 @@
+.summary {
+ padding: 0px 0px 1em;
+ margin: 0px auto 2em;
+ border-width: 2px;
+ border-style: solid;
+ border-color: rgb(240, 241, 242);
+ border-image: initial;
+ background: rgb(255, 255, 255);
+}
+
+.summary b {
+ font-size: 18px;
+ position: relative;
+ top: -0.8em;
+ background: rgb(255, 255, 255);
+ padding: 0.25em 1em;
+ margin: 0px 1em;
+}
+
+.summary p {
+ padding: 0px 2em;
+ margin: 1em 0px 1.5em;
+}
diff --git a/src/components/app/cards/SummaryCard.tsx b/src/components/app/cards/SummaryCard.tsx
new file mode 100644
index 00000000..699161ba
--- /dev/null
+++ b/src/components/app/cards/SummaryCard.tsx
@@ -0,0 +1,20 @@
+import React, { FunctionComponent } from "react";
+import "./SummaryCard.css";
+
+interface SummaryCardProps {
+ locality: string;
+}
+
+const SummaryCard: FunctionComponent = ({ locality }) => {
+ return (
+
+
Summary
+
+ Is your loved one in a facility in {locality}? With our mobile app, you
+ can write letters and send photos for free.
+
+
+ );
+};
+
+export default SummaryCard;
diff --git a/src/components/app/facepile/Facepile.css b/src/components/app/facepile/Facepile.css
new file mode 100644
index 00000000..e74f1cfb
--- /dev/null
+++ b/src/components/app/facepile/Facepile.css
@@ -0,0 +1,10 @@
+.facepile-img {
+ height: 25px;
+ width: 25px;
+ object-fit: cover;
+ margin-right: -0.5rem;
+}
+
+.facepile-caption {
+ font-size: 13px;
+}
diff --git a/src/components/app/facepile/Facepile.tsx b/src/components/app/facepile/Facepile.tsx
new file mode 100644
index 00000000..f0d4364e
--- /dev/null
+++ b/src/components/app/facepile/Facepile.tsx
@@ -0,0 +1,31 @@
+import React from "react";
+import Image from "react-bootstrap/Image";
+import "./Facepile.css";
+import { Link } from "react-router-dom";
+
+const Facepile = () => {
+ const images = [
+ require("../../../assets/ambassadors/Anna-Jones-California.png"),
+ require("../../../assets/ambassadors/Carolyn-Vargas-New-York.png"),
+ require("../../../assets/ambassadors/Connie-Mcbride-Virginia.png"),
+ require("../../../assets/ambassadors/Jessica-Ruiz-Oklahoma.png"),
+ require("../../../assets/ambassadors/Rachel-Wade-New-York.png"),
+ ];
+
+ return (
+
+ {images.map((img) => (
+
+ ))}
+
+ {process.env.REACT_APP_FAMILY_COUNT} families have sent free
+ letters to their loved ones using Ameelio.{" "}
+
+ Learn More
+
+
+
+ );
+};
+
+export default Facepile;
diff --git a/src/components/app/pages/FacilityGuide.tsx b/src/components/app/pages/FacilityGuide.tsx
new file mode 100644
index 00000000..fcb2ee16
--- /dev/null
+++ b/src/components/app/pages/FacilityGuide.tsx
@@ -0,0 +1,80 @@
+import React, { FunctionComponent, useEffect, useState } from "react";
+import { RouteComponentProps } from "react-router-dom";
+// import "./FacilityGuide.css";
+import { getGuideData } from "../../../utils/utils";
+import Spinner from "react-bootstrap/Spinner";
+import Facepile from "../facepile/Facepile";
+import SummaryCard from "../cards/SummaryCard";
+import AuthorCard from "../cards/AuthorCard";
+
+export interface FacilityItem {
+ state: string;
+ facility: string;
+ description: string;
+ address: string;
+ telephone: string;
+ mailing: string;
+ route: string;
+}
+
+const FacilityGuide = ({
+ location,
+}: RouteComponentProps<{}, any, FacilityItem | any>) => {
+ const [facility, setFacility] = useState(
+ location.state ? location.state.data : null
+ );
+ const [route, _] = useState(location.pathname.split("/")[2]);
+
+ useEffect(() => {
+ async function fetchData() {
+ const data = await getGuideData(route, "facility");
+
+ setFacility(data.length > 0 ? data[0] : null);
+ }
+ if (!facility) {
+ fetchData();
+ }
+ }, [facility, route]);
+
+ // For this use case, dangerouslySetInnerHTML seems safe given that we are the only with edit access to the markup being passed down
+ // The flag is primarily to alert users of the vulnearbility to XSS scripting
+ // Source: https://stackoverflow.com/questions/46832912/is-dangerouslysetinnerhtml-the-only-way-to-render-html-from-an-api-in-react
+ return (
+
+ {facility ? (
+
+ {" "}
+
{facility.state}
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
+
+ )}
+
+ );
+};
+
+export default FacilityGuide;
diff --git a/src/components/app/pages/StateMailingGuide.tsx b/src/components/app/pages/StateMailingGuide.tsx
new file mode 100644
index 00000000..060ecc9b
--- /dev/null
+++ b/src/components/app/pages/StateMailingGuide.tsx
@@ -0,0 +1,87 @@
+import React, { Component } from "react";
+import { FacilityItem } from "./FacilityGuide";
+import { RouteComponentProps } from "react-router-dom";
+import { getGuideData, getStateName } from "../../../utils/utils";
+import { Link, withRouter } from "react-router-dom";
+import _ from "lodash";
+import Facepile from "../facepile/Facepile";
+import SummaryCard from "../cards/SummaryCard";
+import AuthorCard from "../cards/AuthorCard";
+import CtaCard from "../cards/CtaCard";
+interface FacilityItems extends Array {}
+
+type StateMailingGuideState = {
+ facilities: FacilityItems;
+ region: string;
+};
+
+type TParams = {
+ region: string;
+};
+
+class StateMailingGuide extends Component<
+ RouteComponentProps,
+ StateMailingGuideState
+> {
+ constructor(props) {
+ super(props);
+ this.state = {
+ facilities: [],
+ region: this.props.location.pathname.replace("/", ""),
+ };
+ }
+
+ async componentDidMount() {
+ const data = await getGuideData(
+ this.props.match.url.replace("/", ""),
+ "state"
+ );
+ this.setState({ facilities: data });
+ }
+
+ render() {
+ const { url } = this.props.match;
+
+ const links = this.state.facilities.map((item) => (
+
+ {item.facility}
+
+ ));
+
+ const catalogue = _.chunk(links, 10).map(function (group) {
+ return {group}
;
+ });
+
+ const state = getStateName(this.state.region);
+ return (
+
+
+
+
{state}
+
+ {state}, Mailing & Visiting
+
+
+
+
+
+
+
+
+
{catalogue}
+
+
+
+ );
+ }
+}
+
+export default withRouter(StateMailingGuide);
diff --git a/src/components/app/pages/landing.tsx b/src/components/app/pages/landing.tsx
index 998fcdd0..0bbba2ca 100644
--- a/src/components/app/pages/landing.tsx
+++ b/src/components/app/pages/landing.tsx
@@ -6,6 +6,8 @@ import LettersForFamilies from "../sections/LettersFamilies";
import LettersForOrgs from "../sections/LettersOrgs";
import GetStarted from "../sections/GetStarted";
import Careers from "../sections/Careers";
+import GuidesCatalogue from "../sections/GuidesCatalogue";
+import { US_STATES } from "../../../utils/us_states";
const Landing: React.FC = () => {
return (
@@ -17,6 +19,7 @@ const Landing: React.FC = () => {
+
);
};
diff --git a/src/components/app/sections/GuidesCatalogue.css b/src/components/app/sections/GuidesCatalogue.css
new file mode 100644
index 00000000..e69de29b
diff --git a/src/components/app/sections/GuidesCatalogue.tsx b/src/components/app/sections/GuidesCatalogue.tsx
new file mode 100644
index 00000000..3845363e
--- /dev/null
+++ b/src/components/app/sections/GuidesCatalogue.tsx
@@ -0,0 +1,39 @@
+import React, { Component, FunctionComponent } from "react";
+import { Link } from "react-router-dom";
+import _ from "lodash";
+import "./GuidesCatalogue.css";
+
+interface GuideItem {
+ abbreviation: string;
+ name: string;
+}
+
+interface GuidesCatalogueProps {
+ items: Array;
+}
+
+const GuidesCatalogue: FunctionComponent = ({
+ items,
+}) => {
+ const links = items.map((item) => (
+
+ {item.name}
+
+ ));
+
+ const catalogue = _.chunk(links, 11).map(function (group) {
+ return {group}
;
+ });
+
+ return (
+
+
+ Mailing
+ Guides by State
+
+
{catalogue}
+
+ );
+};
+
+export default GuidesCatalogue;
diff --git a/src/utils/us_states.tsx b/src/utils/us_states.tsx
new file mode 100644
index 00000000..8a42b2a0
--- /dev/null
+++ b/src/utils/us_states.tsx
@@ -0,0 +1,215 @@
+export interface State {
+ name: string;
+ abbreviation: string;
+}
+
+export const US_STATES: State[] = [
+ {
+ name: "Alabama",
+ abbreviation: "AL",
+ },
+ {
+ name: "Alaska",
+ abbreviation: "AK",
+ },
+ {
+ name: "Arizona",
+ abbreviation: "AZ",
+ },
+ {
+ name: "Arkansas",
+ abbreviation: "AR",
+ },
+ {
+ name: "California",
+ abbreviation: "CA",
+ },
+ {
+ name: "Colorado",
+ abbreviation: "CO",
+ },
+ {
+ name: "Connecticut",
+ abbreviation: "CT",
+ },
+ {
+ name: "Delaware",
+ abbreviation: "DE",
+ },
+ {
+ name: "District Of Columbia",
+ abbreviation: "DC",
+ },
+ {
+ name: "Florida",
+ abbreviation: "FL",
+ },
+ {
+ name: "Georgia",
+ abbreviation: "GA",
+ },
+ {
+ name: "Hawaii",
+ abbreviation: "HI",
+ },
+ {
+ name: "Idaho",
+ abbreviation: "ID",
+ },
+ {
+ name: "Illinois",
+ abbreviation: "IL",
+ },
+ {
+ name: "Indiana",
+ abbreviation: "IN",
+ },
+ {
+ name: "Iowa",
+ abbreviation: "IA",
+ },
+ {
+ name: "Kansas",
+ abbreviation: "KS",
+ },
+ {
+ name: "Kentucky",
+ abbreviation: "KY",
+ },
+ {
+ name: "Louisiana",
+ abbreviation: "LA",
+ },
+ {
+ name: "Maine",
+ abbreviation: "ME",
+ },
+ {
+ name: "Maryland",
+ abbreviation: "MD",
+ },
+ {
+ name: "Massachusetts",
+ abbreviation: "MA",
+ },
+ {
+ name: "Michigan",
+ abbreviation: "MI",
+ },
+ {
+ name: "Minnesota",
+ abbreviation: "MN",
+ },
+ {
+ name: "Mississippi",
+ abbreviation: "MS",
+ },
+ {
+ name: "Missouri",
+ abbreviation: "MO",
+ },
+ {
+ name: "Montana",
+ abbreviation: "MT",
+ },
+ {
+ name: "Nebraska",
+ abbreviation: "NE",
+ },
+ {
+ name: "Nevada",
+ abbreviation: "NV",
+ },
+ {
+ name: "New Hampshire",
+ abbreviation: "NH",
+ },
+ {
+ name: "New Jersey",
+ abbreviation: "NJ",
+ },
+ {
+ name: "New Mexico",
+ abbreviation: "NM",
+ },
+ {
+ name: "New York",
+ abbreviation: "NY",
+ },
+ {
+ name: "North Carolina",
+ abbreviation: "NC",
+ },
+ {
+ name: "North Dakota",
+ abbreviation: "ND",
+ },
+ {
+ name: "Ohio",
+ abbreviation: "OH",
+ },
+ {
+ name: "Oklahoma",
+ abbreviation: "OK",
+ },
+ {
+ name: "Oregon",
+ abbreviation: "OR",
+ },
+ {
+ name: "Pennsylvania",
+ abbreviation: "PA",
+ },
+ {
+ name: "Rhode Island",
+ abbreviation: "RI",
+ },
+ {
+ name: "South Carolina",
+ abbreviation: "SC",
+ },
+ {
+ name: "South Dakota",
+ abbreviation: "SD",
+ },
+ {
+ name: "Tennessee",
+ abbreviation: "TN",
+ },
+ {
+ name: "Texas",
+ abbreviation: "TX",
+ },
+ {
+ name: "Utah",
+ abbreviation: "UT",
+ },
+ {
+ name: "Vermont",
+ abbreviation: "VT",
+ },
+ {
+ name: "Virgin Islands",
+ abbreviation: "VI",
+ },
+ {
+ name: "Virginia",
+ abbreviation: "VA",
+ },
+ {
+ name: "Washington",
+ abbreviation: "WA",
+ },
+ {
+ name: "West Virginia",
+ abbreviation: "WV",
+ },
+ {
+ name: "Wisconsin",
+ abbreviation: "WI",
+ },
+ {
+ name: "Wyoming",
+ abbreviation: "WY",
+ },
+];
diff --git a/src/utils/utils.js b/src/utils/utils.js
deleted file mode 100644
index 823f6357..00000000
--- a/src/utils/utils.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export function numberWithCommas(x) {
- return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
-}
diff --git a/src/utils/utils.tsx b/src/utils/utils.tsx
new file mode 100644
index 00000000..6bbc6e1a
--- /dev/null
+++ b/src/utils/utils.tsx
@@ -0,0 +1,54 @@
+import { US_STATES } from "./us_states";
+
+export function numberWithCommas(x: number): string {
+ return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+}
+
+interface FacilityItem {
+ state: string;
+ facility: string;
+ description: string;
+ address: string;
+ telephone: string;
+ mailing: string;
+ route: string;
+}
+
+type GuideFilter = "state" | "facility";
+
+export async function getGuideData(keyword: string, type: GuideFilter) {
+ // const url = `https://sheets.googleapis.com/v4/spreadsheets/${process.env.REACT_APP_GSHEETS_ID}/values:batchGet?ranges=${abbrev}&majorDimension=ROWS&key=${process.env.REACT_APP_GSHEETS_KEY}`;
+ const url = `https://sheets.googleapis.com/v4/spreadsheets/${process.env.REACT_APP_GSHEETS_ID}/values:batchGet?ranges=Main&majorDimension=ROWS&key=${process.env.REACT_APP_GSHEETS_KEY}`;
+
+ const response = await fetch(url);
+ const data = await response.json();
+ let batchRowValues = data.valueRanges[0].values;
+
+ let rows: FacilityItem[] = [];
+
+ for (let i = 1; i < batchRowValues.length; i++) {
+ let rowObject: FacilityItem = {} as FacilityItem;
+
+ for (let j = 0; j < batchRowValues[i].length; j++) {
+ rowObject[batchRowValues[0][j]] = batchRowValues[i][j];
+ }
+ rows.push(rowObject);
+ }
+ switch (type) {
+ case "state":
+ rows = rows.filter((row) => row.state === getStateName(keyword));
+ break;
+ case "facility":
+ rows = rows.filter((row) => row.route === keyword);
+ break;
+ }
+
+ return rows;
+}
+
+export function getStateName(abbrev: string): string {
+ const matches = US_STATES.filter((state) => {
+ return state.abbreviation === abbrev;
+ });
+ return matches[Object.keys(matches)[0]]["name"];
+}