diff --git a/.gitignore b/.gitignore index 7499b22..248ff09 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,4 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -.vscode \ No newline at end of file +.vscode diff --git a/src/App.css b/src/App.css index 469efae..ecab42f 100644 --- a/src/App.css +++ b/src/App.css @@ -74,7 +74,7 @@ @media only screen and (max-width: 840px) { .voteplan-title { - font-size: 68px; + font-size: 80px; } } diff --git a/src/components/Plan.css b/src/components/Plan.css index 21b530d..68d3850 100644 --- a/src/components/Plan.css +++ b/src/components/Plan.css @@ -5,4 +5,17 @@ .plan-congrats { font-size: 28px; +} + +.plan-page-body { + max-width: 800px; + margin-left: auto; + margin-right: auto; + padding: 20px; +} + +@media only screen and (max-width: 840px) { + .plan-congrats { + font-size: 22px; + } } \ No newline at end of file diff --git a/src/components/Plan.tsx b/src/components/Plan.tsx index dccf05e..53c61ec 100644 --- a/src/components/Plan.tsx +++ b/src/components/Plan.tsx @@ -1,9 +1,11 @@ import * as React from 'react'; import { Dispatch } from 'redux'; import { connect} from 'react-redux'; +import { PlanEmotion } from './PlanEmotion'; import { PlanStep } from './PlanStep'; +import { ReasonToVote } from './ReasonToVote'; import { StartOverButton } from './StartOverButton'; -import { ALL_QUESTION_IDS, IConnectedReduxProps, IQuestionAndAnswer, QuestionId, recordPlanPage, VotingStateId } from '../store'; +import { ALL_QUESTION_IDS, IConnectedReduxProps, IQuestionAndAnswer, QuestionId, QUESTIONS, recordPlanPage, VotingStateId } from '../store'; import './Plan.css'; @@ -29,7 +31,7 @@ class InternalPlan extends React.Component +
Congratulations! Here's your
@@ -38,13 +40,30 @@ class InternalPlan extends React.Component
-
+
{ALL_QUESTION_IDS.map( (questionId: QuestionId) => { const answer = this.props.answers.find(qa => qa.questionId === questionId); if (answer !== undefined) { - return + const question = QUESTIONS.find(q => q.id === questionId); + + const planStepId = question!.resultingPlanStep(answer!.answerId); + + if (planStepId !== undefined) { + if (questionId === QuestionId.ReasonToVote) { + return + } + else if (questionId === QuestionId.Emotion) { + return + } + else { + return + } + } + else { + return + } } else { return diff --git a/src/components/PlanEmotion.css b/src/components/PlanEmotion.css new file mode 100644 index 0000000..1ad41a2 --- /dev/null +++ b/src/components/PlanEmotion.css @@ -0,0 +1,11 @@ +.plan-page-emoji-img { + width: 216px; + height: 216px; +} + +@media only screen and (max-width: 840px) { + .plan-page-emoji-img { + width: 180px; + height: 180px; + } +} \ No newline at end of file diff --git a/src/components/PlanEmotion.tsx b/src/components/PlanEmotion.tsx new file mode 100644 index 0000000..90f20ae --- /dev/null +++ b/src/components/PlanEmotion.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { PlanStepId, VotingStateId } from '../store'; +import { getEmojiAltText, getPlanStepStrings } from '../strings'; +import { renderPlanStepCallToAction } from './renderPlanStepCallToAction'; + +import emoji_angry from './emoji_angry.svg'; +import emoji_concerned from './emoji_concerned.svg'; +import emoji_excited from './emoji_excited.svg'; +import emoji_meh from './emoji_meh.svg'; +import emoji_shocked from './emoji_shocked.svg'; + +import './PlanEmotion.css'; +import './PlanStep.css'; +import './ReasonToVote.css'; + +interface IPlanEmotionProps { + planStepId: PlanStepId, + votingStateId: VotingStateId, +} + +export class PlanEmotion extends React.Component { + public render() { + const { planStepId, votingStateId } = this.props; + + const { header, text, callToAction, link } = getPlanStepStrings(planStepId, votingStateId); + + return ( +
+
{ this.getSvg(planStepId) }
+ +
{header}
+
{text}
+ + { renderPlanStepCallToAction(callToAction, link) } +
+ ); + } + + public getSvg(planStepId: PlanStepId): JSX.Element { + const className = "plan-page-emoji-img"; + const altText = getEmojiAltText(planStepId); + + switch (planStepId) { + case PlanStepId.Excited: { + // TODO: I bet we can just return the SVG src as a string? + return {altText}; + } + case PlanStepId.Concerned: { + return {altText}; + } + case PlanStepId.Shocked: { + return {altText}; + } + case PlanStepId.Angry: { + return {altText}; + } + case PlanStepId.Meh: { + return {altText}; + } + default: + throw new Error("Unhandled PlanStepId"); + } + } +} \ No newline at end of file diff --git a/src/components/PlanStep.css b/src/components/PlanStep.css index 43bdf23..1a81382 100644 --- a/src/components/PlanStep.css +++ b/src/components/PlanStep.css @@ -3,16 +3,36 @@ font-size: 48px; margin-top: 48px; margin-bottom: -10px; + color: #1c2856; + line-height: 110%; } .plan-step-text { + font-size: 18px; margin-top: 18px; margin-bottom: 18px; - font-size: 18px; } .plan-step-call-to-action { text-transform: uppercase; font-size: 22px; margin-bottom: 40px; +} + +.plan-step-call-to-action a:link, .plan-step-call-to-action a:visited { + color: #cc1a3b; +} + +@media only screen and (max-width: 840px) { + .plan-step-header { + margin-top: 26px; + font-size: 26px; + } + + .plan-step-text { + font-size: 16px; + line-height: 120%; + margin-top: 22px; + margin-bottom: 12px; + } } \ No newline at end of file diff --git a/src/components/PlanStep.tsx b/src/components/PlanStep.tsx index 3cf503a..7615e9c 100644 --- a/src/components/PlanStep.tsx +++ b/src/components/PlanStep.tsx @@ -1,54 +1,35 @@ import * as React from 'react'; import { IIndexHolder } from './Plan'; -import { AnswerId, QUESTIONS, QuestionId, VotingStateId } from '../store'; +import { PlanStepId, VotingStateId } from '../store'; import { getPlanStepStrings, planStepHeaderFormattedString } from '../strings'; +import { renderPlanStepCallToAction } from './renderPlanStepCallToAction'; import './PlanStep.css'; interface IPlanStepProps { indexHolder: IIndexHolder, - questionId: QuestionId, - answerId: AnswerId, + planStepId: PlanStepId, votingStateId: VotingStateId, } export class PlanStep extends React.Component { public render() { - const { indexHolder, questionId, answerId, votingStateId } = this.props; + const { indexHolder, planStepId, votingStateId } = this.props; - const question = QUESTIONS.find(q => q.id === questionId); + const { header, text, callToAction, link } = getPlanStepStrings(planStepId, votingStateId); - const planStepId = question!.resultingPlanStep(answerId); + const fullHeaderString = planStepHeaderFormattedString(indexHolder.index, header); - if (planStepId !== undefined) { - const { header, text, callToAction, link } = getPlanStepStrings(planStepId, votingStateId); + // Increment the index holder so that the next plan step uses the next number. + indexHolder.index++; + + return ( +
+
{fullHeaderString}
+
{text}
- const fullHeaderString = planStepHeaderFormattedString(indexHolder.index, header); - - // Increment the index holder so that the next plan step uses the next number. - indexHolder.index++; - - return ( -
-
{fullHeaderString}
-
{text}
- - { this.renderCallToAction(callToAction, link) } -
- ); - } - else { - return - } - } - - private renderCallToAction(callToAction: string | undefined, link: string | undefined): JSX.Element { - if (callToAction !== undefined && link !== undefined) { - // TODO: Link should open in a new tab. - return - } - else { - return - } + { renderPlanStepCallToAction(callToAction, link) } +
+ ); } } \ No newline at end of file diff --git a/src/components/ReasonToVote.css b/src/components/ReasonToVote.css new file mode 100644 index 0000000..f6a5c16 --- /dev/null +++ b/src/components/ReasonToVote.css @@ -0,0 +1,34 @@ +.reason-to-vote-header { + font-size: 36px; + margin-top: 48px; +} + +.reason-to-vote-text { + font-size: 120px; + font-family: "Pacifico"; + color: #1c2856; + text-shadow: 4px 4px #e81c38; + padding-bottom: 10px; + margin-top: 20px; + margin-bottom: 50px; + line-height: 110%; +} + +.reason-to-vote-plan-step-text { + /* Since the ReasonToVote component doesn't have a call-to-action link, we need to add more space underneath it */ + margin-bottom: 50px; +} + +@media only screen and (max-width: 840px) { + .reason-to-vote-header { + font-size: 22px; + margin-top: 30px; + } + + .reason-to-vote-text { + font-size: 72px; + text-shadow: 1.5px 1.5px #e81c38; + margin-top: 10px; + margin-bottom: 25px; + } +} \ No newline at end of file diff --git a/src/components/ReasonToVote.tsx b/src/components/ReasonToVote.tsx new file mode 100644 index 0000000..fad8676 --- /dev/null +++ b/src/components/ReasonToVote.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { PlanStepId } from '../store'; +import { getReasonToVoteStrings } from '../strings'; + +import './PlanStep.css'; +import './ReasonToVote.css'; + +interface IReasonToVoteProps { + planStepId: PlanStepId, +} + +export class ReasonToVote extends React.Component { + public render() { + const { planStepId } = this.props; + + const { header, reasonText, bodyText } = getReasonToVoteStrings(planStepId); + + return ( +
+
{header}
+
{reasonText}
+
{bodyText}
+
+ ); + } +} \ No newline at end of file diff --git a/src/components/emoji_angry.svg b/src/components/emoji_angry.svg new file mode 100644 index 0000000..13cc444 --- /dev/null +++ b/src/components/emoji_angry.svg @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/emoji_concerned.svg b/src/components/emoji_concerned.svg new file mode 100644 index 0000000..a623e32 --- /dev/null +++ b/src/components/emoji_concerned.svg @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/emoji_excited.svg b/src/components/emoji_excited.svg new file mode 100644 index 0000000..60cc8bc --- /dev/null +++ b/src/components/emoji_excited.svg @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/emoji_meh.svg b/src/components/emoji_meh.svg new file mode 100644 index 0000000..2ef26ad --- /dev/null +++ b/src/components/emoji_meh.svg @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/emoji_shocked.svg b/src/components/emoji_shocked.svg new file mode 100644 index 0000000..fbc087c --- /dev/null +++ b/src/components/emoji_shocked.svg @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/renderPlanStepCallToAction.tsx b/src/components/renderPlanStepCallToAction.tsx new file mode 100644 index 0000000..2fe1244 --- /dev/null +++ b/src/components/renderPlanStepCallToAction.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; + +export function renderPlanStepCallToAction(callToAction: string | undefined, link: string | undefined): JSX.Element { + if (callToAction !== undefined && link !== undefined) { + return + } + else { + return + } +} \ No newline at end of file diff --git a/src/store/Questions.ts b/src/store/Questions.ts index 3807d76..1dffdb9 100644 --- a/src/store/Questions.ts +++ b/src/store/Questions.ts @@ -9,11 +9,11 @@ export const QUESTIONS : IQuestion[] = [ dotNavStep: 1, nextQuestionId: (answer) => QuestionId.OverseasMilitary, answers: [ - AnswerId.EmphaticYes, + AnswerId.Yes, AnswerId.No, AnswerId.DontKnow, ], - resultingPlanStep: (answer) => answer === AnswerId.No || answer === AnswerId.DontKnow ? PlanStepId.Register : undefined, + resultingPlanStep: (answer) => answer === AnswerId.Yes ? PlanStepId.CheckRegistration : (answer === AnswerId.No ? PlanStepId.Register : PlanStepId.MaybeRegister), }, { id: QuestionId.OverseasMilitary, diff --git a/src/store/Types.ts b/src/store/Types.ts index 5c88ac0..ef16621 100644 --- a/src/store/Types.ts +++ b/src/store/Types.ts @@ -39,7 +39,6 @@ export const ALL_QUESTION_IDS: QuestionId[] = [ export enum AnswerId { Yes = 'Y', No = 'N', - EmphaticYes = 'EY', DontKnow = 'DN', Washington = 'WA', Oregon = 'OR', @@ -138,6 +137,8 @@ export interface IAnswerQuestionPayload { export enum PlanStepId { Register, + CheckRegistration, + MaybeRegister, RequestOverseasBallot, HaveBallot, NoBallotYet, diff --git a/src/strings.ts b/src/strings.ts index 97e3ab9..4ed9d79 100644 --- a/src/strings.ts +++ b/src/strings.ts @@ -59,9 +59,6 @@ export function getAnswerLabel(answer: AnswerId): string { case AnswerId.No: return 'No'; - case AnswerId.EmphaticYes: - return 'Yes!'; - case AnswerId.DontKnow: return 'Not sure'; @@ -168,6 +165,24 @@ export function getPlanStepStrings(step: PlanStepId, state: VotingStateId): IPla }; } + case PlanStepId.CheckRegistration: { + return { + header: "Check your registration", + text: "Great \u2014 you're registered! Now take a quick look to make sure everything is up-to-date.", + callToAction: "Double-check your registration", + link: "https://www.rockthevote.org/voting-information/am-i-registered-to-vote/", + }; + } + + case PlanStepId.MaybeRegister: { + return { + header: "Check your registration", + text: "Not sure whether you're registered? Take a minute to double-check and make sure you're vote ready.", + callToAction: "Double-check your registration", + link: "https://www.rockthevote.org/voting-information/am-i-registered-to-vote/", + }; + } + case PlanStepId.RequestOverseasBallot: { return { header: "Request your absentee ballot", @@ -335,8 +350,8 @@ export function getPlanStepStrings(step: PlanStepId, state: VotingStateId): IPla case PlanStepId.FindPollingLocation: { return { header: "Find your polling location", - text: "Make it easy on yourself! Find your state's polling locations through the link below.", - callToAction: "This way to the polls (scroll for your state)!", + text: "Make it easy on yourself! Find your polling location through the link below.", + callToAction: "This way to the polls!", link: "https://www.vote.org/polling-place-locator/", } } @@ -408,7 +423,7 @@ export function getPlanStepStrings(step: PlanStepId, state: VotingStateId): IPla case PlanStepId.ReviewBallotIssues: { return { header: "Your ballot, in a nutshell", - text: "You're familiar with the candidates and issues \u2014 now put it all together using this handy app.", + text: "You're familiar with the candidates and issues. The Ballot Ready app can help you make informed choices.", callToAction: "Plan your ballot choices", link: "https://www.ballotready.org/", } @@ -417,7 +432,7 @@ export function getPlanStepStrings(step: PlanStepId, state: VotingStateId): IPla case PlanStepId.ResearchBallotIssues: { return { header: "Introducing...your ballot!", - text: "Now's the perfect time to research the candidates and issues you'll be voting for. Here's an app to help.", + text: "Now's the perfect time to research the candidates and issues you'll be voting for. The Ballot Ready app can help you make informed choices.", callToAction: "Plan your ballot choices", link: "https://www.ballotready.org/", } @@ -425,54 +440,8 @@ export function getPlanStepStrings(step: PlanStepId, state: VotingStateId): IPla case PlanStepId.InvitePeople: { return { - header: "Invite your crowd to VotePlan!", // TODO: Review: "your crowd?", - text: "Let's keep each other accountable \u2014 voting's even better when we can do it together! Share your plans with the people in your life.", - callToAction: undefined, - link: undefined, - } - } - - // TODO: Better styling for "FOR FAMILY" etc. - case PlanStepId.ForMyKidsAndFamily: { - return { - header: "Remember your reason for voting!", - text: "FOR FAMILY. Whether you're looking out for the next generation or for your family members today, you're right: your vote has an impact!", - callToAction: undefined, - link: undefined, - } - } - - case PlanStepId.VotingIsPrivilege: { - return { - header: "Remember your reason for voting!", - text: "IT'S MY PRIVILEGE. Yep, you've got the right idea, and we agree. Let's not take voting for granted!", - callToAction: undefined, - link: undefined, - } - } - - case PlanStepId.DriveChange: { - return { - header: "Remember your reason for voting!", - text: "FOR CHANGE. You're about to do the single best thing you can to weigh in on issues and decision-makers. Do you approve? Disapprove? Your vote is a meaningful message \u2014 pass it on!", - callToAction: undefined, - link: undefined, - } - } - - case PlanStepId.AlwaysVoted: { - return { - header: "Remember your reason for voting!", - text: "IT'S MY M.O.  You've always voted--good on you! Spread the word and keep up the great (and extremely important) civic habit.", - callToAction: undefined, - link: undefined, - } - } - - case PlanStepId.OtherReason: { - return { - header: "Remember your reason for voting!", - text: "CIVIC DUTY.  You're about to do the single best thing you can to weigh in on issues and decision-makers. Do you approve? Disapprove? Your vote is a meaningful message \u2014 pass it on!", + header: "Invite your crowd to vote with you", // TODO: Review: "your crowd?", + text: "Let's keep each other accountable \u2014 voting's even better when we can do it together!", callToAction: undefined, link: undefined, } @@ -485,7 +454,7 @@ export function getPlanStepStrings(step: PlanStepId, state: VotingStateId): IPla case PlanStepId.Meh: { let header = "Put those feelings toward action"; let text: string | undefined; - const callToAction: string | undefined = "Your voice matters \u2014 take a look"; + const callToAction: string | undefined = "See what happens When We All Vote"; const link: string | undefined = "https://www.whenweallvote.org/"; switch (step) { @@ -555,4 +524,89 @@ function numberToString(n: number): string { export function planStepHeaderFormattedString(index: number, header: string) { const numberString = numberToString(index + 1); return `Step ${numberString}: ${header}`; +} + +export interface IReasonToVoteStrings { + header: string, + reasonText: string, + bodyText: string, +} + +export function getReasonToVoteStrings(planStepId: PlanStepId): IReasonToVoteStrings { + const header = "Remember your reason for voting!"; + + switch (planStepId) { + // TODO: Should these strings just say "Family", "Change", etc.? + case PlanStepId.ForMyKidsAndFamily: { + return { + header, + reasonText: "For Family", + bodyText: "Whether you're looking out for the next generation or for your family members today, you're right: your vote has an impact!" + } + } + + case PlanStepId.VotingIsPrivilege: { + return { + header, + reasonText: "It's My Privilege", + bodyText: "Yep, you've got the right idea, and we agree. Let's not take voting for granted!", + } + } + + case PlanStepId.DriveChange: { + return { + header, + reasonText: "For Change", + bodyText: "You're about to do the single best thing you can to weigh in on issues and decision-makers. Do you approve? Disapprove? Your vote is a meaningful message \u2014 pass it on!", + } + } + + case PlanStepId.AlwaysVoted: { + return { + header, + reasonText: "It's My M.O.", // TODO: Does everyone know what M.O. means? + bodyText: "You've always voted--good on you! Spread the word and keep up the great (and extremely important) civic habit.", + } + } + + case PlanStepId.OtherReason: { + return { + header, + reasonText: "Civic Duty", + bodyText: "You're about to do the single best thing you can to weigh in on issues and decision-makers. Do you approve? Disapprove? Your vote is a meaningful message \u2014 pass it on!", + } + } + + default: { + throw new Error("Unhandled PlanStepId"); + } + } +} + +export interface IPlanEmotionStrings { + header: string, + text: string, +} + +export function getEmojiAltText(planStepId: PlanStepId): string { + switch (planStepId) { + case PlanStepId.Excited: { + return "Excited emoji"; + } + case PlanStepId.Concerned: { + return "Concerned emoji"; + } + case PlanStepId.Shocked: { + return "Shocked emoji"; + } + case PlanStepId.Angry: { + return "Angry emoji"; + } + case PlanStepId.Meh: { + return "Meh emoji"; + } + default: { + throw new Error("Unhandled PlanStepId"); + } + } } \ No newline at end of file