670 lines
19 KiB
JavaScript
670 lines
19 KiB
JavaScript
import React from "react";
|
|
import ReactDOM from "react-dom";
|
|
import firebase from "firebase/app";
|
|
import "firebase/functions";
|
|
|
|
const FIREBASE_CONFIG = {
|
|
apiKey: "AIzaSyDKw8HXWRn3dtkIRETv-BxtwnklZIpIkxU",
|
|
authDomain: "vaxandme.firebaseapp.com",
|
|
projectId: "vaxandme",
|
|
storageBucket: "vaxandme.appspot.com",
|
|
messagingSenderId: "614738281692",
|
|
appId: "1:614738281692:web:7a89e0af76675e4f0ea019",
|
|
measurementId: "G-N016TVJ6JC",
|
|
};
|
|
|
|
firebase.initializeApp(FIREBASE_CONFIG);
|
|
if (window.location.host.startsWith("localhost")) {
|
|
firebase.functions().useEmulator("localhost", 5001);
|
|
}
|
|
const fb_functions = firebase.functions();
|
|
const saveResult = fb_functions.httpsCallable("saveResult");
|
|
const getPastResult = fb_functions.httpsCallable("getPastResult");
|
|
|
|
const VAX_NAMES = {
|
|
PB: "Pfizer-BioNTech",
|
|
MD: "Moderna",
|
|
JJ: "J&J Janssen",
|
|
};
|
|
|
|
const VAX_DOSES = {
|
|
ONE: "Dose 1",
|
|
TWO: "Dose 2",
|
|
};
|
|
|
|
const VAX_AGES = {
|
|
"Pfizer-BioNTech": {
|
|
YOUNGER: "18 - 55",
|
|
OLDER: "56 & Older",
|
|
},
|
|
Moderna: {
|
|
YOUNGER: "18 - 64",
|
|
OLDER: "65 & Older",
|
|
},
|
|
"J&J Janssen": {
|
|
YOUNGER: "18 - 59",
|
|
OLDER: "60 & Older",
|
|
},
|
|
};
|
|
|
|
const PB_DOSE1_YOUNGER_RESULT = {
|
|
"PAIN AT SITE": "8",
|
|
"RED AT SITE": "1",
|
|
"SWELLING AT SITE": "1",
|
|
FATIGUE: "5",
|
|
HEADACHE: "4",
|
|
"NEED PAIN MED": "3",
|
|
"MUSCLE PAIN": "2",
|
|
CHILLS: "1",
|
|
DIRRHEA: "1",
|
|
"JOINT PAIN": "1",
|
|
FEVER: "<1",
|
|
VOMITING: "<1",
|
|
};
|
|
const PB_DOSE2_YOUNGER_RESULT = {
|
|
"PAIN AT SITE": "8",
|
|
"RED AT SITE": "1",
|
|
"SWELLING AT SITE": "1",
|
|
FATIGUE: "6",
|
|
HEADACHE: "5",
|
|
"NEED PAIN MED": "5",
|
|
"MUSCLE PAIN": "4",
|
|
CHILLS: "4",
|
|
"JOINT PAIN": "2",
|
|
FEVER: "2",
|
|
DIRRHEA: "1",
|
|
VOMITING: "<1",
|
|
};
|
|
const PB_DOSE1_OLDER_RESULT = {
|
|
"PAIN AT SITE": "7",
|
|
"RED AT SITE": "1",
|
|
"SWELLING AT SITE": "1",
|
|
FATIGUE: "3",
|
|
HEADACHE: "3",
|
|
"NEED PAIN MED": "2",
|
|
"MUSCLE PAIN": "1",
|
|
CHILLS: "1",
|
|
DIRRHEA: "1",
|
|
"JOINT PAIN": "1",
|
|
FEVER: "<1",
|
|
VOMITING: "<1",
|
|
};
|
|
const PB_DOSE2_OLDER_RESULT = {
|
|
"PAIN AT SITE": "7",
|
|
"RED AT SITE": "1",
|
|
"SWELLING AT SITE": "1",
|
|
FATIGUE: "5",
|
|
HEADACHE: "4",
|
|
"NEED PAIN MED": "4",
|
|
"MUSCLE PAIN": "3",
|
|
CHILLS: "2",
|
|
"JOINT PAIN": "2",
|
|
DIRRHEA: "1",
|
|
FEVER: "1",
|
|
VOMITING: "<1",
|
|
};
|
|
const MD_DOSE1_YOUNGER_RESULT = {
|
|
"PAIN AT SITE": "9",
|
|
"RED AT SITE": "<1",
|
|
"SWELLING AT SITE": "<1",
|
|
HEADACHE: "4",
|
|
FATIGUE: "4",
|
|
"MUSCLE PAIN": "2",
|
|
"JOINT PAIN": "2",
|
|
"NAUSEA OR VOMITING": "1",
|
|
FEVER: "1",
|
|
CHILLS: "1",
|
|
};
|
|
const MD_DOSE2_YOUNGER_RESULT = {
|
|
"PAIN AT SITE": "9",
|
|
"RED AT SITE": "1",
|
|
"SWELLING AT SITE": "1",
|
|
FATIGUE: "7",
|
|
HEADACHE: "6",
|
|
"JOINT PAIN": "5",
|
|
CHILLS: "5",
|
|
FEVER: "2",
|
|
"NAUSEA OR VOMITING": "2",
|
|
"MUSCLE PAIN": "1",
|
|
};
|
|
const MD_DOSE1_OLDER_RESULT = {
|
|
"PAIN AT SITE": "7",
|
|
"RED AT SITE": "<1",
|
|
"SWELLING AT SITE": "<1",
|
|
HEADACHE: "3",
|
|
FATIGUE: "3",
|
|
"MUSCLE PAIN": "2",
|
|
"JOINT PAIN": "2",
|
|
"NAUSEA OR VOMITING": "1",
|
|
CHILLS: "1",
|
|
FEVER: "<1",
|
|
};
|
|
const MD_DOSE2_OLDER_RESULT = {
|
|
"PAIN AT SITE": "8",
|
|
"RED AT SITE": "1",
|
|
"SWELLING AT SITE": "1",
|
|
FATIGUE: "6",
|
|
HEADACHE: "5",
|
|
"MUSCLE PAIN": "5",
|
|
"JOINT PAIN": "4",
|
|
CHILLS: "3",
|
|
"NAUSEA OR VOMITING": "1",
|
|
FEVER: "1",
|
|
};
|
|
const JJ_YOUNGER_RESULT = {
|
|
"PAIN AT SITE": "6",
|
|
"RED AT SITE": "1",
|
|
"SWELLING AT SITE": "1",
|
|
HEADACHE: "4",
|
|
FATIGUE: "4",
|
|
"MUSCLE PAIN": "4",
|
|
"NEED PAIN MED": "3",
|
|
"NAUSEA OR VOMITING": "2",
|
|
FEVER: "1",
|
|
};
|
|
const JJ_OLDER_RESULT = {
|
|
"PAIN AT SITE": "3",
|
|
"RED AT SITE": "<1",
|
|
"SWELLING AT SITE": "<1",
|
|
HEADACHE: "3",
|
|
FATIGUE: "3",
|
|
"MUSCLE PAIN": "2",
|
|
"NAUSEA OR VOMITING": "1",
|
|
"NEED PAIN MED": "1",
|
|
FEVER: "<1",
|
|
};
|
|
|
|
const VAX_RESULTS = {
|
|
"Pfizer-BioNTech": {
|
|
"Dose 1": {
|
|
"18 - 55": PB_DOSE1_YOUNGER_RESULT,
|
|
"56 & Older": PB_DOSE1_OLDER_RESULT,
|
|
},
|
|
"Dose 2": {
|
|
"18 - 55": PB_DOSE2_YOUNGER_RESULT,
|
|
"56 & Older": PB_DOSE2_OLDER_RESULT,
|
|
},
|
|
},
|
|
Moderna: {
|
|
"Dose 1": {
|
|
"18 - 64": MD_DOSE1_YOUNGER_RESULT,
|
|
"65 & Older": MD_DOSE1_OLDER_RESULT,
|
|
},
|
|
"Dose 2": {
|
|
"18 - 64": MD_DOSE2_YOUNGER_RESULT,
|
|
"65 & Older": MD_DOSE2_OLDER_RESULT,
|
|
},
|
|
},
|
|
"J&J Janssen": {
|
|
"18 - 59": JJ_YOUNGER_RESULT,
|
|
"60 & Older": JJ_OLDER_RESULT,
|
|
},
|
|
};
|
|
|
|
const ICONS = {
|
|
"PAIN AT SITE": "💪",
|
|
"RED AT SITE": "🔴",
|
|
"SWELLING AT SITE": "💢",
|
|
FATIGUE: "🥱",
|
|
HEADACHE: "🤕",
|
|
"NEED PAIN MED": "💊",
|
|
"MUSCLE PAIN": "😫",
|
|
CHILLS: "🥶",
|
|
DIRRHEA: "💩",
|
|
"JOINT PAIN": "🦵",
|
|
FEVER: "🤒",
|
|
VOMITING: "🤮",
|
|
"NAUSEA OR VOMITING": "🤮",
|
|
};
|
|
|
|
class App extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
name: "",
|
|
dose: "",
|
|
age: "",
|
|
gid: "",
|
|
showInputPopup: false,
|
|
answers: {},
|
|
loading: true,
|
|
};
|
|
}
|
|
componentDidMount() {
|
|
const params = window.location.pathname
|
|
.toUpperCase()
|
|
.split("/")
|
|
.filter((v) => v.length > 0);
|
|
if (params) {
|
|
if (params[0] && VAX_NAMES[params[0]]) {
|
|
const name = VAX_NAMES[params[0]];
|
|
this.setState({ name });
|
|
switch (name) {
|
|
case VAX_NAMES.PB:
|
|
case VAX_NAMES.MD:
|
|
if (params[1] && VAX_DOSES[params[1]]) {
|
|
const dose = VAX_DOSES[params[1]];
|
|
this.setState({ dose });
|
|
if (params[2] && VAX_AGES[name][params[2]]) {
|
|
const age = VAX_AGES[name][params[2]];
|
|
this.setState({ age });
|
|
}
|
|
}
|
|
break;
|
|
case VAX_NAMES.JJ:
|
|
if (params[1] && VAX_AGES[name][params[1]]) {
|
|
const age = VAX_AGES[name][params[1]];
|
|
this.setState({ age });
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!window.localStorage.getItem("hasExistingAnswer")) {
|
|
this.setState({ loading: false });
|
|
}
|
|
|
|
if (window.gtag) {
|
|
window.gtag("get", "G-7VZCY223NG", "client_id", (client_id) => {
|
|
if (!!client_id) {
|
|
this.setState({ gid: client_id });
|
|
getPastResult({ gid: client_id })
|
|
.then((v) => {
|
|
const { data } = v;
|
|
if (!!data) {
|
|
const { name, age, dose } = data;
|
|
this.setState({ name, age, dose, showInputPopup: true });
|
|
delete data.name;
|
|
delete data.age;
|
|
delete data.dose;
|
|
this.setState({ answers: data, loading: false });
|
|
const inputPopupSubmit = document.querySelector(
|
|
".input-popup-submit"
|
|
);
|
|
if (inputPopupSubmit) {
|
|
inputPopupSubmit.classList.add("submitted");
|
|
inputPopupSubmit.innerHTML = "UPDATE ANSWER";
|
|
}
|
|
window.localStorage.setItem("hasExistingAnswer", true);
|
|
} else {
|
|
this.setState({ loading: false });
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error("ERROR Getting Past Answer", err);
|
|
this.setState({ loading: false });
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
this.setState({ loading: false });
|
|
}
|
|
}
|
|
shouldShowAgeGroup = () => {
|
|
const { name, dose } = this.state;
|
|
switch (name) {
|
|
case VAX_NAMES.JJ:
|
|
return true;
|
|
case VAX_NAMES.MD:
|
|
case VAX_NAMES.PB:
|
|
return !!dose;
|
|
default:
|
|
return false;
|
|
}
|
|
};
|
|
shouldShowResults = () => {
|
|
const { name, dose, age } = this.state;
|
|
switch (name) {
|
|
case VAX_NAMES.JJ:
|
|
return !!age;
|
|
case VAX_NAMES.MD:
|
|
case VAX_NAMES.PB:
|
|
return !!dose && !!age;
|
|
default:
|
|
return false;
|
|
}
|
|
};
|
|
getResults = () => {
|
|
const { name, dose, age } = this.state;
|
|
switch (name) {
|
|
case VAX_NAMES.JJ:
|
|
return VAX_RESULTS[name][age];
|
|
case VAX_NAMES.MD:
|
|
case VAX_NAMES.PB:
|
|
return VAX_RESULTS[name][dose][age];
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
copyShareLink = () => {
|
|
const { name, dose, age } = this.state;
|
|
let SHARE_URL = window.location.host;
|
|
const serialize_name = Object.keys(VAX_NAMES).filter(
|
|
(k) => VAX_NAMES[k] === name
|
|
)[0];
|
|
const serialize_dose = Object.keys(VAX_DOSES).filter(
|
|
(k) => VAX_DOSES[k] === dose
|
|
)[0];
|
|
const serialize_age = Object.keys(VAX_AGES[name]).filter(
|
|
(k) => VAX_AGES[name][k] === age
|
|
)[0];
|
|
if (serialize_name) {
|
|
SHARE_URL += `/${serialize_name}`;
|
|
}
|
|
if (serialize_dose) {
|
|
SHARE_URL += `/${serialize_dose}`;
|
|
}
|
|
if (serialize_age) {
|
|
SHARE_URL += `/${serialize_age}`;
|
|
}
|
|
|
|
const copyTextInput = document.createElement("input");
|
|
document.body.appendChild(copyTextInput);
|
|
copyTextInput.value = SHARE_URL.toLowerCase();
|
|
copyTextInput.select();
|
|
document.execCommand("copy");
|
|
copyTextInput.remove();
|
|
|
|
document.querySelector("#share-result").innerHTML = "Copied Link";
|
|
setTimeout(() => {
|
|
document.querySelector("#share-result").innerHTML = "Share ↗";
|
|
}, 1500);
|
|
};
|
|
updateAnswers = (symptom) => {
|
|
const { answers } = this.state;
|
|
const newAnswers = Object.assign({}, answers);
|
|
if (answers[symptom]) {
|
|
newAnswers[symptom] = !answers[symptom];
|
|
} else {
|
|
newAnswers[symptom] = true;
|
|
}
|
|
this.setState({ answers: newAnswers });
|
|
};
|
|
submitAnswer = () => {
|
|
document.querySelector(".input-popup-submit").innerHTML = "SENDING...";
|
|
const { name, dose, age, answers, gid } = this.state;
|
|
const result = Object.assign({}, this.getResults());
|
|
const symptoms = {};
|
|
Object.keys(result).forEach((symptom) => {
|
|
symptoms[symptom] = false;
|
|
});
|
|
const submitAnswers = Object.assign({}, symptoms, answers, {
|
|
name,
|
|
dose,
|
|
age,
|
|
gid,
|
|
});
|
|
saveResult(submitAnswers)
|
|
.then(() => {
|
|
const inputPopupSubmit = document.querySelector(".input-popup-submit");
|
|
if (!inputPopupSubmit.classList.contains("submitted")) {
|
|
inputPopupSubmit.classList.add("submitted");
|
|
}
|
|
inputPopupSubmit.innerHTML = "THANK YOU!";
|
|
setTimeout(() => {
|
|
inputPopupSubmit.innerHTML = "UPDATE ANSWER";
|
|
}, 1500);
|
|
})
|
|
.catch((err) => console.error("ERROR Saving Answer", err));
|
|
};
|
|
render() {
|
|
const { name, dose, age, showInputPopup, answers, loading } = this.state;
|
|
let result = {};
|
|
let resultAtSite = {};
|
|
if (this.shouldShowResults()) {
|
|
result = Object.assign({}, this.getResults());
|
|
Object.keys(result).forEach((s) => {
|
|
if (s.includes("AT SITE")) {
|
|
resultAtSite[s] = result[s];
|
|
delete result[s];
|
|
}
|
|
});
|
|
}
|
|
return (
|
|
<div className="app-container">
|
|
<header>
|
|
<div className="logo-container">
|
|
<div className="logo"> </div>
|
|
</div>
|
|
<div className="intro">
|
|
<h6 className="overline" data-font="sans">
|
|
Getting vaccinated?
|
|
</h6>
|
|
<h3 className="subheader" data-font="sans-bold">
|
|
Find out what to expect.
|
|
</h3>
|
|
</div>
|
|
</header>
|
|
<main>
|
|
{loading ? (
|
|
<div className="loading-container">
|
|
<p className="question" data-font="sans">
|
|
Checking for existing answers...
|
|
</p>
|
|
<div className="loading-bar-container">
|
|
<div className="loading-bar"></div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
{!loading ? (
|
|
<div className="question-container">
|
|
<p className="question" data-font="sans-italic">
|
|
Select Vaccine
|
|
</p>
|
|
<div className="answers-container">
|
|
{Object.values(VAX_NAMES).map((v) => (
|
|
<button
|
|
key={`name-group-${v}`}
|
|
onClick={() =>
|
|
this.setState({
|
|
name: v,
|
|
dose: "",
|
|
age: "",
|
|
answers: {},
|
|
showInputPopup: false,
|
|
})
|
|
}
|
|
data-font="sans-bold"
|
|
className={`vaccine answer${name === v ? " selected" : ""}${
|
|
name && name !== v ? " unselected" : ""
|
|
}`}
|
|
>
|
|
{v}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
{name && (name === VAX_NAMES.PB || name === VAX_NAMES.MD) ? (
|
|
<div className="question-container">
|
|
<p className="question" data-font="sans-italic">
|
|
Select Dose
|
|
</p>
|
|
<div className="answers-container">
|
|
{Object.values(VAX_DOSES).map((v) => (
|
|
<button
|
|
key={`dose-group-${v}`}
|
|
onClick={() =>
|
|
this.setState({
|
|
dose: v,
|
|
age: "",
|
|
answers: {},
|
|
showInputPopup: false,
|
|
})
|
|
}
|
|
data-font="sans-bold"
|
|
className={`dose answer${dose === v ? " selected" : ""}${
|
|
dose && dose !== v ? " unselected" : ""
|
|
}`}
|
|
>
|
|
{v}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{this.shouldShowAgeGroup() ? (
|
|
<div className="question-container">
|
|
<p className="question" data-font="sans-italic">
|
|
Select Age Group
|
|
</p>
|
|
<div className="answers-container">
|
|
{Object.values(VAX_AGES[name]).map((v) => (
|
|
<button
|
|
key={`age-group-${v}`}
|
|
onClick={() =>
|
|
this.setState({
|
|
age: v,
|
|
answers: {},
|
|
showInputPopup: false,
|
|
})
|
|
}
|
|
data-font="sans-bold"
|
|
className={`age answer${age === v ? " selected" : ""}${
|
|
age && age !== v ? " unselected" : ""
|
|
}`}
|
|
>
|
|
{v}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{this.shouldShowResults() ? (
|
|
<div className="symptom-container subheader">
|
|
<h3 data-font="sans-bold">Here's what to expect.</h3>
|
|
<h6
|
|
id="share-result"
|
|
onClick={this.copyShareLink}
|
|
className="overline"
|
|
data-font="sans"
|
|
data-button
|
|
>
|
|
Share ↗
|
|
</h6>
|
|
<h6 className="overline" data-font="sans" data-button>
|
|
<a href="/vaxandme_chart.pdf">Download Chart ↗</a>
|
|
</h6>
|
|
</div>
|
|
) : null}
|
|
|
|
{this.shouldShowResults() ? (
|
|
<div className="symptom-container">
|
|
{Object.keys(resultAtSite).map((symptom) => (
|
|
<div className="symptom at-site" key={`symptom-${symptom}`}>
|
|
<span className="symptom-emoji">{ICONS[symptom]}</span>
|
|
<p data-font="sans" className="symptom-name">
|
|
{symptom}
|
|
</p>
|
|
<p data-font="sans-bold" className="symptom-chance">
|
|
{resultAtSite[symptom]}/10
|
|
</p>
|
|
</div>
|
|
))}
|
|
{Object.keys(result).map((symptom) => (
|
|
<div className="symptom" key={`symptom-${symptom}`}>
|
|
<span className="symptom-emoji">{ICONS[symptom]}</span>
|
|
<p data-font="sans" className="symptom-name">
|
|
{symptom}
|
|
</p>
|
|
<p data-font="sans-bold" className="symptom-chance">
|
|
{result[symptom]}/10
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
</main>
|
|
{this.shouldShowResults() ? (
|
|
<div className={`input-popup${showInputPopup ? " expanded" : ""}`}>
|
|
<div
|
|
className="input-popup-header"
|
|
onClick={() => this.setState({ showInputPopup: !showInputPopup })}
|
|
>
|
|
<div className="input-popup-header-text">
|
|
<h6 className="overline" data-font="sans">
|
|
Already vaxxed?
|
|
</h6>
|
|
<h3 className="subheader" data-font="sans-bold">
|
|
Tell us how it went.
|
|
</h3>
|
|
</div>
|
|
<button
|
|
data-font="sans"
|
|
className="subheader input-popup-header-icon"
|
|
onClick={() =>
|
|
this.setState({ showInputPopup: !showInputPopup })
|
|
}
|
|
>
|
|
▼
|
|
</button>
|
|
</div>
|
|
<div className="input-popup-body">
|
|
<p className="question" data-font="sans">
|
|
I am in age group <strong>{age}</strong> and got
|
|
<strong>{!!dose ? ` ${dose} ` : ""}</strong>
|
|
{!!dose ? `of` : ""} the
|
|
<strong> {name}</strong> vaccine. I experienced:
|
|
</p>
|
|
<div className="question-container">
|
|
<p className="question" data-font="sans-italic">
|
|
(Select all side effects that you experienced)
|
|
</p>
|
|
<div className="answers-container">
|
|
{Object.keys(resultAtSite).map((symptom) => (
|
|
<button
|
|
key={`ans-${symptom}`}
|
|
data-font="sans-bold"
|
|
className={`answer${answers[symptom] ? " selected" : ""}`}
|
|
onClick={() => this.updateAnswers(symptom)}
|
|
>
|
|
{symptom}
|
|
</button>
|
|
))}
|
|
{Object.keys(result).map((symptom) => (
|
|
<button
|
|
key={`ans-${symptom}`}
|
|
data-font="sans-bold"
|
|
className={`answer${answers[symptom] ? " selected" : ""}`}
|
|
onClick={() => this.updateAnswers(symptom)}
|
|
>
|
|
{symptom}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="question-container">
|
|
<p className="question" data-font="sans">
|
|
You can comeback and update your experience later!
|
|
</p>
|
|
<p className="question" data-font="sans-italic">
|
|
(Just use the same device)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div
|
|
onClick={this.submitAnswer}
|
|
className="input-popup-submit"
|
|
data-font="sans-bold"
|
|
>
|
|
SUBMIT
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
ReactDOM.render(
|
|
<React.StrictMode>
|
|
<App />
|
|
</React.StrictMode>,
|
|
document.querySelector("#root")
|
|
);
|