diff options
author | Galen Guyer <galen@galenguyer.com> | 2022-07-11 17:10:23 -0400 |
---|---|---|
committer | Galen Guyer <galen@galenguyer.com> | 2022-07-11 17:10:23 -0400 |
commit | 935ef7d2f76cca7bc1f9840dc357de85f93a1ec2 (patch) | |
tree | cb8ee298a747d6084107266e5d2882e60a6b5daa /src | |
parent | 3694bf061ecfe0f6733a0cb9028d044dfc8cf9d2 (diff) |
Update for new dashboard frontend
Diffstat (limited to 'src')
-rw-r--r-- | src/App.css | 49 | ||||
-rw-r--r-- | src/App.js | 215 | ||||
-rw-r--r-- | src/App.jsx | 205 | ||||
-rw-r--r-- | src/App.test.js | 8 | ||||
-rw-r--r-- | src/Card.css | 10 | ||||
-rw-r--r-- | src/Card.js | 25 | ||||
-rw-r--r-- | src/GoatCounter.js | 26 | ||||
-rw-r--r-- | src/History.js | 82 | ||||
-rw-r--r-- | src/HistoryTable.js | 40 | ||||
-rw-r--r-- | src/MainPage.js | 126 | ||||
-rw-r--r-- | src/components/Card.css | 41 | ||||
-rw-r--r-- | src/components/Card.jsx | 18 | ||||
-rw-r--r-- | src/index.css | 8 | ||||
-rw-r--r-- | src/index.js | 22 | ||||
-rw-r--r-- | src/main.jsx | 13 | ||||
-rw-r--r-- | src/pages/Graph.css | 33 | ||||
-rw-r--r-- | src/pages/Graph.jsx | 118 | ||||
-rw-r--r-- | src/pages/Index.css | 32 | ||||
-rw-r--r-- | src/pages/Index.jsx | 76 | ||||
-rw-r--r-- | src/reportWebVitals.js | 13 | ||||
-rw-r--r-- | src/setupTests.js | 5 | ||||
-rw-r--r-- | src/useFetch.js | 17 |
22 files changed, 596 insertions, 586 deletions
diff --git a/src/App.css b/src/App.css index 78106d4..f4846a6 100644 --- a/src/App.css +++ b/src/App.css @@ -1,15 +1,44 @@ .App { text-align: center; - padding-bottom: 2rem; - padding-top: 1rem; - width: 90%; - margin-left: auto; - margin-right: auto; -} + margin: auto; + max-width: 90vw; -.Section { + position: relative; + min-height: 100vh; display: flex; - justify-content: center; - margin-left: auto; - margin-right: auto; + flex-direction: column; +} + +.App h1 { + font-size: 2.4em; + margin: 18px 0px 8px 0px; +} +.Updated .Latest { + font-size: 1.3em; + margin: 0px; +} +.Updated .Prior { + margin: 6px; + color: #666; +} + +.App a { + text-decoration: none; + color: #000; +} + +.BlueLink { + color: #008 !important; +} + +footer { + margin-top: auto; + margin-bottom: 20px; +} + +@media screen and (max-width: 600px) { + .App { + margin: auto; + max-width: 95vw; + } } diff --git a/src/App.js b/src/App.js deleted file mode 100644 index fd5e6d0..0000000 --- a/src/App.js +++ /dev/null @@ -1,215 +0,0 @@ -import React from "react"; -import useSWR from "swr"; -import { DateTime } from "luxon"; -import { BrowserRouter, Route, Switch, Link } from "react-router-dom"; -import MainPage from "./MainPage"; -import History from "./History"; -import HistoryTable from "./HistoryTable"; -import "./App.css"; - -const url = "/data.json"; - -function App() { - let { data: rawData, error: error } = useSWR(url); - - const [timeDifference, setTimeDifference] = React.useState(1); - const [showAllTime, setShowAllTime] = React.useState(false); - - if (error) - return ( - <div className="App"> - <h1>RIT Covid Dashboard</h1> - <h2>An error occurred</h2> - </div> - ); - if (!rawData) - return ( - <div className="App"> - <h1>RIT Covid Dashboard</h1> - <h2>Loading latest data...</h2> - </div> - ); - - // rawData = rawData.slice(0, 177); - let data = rawData; - console.log(data.length); - const local = DateTime.local().zoneName; - const semesterStart = DateTime.fromISO("2021-01-01"); - // if (!showAllTime) { - // data = rawData.filter((d) => { - // let date = DateTime.fromSQL(d.last_updated, { zone: "UTC" }).setZone(local); - // return date > semesterStart; - // }); - // const last = rawData[rawData.length - data.length - 1]; - // data = data.map((d) => { - // return { - // alert_level: d.alert_level, - // beds_available: d.beds_available, - // isolation_off_campus: d.isolation_off_campus, - // isolation_on_campus: d.isolation_on_campus, - // last_updated: d.last_updated, - // new_staff: d.new_staff, - // new_students: d.new_students, - // quarantine_off_campus: d.quarantine_off_campus, - // quarantine_on_campus: d.quarantine_on_campus, - // tests_administered: d.tests_administered - last.tests_administered, - // total_staff: d.total_staff - last.total_staff, - // total_students: d.total_students - last.total_students, - // }; - // }); - // } - - const latest = data[data.length - 1]; - const prior = data[data.length - (1 + timeDifference)]; - return ( - <BrowserRouter> - <div className="App"> - <h1 className="text-4xl"> - <Link to="/">RIT Covid Dashboard</Link> - </h1> - {/* - <h3> - Last Updated:{" "} - {lastUpdate.toLocaleString({ - weekday: "long", - month: "long", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - })} - </h3> - <h4 className="text-sm text-gray-600"> - Prior Update:{" "} - {priorUpdate.toLocaleString({ - weekday: "long", - month: "long", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - })}{" "} - ({timeDifference == 1 ? "one day ago" : timeDifference == 5 ? "one week ago" : "two weeks ago"}) - </h4> - */} - {/* <button - onClick={() => setTimeDifference(timeDifference == 1 ? 5 : timeDifference == 5 ? 10 : 1)} - className="bg-transparent text-sm hover:bg-orange-400 text-gray-600 hover:text-white py-1 my-1 px-2 border border-orange-300 hover:border-transparent rounded transition ease-in-out duration-300" - > - Use {timeDifference == 10 ? "one day" : timeDifference == 5 ? "two weeks" : "one week"} ago - </button> - - <button - onClick={() => setShowAllTime(showAllTime ? false : true)} - className="bg-transparent text-sm hover:bg-orange-400 text-gray-600 hover:text-white py-1 my-1 px-2 border border-orange-300 hover:border-transparent rounded transition ease-in-out duration-300" - > - Show {showAllTime ? "current semester" : "all time"} - </button> */} - <br /> - <Switch> - <Route exact path="/"> - <MainPage data={data} timeDifference={timeDifference} showAllTime={showAllTime} /> - </Route> - <Route path="/totalstudents"> - <History - name="Total Student Cases" - data={data.map((d) => { - return { value: d.total_students, date: d.last_updated }; - })} - /> - </Route> - <Route path="/totalstaff"> - <History - name="Total Staff Cases" - data={data.map((d) => { - return { value: d.total_staff, date: d.last_updated }; - })} - /> - </Route> - <Route path="/newstudents"> - <History - name="New Student Cases" - data={data.map((d) => { - return { value: d.new_students, date: d.last_updated }; - })} - /> - </Route> - <Route path="/newstaff"> - <History - name="New Staff Cases" - data={data.map((d) => { - return { value: d.new_staff, date: d.last_updated }; - })} - /> - </Route> - <Route path="/quarantineoncampus"> - <History - name="Quarantine On Campus" - data={data.map((d) => { - return { value: d.quarantine_on_campus, date: d.last_updated }; - })} - /> - </Route> - <Route path="/quarantineoffcampus"> - <History - name="Quarantine Off Campus" - data={data.map((d) => { - return { value: d.quarantine_off_campus, date: d.last_updated }; - })} - /> - </Route> - - <Route path="/isolationoncampus"> - <History - name="Isolation On Campus" - data={data.map((d) => { - return { value: d.isolation_on_campus, date: d.last_updated }; - })} - /> - </Route> - <Route path="/isolationoffcampus"> - <History - name="Isolation Off Campus" - data={data.map((d) => { - return { value: d.isolation_off_campus, date: d.last_updated }; - })} - /> - </Route> - <Route path="/tests"> - <History - name="Tests Administered" - data={data.map((d) => { - return { value: d.tests_administered, date: d.last_updated }; - })} - /> - </Route> - <Route path="/beds"> - <History - name="Bed Availability" - data={data.map((d) => { - return { value: d.beds_available, date: d.last_updated }; - })} - /> - </Route> - </Switch> - <br /> - <p> - By Galen Guyer. Source available on{" "} - <a className="text-blue-700" href="https://github.com/galenguyer/rit-covid-dashboard"> - GitHub - </a>{" "} - ( - <a className="text-blue-700" href="https://github.com/galenguyer/rit-covid-dashboard/issues"> - Report Issue - </a> - ) - </p> - <p> - <a className="text-blue-700" href="https://galenguyer.com/projects/ritcoviddashboard"> - API Documentation - </a> - </p> - </div> - </BrowserRouter> - ); -} - -export default App; diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..7f1f9c3 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,205 @@ +import { useLocation, Routes, Route, Link } from "react-router-dom"; +import { useState, lazy, Suspense } from "react"; +import useFetch from "./useFetch"; +import { DateTime } from "luxon"; +import "./App.css"; +const Index = lazy(() => import("./pages/Index")); +const Graph = lazy(() => import("./pages/Graph")); +import { useEffect } from "react"; + +const App = () => { + const url = localStorage.getItem("url") ?? "/data.json"; + + let routerLocation = useLocation(); + useEffect(() => { + !window.goatcounter ?? + window.goatcounter.count({ + path: location.pathname + location.search + location.hash, + }); + }, [routerLocation]); + + const response = useFetch(url); + + const [timeDifference, setTimeDifference] = useState(1); + + return ( + <div className="App"> + <header> + <Link to="/"> + <h1>2020-2021 RIT COVID Dashboard</h1> + </Link> + </header> + <Routes> + <Route + path="/totalstudents" + element={ + <Suspense fallback={null}> + <Graph + name={"Total Student Cases"} + response={response} + dataKey={"total_students"} + timeDifference={timeDifference} + /> + </Suspense> + } + ></Route> + <Route + path="/totalstaff" + element={ + <Suspense fallback={null}> + <Graph + name={"Total Staff Cases"} + response={response} + dataKey={"total_staff"} + timeDifference={timeDifference} + /> + </Suspense> + } + ></Route> + <Route + path="/newstudents" + element={ + <Suspense fallback={null}> + <Graph + name={"New Student Cases"} + response={response} + dataKey={"new_students"} + timeDifference={timeDifference} + /> + </Suspense> + } + ></Route> + <Route + path="/newstaff" + element={ + <Suspense fallback={null}> + <Graph + name={"New Staff Cases"} + response={response} + dataKey={"new_staff"} + timeDifference={timeDifference} + /> + </Suspense> + } + ></Route> + <Route + path="/quarantineoncampus" + element={ + <Suspense fallback={null}> + <Graph name={"Quarantine On Campus"} response={response} dataKey={"quarantine_on_campus"} /> + </Suspense> + } + ></Route> + <Route + path="/quarantineoffcampus" + element={ + <Suspense fallback={null}> + <Graph + name={"Quarantine Off Campus"} + response={response} + dataKey={"quarantine_off_campus"} + /> + </Suspense> + } + ></Route> + + <Route + path="/isolationoncampus" + element={ + <Suspense fallback={null}> + <Graph name={"Isolation On Campus"} response={response} dataKey={"isolation_on_campus"} /> + </Suspense> + } + ></Route> + <Route + path="/isolationoffcampus" + element={ + <Suspense fallback={null}> + <Graph name={"Isolation Off Campus"} response={response} dataKey={"isolation_off_campus"} /> + </Suspense> + } + ></Route> + <Route + path="/tests" + element={ + <Suspense fallback={null}> + <Graph name={"Tests Administered"} response={response} dataKey={"tests_administered"} /> + </Suspense> + } + ></Route> + <Route + path="/beds" + element={ + <Suspense fallback={null}> + <Graph name={"Quarantine/Isolation Bed Availability On-campus"} response={response} dataKey={"beds_available"} /> + </Suspense> + } + ></Route> + + <Route + exact + path="/" + element={ + <Suspense fallback={null}> + <Index response={response} timeDifference={timeDifference} /> + </Suspense> + } + /> + </Routes> + <footer> + <p> + By Galen Guyer. Source available on{" "} + <a className="BlueLink" href="https://github.com/galenguyer/rit-covid-dashboard"> + GitHub + </a>{" "} + ( + <a className="BlueLink" href="https://github.com/galenguyer/rit-covid-dashboard/issues"> + Report Issue + </a> + ) + </p> + <p> + <a className="BlueLink" href="https://galenguyer.com/projects/ritcoviddashboard"> + API Documentation + </a> + </p> + </footer> + <script data-goatcounter="https://rcd.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script> + </div> + ); +}; + +const Updated = (props) => { + const { loading, lastUpdate, priorUpdate, timeDifference } = props; + if (loading) { + return <div></div>; + } + + return ( + <div className="Updated"> + <div className="Latest"> + Last Updated:{" "} + {lastUpdate.toLocaleString({ + weekday: "long", + month: "long", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + })} + </div> + <div className="Prior"> + Prior Update:{" "} + {priorUpdate.toLocaleString({ + weekday: "long", + month: "long", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + })}{" "} + ({timeDifference == 1 ? "one weekday ago" : timeDifference == 5 ? "one week ago" : "two weeks ago"}) + </div> + </div> + ); +}; + +export default App; diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index 7c46384..0000000 --- a/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import App from "./App"; - -test("renders learn react link", () => { - render(<App />); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/Card.css b/src/Card.css deleted file mode 100644 index 785b3e5..0000000 --- a/src/Card.css +++ /dev/null @@ -1,10 +0,0 @@ -.Card { - padding: 16px; - width: 40%; -} - -@media screen and (min-width: 768px) { - .Card { - width: 20%; - } -} diff --git a/src/Card.js b/src/Card.js deleted file mode 100644 index a58f611..0000000 --- a/src/Card.js +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; -import { Link } from "react-router-dom"; -import "./Card.css"; - -const Card = (props) => { - let diff = props.diff.toString(); - if (diff.charAt(0) != "-") { - diff = "+" + diff; - } - - return ( - <Link className="Card" style={{ padding: 0 }} to={props.link}> - <div className="group bg-white hover:bg-orange-400 rounded-lg border-2 border-orange-300 hover:border-orange-400 p-2 m-6 transition ease-in-out duration-300"> - <p> - <span className="text-2xl group-hover:text-white transition ease-in-out duration-300"> - {props.latest}{" "} - </span> - </p> - <h3 className="text-base group-hover:text-white transition ease-in-out duration-300">{props.name}</h3> - </div> - </Link> - ); -}; - -export default Card; diff --git a/src/GoatCounter.js b/src/GoatCounter.js deleted file mode 100644 index 07a768a..0000000 --- a/src/GoatCounter.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from "react"; - -class GoatCounter extends React.Component { - componentDidMount() { - window.counter = "https://rcd.goatcounter.com/count"; - const script = window.document.createElement("script"); - script.async = 1; - script.src = "https://gc.zgo.at/count.js"; - script.id = "goatcounter"; - script.setAttribute("data-goatcounter", "https://rcd.goatcounter.com/count"); - (window.document.head || window.document.body).appendChild(script); - } - - componentWillUnmount() { - const script = window.document.getElementById("goatcounter"); - if (script) { - script.parentNode.removeChild(script); - } - } - - render() { - return null; - } -} - -export default GoatCounter; diff --git a/src/History.js b/src/History.js deleted file mode 100644 index 7425b61..0000000 --- a/src/History.js +++ /dev/null @@ -1,82 +0,0 @@ -import { React, PureComponent } from "react"; -import { DateTime } from "luxon"; -import { - BarChart, - Bar, - LineChart, - Line, - CartesianGrid, - XAxis, - YAxis, - Tooltip, - ResponsiveContainer, - Label, -} from "recharts"; -import GoatCounter from "./GoatCounter"; - -const History = (props) => { - const offset = DateTime.fromSQL(props.data[0].date, { zone: "UTC" }).setZone(DateTime.local().zoneName).toSeconds(); - const data = props.data.map((d) => { - return { - value: d.value, - date: DateTime.fromSQL(d.date, { zone: "UTC" }).setZone(DateTime.local().zoneName).toSeconds(), - }; - }); - - return ( - <> - <h3 className="text-3xl">{props.name}</h3> - <LineChart - style={{ marginLeft: "auto", marginRight: "auto" }} - width={730} - height={500} - margin={{ top: 15, right: 30, left: 20, bottom: 5 }} - data={data} - > - <Line type="monotone" dataKey="value" stroke="#CD8508" dot={false} /> - <CartesianGrid strokeDasharray="3 3" /> - <XAxis - dataKey="date" - type="number" - tickCount={14} - domain={["dataMin", "dataMax"]} - tick={<CustomizedAxisTick />} - height={90} - /> - <YAxis dataKey="value" type="number"></YAxis> - <Tooltip content={CustomTooltip} /> - </LineChart> - <GoatCounter /> - </> - ); -}; - -class CustomizedAxisTick extends PureComponent { - render() { - const { x, y, payload } = this.props; - - return ( - <g transform={`translate(${x},${y})`}> - <text className="Graph-Label" x={0} y={0} dy={16} textAnchor="end" fill="#666" transform="rotate(-40)"> - {DateTime.fromSeconds(payload.value).toLocaleString()} - </text> - </g> - ); - } -} - -const CustomTooltip = ({ active, payload, label }) => { - if (active) { - return ( - <div className="custom-tooltip bg-white border-orange-300 border-2 rounded-lg p-2"> - <p className="label"> - {DateTime.fromSeconds(label).toLocaleString({ weekday: "long", month: "long", day: "2-digit" })} - </p> - <p className="desc">{payload[0].value}</p> - </div> - ); - } - return null; -}; - -export default History; diff --git a/src/HistoryTable.js b/src/HistoryTable.js deleted file mode 100644 index fb27f0e..0000000 --- a/src/HistoryTable.js +++ /dev/null @@ -1,40 +0,0 @@ -import { React } from "react"; -import { DateTime } from "luxon"; -import GoatCounter from "./GoatCounter"; - -const HistoryTable = (props) => { - const data = props.data; - console.log(data); - let table = ( - <table className="table-auto" style={{ marginLeft: "auto", marginRight: "auto" }}> - <tbody> - <tr> - <td className="border py-2 px-4">Date</td> - <td className="border py-2 px-4">Positive Case Rate</td> - </tr> - {data.map((element) => { - return ( - <tr> - <td className="border px-4" py-2> - {DateTime.fromSQL(element.date, { zone: "UTC" }) - .setZone(DateTime.local().zoneName) - .toLocaleString({ weekday: "long", month: "long", day: "2-digit" })} - </td> - <td className="border px-4 py-2">{element.value}%</td> - </tr> - ); - })} - </tbody> - </table> - ); - - return ( - <> - <h3 className="text-3xl">{props.name}</h3> - {table} - <GoatCounter /> - </> - ); -}; - -export default HistoryTable; diff --git a/src/MainPage.js b/src/MainPage.js deleted file mode 100644 index fcc2e0b..0000000 --- a/src/MainPage.js +++ /dev/null @@ -1,126 +0,0 @@ -import React from "react"; -import Card from "./Card"; -import GoatCounter from "./GoatCounter"; - -const MainPage = (props) => { - const data = props.data; - const latest = data[data.length - 1]; - const prior = data[data.length - (1 + props.timeDifference)]; - - return ( - <> - <h2 className="text-xl"> - This site shows data from the 2020 Fall and 2021 Spring semesters. - </h2> - <br /> - <div id="total"> - <h4 className="text-2xl"> - Total Positive Cases Since August 19 (First Day of Classes) - </h4> - <div className="Section"> - <Card - name="Students" - latest={latest.total_students} - diff={latest.total_students - prior.total_students} - link="/totalstudents" - /> - <Card - name="Staff" - latest={latest.total_staff} - diff={latest.total_staff - prior.total_staff} - link="/totalstaff" - /> - </div> - </div> - <br /> - <div id="new"> - <h4 className="text-2xl">New Positive Cases From Past 14 Days</h4> - <div className="Section"> - <Card - name="Students" - latest={latest.new_students} - diff={latest.new_students - prior.new_students} - link="/newstudents" - /> - <Card - name="Staff" - latest={latest.new_staff} - diff={latest.new_staff - prior.new_staff} - link="/newstaff" - /> - </div> - </div> - <br /> - <div id="quarantine"> - <h4 className="text-2xl">Number of Students in Quarantine</h4> - <h5 className="text-base"> - Quarantine separates and restricts the movement of people who were exposed to a contagious disease - to see if they become sick. - </h5> - <div className="Section"> - <Card - name="On Campus" - latest={latest.quarantine_on_campus} - diff={latest.quarantine_on_campus - prior.quarantine_on_campus} - link="/quarantineoncampus" - /> - <Card - name="Off Campus" - latest={latest.quarantine_off_campus} - diff={latest.quarantine_off_campus - prior.quarantine_off_campus} - link="/quarantineoffcampus" - /> - </div> - </div> - <br /> - <div id="isolation"> - <h4 className="text-2xl">Number of Students in Isolation</h4> - <h5 className="text-base"> - Isolation separates sick people with a contagious disease from people who are not sick. - </h5> - <div className="Section"> - <Card - name="On Campus" - latest={latest.isolation_on_campus} - diff={latest.isolation_on_campus - prior.isolation_on_campus} - link="isolationoncampus" - /> - <Card - name="Off Campus" - latest={latest.isolation_off_campus} - diff={latest.isolation_off_campus - prior.isolation_off_campus} - link="isolationoffcampus" - /> - </div> - </div> - <br /> - <div id="tests"> - <h4 className="text-2xl">Tests</h4> - <div className="Section"> - <Card - name="Tests Administered" - latest={latest.tests_administered} - diff={latest.tests_administered - prior.tests_administered} - link="/tests" - /> - </div> - </div> - <br /> - <div id="beds"> - <h4 className="text-2xl">Quarantine/Isolation Bed Availability On-campus</h4> - <div className="Section"> - <Card - name="Beds Available" - latest={latest.beds_available + "%"} - diff={latest.beds_available - prior.beds_available + "%"} - suffix="%" - link="/beds" - /> - </div> - </div> - <GoatCounter /> - </> - ); -}; - -export default MainPage; diff --git a/src/components/Card.css b/src/components/Card.css new file mode 100644 index 0000000..fddfe22 --- /dev/null +++ b/src/components/Card.css @@ -0,0 +1,41 @@ +.cardLink { + text-decoration: none; + color: black; + margin: 12px 24px; + flex-basis: 100%; + max-width: 200px; +} + +.Card { + text-decoration: none; + color: #000; + border: 2px solid #fbd38d; + border-radius: 12px; + padding: 0px 40px; +} + +@media screen and (max-width: 600px) { + .Card { + padding: 0px 16px; + margin: 10px 0px; + } +} + +.Latest { + font-size: 1.8em; +} + +.Card:hover { + background-color: #ffc869; + color: #fff; +} +.Card:hover .Diff { + color: #fff; +} + +.animate { + -moz-transition: color, background-color 0.3s; + -webkit-transition: color, background-color 0.3s; + -ms-transition: color, background-color 0.3s; + transition: color, background-color 0.3s; +} diff --git a/src/components/Card.jsx b/src/components/Card.jsx new file mode 100644 index 0000000..0535033 --- /dev/null +++ b/src/components/Card.jsx @@ -0,0 +1,18 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import "./Card.css"; + +const Card = (props) => { + return ( + <Link className="cardLink" to={props.link}> + <div className="Card animate"> + <p> + <span className="Latest">{props.latest}{props.suffix ?? ""}</span> + </p> + <h3>{props.name}</h3> + </div> + </Link> + ); +}; + +export default Card; diff --git a/src/index.css b/src/index.css index 868b6c6..a7ae2da 100644 --- a/src/index.css +++ b/src/index.css @@ -1,7 +1,3 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", @@ -13,3 +9,7 @@ body { code { font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } + +* { + font-weight: 400; +} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 4c1dc74..0000000 --- a/src/index.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom"; -import { SWRConfig } from "swr"; -import "./index.css"; -import App from "./App"; -import reportWebVitals from "./reportWebVitals"; - -const fetcher = (...args) => fetch(...args).then((res) => res.json()); - -ReactDOM.render( - <React.StrictMode> - <SWRConfig value={{ fetcher }}> - <App /> - </SWRConfig> - </React.StrictMode>, - document.getElementById("root") -); - -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..8601f97 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import { BrowserRouter } from "react-router-dom"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./index.css"; + +ReactDOM.createRoot(document.getElementById("root")).render( + <React.StrictMode> + <BrowserRouter> + <App /> + </BrowserRouter> + </React.StrictMode> +); diff --git a/src/pages/Graph.css b/src/pages/Graph.css new file mode 100644 index 0000000..8cb98ee --- /dev/null +++ b/src/pages/Graph.css @@ -0,0 +1,33 @@ +.Title { + font-size: 2em; + margin-top: 24px; +} + +.ToTheMoon { + -webkit-animation-duration: 0.5s; + animation-duration: 0.5s; + -webkit-animation-delay: 1.5s; + animation-delay: 1.5s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + -webkit-animation-name: fadeIn; + animation-name: fadeIn; +} + +@-webkit-keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} diff --git a/src/pages/Graph.jsx b/src/pages/Graph.jsx new file mode 100644 index 0000000..bbfa43d --- /dev/null +++ b/src/pages/Graph.jsx @@ -0,0 +1,118 @@ +import { DateTime } from "luxon"; +import { + LineChart, + Line, + CartesianGrid, + XAxis, + YAxis, + Tooltip, + ReferenceLine, + ReferenceDot, + Label, +} from "recharts"; +import "./Graph.css"; + +const Graph = (props) => { + const { name, response, dataKey } = props; + const { data, loading, error } = response; + + if (loading) { + return <div></div>; + } + + const eventStyle = { fill: "#767676" }; + + const parsed = data.map((d) => { + return { + date: DateTime.fromSQL(d["last_updated"], { zone: "UTC" }).setZone(DateTime.local().zoneName).toSeconds(), + value: d[dataKey], + }; + }); + + const latest = parsed[parsed.length - 1]; + const prior = parsed[parsed.length - 2]; + const toTheMoon = latest.value > prior.value + 1; + + return ( + <div> + <div className="Title">{name}</div> + <LineChart + style={{ marginLeft: "auto", marginRight: "auto" }} + width={window.innerWidth > 600 ? 750 : window.innerWidth * 0.9} + height={500} + margin={{ top: 15, right: 30, left: 0, bottom: 5 }} + data={parsed} + > + <Line type="monotone" dataKey="value" stroke="#CD8508" dot={false} /> + <ReferenceLine + x={1644594525} + label={{ + value: "Visitor Policy adjusted", + angle: -90, + style: eventStyle, + position: "left", + }} + /> + <ReferenceLine + x={1647550274} + label={{ + value: "Mask Mandate dropped", + angle: -90, + style: eventStyle, + position: "left", + }} + /> + {} + <CartesianGrid strokeDasharray="3 3" /> + {toTheMoon ? ( + <ReferenceDot + x={latest["date"]} + y={latest["value"]} + r={0} + label={<Label className="ToTheMoon">🚀</Label>} + /> + ) : null} + <XAxis + dataKey="date" + type="number" + tickCount={14} + domain={["dataMin", "dataMax"]} + tick={<CustomizedAxisTick />} + height={90} + /> + <YAxis dataKey="value" type="number"></YAxis> + <Tooltip content={CustomTooltip} /> + </LineChart> + </div> + ); +}; + +const CustomizedAxisTick = ({ x, y, payload }) => { + return ( + <g transform={`translate(${x},${y})`}> + <text className="Graph-Label" x={0} y={0} dy={16} textAnchor="end" fill="#666" transform="rotate(-40)"> + {DateTime.fromSeconds(payload.value).toLocaleString()} + </text> + </g> + ); +}; + +const CustomTooltip = ({ active, payload, label }) => { + if (active) { + return ( + <div className="custom-tooltip bg-white border-orange-300 border-2 rounded-lg p-2"> + <p className="label"> + {DateTime.fromSeconds(label).toLocaleString({ + weekday: "long", + month: "long", + day: "2-digit", + })} + </p> + <p className="desc">{payload[0].value}</p> + </div> + ); + } + return null; +}; + +export default Graph; diff --git a/src/pages/Index.css b/src/pages/Index.css new file mode 100644 index 0000000..3816796 --- /dev/null +++ b/src/pages/Index.css @@ -0,0 +1,32 @@ +.Message { + margin-top: 24px; + font-size: 1.4em; +} + +@media screen and (max-width: 600px) { + .Message { + display: none; + } +} + +.Section .Title { + text-align: center; + font-size: 1.6em; + flex-basis: 100%; + margin-top: 48px; +} + +@media screen and (max-width: 600px) { + .Section .Title { + margin-top: 12px; + } +} + +.Cards { + display: flex; + justify-content: center; +} + +.Tip { + margin-top: 2px; +}
\ No newline at end of file diff --git a/src/pages/Index.jsx b/src/pages/Index.jsx new file mode 100644 index 0000000..78c32f0 --- /dev/null +++ b/src/pages/Index.jsx @@ -0,0 +1,76 @@ +import Card from "../components/Card"; +import "./Index.css"; + +const Index = (props) => { + const response = props.response; + if (response.loading) { + return <div>Loading...</div>; + } + + const data = response.data; + + const latest = data[data.length - 1]; + + return ( + <div> + <div> + <div className="Message"> + This site shows data from the 2020 Fall and 2021 Spring semesters. For the latest data, visit{" "} + <a href="https://ritcoviddashboard.com/">ritcoviddashboard.com</a> + </div> + </div> + <div className="Section" id="total"> + <div className="Title">Total Positive Cases Since August 19 (First Day of Classes)</div> + <div className="Cards"> + <Card name="Students" link="/totalstudents" latest={latest["total_students"]} /> + <Card name="Staff" link="/totalstaff" latest={latest["total_staff"]} /> + </div> + </div> + + <div className="Section" id="new"> + <div className="Title">New Positive Cases From Past 14 Days</div> + <div className="Cards"> + <Card name="Students" link="/newstudents" latest={latest["new_students"]} /> + <Card name="Staff" link="/newstaff" latest={latest["new_staff"]} /> + </div> + </div> + + <div className="Section" id="quarantine"> + <div className="Title">Number of Students in Quarantine</div> + <p className="Tip"> + Quarantine separates and restricts the movement of people who were exposed to a contagious disease + to see if they become sick. + </p> + <div className="Cards"> + <Card name="On Campus" link="/quarantineoncampus" latest={latest["quarantine_on_campus"]} /> + <Card name="Off Campus" link="/quarantineoffcampus" latest={latest["quarantine_off_campus"]} /> + </div> + </div> + + <div className="Section" id="isolation"> + <div className="Title">Number of Students in Isolation</div> + <p className="Tip">Isolation separates sick people with a contagious disease from people who are not sick.</p> + <div className="Cards"> + <Card name="On Campus" link="/isolationoncampus" latest={latest["isolation_on_campus"]} /> + <Card name="Off Campus" link="/isolationoffcampus" latest={latest["isolation_off_campus"]} /> + </div> + </div> + + <div className="Section" id="tests"> + <div className="Title">Tests</div> + <div className="Cards"> + <Card name="Tests Administered" link="/tests" latest={latest["tests_administered"]} /> + </div> + </div> + + <div className="Section" id="beds"> + <div className="Title">Quarantine/Isolation Bed Availability On-campus</div> + <div className="Cards"> + <Card name="Beds Available" link="/beds" latest={latest["beds_available"]} suffix="%" /> + </div> + </div> + </div> + ); +}; + +export default Index; diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js deleted file mode 100644 index 7dc6b90..0000000 --- a/src/reportWebVitals.js +++ /dev/null @@ -1,13 +0,0 @@ -const reportWebVitals = (onPerfEntry) => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/src/setupTests.js b/src/setupTests.js deleted file mode 100644 index 1dd407a..0000000 --- a/src/setupTests.js +++ /dev/null @@ -1,5 +0,0 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import "@testing-library/jest-dom"; diff --git a/src/useFetch.js b/src/useFetch.js new file mode 100644 index 0000000..d098e2f --- /dev/null +++ b/src/useFetch.js @@ -0,0 +1,17 @@ +import { useEffect, useState } from "react"; + +export default function useFetch(url, options) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetch(url, options) + .then((resp) => resp.json()) + .then((resp) => setData(resp)) + .catch((err) => setError(err)) + .finally(() => setLoading(false)); + }, []); + + return { data, loading, error }; +} |