diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/App.css | 44 | ||||
-rw-r--r-- | src/App.js | 229 | ||||
-rw-r--r-- | src/App.jsx | 142 | ||||
-rw-r--r-- | src/App.test.js | 8 | ||||
-rw-r--r-- | src/Card.css | 10 | ||||
-rw-r--r-- | src/Card.js | 28 | ||||
-rw-r--r-- | src/History.js | 89 | ||||
-rw-r--r-- | src/HistoryTable.js | 40 | ||||
-rw-r--r-- | src/MainPage.js | 166 | ||||
-rw-r--r-- | src/components/Card.css | 43 | ||||
-rw-r--r-- | src/components/Card.jsx | 25 | ||||
-rw-r--r-- | src/components/GoatCounter.jsx (renamed from src/GoatCounter.js) | 0 | ||||
-rw-r--r-- | src/index.css | 22 | ||||
-rw-r--r-- | src/index.js | 22 | ||||
-rw-r--r-- | src/main.jsx | 10 | ||||
-rw-r--r-- | src/pages/Graph.css | 4 | ||||
-rw-r--r-- | src/pages/Graph.jsx | 96 | ||||
-rw-r--r-- | src/pages/Index.css | 22 | ||||
-rw-r--r-- | src/pages/Index.jsx | 66 | ||||
-rw-r--r-- | src/reportWebVitals.js | 13 | ||||
-rw-r--r-- | src/setupTests.js | 5 | ||||
-rw-r--r-- | src/useFetch.js | 17 |
22 files changed, 470 insertions, 631 deletions
diff --git a/src/App.css b/src/App.css index 78106d4..50e39d9 100644 --- a/src/App.css +++ b/src/App.css @@ -1,15 +1,37 @@ .App { - text-align: center; - padding-bottom: 2rem; - padding-top: 1rem; - width: 90%; - margin-left: auto; - margin-right: auto; + text-align: center; + margin: auto; + max-width: 90vw; + + position: relative; + min-height: 100vh; + display: flex; + 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; } -.Section { - display: flex; - justify-content: center; - margin-left: auto; - margin-right: auto; +.BlueLink { + color: #008 !important; } + +footer { + margin-top: auto; + margin-bottom: 20px; +}
\ No newline at end of file diff --git a/src/App.js b/src/App.js deleted file mode 100644 index 3edd599..0000000 --- a/src/App.js +++ /dev/null @@ -1,229 +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 = "https://ritcoviddashboard.com/api/v0/history"; - -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)]; - const lastUpdate = DateTime.fromSQL(latest.last_updated).setZone(local); - const priorUpdate = DateTime.fromSQL(prior.last_updated).setZone(local); - // let positiveCases = []; - // for (let i = 5; i < data.length; i++) { - // positiveCases.push({ - // date: data[i].last_updated, - // value: ( - // ((data[i].total_students - data[i - 5].total_students) * 100) / - // (data[i].tests_administered - data[i - 5].tests_administered) - // ).toFixed(1), - // }); - // } - // positiveCases = positiveCases.filter((o) => o.value > 0 && o.value <= 100); - 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="/positivetests"> - <History name="Positive Test Rate (Over One Week)" data={positiveCases} /> - </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..af8f570 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,142 @@ +import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; +import { useState } from "react"; +import useFetch from "./useFetch"; +import Index from "./pages/Index"; +import { DateTime } from "luxon"; +import "./App.css"; +import Graph from "./pages/Graph"; + +const App = () => { + const url = localStorage.getItem("url") ?? "https://ritcoviddashboard.com/api/v0/history"; + + const response = useFetch(url); + + const [timeDifference, setTimeDifference] = useState(1); + const local = DateTime.local().zoneName; + + let data = response.data ?? []; + + const latest = response.loading ? null : data[data.length - 1]; + const prior = response.loading ? null : data[data.length - (1 + timeDifference)]; + + const lastUpdate = response.loading ? null : DateTime.fromSQL(latest.last_updated).setZone(local); + const priorUpdate = response.loading ? null : DateTime.fromSQL(prior.last_updated).setZone(local); + + return ( + <BrowserRouter> + <div className="App"> + <header> + <Link to="/"> + <h1>RIT COVID Dashboard</h1> + </Link> + <Updated + loading={response.loading} + lastUpdate={lastUpdate} + priorUpdate={priorUpdate} + timeDifference={timeDifference} + /> + </header> + <Routes> + <Route + path="/totalstudents" + element={ + <Graph + name={"Total Student Cases"} + response={response} + dataKey={"total_students"} + timeDifference={timeDifference} + /> + } + ></Route> + <Route + path="/totalstaff" + element={ + <Graph + name={"Total Staff Cases"} + response={response} + dataKey={"total_staff"} + timeDifference={timeDifference} + /> + } + ></Route> + <Route + path="/newstudents" + element={ + <Graph + name={"New Student Cases"} + response={response} + dataKey={"new_students"} + timeDifference={timeDifference} + /> + } + ></Route> + <Route + path="/newstaff" + element={ + <Graph + name={"New Staff Cases"} + response={response} + dataKey={"new_staff"} + timeDifference={timeDifference} + /> + } + ></Route> + <Route exact path="/" element={<Index response={response} timeDifference={timeDifference} />} /> + </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> + </div> + </BrowserRouter> + ); +}; + +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 14b4282..0000000 --- a/src/Card.js +++ /dev/null @@ -1,28 +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> - <span className="Diff text-gray-600 group-hover:text-gray-100 text-sm transition ease-in-out duration-300"> - ({diff}) - </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/History.js b/src/History.js deleted file mode 100644 index 58085ac..0000000 --- a/src/History.js +++ /dev/null @@ -1,89 +0,0 @@ -import { React, PureComponent } from "react"; -import { DateTime } from "luxon"; -import { - BarChart, - Bar, - LineChart, - Line, - CartesianGrid, - XAxis, - YAxis, - Tooltip, - ReferenceLine, - 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(), - }; - }); - - if (process.env.NODE_ENV == "development") { - console.log(data); - } - - 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} /> - {/* <ReferenceLine x={1644594525} label={{ value: "Visitor Policy adjusted", angle: -90, position: 'center' }} /> */} - <ReferenceLine x={1647550274} label={{ value: "Mask Mandate dropped", angle: -90, position: 'left' }} /> - <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 b663521..0000000 --- a/src/MainPage.js +++ /dev/null @@ -1,166 +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)]; - // const priorPrior = data[Math.max(0, data.length - (1 + props.timeDifference * 2))]; - - // const positiveTestRate = Math.max( - // 0, - // Math.min( - // 100, - // ((latest.total_students - prior.total_students) * 100) / - // (latest.tests_administered - prior.tests_administered) - // ) - // ).toFixed(1); - // const priorPositiveTestRate = Math.max( - // 0, - // Math.min( - // 100, - // ((prior.total_students - priorPrior.total_students) * 100) / - // (prior.tests_administered - priorPrior.tests_administered) - // ) - // ).toFixed(1); - - return ( - <> - {/* <h4 className="text-2xl"> - Alert Level: {latest.alert_level.charAt(0).toUpperCase() + latest.alert_level.slice(1)} - </h4> - <h5 className="text-gray-600 text-sm"> - (Prior Alert Level: {prior.alert_level.charAt(0).toUpperCase() + prior.alert_level.slice(1)}) - </h5> - <br /> */} - <h2 className="text-xl"> - This dashboard has been refreshed for the new semester. Historical data from the 2020-2021 school year is - available at <a href="//2020.ritcoviddashboard.com">2020.ritcoviddashboard.com</a>. Data from the Fall 2021 - semester is available at <a href="//2021.ritcoviddashboard.com">2021.ritcoviddashboard.com</a>. - </h2> - <br /> - <div id="total"> - <h4 className="text-2xl"> - {/* Total Positive Cases Since {props.showAllTime ? "August 19 (First Day of Classes)" : "January 1"} */} - Total Positive Cases Since January 10 (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> - <h5 className="text-base"> - Positive Test Rate is calculated using the difference in total cases divided by the difference in - tests administed for the selected time frame (one day or one week). The daily positive test rate - fluctuates wildly and should be taken with caution, while the weekly positive test rate is far more - stable and useful. - </h5> - <div className="Section"> - <Card - name="Tests Administered" - latest={latest.tests_administered} - diff={latest.tests_administered - prior.tests_administered} - link="/tests" - /> - <Card - name="Positive Test Rate" - latest={positiveTestRate + "%"} - diff={(positiveTestRate - priorPositiveTestRate).toFixed(1) + "%"} - link="/positivetests" - /> - </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..cca784b --- /dev/null +++ b/src/components/Card.css @@ -0,0 +1,43 @@ +.cardLink { + text-decoration: none; + color: black; +} + +.Card { + text-decoration: none; + color: #000; + border: 2px solid #fbd38d; + border-radius: 12px; + padding: 0px 40px; + margin: 24px; +} + +@media screen and (max-width: 600px) { + .Card { + padding: 0px 20px; + margin: 18px; + } +} + +.Latest { + font-size: 1.8em; +} + +.Diff { + color: #666; +} + +.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..95bd583 --- /dev/null +++ b/src/components/Card.jsx @@ -0,0 +1,25 @@ +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="cardLink" to={props.link}> + <div className="Card animate"> + <p> + <span className="Latest">{props.latest}</span> <span className="Diff animate">({diff})</span> + </p> + <h3> + {props.name} + </h3> + </div> + </Link> + ); +}; + +export default Card; diff --git a/src/GoatCounter.js b/src/components/GoatCounter.jsx index 07a768a..07a768a 100644 --- a/src/GoatCounter.js +++ b/src/components/GoatCounter.jsx diff --git a/src/index.css b/src/index.css index 868b6c6..c4c1ceb 100644 --- a/src/index.css +++ b/src/index.css @@ -1,15 +1,17 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", - "Droid Sans", "Helvetica Neue", sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; } + +* { + font-weight: 400; +}
\ No newline at end of file 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..9af0bb6 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + <React.StrictMode> + <App /> + </React.StrictMode> +) diff --git a/src/pages/Graph.css b/src/pages/Graph.css new file mode 100644 index 0000000..c28c8bd --- /dev/null +++ b/src/pages/Graph.css @@ -0,0 +1,4 @@ +.Title { + font-size: 2.0em; + margin-top: 24px; +} diff --git a/src/pages/Graph.jsx b/src/pages/Graph.jsx new file mode 100644 index 0000000..6754905 --- /dev/null +++ b/src/pages/Graph.jsx @@ -0,0 +1,96 @@ +import { DateTime } from "luxon"; +import { + BarChart, + Bar, + LineChart, + Line, + CartesianGrid, + XAxis, + YAxis, + Tooltip, + ReferenceLine, + ResponsiveContainer, + Label, +} from "recharts"; +import GoatCounter from "../components/GoatCounter"; +import "./Graph.css"; + +const Graph = (props) => { + const { name, response, dataKey, timeDifference } = 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], + }; + }); + + 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" /> + <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 /> + </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..5476eb1 --- /dev/null +++ b/src/pages/Index.css @@ -0,0 +1,22 @@ +.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; +} + +.Cards { + display: flex; + justify-content: center; +} diff --git a/src/pages/Index.jsx b/src/pages/Index.jsx new file mode 100644 index 0000000..3beb6fd --- /dev/null +++ b/src/pages/Index.jsx @@ -0,0 +1,66 @@ +import Card from "../components/Card"; +import GoatCounter from "../components/GoatCounter"; +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]; + const prior = data[data.length - (1 + props.timeDifference)]; + + return ( + <div> + <div> + <div className="Message"> + This dashboard has been refreshed for the new semester. Historical data from the 2020-2021 school + year is available at <a href="//2020.ritcoviddashboard.com">2020.ritcoviddashboard.com</a>. Data + from the Fall 2021 semester is available at{" "} + <a href="//2021.ritcoviddashboard.com">2021.ritcoviddashboard.com</a>. + </div> + </div> + <div className="Section" id="total"> + <div className="Title">Total Positive Cases Since January 10 (First Day of Classes)</div> + <div className="Cards"> + <Card + name="Students" + link="/totalstudents" + latest={latest["total_students"]} + diff={latest["total_students"] - prior["total_students"]} + /> + <Card + name="Staff" + link="/totalstaff" + latest={latest["total_staff"]} + diff={latest["total_staff"] - prior["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"]} + diff={latest["new_students"] - prior["new_students"]} + /> + <Card + name="Staff" + link="/newstaff" + latest={latest["new_staff"]} + diff={latest["new_staff"] - prior["new_staff"]} + /> + </div> + </div> + <GoatCounter /> + </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 }; +} |