aboutsummaryrefslogtreecommitdiff
path: root/react-frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'react-frontend/src')
-rw-r--r--react-frontend/src/App.js161
-rw-r--r--react-frontend/src/App.test.js8
-rw-r--r--react-frontend/src/components/DateSelector.js17
-rw-r--r--react-frontend/src/components/Hub.js24
-rw-r--r--react-frontend/src/components/HubList.js63
-rw-r--r--react-frontend/src/components/InvestorInfo.js66
-rw-r--r--react-frontend/src/components/Loading.js20
-rw-r--r--react-frontend/src/components/Modal.js70
-rw-r--r--react-frontend/src/components/TimeSelector.js48
-rw-r--r--react-frontend/src/components/Visualization.js103
-rw-r--r--react-frontend/src/components/WatchDogs.js89
-rw-r--r--react-frontend/src/components/images/logo.pngbin0 -> 43282 bytes
-rw-r--r--react-frontend/src/components/images/person.svg1
-rw-r--r--react-frontend/src/css/App.css86
-rw-r--r--react-frontend/src/css/Canvas.css7
-rw-r--r--react-frontend/src/css/CoordSelector.css52
-rw-r--r--react-frontend/src/css/Landing.css256
-rw-r--r--react-frontend/src/css/Modal.css146
-rw-r--r--react-frontend/src/css/Route.css56
-rw-r--r--react-frontend/src/css/UserCheckin.css94
-rw-r--r--react-frontend/src/images/mainlogo.pngbin0 -> 269065 bytes
-rw-r--r--react-frontend/src/images/previewwatchdog.pngbin0 -> 321433 bytes
-rw-r--r--react-frontend/src/images/reagan.pngbin0 -> 2150995 bytes
-rw-r--r--react-frontend/src/index.css14
-rw-r--r--react-frontend/src/index.js17
-rw-r--r--react-frontend/src/logo.svg1
-rw-r--r--react-frontend/src/reportWebVitals.js13
-rw-r--r--react-frontend/src/setupTests.js5
28 files changed, 1417 insertions, 0 deletions
diff --git a/react-frontend/src/App.js b/react-frontend/src/App.js
new file mode 100644
index 0000000..a639edd
--- /dev/null
+++ b/react-frontend/src/App.js
@@ -0,0 +1,161 @@
+// React/component imports
+import React, {useEffect, useState} from 'react';
+import WatchDogs from './components/WatchDogs.js';
+import reagan from './images/reagan.png';
+import previewwatchdog from './images/previewwatchdog.png';
+import mainlogo from './images/mainlogo.png';
+
+
+import './css/Landing.css';
+
+function App() {
+ const [startApp, setStartApp] = useState(false);
+
+ const startModal = () => {
+ document.getElementById("main-modal").style.display = 'block';
+ setStartApp(false);
+ }
+ const exitModal = () => {
+ document.getElementById("main-modal").style.display = 'none';
+ setStartApp(true);
+ }
+
+
+ return (
+ <>
+
+ {(!startApp) ?
+ <div className="body" id="main-modal">
+ <div class="nav-bar">
+ <div class="topnav">
+ <a href="#team">Team</a>
+ <a href="#app-intro">About</a>
+ <a href="#intro">Home</a>
+ </div>
+ </div>
+
+ <main>
+
+ <section id="intro" class="intro">
+ <img src={mainlogo} alt="logo"></img>
+ <button id="enter-watchdogs" onClick={exitModal}>ENTER</button>
+ </section>
+
+ <section id="1" class="app-preview">
+ <img id="preview" src={previewwatchdog}></img>
+ <span id="preview-text">preview</span>
+ </section>
+
+ <section id="app-intro">
+ <h1 class="heading">About Our App</h1>
+ <p></p>
+ <p class="text">WatchDogs utilizes a simple,
+ interactive interface to provide you with the latest
+ data relating to high-profile investors’ trades. Directly from
+ the SEC, WatchDogs relays information regarding “inside” investors
+ and their recent trades, as well as provides computed values such
+ as an individual’s net profit from a stock at time of trade and a
+ ranking of individuals most likely recently involved in insider trading.
+ The computed “suspicion ranks” are determined using multiple factors
+ (including recent trade profitability and investor connectedness)
+ and a complex algorithm. For added convenience, YOU choose the
+ timeframe WatchDogs considers when analyzing trade data. WatchDogs
+ makes insider trade data accessible to the public, and provides
+ low-level intuition regarding which investors are more and less
+ likely to be committing unlawful insider trading.</p>
+ <div class="appinfotxt">It is important to remember that suspicion
+ ranks returned by WatchDogs’ algorithm DO NOT prove--or even suggest--that
+ an individual has engaged in insider trading. WatchDogs suspicion
+ ranks should not be interpreted as indication of an individual’s
+ participation in illegal activity. WatchDogs data, including suspicion ranks,
+ cannot be used as evidence in legal proceedings. Please use WatchDogs
+ as it is intended, and use discretion when interpreting algorithmic
+ results.</div>
+ <p></p>
+ <p class="text h2">The Data</p>
+ <div class="text">WatchDogs uses data retrieved from SEC.gov’s
+ EDGAR API. Trades analyzed by WatchDogs are of the type Form 4,
+ meaning the filing individual is an “insider” (e.g. the CEO) at
+ the company whose stock they are trading. In-app data relating
+ to investors, trades, and profitability is accessible to the public via
+ the SEC, and only public data is input to the SuspicionRank algorithm.</div>
+ <p></p>
+ <p class="text h2">Our Algorithm</p>
+ <div class="text">WatchDogs suspicion rank represents the likelihood
+ of an individual being involved in insider trading. The SuspicionRank algorithm,
+ a derivative of Lary Page’s PageRank algorithm, considers “insiders” who similarly
+ trade stocks (within a given timeframe) as “linked”, while simultaneously
+ considering involved individuals’ net profit on their stocks at the time
+ of trade. (Obviously, an “inside” investor who makes a counter-productive
+ trade is not likely to be insider trading.)</div>
+ <p></p><p></p><p></p><p></p><p></p>
+ </section>
+
+ <section id="team">
+ <div class="team">
+ <div id="team-holder" class="center">
+ <h1 id="team-heading" class="heading">Meet the Team</h1>
+ <div id="people-holder">
+ <div class="team-person-holder">
+ <div class="team-person">
+ <img src="" height="100%"></img>
+ </div>
+ <p class="team-text">Clark Oh-Willeke</p>
+ </div>
+
+ <div class="team-person-holder">
+ <div class="team-person">
+ <img src="" height="100%"></img>
+ </div>
+ <p class="team-text">Julia McCauley</p>
+ </div>
+
+ <div class="team-person-holder">
+ <div class="team-person">
+ <img src="" height="100%"></img>
+ </div>
+ <p class="team-text">Michael Foiani</p>
+ </div>
+
+ <div class="team-person-holder">
+ <div class="team-person">
+ <img src={reagan} height="100%"></img>
+ </div>
+ <p class="team-text">Reagan Hunt</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ </main>
+
+ <footer id="footer">
+ <p></p>
+ <div class="footer-item">Disclaimer</div>
+ <p></p>
+ <div class="footer-subtext">Suspicion ranks returned by <span>WatchDogs</span>’
+ algorithm DO NOT prove--or even suggest--that an individual has
+ engaged in insider trading. <span>WatchDogs</span> suspicion ranks should not
+ be interpreted as indication of an individual’s participation in
+ illegal activity. <span>WatchDogs</span> data, including suspicion ranks, cannot
+ be used as evidence in legal proceedings. Please use WatchDogs as
+ it is intended, and use discretion when interpreting algorithmic results.
+ </div>
+ <p></p>
+ </footer>
+ </div>
+
+ :
+ <WatchDogs></WatchDogs>
+ }
+
+ </>
+ );
+}
+
+
+
+
+
+
+export default App; \ No newline at end of file
diff --git a/react-frontend/src/App.test.js b/react-frontend/src/App.test.js
new file mode 100644
index 0000000..1f03afe
--- /dev/null
+++ b/react-frontend/src/App.test.js
@@ -0,0 +1,8 @@
+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/react-frontend/src/components/DateSelector.js b/react-frontend/src/components/DateSelector.js
new file mode 100644
index 0000000..bf01d44
--- /dev/null
+++ b/react-frontend/src/components/DateSelector.js
@@ -0,0 +1,17 @@
+// CSS import
+import '../css/Route.css';
+import '../css/CoordSelector.css';
+
+/**
+ * The component that selects and displays a coordinate.
+ * @param {Object} props The props for the element.
+ */
+function DateSelector(props) {
+ return (
+ <div className="Flex-coord">
+ <input type="date" value={props.value} onChange={(e) => props.changedFunc(new Date(e.target.value))} onClick={() => props.clickedFunc()}/>
+ </div>
+ );
+}
+
+export default DateSelector; \ No newline at end of file
diff --git a/react-frontend/src/components/Hub.js b/react-frontend/src/components/Hub.js
new file mode 100644
index 0000000..8a3ac1c
--- /dev/null
+++ b/react-frontend/src/components/Hub.js
@@ -0,0 +1,24 @@
+// React import
+import { useState } from "react";
+
+// CSS import
+import '../css/UserCheckin.css';
+
+/**
+ * Componenet for checkins. Has a toggle to show more info.
+ * @param {Object} props The props of the component.
+ * @returns {import('react').HtmlHTMLAttributes} A list element holding a checkin's info.
+ */
+function Hub(props) {
+ // State - toggled
+
+ return (
+ <li className='Checkin'>
+ <div className="Img-flex">
+ <span className="Clickable-name" onClick= {() => console.log(props.id)}>{props.name}</span>
+ <span>{props.value.toFixed(3)}</span>
+ </div>
+ </li>);
+}
+
+export default Hub; \ No newline at end of file
diff --git a/react-frontend/src/components/HubList.js b/react-frontend/src/components/HubList.js
new file mode 100644
index 0000000..c9a5156
--- /dev/null
+++ b/react-frontend/src/components/HubList.js
@@ -0,0 +1,63 @@
+// React and component imports
+import { useEffect, useState } from "react";
+import Hub from "./Hub.js";
+import InvestorInfo from "./InvestorInfo.js";
+
+// CSS import
+import '../css/UserCheckin.css';
+
+/**
+ * Component that build the checkin list and displays checkin info.
+ * @returns {import('react').HtmlHTMLAttributes} A div with the hubs
+ * in a vertical layout.
+ */
+function HubList(props) {
+ const [hubItems, setHubItems] = useState([]);
+ const [isSelected, setIsSelected] = useState(false);
+ const [name, setName] = useState('');
+
+ /**
+ * Loads new the checkins into the current cache/map of hubs.
+ */
+ const updateHubItems = () => {
+ // sort and create the elemnts
+ let hubs = [];
+ //const sorted = props.data.sort((a, b) => b.suspicionScore - a.suspicionScore);
+ props.data.forEach(hub => hubs.push(
+ <Hub key={hub.id} id={hub.id} name={hub.name} value={hub.suspicionScore} setSelected={props.setSelected}></Hub>
+ ));
+
+ setHubItems(hubs);
+ }
+
+ const getName = () => {
+ props.data.forEach(hub => {
+ if (hub.id === props.selected) {
+ setName(hub.name);
+ }
+ })
+ setName('');
+ }
+
+
+ // React hook that updates when the hubs are recalculated
+ useEffect(() => updateHubItems(), [props.data]);
+
+ //React hook to show data for an investor
+ useEffect(() => {
+ setIsSelected(true)
+ getName();
+ }, [props.selected]);
+
+ return (
+ <div className="User-checkin">
+ <div className="Checkins">
+ <h2>Suspicion Ranks</h2>
+ <ul className='Checkin-list'>{hubItems}</ul>
+ </div>
+ <InvestorInfo personId={props.selected} isSelected={isSelected} name={name} dates={props.dates}></InvestorInfo>
+ </div>
+ );
+}
+
+export default HubList; \ No newline at end of file
diff --git a/react-frontend/src/components/InvestorInfo.js b/react-frontend/src/components/InvestorInfo.js
new file mode 100644
index 0000000..d368984
--- /dev/null
+++ b/react-frontend/src/components/InvestorInfo.js
@@ -0,0 +1,66 @@
+// React import
+import { useEffect, useState } from "react";
+
+// CSS import
+import '../css/UserCheckin.css';
+
+/**
+ * Componenet for checkins. Has a toggle to show more info.
+ * @param {Object} props The props of the component.
+ * @returns {import('react').HtmlHTMLAttributes} A list element holding a checkin's info.
+ */
+function InvestorInfo(props) {
+ const [info, setInfo] = useState({});
+
+ const toEpochMilli = date => Date.parse(date);
+ const getInfo = () => {
+ console.log({
+ person: props.name,
+ start: toEpochMilli(props.dates.start),
+ end: toEpochMilli(props.dates.end)
+ });
+
+ if (props.name === "") {
+ return;
+ }
+
+ fetch("http://localhost:4567/profit", {
+ method: "POST",
+ body: JSON.stringify({
+ person: props.name,
+ start: toEpochMilli(props.dates.start),
+ end: toEpochMilli(props.dates.end)
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ credentials: "same-origin"
+ })
+ .then(res => {
+ console.log(res);
+ res.json();
+ })
+ .then(data => {
+ console.log(data);
+ setInfo(data);
+ })
+ .catch(err => console.log(err));
+ }
+ /*
+
+ const coords = userCoords.map((coord, index) =>
+ <li key={index}>
+ <span>{'('+coord[0].toFixed(6)}, {coord[1].toFixed(6)+')'}</span>
+ </li>
+ );*/
+
+ useEffect(() => getInfo(), [props.name, props.isSelected, props.personId])
+
+ return (
+ <div className="Chosen-user" hidden={props.isSelected}>
+ hi
+ </div>
+ );
+}
+
+export default InvestorInfo; \ No newline at end of file
diff --git a/react-frontend/src/components/Loading.js b/react-frontend/src/components/Loading.js
new file mode 100644
index 0000000..6fdf5ba
--- /dev/null
+++ b/react-frontend/src/components/Loading.js
@@ -0,0 +1,20 @@
+// CSS import
+import '../css/App.css';
+
+/**
+ * Component that shows the program initially loading.
+ * @returns The loading widget - spinning logo :)
+ */
+function Loading() {
+ return (
+ <div className="App Loading">
+ <header className="App-header">
+ <img src={"./logo512.png"} className="App-logo" alt="logo" />
+ <h1 className="App-title">Loading WatchDogs</h1>
+ <p className="App-title">It takes a minute to connect to the servers and database :)</p>
+ </header>
+ </div>
+ );
+}
+
+export default Loading; \ No newline at end of file
diff --git a/react-frontend/src/components/Modal.js b/react-frontend/src/components/Modal.js
new file mode 100644
index 0000000..ad69650
--- /dev/null
+++ b/react-frontend/src/components/Modal.js
@@ -0,0 +1,70 @@
+import { useEffect, useState } from "react";
+
+import '../css/Modal.css';
+
+
+function Modal() {
+
+ const [count, setCount] = useState(0);
+
+ const nextModal1 = () => {
+ setCount(1);
+ }
+ const nextModal2 = () => {
+ setCount(2);
+ }
+ const nextModal3 = () => {
+ setCount(3);
+ }
+ const nextModal4 = () => {
+ setCount(4);
+ }
+ const startModal = () => {
+ document.getElementById("main-modal").style.display = 'block';
+ setCount(0);
+ }
+ const exitModal = () => {
+ document.getElementById("main-modal").style.display = 'none';
+ }
+
+
+ return (
+ <div>
+ <div id="main-modal"className="modal" style={{display : 'block'}}>
+ {(count == 0) && <p className="m modal0">
+ <p className="align-center"><span className="span">Welcome to WatchDogs!</span></p>
+ <p className="align-center">Click start for an introduction to the WatchDogs interface.</p>
+ <p className="align-center"><button className="next" onClick={nextModal1}>Start</button></p>
+ <p className="align-center"><button className="skip" onClick={exitModal}>Skip</button></p>
+ </p>}
+ {(count == 1) && <p className="m modal1">
+ <p className="align-right"><i class="arrow right"></i></p>
+ <span className="span">This is the suspicion ranking pane,</span> which displays
+ high-profile traders and the suspicion score our algorithm assigned
+ to them. The higher an individual is ranked, the more likely they are involved in insider trading.
+ <p className="align-center"><button className="next" onClick={nextModal2}>Next</button></p>
+ </p>}
+ {(count == 2) && <p className="m modal2">
+ <p></p>
+ <span className="span">The Timeframe pane </span>
+ allows you to chose the timespan you'd like to see trade data from. Individuals' suspicion score will
+ be calculated based on the trades that occured during the timeframe you select.
+ <p className="align-center"><button className="next" onClick={nextModal3}>Next</button></p>
+ <p className="align-right"><i class="arrow down"></i></p>
+ </p>}
+ {count == 3 && <p className="m modal3">
+ <p className="align-left"><i class="arrow left"></i></p>
+ <span className="span">The Trader Graph </span>
+ shows how traders are related. Click on a name in the graph to see more information about that individual.
+ <p className="align-center"><button className="next" onClick={nextModal4}>Next</button></p>
+ </p>}
+ {count == 4 && <p className="m modal4">
+ <button className="next" onClick={exitModal}>You're ready to start using WatchDogs!</button>
+ </p>}
+ </div>
+ <span className="restart-modal align-center" onClick={startModal}></span>
+ </div>
+ );
+}
+
+export default Modal; \ No newline at end of file
diff --git a/react-frontend/src/components/TimeSelector.js b/react-frontend/src/components/TimeSelector.js
new file mode 100644
index 0000000..652a9ec
--- /dev/null
+++ b/react-frontend/src/components/TimeSelector.js
@@ -0,0 +1,48 @@
+// React/Component imports
+import { useEffect, useState } from "react";
+import DateSelector from './DateSelector.js';
+
+// CSS imports
+import '../css/Route.css';
+
+
+/**
+ * The component that hold the forms for routing.
+ * @param {Object} props
+ */
+function TimeSelector(props) {
+ const [current, setCurrent] = useState("");
+
+ const toValue = date => new Date(date).toISOString().slice(0, 10);
+
+ const [startDate, setStartDate] = useState(props.dates.start);
+ const [endDate, setEndDate] = useState(props.dates.end);
+
+ const changeTimeframe = () => {
+ props.setDates({
+ start: startDate,
+ end: endDate
+ });
+ }
+
+ useEffect(() => setCurrent(""), [startDate, endDate]);
+
+ // The div with the html elements for routing.
+ return (
+ <div className="Route">
+ <div className="Coord-selectors-flex">
+ <DateSelector side={"left"} name={"Start Date"} className="Coord-select-left" clickedFunc={setCurrent}
+ changedFunc={setStartDate} disabled={current==='start' || props.isChanging} value={toValue(startDate)}></DateSelector>
+ <div>
+ <h2>Adjust Timeframe</h2>
+ <button className="Btn Route-btn" onClick={() => changeTimeframe()}
+ disabled={current!=="" || props.isChanging}>Change Timeframe</button>
+ </div>
+ <DateSelector side={"right"} name={"End Date"} className="Coord-select-right" clickedFunc={setCurrent}
+ changedFunc={setEndDate} disabled={current==='end' || props.isChanging} value={toValue(endDate)}></DateSelector>
+ </div>
+ </div>
+ );
+}
+
+export default TimeSelector; \ No newline at end of file
diff --git a/react-frontend/src/components/Visualization.js b/react-frontend/src/components/Visualization.js
new file mode 100644
index 0000000..0a0c82a
--- /dev/null
+++ b/react-frontend/src/components/Visualization.js
@@ -0,0 +1,103 @@
+// JS module imports
+import { useEffect, useRef, useState } from "react";
+import uuid from 'react-uuid';
+import Graph from 'vis-react';
+
+// CSS imports
+import '../css/Canvas.css';
+
+
+/**
+ * This function renders and mantains thhe canvas.
+ * @param {Object} props The props for the canvas.
+ * @returns {import("react").HtmlHTMLAttributes} The canvas to be retured.
+ */
+function Visualization(props) {
+ const options = {
+ edges: {
+ color: "#ffffff"
+ }
+ };
+ const events = {
+ select: () => event => props.setSelected(event.nodes[0])
+ };
+
+ const [graphState, setGraphState] = useState({
+ nodes: [],
+ edges: []
+ });
+ const getNodes = () => {
+ let nodes = [];
+ props.data.forEach(hub => {
+ if (hub.followers) {
+ let colorVal = '#f6f7d4';
+ const score = hub.suspicionScore;
+
+ if(score > 0.8){
+ colorVal = '#d92027'
+ }
+ if(score < 0.8 && score > 0.6){
+ colorVal = '#f37121'
+ }
+ if(score < 0.6 && score > 0.4){
+ colorVal = '#fdca40'
+ }
+ nodes.push({
+ id: hub.id,
+ autoResize: true,
+ label: hub.name,
+ labelHighlightBold: true,
+ shape: "dot",
+ value: hub.suspicionScore*1000,
+ color: {
+ background: colorVal,
+ border: '#2b2e4a',
+ highlight:{
+ background: '#29bb89',
+ border: '#fdca40'
+ }
+ },
+ font: {
+ color: '#9fd8df',
+ size: 20,
+ }
+ });
+ }
+ });
+ return nodes;
+ }
+ const getEdges = () => {
+ let edges = []
+ props.data.forEach(hub => {
+ hub.followers.forEach(follower => {
+ edges.push({
+ from: follower.id,
+ to: hub.id,
+ dashes: false,
+ color:{
+ opacity: 0.7,
+ highlight:'#fdca40',
+ }
+ });
+ });
+ });
+ return edges;
+ }
+
+ // Hooks to update graph state
+ useEffect(() => setGraphState({nodes: getNodes(), edges: getEdges()}), [JSON.stringify(props.data)]);
+
+ return (
+ <div className="Map-canvas">
+ <Graph
+ key={uuid()}
+ graph={graphState}
+ options={options}
+ events={events}>
+ </Graph>
+ </div>
+ );
+}
+
+export default Visualization;
+
diff --git a/react-frontend/src/components/WatchDogs.js b/react-frontend/src/components/WatchDogs.js
new file mode 100644
index 0000000..d631ea9
--- /dev/null
+++ b/react-frontend/src/components/WatchDogs.js
@@ -0,0 +1,89 @@
+// React/component imports
+import React, {useEffect, useState} from 'react';
+import TimeSelector from './TimeSelector.js';
+import Visualization from './Visualization.js';
+import HubList from './HubList.js';
+import Loading from './Loading.js';
+import Modal from './Modal.js';
+import logo from './images/logo.png';
+
+// CSS import
+import '../css/App.css';
+
+/**
+ * Main component of the app. Holds the main layout of the big components.
+ * @returns {import('react').HtmlHTMLAttributes} A div of the body of the page.
+ */
+function WatchDogs() {
+ // State to open app when loaded
+ const [hasLoaded, setHasLoaded] = useState(false);
+ // State indicate if canvas is redrawing
+ const [isChanging, setIsChanging] = useState(false);
+ // State to hold dates -> two weeks apart on initialization.
+ const [dates, setDates] = useState({
+ start: new Date(Date.now() - 12096e5),
+ end: new Date()
+ });
+ // State for visualization data
+ const [data, setData] = useState([]);
+ // State for selected person
+ const [selected, setSelected] = useState(-1);
+
+ const toEpochMilli = date => Date.parse(date);
+ const getGraphData = () => {
+ console.log({
+ start: toEpochMilli(dates.start),
+ end: toEpochMilli(dates.end)
+ });
+ fetch("http://localhost:4567/data", {
+ method: "POST",
+ body: JSON.stringify({
+ start: toEpochMilli(dates.start),
+ end: toEpochMilli(dates.end)
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ credentials: "same-origin"
+ })
+ .then(res => res.json())
+ .then(data => {
+ //TODO: optimize this
+ const sliced = data.holders.slice(0, 500);
+ console.log(sliced);
+ setData(sliced);
+ setHasLoaded(true);
+ })
+ .catch(err => console.log(err));
+
+ setIsChanging(false);
+ }
+
+
+ // Hooks to update data on init and switching of data
+ //useEffect(() => getGraphData(), []);
+ useEffect(() => {
+ setIsChanging(true);
+ getGraphData();
+ }, [dates]);
+
+ return (
+ <>
+ {(!hasLoaded) ? <Loading></Loading> :
+ <div className="App">
+ <Modal>PHP</Modal>
+ <header id="in-app-logo-holder" className="App-header">
+ <img id="in-app-logo" src={logo} alt="logo"/></header>
+ <div className="Canvas-filler Canvas-filler-1"></div>
+ <div className="Canvas-filler Canvas-filler-2"></div>
+ <div className="Canvas-filler Canvas-filler-3"></div>
+ <HubList setHasLoaded={setHasLoaded} data={data} setSelected={setSelected} selected={selected} dates={dates}></HubList>
+ <TimeSelector isChanging={isChanging} dates={dates} setDates={setDates}></TimeSelector>
+ <Visualization hasLoaded={hasLoaded} data={data} setSelected={setSelected}></Visualization>
+ </div>
+ }
+ </>
+ );
+}
+
+export default WatchDogs;
diff --git a/react-frontend/src/components/images/logo.png b/react-frontend/src/components/images/logo.png
new file mode 100644
index 0000000..7e4e9ee
--- /dev/null
+++ b/react-frontend/src/components/images/logo.png
Binary files differ
diff --git a/react-frontend/src/components/images/person.svg b/react-frontend/src/components/images/person.svg
new file mode 100644
index 0000000..6a93d8f
--- /dev/null
+++ b/react-frontend/src/components/images/person.svg
@@ -0,0 +1 @@
+<svg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'><title>Person</title><path d='M332.64 64.58C313.18 43.57 286 32 256 32c-30.16 0-57.43 11.5-76.8 32.38-19.58 21.11-29.12 49.8-26.88 80.78C156.76 206.28 203.27 256 256 256s99.16-49.71 103.67-110.82c2.27-30.7-7.33-59.33-27.03-80.6zM432 480H80a31 31 0 01-24.2-11.13c-6.5-7.77-9.12-18.38-7.18-29.11C57.06 392.94 83.4 353.61 124.8 326c36.78-24.51 83.37-38 131.2-38s94.42 13.5 131.2 38c41.4 27.6 67.74 66.93 76.18 113.75 1.94 10.73-.68 21.34-7.18 29.11A31 31 0 01432 480z'/></svg> \ No newline at end of file
diff --git a/react-frontend/src/css/App.css b/react-frontend/src/css/App.css
new file mode 100644
index 0000000..e39eb3e
--- /dev/null
+++ b/react-frontend/src/css/App.css
@@ -0,0 +1,86 @@
+.App {
+ display: grid;
+ grid-template-areas: "head canvasFill2 canvasFill3 checkin"
+ "canvasFill1 canvasFill2 canvasFill3 checkin"
+ "route canvasFill2 canvasFill3 checkin";
+ grid-template-rows: max-content auto max-content;
+ grid-template-columns: max-content auto max-content max-content;
+ background-color: #121212;
+
+}
+
+.App-logo {
+ height: 40vmin;
+ pointer-events: none;
+}
+
+
+.Canvas-filler {
+ width: 100%;
+ height: 100%;
+
+ z-index: 1;
+}
+
+.Canvas-filler-1 {
+ grid-area: canvasFill1;
+}
+.Canvas-filler-2 {
+ grid-area: canvasFill2;
+}
+.Canvas-filler-3 {
+ grid-area: canvasFill3;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .App-logo {
+ animation: App-logo-spin infinite 20s linear;
+ }
+}
+
+.App-header {
+ grid-area: head;
+ min-height: 7vh;
+ width: max-content;
+ display: flex;
+ padding: 0 20px;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ font-size: calc(10px + 2vmin);
+ color: white;
+ z-index: 10;
+ margin: 5px;
+
+}
+
+#in-app-logo-holder {
+ border: 7px solid rgb(7, 94, 12);
+ border-radius: 5px;
+ background-color: white;
+}
+
+.App-link {
+ color: #61dafb;
+}
+
+@keyframes App-logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.Loading {
+ z-index: 100;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+#in-app-logo {
+ width: 200px;
+} \ No newline at end of file
diff --git a/react-frontend/src/css/Canvas.css b/react-frontend/src/css/Canvas.css
new file mode 100644
index 0000000..e67d87d
--- /dev/null
+++ b/react-frontend/src/css/Canvas.css
@@ -0,0 +1,7 @@
+.Map-canvas {
+ /*touch-action: none; */
+ position: absolute;
+ z-index: 5;
+ width: 100vw;
+ height: 100vh;
+} \ No newline at end of file
diff --git a/react-frontend/src/css/CoordSelector.css b/react-frontend/src/css/CoordSelector.css
new file mode 100644
index 0000000..881be08
--- /dev/null
+++ b/react-frontend/src/css/CoordSelector.css
@@ -0,0 +1,52 @@
+/* CSS adapted from w3school buttons */
+.Btn-select-left > p, .Btn-select-right > p {
+ padding: 0;
+ margin: 0;
+}
+
+.Btn-select-left {
+ background-color: #424242;
+ border: 4px solid pink;
+}
+
+.Btn-select-left:hover {
+ box-shadow: 3px 3px #888888;
+ color: black;
+ background-color: pink;
+}
+
+.Btn-select-right {
+ background-color: #424242;
+ border: 4px solid lightblue;
+}
+
+.Btn-select-right:hover {
+ box-shadow: 3px 3px #888888;
+ color: black;
+ background-color: lightblue;
+}
+
+.Btn:disabled,
+.Btn[disabled]{
+ border: 1px solid #999999;
+ background-color: #cccccc;
+ color: #666666;
+ box-shadow: none;
+ cursor: default;
+}
+
+/*
+.Btn:disabled:hover,
+.Btn[disabled]:hover{
+}
+*/
+
+.Textbox {
+ width: 100px;
+}
+
+.Number-input {
+ width: 90%;
+}
+
+
diff --git a/react-frontend/src/css/Landing.css b/react-frontend/src/css/Landing.css
new file mode 100644
index 0000000..58658b0
--- /dev/null
+++ b/react-frontend/src/css/Landing.css
@@ -0,0 +1,256 @@
+html {
+ scroll-behavior: smooth;
+ z-index: 1000;
+
+}
+
+.nav-bar{
+ position: fixed;
+ top: 0px;
+ left: 0px;
+ width: 100vw;
+ height: 70px;
+ background-color: rgb(7, 7, 44);
+ z-index: 100;
+}
+
+.topnav {
+ background-color: rgb(7, 7, 44);
+ overflow: hidden;
+ margin-right: 30px;
+}
+
+.topnav a {
+ float: right;
+ color: #f2f2f2;
+ text-align: center;
+ padding: 14px 16px;
+ text-decoration: none;
+ font-size: 17px;
+ height: 30px;
+ padding-top: 25px;
+}
+
+.topnav a:hover {
+ background-color: rgb(17, 11, 99);
+ color: white;
+}
+
+.topnav a.active {
+ background-color: #4CAF50;
+ color: white;
+}
+
+.body {
+ background-color: #f3f3f3;
+ font-family:Verdana, Geneva, Tahoma, sans-serif;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ display: flex;
+ flex-direction: column;
+ width: 100vw;
+ scroll-behavior: smooth slow;
+ z-index: 100;
+}
+main {
+ margin-top: 65px;
+ display: flex;
+ flex-direction: column;
+}
+
+.intro {
+ height: 900px;
+ width: 100vw;
+ background-color: rgb(3, 2, 24);
+ color: white;
+ position: relative;
+ left: -10px;
+}
+
+#enter-watchdogs {
+ background-color: rgb(206, 206, 206);
+ color: rgba(10, 9, 71);
+ border: 2px solid rgb(255, 255, 255);
+ border-radius: 5px;
+ padding: 5px 10px;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ font-size: 16px;
+ height: 70px;
+ width: 200px;
+ font-weight: bold;
+ font-family:sans-serif;
+}
+#enter-watchdogs:hover {
+ background-color: #94ff73;
+}
+
+.app-preview {
+ height: 900px;
+ width: 100vw;
+ background-color: #b7ffb9;
+ color: white;
+ position: relative;
+ left: 0px;
+ position: relative;
+ left: -10px;
+}
+
+#preview {
+ width: 70vw;
+ border: 10px solid rgb(255, 255, 255);
+ border-radius: 5px;
+}
+
+#preview-text {
+ color:#868686;
+ font-size: small;
+}
+
+section {
+width: 100vw;
+overflow: hidden;
+display: flex;
+flex-direction: column;
+justify-content: center;
+align-items: center;
+
+}
+
+#app-intro{
+padding-top: 160px;
+height: fit-content;
+width: 100vw;
+}
+
+.team {
+width: 100%;
+max-width: 1600px;
+display: flex;
+flex-direction: row;
+justify-content: space-evenly;
+padding-top: 0px;
+width: 80%;
+min-height: 500px;
+align-items: center;
+}
+
+.center {
+display: flex;
+flex-direction: column;
+justify-content: center;
+}
+
+.heading{
+font-size: 45px;
+margin: 10px 0 0 0;
+text-align: center;
+font-weight: bold;
+}
+
+.text{
+color: rgba(0,0,0, 0.6);
+font-size: 18px;
+display: flex;
+justify-content: center;
+align-items: center;
+text-align: center;
+max-width: 70vw;
+margin: 10px 0 20px 0;
+}
+
+#team {
+width: 100vw;
+padding: 50px 0 200px 0;
+justify-content: flex-start;
+background-color: #cccccc;
+position: relative;
+left: -10px;
+}
+
+#team-heading{
+margin-bottom: 0px;
+}
+
+#team-holder {
+flex-direction: column;
+max-width: 1000px;
+max-height: 500px;
+height: 500px;
+}
+
+#people-holder{
+display: flex;
+flex-direction: row;
+justify-content: center;
+align-items: center;
+width: 100%;
+}
+
+.team-person-holder{
+display: flex;
+flex-direction: column;
+justify-content: center;
+align-items: center;
+margin-top: 50px;
+}
+
+.team-person{
+border-radius: 100px;
+overflow: hidden;
+margin: 20px;
+height: 200px;
+width: 200px;
+box-shadow: 0 0 2px 2px rgba(0,0,0,0.15);
+}
+
+.team-text{
+color: rgba(0,0,0, 1);
+font-size: 18px;
+display: flex;
+justify-content: center;
+align-items: center;
+text-align: center;
+max-width: 700px;
+margin: 0;
+}
+
+#footer {
+ width: 100vw;
+ background: rgb(7, 7, 44);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+ left: -10px;
+ height: fit-content;
+ width: 100vw;
+}
+
+.footer-item{
+ color: white;
+ font-size: 20px;
+}
+.footer-subtext{
+ color: white;
+ font-size: 15px;
+ width: 80vw;
+}
+
+.appinfotxt {
+font-size: 15px;
+color:rgb(3, 2, 24);
+max-width: 70vw;
+}
+
+.h2 {
+font-size: 30px;
+color: black;
+}
+
+#return-to-landing {
+z-index: 1000;
+}
+
diff --git a/react-frontend/src/css/Modal.css b/react-frontend/src/css/Modal.css
new file mode 100644
index 0000000..96f9066
--- /dev/null
+++ b/react-frontend/src/css/Modal.css
@@ -0,0 +1,146 @@
+.modal {
+ background-color: aqua;
+ display: block; /* Hidden by default */
+ position: fixed; /* Stay in place */
+ z-index: 100; /* Sit on top */
+ padding-top: 100px; /* Location of the box */
+ left: 0;
+ top: 0;
+ width: 100%; /* Full width */
+ height: 100%; /* Full height */
+ overflow: auto; /* Enable scroll if needed */
+ background-color: rgba(0, 0, 0, 0.8);
+ color: white;
+}
+
+.m{
+ border: 7px solid rgb(7, 94, 12);
+ border-radius: 5px;
+ background-color: white;
+ color: black;
+ position: fixed;
+ height: fit-content;
+ width: 20vw;
+ padding: .5%;
+}
+
+.modal0{
+ top: 30vh;
+ left: 40vw;
+}
+.modal1{
+ top: 7vh;
+ right: 29vw;
+}
+@media (max-width: 750px) {
+ .modal1{
+ top: 7vh;
+ right: 0vw;
+ }
+}
+.modal2{
+ bottom: 20vh;
+ left: 5vw;
+}
+.modal3{
+ top: 35vh;
+ left: 45vw;
+}
+.modal4{
+ top: 30vh;
+ left: 37vw;
+ padding: 2%;
+}
+
+.restart-modal:after{
+ z-index: 101;
+ border: 7px solid rgb(7, 94, 12);
+ border-radius: 15px;
+ background-color: white;
+ color: black;
+ position: fixed;
+ height: 25px;
+ width: 30px;
+ bottom: 30px;
+ right: 3vw;
+ content:'?';
+}
+.restart-modal:hover:after{
+ content:'Restart Orientation';
+ width: 150px;
+}
+
+.span {
+ font-weight: bold;
+ color:rgba(10, 9, 71);
+}
+
+.arrow {
+ border: solid black;
+ border-width: 0 3px 3px 0;
+ display: inline-block;
+ padding: 3px;
+ }
+
+ .right {
+ transform: rotate(-45deg);
+ -webkit-transform: rotate(-45deg);
+ }
+
+ .left {
+ transform: rotate(135deg);
+ -webkit-transform: rotate(135deg);
+ }
+
+ .up {
+ transform: rotate(-135deg);
+ -webkit-transform: rotate(-135deg);
+ }
+
+ .down {
+ transform: rotate(45deg);
+ -webkit-transform: rotate(45deg);
+ }
+
+ .align-right{
+ text-align: right;
+ margin-right: 15px;
+ }
+
+ .align-center{
+ text-align: center;
+ }
+
+ .align-left{
+ text-align: left;
+ margin-left: 15px;
+ }
+
+ .next{
+ background-color: rgb(206, 206, 206);
+ color: black;
+ border: 2px solid rgba(10, 9, 71);
+ border-radius: 5px;
+ padding: 5px 10px;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ font-size: 16px;
+ }
+
+ .next:hover {
+ background-color: rgb(7, 94, 12);
+ color: white;
+ }
+
+ .skip{
+ background-color: white;
+ border: none;
+ color:rgb(119, 119, 119);
+ }
+
+ .skip:hover{
+ color: black;
+ }
+
+
diff --git a/react-frontend/src/css/Route.css b/react-frontend/src/css/Route.css
new file mode 100644
index 0000000..efc4868
--- /dev/null
+++ b/react-frontend/src/css/Route.css
@@ -0,0 +1,56 @@
+.Route {
+ grid-area: route;
+ z-index: 10;
+ color: white;
+ border-radius: 10px;
+ background-color: #121212;
+ /*cursor: default;*/
+ /* Transparent background */
+ background: rgba(0, 0, 0, 0);
+}
+
+.Coord-selectors-flex {
+ display: flex;
+ gap: 20px;
+ padding: 8px;
+ margin: 0;
+ align-content: flex-end;
+ background-color: #333333;
+ margin: 5px;
+ border-radius: 3px;
+}
+
+/* CSS adapted from w3school buttons */
+.Btn {
+ color: white;
+ padding: 16px 32px;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ font-size: 16px;
+ margin: 4px 2px;
+ transition-duration: 0.4s;
+ cursor: pointer;
+ outline: none;
+}
+
+.Route-btn:hover {
+ box-shadow: 3px 3px #ccc;
+ color: black;
+ background-color: lightgreen;
+}
+
+.Route-btn {
+ background-color: #424242;
+ border: 2px solid lightgreen;
+ box-shadow: .5px .5px 0 2px lightgreen;
+}
+
+.Btn:disabled,
+.Btn[disabled]{
+ border: 1px solid #999999;
+ background-color: #cccccc;
+ color: #666666;
+ /*cursor: default;*/
+ box-shadow: none;
+} \ No newline at end of file
diff --git a/react-frontend/src/css/UserCheckin.css b/react-frontend/src/css/UserCheckin.css
new file mode 100644
index 0000000..141cc01
--- /dev/null
+++ b/react-frontend/src/css/UserCheckin.css
@@ -0,0 +1,94 @@
+.User-checkin {
+ grid-area: checkin;
+ height: 100vh;
+ background-color: #121212;
+ z-index: 10;
+ color: white;
+ border-radius: 10px;
+ display: flex;
+ font-size: 18px;
+ cursor: default;
+ /* Transparent background */
+ background: rgba(0, 0, 0, 0);
+}
+
+ul {
+ list-style-type: none;
+}
+
+.User-checkin > div {
+ z-index: 10;
+ background-color: #333333;
+ border-radius: 20px;
+ margin: 5px;
+}
+
+.Coord-ex {
+ height: 1vh;
+ margin: 0;
+ padding: 0;
+ text-align: center;
+}
+
+.Chosen-user > h2, .Checkins > h2 {
+ display: flex;
+ justify-content: space-evenly;
+ height: 5vh;
+ padding: 0 10px;
+}
+
+.Checkin-list {
+ padding: 0 20px;
+ height: 86vh;
+ overflow-y: scroll;
+ cursor: default;
+}
+
+.User-checkin-list {
+ height: 80vh;
+ overflow-y: scroll;
+
+ list-style-position: inside;
+ padding: 0 20px;
+ text-align: center;
+ text-indent: -12px;
+}
+
+.User-checkin-list > li {
+ margin-bottom: 20px;
+}
+
+
+.Checkin {
+ padding-top: 10px;
+ border-bottom: 1px solid #e6ecf0;
+}
+
+.Checkin:last-child {
+ border-bottom: none;
+}
+
+.Img-flex {
+ margin: 5px 10px 10px 0;
+ gap: 20px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.Img-btn {
+ background-color: #424242;
+ border-radius: 50%;
+ margin-right: 10px;
+}
+
+.Img-btn:hover {
+ box-shadow: 3px 3px #333333;
+ cursor: pointer;
+}
+
+.Clickable-name {
+ cursor: pointer;
+ text-decoration: underline;
+ color: lightgreen;
+} \ No newline at end of file
diff --git a/react-frontend/src/images/mainlogo.png b/react-frontend/src/images/mainlogo.png
new file mode 100644
index 0000000..a26df0f
--- /dev/null
+++ b/react-frontend/src/images/mainlogo.png
Binary files differ
diff --git a/react-frontend/src/images/previewwatchdog.png b/react-frontend/src/images/previewwatchdog.png
new file mode 100644
index 0000000..56669d8
--- /dev/null
+++ b/react-frontend/src/images/previewwatchdog.png
Binary files differ
diff --git a/react-frontend/src/images/reagan.png b/react-frontend/src/images/reagan.png
new file mode 100644
index 0000000..15c02f0
--- /dev/null
+++ b/react-frontend/src/images/reagan.png
Binary files differ
diff --git a/react-frontend/src/index.css b/react-frontend/src/index.css
new file mode 100644
index 0000000..72f4b2d
--- /dev/null
+++ b/react-frontend/src/index.css
@@ -0,0 +1,14 @@
+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;
+ background-color: #121212;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
diff --git a/react-frontend/src/index.js b/react-frontend/src/index.js
new file mode 100644
index 0000000..ef2edf8
--- /dev/null
+++ b/react-frontend/src/index.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import './index.css';
+import App from './App';
+import reportWebVitals from './reportWebVitals';
+
+ReactDOM.render(
+ <React.StrictMode>
+ <App />
+ </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/react-frontend/src/logo.svg b/react-frontend/src/logo.svg
new file mode 100644
index 0000000..9dfc1c0
--- /dev/null
+++ b/react-frontend/src/logo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg> \ No newline at end of file
diff --git a/react-frontend/src/reportWebVitals.js b/react-frontend/src/reportWebVitals.js
new file mode 100644
index 0000000..5253d3a
--- /dev/null
+++ b/react-frontend/src/reportWebVitals.js
@@ -0,0 +1,13 @@
+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/react-frontend/src/setupTests.js b/react-frontend/src/setupTests.js
new file mode 100644
index 0000000..8f2609b
--- /dev/null
+++ b/react-frontend/src/setupTests.js
@@ -0,0 +1,5 @@
+// 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';