From 6775f7dc9144d82c5ed846dc0754aa2335cd9193 Mon Sep 17 00:00:00 2001 From: ceshion Date: Sat, 11 Jul 2020 13:49:36 -0400 Subject: [PATCH] added phone script list, page, folder, file import, and README --- _config.yml | 2 +- _emails/README.md | 2 +- _phoneScripts/README.md | 59 +++++++ _phoneScripts/us/california/san_francisco.md | 22 +++ _sass/2-layout/_phone.scss | 15 ++ gatsby-config.js | 8 + gatsby-node.ts | 11 +- src/components/phone/Phone.tsx | 159 +++++++++++++++++++ src/components/template-list/PhoneList.tsx | 9 ++ src/pages/phoneScripts.tsx | 32 ++++ src/types/PropTypes.ts | 20 +++ src/types/SiteConfig.ts | 2 + src/types/TemplateData.ts | 24 ++- 13 files changed, 358 insertions(+), 7 deletions(-) create mode 100644 _phoneScripts/README.md create mode 100644 _phoneScripts/us/california/san_francisco.md create mode 100644 _sass/2-layout/_phone.scss create mode 100644 src/components/phone/Phone.tsx create mode 100644 src/components/template-list/PhoneList.tsx create mode 100644 src/pages/phoneScripts.tsx diff --git a/_config.yml b/_config.yml index 3eb1de02c6..0c657b36bc 100644 --- a/_config.yml +++ b/_config.yml @@ -27,7 +27,7 @@ defaults: body: Email message (remember to include placeholders for name) siteTitle: Defund12.org permalink: pretty -subtitle: Email and mail government officials and council members to reallocate egregious +subtitle: Email, call, and mail government officials and council members to reallocate egregious police budgets towards education, social services, and dismantling racial injustice. meta: Email government officials and council members to reallocate police budgets towards education, social services, and dismantling racial injustice. diff --git a/_emails/README.md b/_emails/README.md index 5eff725f66..e2ad643459 100644 --- a/_emails/README.md +++ b/_emails/README.md @@ -8,7 +8,7 @@ When adding templates, please ensure you are using UTF-8 encoding. Please do not | --------------- | -------- | ------ | ----------------------------------------------------------------------------------- | | title | yes | String | City, State (for CMS tracking purposes) | | layout | yes | String | Layout for markdown file (must be `email`) | -| permalink | yes | String | Path to url on the site (e.g., /nyc | +| permalink | yes | String | Path to url on the site (e.g., /nyc) | | name | yes | String | Name of the email that will be displayed to users | | city | yes | String | City the email template is for | | state | yes | String | State (abbreviated) the email template is for (e.g., NY) | diff --git a/_phoneScripts/README.md b/_phoneScripts/README.md new file mode 100644 index 0000000000..f7bfa988d7 --- /dev/null +++ b/_phoneScripts/README.md @@ -0,0 +1,59 @@ +# Notice for template implementers + +When adding templates, please ensure you are using UTF-8 encoding. Please do not use UTF-8-BOM or "ANSI", as this may cause problems when rendering the template. + +## Fields + +### Call Script +| Key | Required | Type | Description | +| --------------- | -------- | ------ | ----------------------------------------------------------------------------------------- | +| title | yes | String | City, State (for CMS tracking purposes) | +| layout | yes | String | Layout for markdown file (must be `phone`) | +| permalink | yes | String | Path to url on the site (e.g., /nyc) | +| name | yes | String | Name of the call script that will be displayed to users | +| city | yes | String | City the call script is for | +| state | yes | String | State (abbreviated) the call script is for (e.g., NY) | +| body | yes | String | The call script text. Prefix with `|-` and indent underneath (e.g., see below) | +| contacts | yes | Array | Contact information for officials to call with this script using the Contact fields below | +| expiration_date | no | String | YYYY-MM-DD format date that the call script should expire from the site | +| organization | no | String | Name of the organization that curated the call script (if it exists) | +| redirect_from | no | Array | Links that should redirect to the permalink above | + +### Contact +| Key | Required | Type | Description | +| ------ | -------- | ------ | --------------------------------------------------------------------- | +| name | yes | String | The official's name including title, e.g. Board Supervisor First Last | +| number | yes | String | The official's phone number, formatted like e.g. (555) 555-5555 | + +## File naming conventions + +Please use underscores for spaces when creating new markdown files and directories. For example: + +``` +_phone/us/california/san_francisco.md +_phone/us/new_york/hudson_valley.md +``` + +## Example template + +``` +--- +title: +layout: phone +permalink: / +name: +city: +state: +contacts: +- name: + number: +- name: + number: +body: |- + +expiration_date: +organization: +redirect_from: +- / +--- +``` diff --git a/_phoneScripts/us/california/san_francisco.md b/_phoneScripts/us/california/san_francisco.md new file mode 100644 index 0000000000..5e36d7119f --- /dev/null +++ b/_phoneScripts/us/california/san_francisco.md @@ -0,0 +1,22 @@ +--- +title: San Francisco, California +permalink: "/phone/sanfrancisco" +name: Call to the Mayor, Board of Supervisors, and Elected Officers +city: San Francisco +state: CA +layout: phone +contacts: +- name: "Rep" + number: "(555) 555-5555" +- name: "Noth" + number: "(555) 555-5555" +body: |- + My name is [YOUR NAME], and I am a resident of San Francisco. This past week, our nation has been gripped by protests calling for rapid and meaningful change with regard to police behavior, an end to racism and anti-Blackness, and immediate reform in how Black people are treated in America. Our city has been at the forefront of much of this action. Accordingly, it has come to my attention that the budget for 2021 is being decided as these protests continue. + + SFPD has been a waste of our resources. Last year, the SFPD budget was $611,701,869, the majority of which comes from the San Francisco general fund. While we've been spending extraordinary amounts on policing, we have not seen improvements to safety, homelessness, mental health, or affordability in our city. Instead, we see wasteful and harmful actions of our police. + + I call on you to slash the SFPD budget and instead use those extraordinary resources towards solving homelessness, which is felt most by our Black neighbors and veterans. We implore you to give every member of our community experiencing homelessness a place to call home and the treatment they need. + + We can be a beacon for other cities to follow if only we have the courage to change. +--- + diff --git a/_sass/2-layout/_phone.scss b/_sass/2-layout/_phone.scss new file mode 100644 index 0000000000..f9d2e392ea --- /dev/null +++ b/_sass/2-layout/_phone.scss @@ -0,0 +1,15 @@ + +.phoneScriptContentSection { + @include highlightedSection; + padding: 8px 0px; + + .phoneScriptContent { + div { + margin-top: $x_3; + } + } + + >.container { + @include siteWidth; + } +} diff --git a/gatsby-config.js b/gatsby-config.js index 3d50529c7e..40dbeae1d2 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -53,6 +53,14 @@ module.exports = { ignore: ["README.md"], }, }, + { + resolve: "gatsby-source-filesystem", + options: { + name: "phoneScripts", + path: `${__dirname}/_phoneScripts/`, + ignore: ["README.md"], + }, + }, { resolve: "gatsby-source-filesystem", options: { diff --git a/gatsby-node.ts b/gatsby-node.ts index 5d56903347..0c33e9bf6a 100644 --- a/gatsby-node.ts +++ b/gatsby-node.ts @@ -9,7 +9,7 @@ export const createPages: GatsbyNode["createPages"] = async ({ const { createPage, createRedirect } = actions; /** Shared logic between creating email and letter pages */ - async function createEmailOrLetterTemplatePage({ + async function createTemplatePages({ layout, component, }: { @@ -57,13 +57,18 @@ export const createPages: GatsbyNode["createPages"] = async ({ ); } - await createEmailOrLetterTemplatePage({ + await createTemplatePages({ layout: "email", component: require.resolve("./src/components/email/Email.tsx"), }); - await createEmailOrLetterTemplatePage({ + await createTemplatePages({ layout: "letter", component: require.resolve("./src/components/letter/Letter.tsx"), }); + + await createTemplatePages({ + layout: "phone", + component: require.resolve("./src/components/phone/Phone.tsx"), + }); }; diff --git a/src/components/phone/Phone.tsx b/src/components/phone/Phone.tsx new file mode 100644 index 0000000000..9b3372c02b --- /dev/null +++ b/src/components/phone/Phone.tsx @@ -0,0 +1,159 @@ +import { graphql, PageProps } from "gatsby"; +import React from "react"; +import { + PhoneScriptProps, + PhoneScriptConfig, + OptionalLayoutProps, +} from "../../types/PropTypes"; +import { PhoneScriptData, PhoneScriptContact } from "../../types/TemplateData"; +import * as queryString from "query-string"; +import Layout from "../common/Layout"; +import { DefundUtils } from "../../DefundUtils"; +import PhoneList from "../template-list/PhoneList"; + +/** + * The @link {PhoneScript} component state. + */ +class PhoneScriptState { + bodyCopied = false; +} + +/** + * A rendered phone script, containing links to copy the script text or link. + */ +export default class PhoneScript extends React.Component< + PageProps, + PhoneScriptState +> { + siteConfig: PhoneScriptConfig; + phoneScriptData: PhoneScriptData; + layoutProps: OptionalLayoutProps; + /** + * Initialize the component and its state + * @param {PageProps} props + */ + constructor(props: PageProps) { + super(props); + this.siteConfig = this.props.data.siteConfig; + this.phoneScriptData = this.props.data.markdownRemark.frontmatter; + this.layoutProps = { + pageTitle: `Defund12 in ${this.phoneScriptData.city}, ${this.phoneScriptData.state}`, + meta: `View a phone script to call ${this.phoneScriptData.city}, ${this.phoneScriptData.state} officials`, + metaQueryString: queryString.stringify({ + state: this.phoneScriptData.state, + city: this.phoneScriptData.city, + }), + layout: "phone", + }; + this.state = new PhoneScriptState(); + } + + /** + * Updates the component state to reflect which element was clicked and + * copies the selected element to the user's clipboard. + * @param {Pick} statePatch an object containing + * the state update to apply + * @param {string} copy the text to copy to the user's clipboard. + */ + handleClipboardCopy( + statePatch: Pick, + copy: string + ): void { + this.setState(statePatch); + DefundUtils.copyToClipboard(copy); + } + + /** + * Renders the phone script's contact list as list items. + * @return {React.ReactNode} + */ + renderContacts(): React.ReactNode { + return this.phoneScriptData.contacts.map((contact: PhoneScriptContact) => ( +
  • + {contact.name}, {contact.number} +
  • + )); + } + + /** + * React render method. + * @return {React.ReactNode} the rendered component + */ + render(): React.ReactNode { + return ( + +
    +

    {this.phoneScriptData.name}

    + + {this.phoneScriptData.city}, {this.phoneScriptData.state} + + +
      {this.renderContacts()}
    +
    + +
    +
    +
    +
    + Message: + + this.handleClipboardCopy( + { bodyCopied: true }, + this.phoneScriptData.body + ) + } + > + {this.state.bodyCopied ? "✅(copied)" : "🔗"} + + +
    +
    +
    +
    + + +
    + ); + } +} + +export const pageQuery = graphql` + query($permalink: String!) { + markdownRemark( + frontmatter: { permalink: { eq: $permalink }, layout: { eq: "phone" } } + ) { + frontmatter { + body + city + country + date + name + state + permalink + contacts { + name + number + } + } + } + } +`; diff --git a/src/components/template-list/PhoneList.tsx b/src/components/template-list/PhoneList.tsx new file mode 100644 index 0000000000..7a4d4e102b --- /dev/null +++ b/src/components/template-list/PhoneList.tsx @@ -0,0 +1,9 @@ +import TemplateList from "./TemplateList"; +import React from "react"; + +/** List of all call scripts + * @return {React.ReactElement} the rendered list + */ +export default function PhoneList(): React.ReactElement { + return ; +} diff --git a/src/pages/phoneScripts.tsx b/src/pages/phoneScripts.tsx new file mode 100644 index 0000000000..d71e4f1347 --- /dev/null +++ b/src/pages/phoneScripts.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { PageProps } from "gatsby"; +import Layout from "../components/common/Layout"; +import { SiteProps } from "../types/PropTypes"; +import PhoneList from "../components/template-list/PhoneList"; + +/** + * Contains another list just like the homepage, this time with call scripts. + */ +export default class PhoneScripts extends React.Component< + PageProps +> { + /** + * Initialize the component. + * @param {PageProps} props + */ + constructor(props: PageProps) { + super(props); + } + + /** + * React render method. + * @return {React.ReactNode} the rendered component + */ + render(): React.ReactNode { + return ( + + + + ); + } +} diff --git a/src/types/PropTypes.ts b/src/types/PropTypes.ts index 60cde46751..d2aeb7d72e 100644 --- a/src/types/PropTypes.ts +++ b/src/types/PropTypes.ts @@ -4,6 +4,7 @@ import { EmailData, SharedTemplateMetadata, LetterData, + PhoneScriptData, } from "./TemplateData"; // {Page query result properties} @@ -48,6 +49,25 @@ export interface LetterProps { siteConfig: LetterConfig; } +export type PhoneScriptConfig = Pick< + SiteConfig, + | "siteTitle" + | "meta" + | "logoUrl" + | "faviconUrl" + | "phonePageHeader" + | "googleApiKey" +>; + +export type PhonePageConfig = Pick; + +export interface PhoneScriptProps { + markdownRemark: { + frontmatter: PhoneScriptData; + }; + siteConfig: PhoneScriptConfig; +} + export interface OptionalLayoutProps { pageTitle?: string; meta?: string; diff --git a/src/types/SiteConfig.ts b/src/types/SiteConfig.ts index c3d8d4f9fb..d98bad9a80 100644 --- a/src/types/SiteConfig.ts +++ b/src/types/SiteConfig.ts @@ -25,6 +25,8 @@ export interface SiteConfig { contactEmailFooter: string; /** A message that appears before the physical letter list index page */ letterPageHeader: string; + /** A message that appears in the header of the call scripts list page */ + phonePageHeader: string; /** Google API key with Maps and Civic Info perms */ googleApiKey: string; } diff --git a/src/types/TemplateData.ts b/src/types/TemplateData.ts index de75f62db3..00fdf0f725 100644 --- a/src/types/TemplateData.ts +++ b/src/types/TemplateData.ts @@ -1,4 +1,4 @@ -export type LayoutType = "email" | "letter"; +export type LayoutType = "email" | "letter" | "phone"; /** * The top-level data of a template (email or letter). */ @@ -56,7 +56,7 @@ export interface EmailData extends SharedTemplateData { */ cc: Array; - /* an email! */ + /** an email! */ layout: "email"; } @@ -71,6 +71,26 @@ export interface LetterData extends SharedTemplateData { layout: "letter"; } +/** + * Contact information for a call script + */ +export interface PhoneScriptContact { + /** The contact's name */ + name: string; + /** The contact's phone number */ + number: string; +} + +/** + * The full type of a phone script from markdown + */ +export interface PhoneScriptData extends SharedTemplateData { + /** An array of officials' names and phone numbers to call using this script */ + contacts: Array; + /** a phone script! */ + layout: "phone"; +} + // type EmailDataType = TemplateMetadata & EmailData; /**