} // a success message, for eexample: 'Created a rotation HIT ID:{hitId}, for device:{cameraId}'\r\n */\r\nexport const beginCalibration = async (imageURL, cameraId) => {\r\n try {\r\n //console.log (\"Begin Calibration, cameraProps.cameraId: \", cameraProps.cameraId, \"userSelectedTimestamp\", userSelectedTimestamp, \" imageURL\", imageURL);\r\n const noSignatureUrl = imageURL.substr(0, imageURL.indexOf('?'));\r\n //console.log('noSignatureUrl:',noSignatureUrl)\r\n let params = {\r\n body: {\r\n //imageURL : \"https://erm2.s3.amazonaws.com/test/1579277254797.jpg\"\r\n image_url: noSignatureUrl,\r\n cameraId,\r\n },\r\n };\r\n //console.log(\"parameters:\", params)\r\n const response = await API.post(\r\n 'dev-iotvid-ml',\r\n `/createhit/rotation`,\r\n params\r\n );\r\n\r\n return response;\r\n } catch (error) {\r\n throw error;\r\n }\r\n};\r\n","import React from 'react';\r\nimport { useState, useEffect } from 'react';\r\nimport { makeStyles } from '@material-ui/core';\r\nimport { Button } from '@material-ui/core';\r\nimport { FormControl } from '@material-ui/core';\r\nimport { FormLabel } from '@material-ui/core';\r\nimport { FormGroup } from '@material-ui/core';\r\nimport { FormControlLabel } from '@material-ui/core';\r\n//import { Checkbox } from '@material-ui/core';\r\nimport { useSelector } from 'react-redux';\r\nimport { Slider } from '@material-ui/core';\r\n\r\n// services\r\n//import { postSnap, postSnap2 } from './../services/snap.services';\r\nimport { postSnap } from './../services/snap.services';\r\nimport { beginCalibration } from '../services/turk.services';\r\nimport {\r\n getCameraParams,\r\n updateCameraFlash,\r\n} from './../services/cameras.services';\r\n\r\nconst useStyles = makeStyles((theme) => ({\r\n root: {\r\n display: 'flex',\r\n justifyContent: 'center',\r\n },\r\n formControl: {\r\n margin: theme.spacing(3),\r\n },\r\n}));\r\n\r\nconst Snap = ({\r\n imageURL,\r\n setSnapped,\r\n // userSelectedTimestamp, // unused prop\r\n}) => {\r\n // const userSelectedTimestamp = props.userSelectedTimestamp; //passing this in for Begin Calibration handle on good image to use for calibration\r\n const classes = useStyles();\r\n\r\n const userSelectedCamera = useSelector(\r\n ({ camerasReducer }) => camerasReducer.userSelectedCamera\r\n );\r\n\r\n // flash values, range: 0..100, used for construction of params object to send to snap-image lambda\r\n\r\n const [flash1, setFlash1] = useState(50);\r\n const [flash2, setFlash2] = useState(40);\r\n const [flash3, setFlash3] = useState(10);\r\n \r\n\r\n // handleFlash function for toggling flash booleans on checkbox selection\r\n const handleChangeFlash1 = async (e, value) => {\r\n //console.log(\"in handleChangeFlash, flash1, value:\", value)\r\n setFlash1(value);\r\n };\r\n\r\n const handleChangeFlash2 = async (e, value) => {\r\n setFlash2(value);\r\n };\r\n\r\n const handleChangeFlash3 = async (e, value) => {\r\n setFlash3(value);\r\n };\r\n \r\n // save off flash1, flash2, flash3 to dynamoDb for this camera, intensity level from 0..100\r\n const saveFlash = async () => {\r\n \r\n // posting flash1 and flash2 and saving in dynamo db everytime checkbox has been checked.\r\n // first convert the boolean (for checkbox) values to 0(false, off), or 1 (true, on/flash)\r\n //const flash1Number = Number(flash1) //0 or 1\r\n //const flash2Number = Number(flash2)\r\n console.log(\"saving flash settings to dynamodb, flash1, flash2, flash3:\", flash1, flash2, flash3);\r\n \r\n await updateCameraFlash(userSelectedCamera?.cameraId, \r\n { \"flash1\":flash1, \"flash2\":flash2, \"flash3\":flash3 });\r\n\r\n }\r\n\r\n // constructs params object based on flash selections and posts to API\r\n const snapImage = async () => {\r\n let params = {\r\n body: {\r\n cameraId: userSelectedCamera.cameraId,\r\n // flash values range 0..100, 0 off, 100 full brighness\r\n flash1: userSelectedCamera.flash1 || 0,\r\n flash2: userSelectedCamera.flash2 || 0,\r\n flash3: userSelectedCamera.flash3 || 0,\r\n framesize: userSelectedCamera.framesize || 8, //8 is VGA 640 x 480 (sensor.h framesize_t)\r\n vflip: userSelectedCamera.vFlip,\r\n hflip: userSelectedCamera.hFlip,\r\n timestamp: new Date().getTime(),\r\n winX: userSelectedCamera.subXOrig,\r\n winY: userSelectedCamera.subYOrig,\r\n winWidth: userSelectedCamera.subWidth,\r\n winHeight: userSelectedCamera.subHeight,\r\n }, \r\n };\r\n\r\n params.body.flash1 = flash1;\r\n params.body.flash2 = flash2;\r\n params.body.flash3 = flash3;\r\n console.log(\"calling postSnap with params after setting the flash's:\", userSelectedCamera.cameraId, params);\r\n await postSnap(userSelectedCamera.cameraId, params);\r\n\r\n setSnapped(true); // sets snapped boolean in parent to trigger useEffect\r\n };\r\n\r\n useEffect(() => {\r\n const fetchFlashState = async () => {\r\n if (!userSelectedCamera?.cameraId) return;\r\n\r\n // getting flash1 and flash2 from api, value integer 0..100\r\n let { flash1 = 0, flash2 = 0, flash3 =0 } = await getCameraParams(\r\n userSelectedCamera?.cameraId\r\n );\r\n //the value should be stored as a number, 0 for no flash, > 0 (usually 1 or 500) for flash\r\n //if the value is a string, it will be treated as true, even if the string is 'false'\r\n //but the interface wants a boolean, so for now just convert it to boolean, and when \r\n //saving it off, save it off as a 1(true or on), or 0(false or off)\r\n //flash1 = Boolean(flash1)\r\n //flash2 = Boolean(flash2)\r\n\r\n // setting it in the state\r\n setFlash1(flash1); //value pulled in from dynamodb \"flash1\"\r\n setFlash2(flash2);\r\n setFlash3(flash3);\r\n };\r\n fetchFlashState();\r\n\r\n // fetch on mount and when cameraId changes\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [userSelectedCamera?.cameraId]);\r\n\r\n return (\r\n <>\r\n \r\n \r\n \r\n Choose Flash Intensities, each LED Range: 0..100 \r\n \r\n \r\n \r\n }\r\n label=\"Flash 1 (left)\"\r\n />\r\n \r\n }\r\n label=\"Flash 2 (right)\"\r\n />\r\n \r\n }\r\n label=\"Flash 3 (middle)\"\r\n />\r\n \r\n \r\n \r\n \r\n Save Flash Settings\r\n \r\n
\r\n \r\n Snap\r\n \r\n beginCalibration(imageURL, userSelectedCamera.cameraId)}>\r\n Begin Calibration\r\n \r\n {/* \r\n Snap2\r\n */}\r\n >\r\n );\r\n};\r\n\r\nexport default Snap;\r\n","// Device.js shows the most recent snap and lets user take a fresh snap\r\n\r\nimport React, { useMemo } from 'react';\r\nimport { useState, useEffect } from 'react';\r\nimport { useSelector } from 'react-redux';\r\nimport { Typography, Button } from '@material-ui/core';\r\nimport { makeStyles } from '@material-ui/core';\r\nimport ImagesDropdown from '../components/ImagesDropdown';\r\nimport Snap from '../components/Snap';\r\n\r\n// services\r\nimport {\r\n getMostRecentImgAPI,\r\n getAllImagesListAPI,\r\n getOneImageByTimestamp,\r\n} from '../services/images.services';\r\n\r\nimport { getCameraVersion } from '../services/cameras.services';\r\n\r\n//\r\n//\r\n//\r\nconst useStyles = makeStyles({\r\n root: {\r\n textAlign: 'center',\r\n },\r\n typography: {\r\n padding: '10px',\r\n background: '#f5f5f5',\r\n },\r\n imageDiv: {\r\n background: 'black',\r\n overflow: 'hidden',\r\n margin: 'auto',\r\n\r\n // fix console error by checking if height and width are numbers.\r\n height: ({ subHeight }) => (!isNaN(subHeight / 2) ? subHeight / 2 : 'auto'),\r\n width: ({ subWidth }) => (!isNaN(subWidth / 2) ? subWidth / 2 : '100%'),\r\n },\r\n\r\n image: {\r\n // 100% of parent (imageDiv)\r\n height: '100%',\r\n width: '100%',\r\n\r\n // rot passed in props to useStyles\r\n transform: ({ rot }) => `rotate(${rot}deg)`,\r\n },\r\n});\r\n\r\nconst Device = () => {\r\n const userSelectedCamera = useSelector(\r\n ({ camerasReducer }) => camerasReducer.userSelectedCamera\r\n );\r\n\r\n const cameraId = userSelectedCamera?.cameraId;\r\n\r\n const user = useSelector(({ userReducer }) => userReducer.user);\r\n\r\n const subHeight = useMemo(\r\n () =>\r\n userSelectedCamera === undefined\r\n ? 600 * 1.4\r\n : userSelectedCamera?.subHeight * 1.4,\r\n [userSelectedCamera]\r\n ); // check only for this var to change instead of rerendering every time\r\n\r\n const subWidth = useMemo(\r\n () =>\r\n userSelectedCamera === undefined\r\n ? 800 * 1.4\r\n : userSelectedCamera?.subWidth * 1.4,\r\n [userSelectedCamera]\r\n );\r\n\r\n const rot = useMemo(\r\n () => (userSelectedCamera === undefined ? 0 : userSelectedCamera?.rot4),\r\n [userSelectedCamera]\r\n );\r\n\r\n const name = useMemo(\r\n () =>\r\n userSelectedCamera === undefined ? ' ' : userSelectedCamera?.cameraName,\r\n [userSelectedCamera]\r\n );\r\n\r\n const location = useMemo(\r\n () =>\r\n userSelectedCamera === undefined\r\n ? ' '\r\n : userSelectedCamera?.cameraLocation,\r\n [userSelectedCamera]\r\n );\r\n\r\n const classes = useStyles({ rot, subHeight, subWidth }); // pass props to useStyles\r\n\r\n const [displayImg, setDisplayImg] = useState();\r\n const [allImagesList, setAllImagesList] = useState();\r\n const [userSelectedTimestamp, setUserSelectedTimestamp] = useState();\r\n const [snapped, setSnapped] = useState(false);\r\n\r\n // on mount and on change of selected camera, get most recent image and list of previous images\r\n useEffect(() => {\r\n getMostRecentImg(cameraId); //this should also set the userSelectedTimestamp to match this image\r\n getImgList(cameraId);\r\n //setDisplayImg(null);\r\n\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [cameraId, user]);\r\n\r\n // when user selects a different timestamp, get that image from images/{cameraId}/{timestampSaved}\r\n // && is an if statement for executing a single line of code\r\n useEffect(() => {\r\n userSelectedTimestamp && getImg(cameraId, userSelectedTimestamp);\r\n\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [userSelectedTimestamp, cameraId]);\r\n\r\n // this is runs when snapped is set to true by the Snap button.\r\n // data is refreshed in 10 seconds. snapped is immediately reset to false\r\n // using if here instead of && because two functions are called, not one\r\n useEffect(() => {\r\n if (snapped) {\r\n setTimeout(() => getMostRecentImg(cameraId), 10000);\r\n setTimeout(() => getImgList(cameraId), 10000);\r\n }\r\n setSnapped(false);\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [snapped, cameraId]);\r\n\r\n // API call to get list of older images from userSelectedCamera\r\n const getImgList = async (cameraId) => {\r\n const imgList = await getAllImagesListAPI(cameraId);\r\n setAllImagesList(imgList);\r\n };\r\n\r\n // API call to grab an image based on a specific timestamp\r\n const getImg = async (cameraId, timestampSaved) => {\r\n const userSelectedImg = await getOneImageByTimestamp(\r\n cameraId,\r\n timestampSaved\r\n );\r\n setDisplayImg(userSelectedImg);\r\n };\r\n\r\n //parse the timestamp, (and could also parse path if needed) from mostRecentImg signedURL\r\n const parseMostRecentImg = (imgSignedURL) => {\r\n const imageURL = imgSignedURL.substring(0, imgSignedURL.search('.jpg'));\r\n console.log('most recent image URL', imageURL);\r\n const splitResult = imageURL.split('/');\r\n console.log('splitting:', splitResult);\r\n const timestamp = splitResult[splitResult.length - 1]; //was going to pass timestamp to calibration, instead pass unsigned URL\r\n return timestamp;\r\n };\r\n\r\n // API call to get most recent image from userSelectedCamera (state variable from App.js)\r\n const getMostRecentImg = async (cameraId) => {\r\n console.log('cameraId from getMostRecentImg:', cameraId);\r\n const mostRecentImg = await getMostRecentImgAPI(cameraId);\r\n console.log('mostRecentImg', mostRecentImg);\r\n const timestamp = parseMostRecentImg(mostRecentImg); //need the timestamp of the mostRecentImage\r\n setUserSelectedTimestamp(timestamp); //keep the timestamp in sync with the image displayed\r\n setDisplayImg(mostRecentImg);\r\n };\r\n\r\n const consoleLogCameraVersion = async (cameraId) => {\r\n try {\r\n let cameraVersion = await getCameraVersion(cameraId);\r\n\r\n console.log(`camera version for ${cameraId} is: ${cameraVersion}`);\r\n\r\n return;\r\n } catch (error) {\r\n console.log(`could not get camera version for: ${cameraId}`);\r\n }\r\n };\r\n\r\n return (\r\n <>\r\n \r\n
\r\n {cameraId || 'None'} :: {name}\r\n \r\n
\r\n {location}\r\n \r\n\r\n {/* when image comes back from the API call, display it*/}\r\n\r\n {displayImg && (\r\n // div wraps photo in a rectangular div so rotated images appear more cleanly\r\n // inherits dimensions from props and divides them in half\r\n
\r\n {/* photo props inherited from props, size divided in half */}\r\n
\r\n
\r\n )}\r\n\r\n {/* when the list of older images comes back from the API call, render a dropdown with those timestamps*/}\r\n {allImagesList && (\r\n <>\r\n
Timestamp: \r\n {/* render the dropdown in separate component, passing down props of the image list and function\r\n to select that timestamp in order to make another API call */}\r\n
\r\n >\r\n )}\r\n
\r\n
consoleLogCameraVersion(cameraId)}>\r\n Test Button\r\n \r\n
\r\n >\r\n );\r\n};\r\n\r\nexport default Device;\r\n","// NotFound.js renders at any unnamed path \r\n\r\nimport React from \"react\";\r\nimport { Typography } from \"@material-ui/core\";\r\nimport { makeStyles } from \"@material-ui/core\";\r\n\r\nconst useStyles = makeStyles({\r\n root: {\r\n textAlign: \"center\",\r\n },\r\n typography: {\r\n padding: \"10px\"\r\n }\r\n});\r\n\r\nconst NotFound = () => {\r\n const classes = useStyles();\r\n return (\r\n <>\r\n \r\n \r\n Sorry, page not found! \r\n \r\n \r\n Please choose an action from the menu to the left.\r\n \r\n \r\n
\r\n >\r\n );\r\n};\r\n\r\nexport default NotFound;\r\n","// this is the amplify supplied auth system screens for logging in to (currently cognito)\r\n// and for resetting password, creating new user account\r\n//this file modifies the text of the amplify supplied screens, and places the modal menus in the center of the grid\r\n\r\nimport React, {useState} from 'react';\r\nimport { Grid } from '@material-ui/core';\r\nimport {\r\n AmplifyAuthenticator,\r\n AmplifySignIn,\r\n AmplifySignUp,\r\n} from '@aws-amplify/ui-react';\r\n\r\n/**\r\n * @method LoginScreen\r\n * present a login screen to the user, allow password reset, and optionally account creation\r\n * @param {boolean} props.allowSignUp if true, then user can create a new user account\r\n * @returns JSX Grid component\r\n */\r\nexport default function LoginScreen (props) {\r\n /* the allowSignUp prop is always flase for Alpha release, as we do customer signup as Admin, might allow users to do this later */\r\n const [allowSignUp] = useState(props.allowSignUp); //[allowSignUp, setSignUp]\r\n // need to customize the AWS container so minHeight isn't 100vh (makes modal stretch really tall...)\r\n return (\r\n \r\n \r\n {!allowSignUp && //not allowing user to create a new MeterVibe account\r\n \r\n }\r\n {allowSignUp && //enter these items if allowing new account creation\r\n <>\r\n \r\n \r\n \r\n \r\n >\r\n }\r\n \r\n \r\n );\r\n};\r\n\r\n//export default LoginScreen;\r\n","import React from 'react';\r\nimport Typography from '@material-ui/core/Typography';\r\nimport { makeStyles } from '@material-ui/core';\r\n\r\nconst useStyles = makeStyles({\r\n root: {\r\n // color passed as props, if there is a color prop show the color, else default is blue.\r\n color: ({ color }) => color ?? '#2969B1',\r\n fontSize: ({ fontSize }) => fontSize && fontSize,\r\n fontWeight: ({ fontWeight }) => fontWeight && fontWeight,\r\n cursor: ({ cursor }) => cursor && cursor,\r\n },\r\n});\r\n\r\nexport const PublicLandingText = ({\r\n color,\r\n fontSize,\r\n fontWeight,\r\n cursor,\r\n children,\r\n ...rest\r\n}) => {\r\n const classes = useStyles({ color, fontSize, fontWeight, cursor });\r\n\r\n return (\r\n \r\n {children}\r\n \r\n );\r\n};\r\n","// Header component sits at the top of App.js ast all times, holding the logo, user email, and AmplifySignOut button\r\n\r\nimport React, { useState, useEffect } from 'react';\r\nimport { Auth } from 'aws-amplify';\r\nimport { makeStyles } from '@material-ui/core/styles';\r\nimport {\r\n AppBar,\r\n Toolbar,\r\n Typography,\r\n Hidden,\r\n IconButton,\r\n Box,\r\n Grid,\r\n useTheme,\r\n useMediaQuery,\r\n} from '@material-ui/core';\r\nimport Dialog from '@material-ui/core/Dialog';\r\nimport Logo from '../assets/images/LogoTopLeft.png';\r\nimport LoginScreen from '../containers/LoginScreen';\r\nimport MeterVibeButton from './shared/buttons/MeterVibeButton';\r\nimport CartIcon from '@material-ui/icons/ShoppingCart';\r\nimport { Link } from 'react-scroll';\r\nimport useScrollTrigger from '@material-ui/core/useScrollTrigger';\r\nimport Slide from '@material-ui/core/Slide';\r\nimport { PublicLandingText as Text } from './shared/typography/PublicLandingText';\r\nimport CloseIcon from '@material-ui/icons/Close';\r\nimport InfoIcon from '@material-ui/icons/Info';\r\nimport { useHistory } from 'react-router-dom';\r\nimport { useLocation } from 'react-router-dom';\r\n\r\nimport { useDispatch, useSelector } from 'react-redux';\r\nimport { USER_ACTIONS } from './../reducers/user.reducer';\r\n\r\nconst useStyles = makeStyles((theme) => ({\r\n appBar: {\r\n background: '#0272BC',\r\n zIndex: theme.zIndex.drawer + 1, // sets the zIndex of the AppBar as +1 to the drawer so the AppBar will always sit on top of the drawer\r\n },\r\n logo: {\r\n flexGrow: 5, // similar to the grid system, these properties make the logo take up as much space as possible relative to the email and signout button\r\n cursor: 'pointer',\r\n },\r\n userEmail: {\r\n flexGrow: 1,\r\n },\r\n signInButton: {\r\n flexGrow: 1,\r\n },\r\n buyNowButton: {\r\n flexGrow: 3,\r\n [theme.breakpoints.down('xs')]: {\r\n marginRight: '5px',\r\n },\r\n },\r\n navButton: {\r\n [theme.breakpoints.down('xs')]: {\r\n fontSize: 'clamp(12px,3vw,16px)',\r\n },\r\n },\r\n offset: theme.mixins.toolbar,\r\n closeIcon: {\r\n color: '#0272BC',\r\n\r\n '&:focus': {\r\n outline: 'none',\r\n },\r\n },\r\n infoIcon: {\r\n color: '#0272BC',\r\n },\r\n}));\r\n\r\n// hides navbar when user scrolls down or clicks buy now (to avoid navbar hiding stuff).\r\nconst HideOnScroll = ({ children, window }) => {\r\n const trigger = useScrollTrigger({\r\n target: window ? window() : undefined,\r\n });\r\n\r\n return (\r\n \r\n {children}\r\n \r\n );\r\n};\r\n\r\nconst SignInModal = ({ open, handleClose }) => (\r\n <>\r\n \r\n \r\n \r\n >\r\n);\r\n\r\nconst { SET_USER_EMAIL } = USER_ACTIONS;\r\n\r\nexport default function PublicLandingHeader() {\r\n const classes = useStyles();\r\n const { pathname } = useLocation();\r\n const [isSignInOpen, setIsSignInOpen] = useState(false);\r\n const dispatch = useDispatch();\r\n\r\n const userEmail = useSelector(({ userReducer }) => userReducer.userEmail);\r\n\r\n const [showAlert, setShowAlert] = useState(() => {\r\n const storedState = localStorage.getItem('showAlert');\r\n\r\n if (storedState) {\r\n return storedState === 'true' ? true : false;\r\n }\r\n return true;\r\n });\r\n\r\n const theme = useTheme();\r\n const history = useHistory();\r\n\r\n const matchesSm = useMediaQuery(theme.breakpoints.down('sm'));\r\n const matchesXs = useMediaQuery(theme.breakpoints.down('xs'));\r\n\r\n const handleCloseAlert = () => {\r\n localStorage.setItem('showAlert', 'false');\r\n setShowAlert(false);\r\n };\r\n\r\n useEffect(() => {\r\n Auth.currentUserInfo().then((data) => {\r\n if (data) {\r\n // change email when pathname changes (for this navbar only to fix email still showing when signing out)\r\n dispatch({ type: SET_USER_EMAIL, payload: data.attributes.email });\r\n } else {\r\n dispatch({\r\n type: SET_USER_EMAIL,\r\n payload: '',\r\n });\r\n }\r\n });\r\n\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [pathname]);\r\n\r\n return (\r\n <>\r\n \r\n \r\n \r\n history.push('/')}>\r\n
\r\n
\r\n {/* MUI component hides its contents on xs screens and below*/}\r\n\r\n \r\n \r\n {/* react-scroll link, smooth scrolls to location in page */}\r\n \r\n \r\n \r\n
\r\n \r\n \r\n \r\n {/* if is signed in, show email, else show not signed in */}\r\n {Auth.currentAuthenticatedUser() ? userEmail : 'Not signed in'}\r\n \r\n \r\n\r\n \r\n {\r\n // if is signed in, go to the app.\r\n Auth.currentUserInfo().then((data) => {\r\n if (data) {\r\n return history.push('/dashboard');\r\n }\r\n // else open the sign in modal\r\n setIsSignInOpen(true);\r\n });\r\n }}\r\n />\r\n
\r\n \r\n\r\n {showAlert && (\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n Selling \"Alpha\" devices now! Be an early customer and help\r\n us perfect the product!\r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n )}\r\n \r\n \r\n\r\n {/* adding offset to remove the problem of hidden elements with position: fixed;\r\n https://material-ui.com/components/app-bar/#fixed-placement */}\r\n {/* offset here */}\r\n
\r\n\r\n setIsSignInOpen(false)}\r\n />\r\n >\r\n );\r\n}\r\n","// this will be the landing page for unauthenticated users\r\n\r\nimport React from 'react';\r\nimport { PublicLandingText as Text } from './shared/typography/PublicLandingText';\r\nimport { Grid } from '@material-ui/core';\r\nconst PublicLandingPaymentSuccess = () => {\r\n return (\r\n <>\r\n Thanks for your order! \r\n \r\n \r\n Thanks for your order!\r\n \r\n \r\n \r\n \r\n We appreciate your business! If you have any questions, please email\r\n orders@MeterVibe.com .\r\n We will send you a confirming email to the email you entered with your purchase.\r\n \r\n \r\n >\r\n );\r\n};\r\n\r\nexport default PublicLandingPaymentSuccess;\r\n","// this will be the landing page for unauthenticated users\r\n\r\nimport React from 'react';\r\nimport { PublicLandingText as Text } from './shared/typography/PublicLandingText';\r\n\r\nconst PublicLandingPaymentFailure = () => (\r\n <>\r\n Something went wrong \r\n \r\n Something went wrong\r\n \r\n \r\n Something went wrong with your credit card charge, try again or email\r\n support@MeterVibe.com .\r\n for further assistance\r\n
\r\n >\r\n);\r\n\r\nexport default PublicLandingPaymentFailure;\r\n","import React from 'react';\r\n\r\nconst WavyBackground = ({ height }) => (\r\n \r\n \r\n \r\n);\r\n\r\nexport default WavyBackground;\r\n","import React from 'react';\r\nimport { Grid, Box } from '@material-ui/core/';\r\nimport { PublicLandingText as Text } from './shared/typography/PublicLandingText';\r\nimport { Link } from 'react-router-dom';\r\nimport WavyBackground from './shared/fun_backgrounds/WavyBackground';\r\n\r\nconst PRIMARY_COLOR = '#2969B1';\r\n\r\nexport default function PublicLandingFooter({ classes }) {\r\n return (\r\n <>\r\n \r\n \r\n \r\n {/* footer left */}\r\n \r\n \r\n \r\n © 2021 MeterVibe™\r\n \r\n \r\n \r\n\r\n \r\n\r\n {/* footer right */}\r\n \r\n \r\n \r\n support@metervibe.com\r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n {/* will link to privacy policy page */}\r\n \r\n Privacy Policy\r\n \r\n \r\n \r\n \r\n {/* will link to terms of service page */}\r\n \r\n \r\n Terms of Service\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n >\r\n );\r\n}\r\n","import { Global, css } from \"@emotion/core\";\r\nimport React from \"react\";\r\n\r\n\r\nconst GlobalStyles = () => (\r\n <>\r\n \r\n \r\n \r\n >\r\n);\r\n\r\nexport default GlobalStyles;\r\n\r\n//background-color purple : #6772e5\r\n","// this is the amplify supplied auth system screens for logging in to (currently cognito)\r\n// and for resetting password, creating new user account\r\n//this file modifies the text of the amplify supplied screens, and places the modal menus in the center of the grid\r\n\r\nimport React from \"react\";\r\nimport { Grid } from \"@material-ui/core\"\r\nimport { Elements } from '@stripe/react-stripe-js'\r\nimport { loadStripe } from '@stripe/stripe-js'\r\nimport GlobalStyles from \"./prebuilt/GlobalStyles\"\r\nimport Head from \"next/head\";\r\n\r\n\r\n//const PUBLISHABLE_KEY = 'pk_test_51Ivpq9FWfFgbm17XwWSsGASWYvgoobbQ4pDvAe98VbbhIK96PTpP0zMOyYLJvZOIBIzUmHVNZHmpFnJd04u5vUlk00680ppoSs';\r\nconst PUBLISHABLE_KEY ='pk_live_51Ivpq9FWfFgbm17X4uip1W2cH3jZYdpPSzGHma3yzVK34aVNEHEzJx0Lit5rPg0ZMOYJ5wTuHPTn6hmx93rbAUbC00Ww33rfRz';\r\n\r\nconst stripePromise = loadStripe(PUBLISHABLE_KEY);\r\n\r\nconst Layout = ({ children, title }) => {\r\n return (\r\n <>\r\n \r\n \r\n\r\n \r\n \r\n {title} \r\n \r\n \r\n \r\n {children} \r\n \r\n \r\n \r\n \r\n >\r\n )\r\n}\r\n\r\nexport default Layout;","import styled from '@emotion/styled';\r\n\r\nconst Row = styled.div`\r\n width: 475px;\r\n margin: 30px auto;\r\n box-shadow: 0 6px 9px rgba(50, 50, 93, 0.06), 0 2px 5px rgba(0, 0, 0, 0.08),\r\n inset 0 1px 0 #829fff;\r\n border-radius: 4px;\r\n background-color: #7795f8;\r\n position: relative;\r\n\r\n /* I need to do this so the purchase card doesn't break the site on low media queries. */\r\n @media screen and (max-width: 768px) {\r\n width: ${({ responsive }) => responsive && '345px'};\r\n }\r\n`;\r\n\r\nexport default Row;\r\n","import React from 'react';\r\nimport Radio from '@material-ui/core/Radio';\r\nimport RadioGroup from '@material-ui/core/RadioGroup';\r\nimport FormControlLabel from '@material-ui/core/FormControlLabel';\r\nimport FormControl from '@material-ui/core/FormControl';\r\nimport FormLabel from '@material-ui/core/FormLabel';\r\n\r\n//for now only allow one sold at a time, quantity limited to \"1\"\r\nconst QuantityRadio = ({ quantity, onChange }) => {\r\n return (\r\n \r\n Quantity \r\n \r\n } label=\"1\" />\r\n {/* } label=\"2\" /> */}\r\n \r\n \r\n );\r\n};\r\n\r\nexport default QuantityRadio;\r\n","import React from 'react';\r\n//import { Button} from '@material-ui/core/Button';\r\nimport { Box, Button } from '@material-ui/core';\r\n//import { useNavigate } from \"react-router-dom\"; need v6\r\n\r\n// services\r\n//import { postStripe } from './../../services/stripe.services';\r\n\r\n//const PRIMARY_COLOR = '#2969B1';\r\n\r\n/*\r\nconst CardElementContainer = styled.div`\r\n height: 40px;\r\n display: flex;\r\n align-items: center;\r\n\r\n & .StripeElement {\r\n width: 100%;\r\n padding: 15px;\r\n }\r\n`;\r\n\r\nconst CheckoutForm = ({ onSuccessfulCheckout }) => {\r\n const [isProcessing, setProcessingTo] = useState(false);\r\n const [checkoutError, setCheckoutError] = useState();\r\n\r\n const stripe = useStripe();\r\n const elements = useElements();\r\n\r\n // TIP\r\n // use the cardElements onChange prop to add a handler\r\n // for setting any errors:\r\n\r\n const handleCardDetailsChange = ev => {\r\n ev.error ? setCheckoutError(ev.error.message) : setCheckoutError();\r\n };\r\n\r\n const handleFormSubmit = async ev => {\r\n ev.preventDefault();\r\n\r\n const billingDetails = {\r\n name: ev.target.name.value,\r\n email: ev.target.email.value,\r\n address: {\r\n city: ev.target.city.value,\r\n line1: ev.target.address.value,\r\n state: ev.target.state.value,\r\n postal_code: ev.target.zip.value\r\n }\r\n };\r\n\r\n setProcessingTo(true);\r\n\r\n const cardElement = elements.getElement(\"card\");\r\n const amount = 5000;\r\n\r\n try {\r\n const apiName = \"dev-iotvid-app\"; // in development (dev), backend: -iotvid-app\r\n const path = `/post/stripe/${amount}` //serverless.yml http path, method: post\r\n const myInit = {\r\n body: billingDetails, //user entered data sent to stripe backend processing\r\n headers: {}, //optional, not used\r\n }\r\n const { data: clientSecret } = await API.post(apiName, path, billingDetails);\r\n\r\n const paymentMethodReq = await stripe.createPaymentMethod({\r\n type: \"card\",\r\n card: cardElement,\r\n billing_details: billingDetails\r\n });\r\n\r\n if (paymentMethodReq.error) {\r\n setCheckoutError(paymentMethodReq.error.message);\r\n setProcessingTo(false);\r\n return;\r\n }\r\n\r\n const { error } = await stripe.confirmCardPayment(clientSecret, {\r\n payment_method: paymentMethodReq.paymentMethod.id\r\n });\r\n\r\n if (error) {\r\n setCheckoutError(error.message);\r\n setProcessingTo(false);\r\n return;\r\n }\r\n\r\n onSuccessfulCheckout();\r\n } catch (err) {\r\n setCheckoutError(err.message);\r\n }\r\n };\r\n\r\n // Learning\r\n // A common ask/bug that users run into is:\r\n // How do you change the color of the card element input text?\r\n // How do you change the font-size of the card element input text?\r\n // How do you change the placeholder color?\r\n // The answer to all of the above is to use the `style` option.\r\n // It's common to hear users confused why the card element appears impervious\r\n // to all their styles. No matter what classes they add to the parent element\r\n // nothing within the card element seems to change. The reason for this is that\r\n // the card element is housed within an iframe and:\r\n // > styles do not cascade from a parent window down into its iframes\r\n\r\n const iframeStyles = {\r\n base: {\r\n color: \"#fff\",\r\n fontSize: \"16px\",\r\n iconColor: \"#fff\",\r\n \"::placeholder\": {\r\n color: \"#87bbfd\"\r\n }\r\n },\r\n invalid: {\r\n iconColor: \"#FFC7EE\",\r\n color: \"#FFC7EE\"\r\n },\r\n complete: {\r\n iconColor: \"#cbf4c9\"\r\n }\r\n };\r\n\r\n const cardElementOpts = {\r\n iconStyle: \"solid\",\r\n style: iframeStyles,\r\n hidePostalCode: true\r\n };\r\n\r\n return (\r\n \r\n );\r\n};\r\n\r\n//export default CheckoutForm;\r\n\r\n*/\r\n\r\n//*********************** THIS IS THE NEW CODE FROM THE EXAMPLE STIPE PAYMENTS USING EXTERNAL STRIPE PAYMENT SERVICE */\r\n//** 1/28/22 This worked in test mode, but could not get it to work in production, so now replacing this with payment url below */\r\n\r\n//External payment via stripe, then go to success or failure page via backend\r\n//https://stripe.com/docs/payments/accept-a-payment\r\n\r\n/*\r\nexport default function StripePayment({ quantity }) {\r\n const handleClick = async () => {\r\n // Get Stripe.js instance\r\n //console.log(\"in StripePayment quantity\", quantity)\r\n const result = await postStripe(quantity); //for now quantity is always 1 in the OrderCard.js as provided by stripe.js\r\n\r\n if (result.error) {\r\n // If `redirectToCheckout` fails due to a browser or network\r\n // error, display the localized error message to your customer\r\n // using `result.error.message`.\r\n }\r\n };\r\n\r\n return (\r\n \r\n Purchase\r\n \r\n );\r\n}\r\n\r\n*/\r\n\r\n//********************************************************************************************************************* */\r\n\r\n//************************* NOW TRYING THIS WITH A PAYMENT URL DEFINED IN STRIPE *************************************/\r\n\r\n//This calls the url of the payment defined at stripe>payments>payment links, using a product at stripe>product\r\nfunction StripePayment({ quantity }) {\r\n\r\n const handleClick = async () => {\r\n\r\n //const result = window.location.replace('https://buy.stripe.com/test_7sI02Tg4Y5Akb0keUU');\r\n //const result = window.location.assign('https://buy.stripe.com/test_7sI02Tg4Y5Akb0keUU', '_blank');\r\n const result = window.open('https://buy.stripe.com/dR69DA7NZf6f7Mk001', '_blank'); //try opening in a new tab, also could \r\n // or GeeksforGeeks or target=\"_top\"\r\n console.log (\"window.location.assign:\", result);\r\n if (result.error) {\r\n // If `redirectToCheckout` fails due to a browser or network\r\n // error, display the localized error message to your customer\r\n // using `result.error.message`.\r\n }\r\n };\r\n\r\n //goto external stripe defined url to purchase MeterVibe with subscription\r\n //ERROR caused by CLOUDFRONT: checkout-app-init-4f64b720f0ebbe99e72b2bb408efbd61.js:1 Stripe Checkout is not able to run in an iFrame. Please redirect to Checkout at the top level.\r\n return (\r\n\r\n \r\n \r\n \r\n Purchase Now\r\n \r\n \r\n );\r\n}\r\n\r\nexport default StripePayment;","//from DonutShop.jsx in stripe/prebuilt revising to make this MaterialUI like\r\n\r\n//import React, { useState } from 'react';\r\nimport React from 'react';\r\nimport { makeStyles } from '@material-ui/core/styles';\r\n//import clsx from 'clsx';\r\nimport QuantityRadio from './QuantityRadio';\r\nimport CheckoutForm from './stripe/CheckoutForm';\r\nimport Typography from '@material-ui/core/Typography';\r\nimport Grid from '@material-ui/core/Grid';\r\nimport Card from '@material-ui/core/Card';\r\n//import CardActions from '@material-ui/core/CardActions';\r\nimport CardContent from '@material-ui/core/CardContent';\r\n//import Collapse from '@material-ui/core/Collapse';\r\n//import IconButton from '@material-ui/core/IconButton';\r\n//import ExpandMoreIcon from '@material-ui/icons/ExpandMore';\r\n\r\nconst useStyles = makeStyles((theme) => ({\r\n root: {\r\n [theme.breakpoints.down('xs')]: {\r\n maxWidth: 345,\r\n },\r\n padding: '10px',\r\n },\r\n media: {\r\n height: 0,\r\n paddingTop: '56.25%', // 16:9\r\n },\r\n expand: {\r\n transform: 'rotate(0deg)',\r\n marginLeft: 'auto',\r\n transition: theme.transitions.create('transform', {\r\n duration: theme.transitions.duration.shortest,\r\n }),\r\n\r\n '&:focus': {\r\n outline: 'none',\r\n },\r\n },\r\n expandOpen: {\r\n transform: 'rotate(180deg)',\r\n },\r\n}));\r\n\r\nexport default function OrderCard({ numDevices, setNumDevices }) {\r\n const classes = useStyles();\r\n //const [expanded, setExpanded] = useState(false);\r\n\r\n //const handleExpandClick = () => {\r\n // setExpanded(!expanded);\r\n //};\r\n\r\n return (\r\n \r\n {/* */}\r\n \r\n ALPHA Devices Available\r\n \r\n \r\n with 12 month Alpha subscription\r\n \r\n \r\n ---\r\n \r\n \r\n $198.00 \r\n \r\n MeterVibe device plus one year data subscription\r\n \r\n \r\n ---\r\n \r\n \r\n setNumDevices(Number(e.target.value))}\r\n />\r\n \r\n \r\n On purchase: \r\n 1. Your MeterVibe login ID will be the email you enter at time of purchase.\r\n 2. Click \"PURCHASE\" to go to secure external site to complete your purchase.\r\n \r\n \r\n \r\n\r\n{/* */}\r\n \r\n{/*\r\n \r\n \r\n \r\n */}\r\n {/* */}\r\n\r\n {/* */}\r\n \r\n FAQ: \r\n \r\n This purchase is for our introductory MeterVibe Alpha model. Each order contains one device, a USB power block, a USB connector cable, and a 12 month\r\n subscription. When placing your order, we ask for your email address which will become your login user name, \r\n a phone number, for text alerts if this feature is activated, and contact info to troubleshoot issues, and a ship to address for delivery.\r\n \r\n \r\n The MeterVibe alpha is designed to work inside a building, attached to your meter, and is powered with a standard 110V outlet. \r\n The MeterVibe alpha connects to the cloud with available 2.4 Ghz wifi (802.11 b/g/n). \r\n \r\n \r\n This is a new product line and service, so we welcome feedback as we improve our product, and add new features.\r\n \r\n \r\n MeterVibe creates a cloud based \"Snap\" (picture) of your meter.\r\n These pictures are, with a delay, turned into meter readings. \r\n We will track your reading history, and provide you feedback with alerts.\r\n The visual snapshot of your most\r\n recent meter reading will be available after a sucessful\r\n snapshot is taken.\r\n \r\n \r\n Order now, and enjoy your MeterVibe Alpha edition. \r\n \r\n \r\n {/* */}\r\n \r\n \r\n );\r\n}\r\n","import { useState } from 'react';\r\nimport React from 'react';\r\nimport Layout from './stripe/Layout';\r\nimport Row from './stripe/prebuilt/Row';\r\nimport OrderCard from './OrderCard';\r\n\r\nexport default function Stripe() {\r\n const [numDevices, setNumDevices] = useState(1);\r\n\r\n // not needed, we have 2 radio buttons each with a value, we just set numDonuts to the value.\r\n // const addDonut = () => setNumDonuts((num) => Math.min(2, num + 1)); //max order 2 for beta\r\n // const remDonut = () => setNumDonuts((num) => Math.max(1, num - 1));\r\n\r\n return (\r\n <>\r\n \r\n \r\n \r\n
\r\n {/*\r\n Router.push(\"/success\")}\r\n /> */}\r\n \r\n >\r\n );\r\n}\r\n","import React from 'react';\r\nimport { Grid, makeStyles } from '@material-ui/core';\r\nimport { Slide } from 'react-slideshow-image';\r\nimport 'react-slideshow-image/dist/styles.css';\r\nimport ComputerScreenFrame from '../assets/images/computer_screen_frame.png';\r\nimport MeterVibeAppImg1 from '../assets/images/metervibe_app_desktop_1.png';\r\nimport MeterVibeAppImg2 from '../assets/images/metervibe_app_desktop_2.png';\r\nimport MeterVibeAppImg3 from '../assets/images/metervibe_app_desktop_3.png';\r\n\r\nconst useStyles = makeStyles((theme) => ({\r\n root: {\r\n overflow: 'hidden',\r\n position: 'relative',\r\n width: '314px',\r\n },\r\n image: {\r\n maxWidth: '100%',\r\n width: 'auto',\r\n height: '166px',\r\n verticalAlign: 'middle',\r\n border: '0',\r\n },\r\n computerScreen: {\r\n position: 'absolute',\r\n height: '100%',\r\n width: '85%',\r\n left: '25px',\r\n top: '13px',\r\n overflow: 'hidden',\r\n },\r\n}));\r\nexport default function PublicLandingComputerScreen() {\r\n const slideImages = [MeterVibeAppImg1, MeterVibeAppImg2, MeterVibeAppImg3];\r\n const classes = useStyles();\r\n\r\n return (\r\n \r\n \r\n \r\n {slideImages.map((imgSrc, key) => (\r\n \r\n ))}\r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n );\r\n}\r\n","import React from 'react';\r\nimport { Slide } from 'react-slideshow-image';\r\nimport 'react-slideshow-image/dist/styles.css';\r\n\r\nexport default function Slideshow({\r\n images,\r\n slideProps,\r\n containerWidth,\r\n imgWidth,\r\n imgHeight,\r\n ...rest\r\n}) {\r\n // accepts an array of images, no objects, just strings with values of path.\r\n return (\r\n \r\n
\r\n {images.map((imgSrc, key) => (\r\n \r\n ))}\r\n \r\n
\r\n );\r\n}\r\n","// this will be the landing page for unauthenticated users\r\n// test VISA card is 4242 4242 4242 4242\r\n\r\n// hooks\r\nimport React, { useEffect } from 'react';\r\nimport { useLocation, useHistory } from 'react-router-dom';\r\nimport { useMediaQuery, useTheme } from '@material-ui/core';\r\n\r\n// components\r\nimport { Grid, Paper, Box } from '@material-ui/core';\r\nimport LoginScreen from '../containers/LoginScreen';\r\nimport PublicLandingHeader from '../components/PublicLandingHeader';\r\nimport PublicLandingPaymentSuccess from '../components/PublicLandingPaymentSuccess';\r\nimport PublicLandingPaymentFailure from '../components/PublicLandingPaymentFailure';\r\nimport PublicLandingFooter from './PublicLandingFooter';\r\nimport { PublicLandingText as Text } from './shared/typography/PublicLandingText';\r\nimport Stripe from './Stripe';\r\nimport PublicLandingComputerScreen from './PublicLandingComputerScreen';\r\nimport Slideshow from './shared/image_components/Slideshow';\r\nimport WavyBackground from './shared/fun_backgrounds/WavyBackground';\r\n\r\n// images\r\nimport MeterVibeDeviceImg from '../assets/images/metervibe_device_transparent.png';\r\nimport MeterVibeLogoImg from '../assets/images/MeterVibe_logo_1444x596.png'; \r\nimport WaterfallImg from '../assets/images/waterfall.jpeg';\r\nimport MeterImg1 from '../assets/images/meter_1.png';\r\nimport MeterImg2 from '../assets/images/meter_2.png';\r\nimport MeterImg3 from '../assets/images/meter_3.png';\r\nimport MeterImg4 from '../assets/images/meter_4.png';\r\n\r\n// utils\r\nimport { makeStyles } from '@material-ui/core/styles';\r\nimport MeterVibeButton from './shared/buttons/MeterVibeButton';\r\nimport 'react-slideshow-image/dist/styles.css';\r\n\r\n/* colors from app.css\r\n:root {\r\n --amplify-primary-color:#015388; //dark blue\r\n --amplify-primary-tint:#0272bc; //medium blue\r\n --amplify-primary-shade: #3d93cc; //light blue\r\n}\r\n*/\r\n\r\nconst PRIMARY_TEXT_COLOR = '#2969B1';\r\n\r\n// font-size that scale according to screen size\r\nconst LOUD_VOICE_FONT_SIZE = 'clamp(24px,4vw, 40px)';\r\n\r\nconst useStyles = makeStyles((theme) => ({\r\n root: {\r\n '& > *': {\r\n margin: theme.spacing(1),\r\n width: theme.spacing(32),\r\n height: theme.spacing(16),\r\n },\r\n },\r\n mediumBluePaper: {\r\n backgroundColor: '#0272bc',\r\n },\r\n whitePaper: {\r\n backgroundColor: '#FFFFFF',\r\n },\r\n sweetAiranPaper: {\r\n backgroundColor: '#ffffff',\r\n backgroundImage: 'linear-gradient(315deg, #ffffff 0%, #d7e1ec 74%)',\r\n flexGrow: 1,\r\n },\r\n\r\n pageContent: {\r\n flexGrow: 1,\r\n },\r\n\r\n customBorderRadius: {\r\n borderRadius: 25,\r\n },\r\n\r\n innerColumn: {\r\n width: '98%',\r\n maxWidth: '1100px',\r\n marginRight: 'auto',\r\n marginLeft: 'auto',\r\n padding: '10px',\r\n },\r\n\r\n pageSection: {\r\n scrollMarginTop: '2em',\r\n },\r\n\r\n welcome: {\r\n scrollMarginTop: '2em',\r\n minHeight: '75vh',\r\n width: '98%',\r\n maxWidth: '1100px',\r\n marginRight: 'auto',\r\n marginLeft: 'auto',\r\n padding: '10px',\r\n\r\n [theme.breakpoints.down('md')]: {\r\n // show more of first section in medium screens\r\n minHeight: '80vh',\r\n },\r\n\r\n [theme.breakpoints.down('sm')]: {\r\n // show more of first section in phone queries\r\n minHeight: '85vh',\r\n },\r\n\r\n [theme.breakpoints.down('xs')]: {\r\n // show more of first section in phone queries\r\n minHeight: '89vh',\r\n },\r\n },\r\n\r\n pageBreak: {\r\n flexGrow: 1,\r\n padding: '20px',\r\n },\r\n\r\n listMain: {\r\n color: PRIMARY_TEXT_COLOR,\r\n fontSize: '18px',\r\n lineHeight: '25px',\r\n padding: '5px 20px',\r\n [theme.breakpoints.down('xs')]: {\r\n lineHeight: '30px',\r\n },\r\n\r\n [theme.breakpoints.down('sm')]: {\r\n fontSize: '16px',\r\n },\r\n },\r\n listAlt: {\r\n color: '#fff',\r\n fontSize: '1.2rem',\r\n lineHeight: '50px',\r\n },\r\n installationInfoList: {\r\n color: PRIMARY_TEXT_COLOR,\r\n fontSize: '18px',\r\n lineHeight: '40px',\r\n margin: '0 5px',\r\n padding: '0 20px',\r\n\r\n [theme.breakpoints.down('sm')]: {\r\n fontSize: '16px',\r\n },\r\n },\r\n deviceImage: {\r\n width: '100%',\r\n margin: '10px auto',\r\n padding: '10px',\r\n\r\n [theme.breakpoints.down('sm')]: {\r\n width: '60%',\r\n },\r\n\r\n [theme.breakpoints.down('xs')]: {\r\n width: '80%',\r\n },\r\n },\r\n sectionImage: {\r\n [theme.breakpoints.down('sm')]: {\r\n marginTop: '10px',\r\n },\r\n },\r\n}));\r\n\r\nexport default function PublicLanding() {\r\n const classes = useStyles();\r\n const theme = useTheme();\r\n const { pathname } = useLocation();\r\n const history = useHistory();\r\n\r\n const matchesMd = useMediaQuery(theme.breakpoints.down('md'));\r\n const matchesSm = useMediaQuery(theme.breakpoints.down('sm'));\r\n const matchesXS = useMediaQuery(theme.breakpoints.down('xs'));\r\n\r\n useEffect(() => {\r\n console.log(\"pathname:\", pathname);\r\n //for now, if user cancels during payment (back) :{currentURL: \"/payment_cancel\"} we don't have a seperate page, just go back to /, PublicLanding\r\n if (pathname === '/payment_cancel') history.push('/');\r\n // check for this everytime pathname changes.\r\n }, [pathname, history]);\r\n\r\n // switching to if because we might have an else-if for /payment_failure.\r\n if (pathname === '/payment_success') {\r\n return (\r\n <>\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n history.push('/')}\r\n text=\"Go Back\"\r\n />\r\n \r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n >\r\n );\r\n } else if (pathname === '/payment_failure') {\r\n return (\r\n <>\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n history.push('/')}\r\n text=\"Go Back\"\r\n />\r\n \r\n \r\n \r\n
\r\n \r\n >\r\n );\r\n }\r\n\r\n return (\r\n <>\r\n \r\n \r\n \r\n {/* welcome section */}\r\n \r\n \r\n \r\n MeterVibe Water Monitor - Alpha Edition\r\n \r\n \r\n\r\n \r\n \r\n Whole house water monitoring and leak detection.\r\n \r\n\r\n \r\n \"We watch your meter so you don't have to.\"\r\n \r\n \r\n \r\n \r\n \r\n Wifi connected whole house water monitoring\r\n \r\n \r\n easy to install, attaches to your existing water meter \r\n \r\n alerts to your phone, find leaky toilets, spot excessive water usage\r\n \r\n \r\n plan and budget your water use, no more billing surprises\r\n \r\n \r\n \r\n\r\n \r\n \r\n \r\n
\r\n
\r\n \r\n
\r\n
\r\n \r\n The MeterVibe Alpha\r\n \r\n \r\n \r\n\r\n \r\n\r\n {/* app section */}\r\n \r\n \r\n \r\n \r\n Keeps a history of your water use \r\n Easy to navigate, water use at your fingertips \r\n Easy to compare water use over time \r\n Upgrades will include user customized usage alerts \r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n
\r\n \r\n\r\n \r\n \r\n
\r\n\r\n {/* meters section (used to be customer reviews in UI design) */}\r\n \r\n
\r\n \r\n \r\n Selling \"Alpha\" devices now!\r\n \r\n\r\n \r\n Be an early customer and help us perfect the product!\r\n \r\n \r\n \r\n MeterVibe Alpha is easy to install at your house/condo/office\r\n \r\n\r\n \r\n \r\n Attaches to the Water Meter located inside your building.\r\n \r\n Uses wifi to communicate with the Cloud. \r\n Powers from a standard 110V outlet. \r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n\r\n
\r\n\r\n \r\n \r\n \r\n \r\n \r\n Using technology to make a better world\r\n \r\n\r\n \r\n water meters are everywhere, but remote monitoring is not. We\r\n have created an easy to use, non intrusive way, to stop\r\n wasting water. We are only getting started! join us by being\r\n an early customer and help us do our part in making the world\r\n a better place!\r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n
\r\n \r\n\r\n \r\n \r\n
\r\n\r\n \r\n \r\n \r\n Get MeterVibe Today!\r\n \r\n \r\n
\r\n \r\n\r\n
\r\n \r\n\r\n \r\n \r\n
\r\n\r\n \r\n \r\n >\r\n );\r\n}\r\n\r\n/*\r\n postPaymentIntent(amount) }>\r\n Checkout Button\r\n \r\n */\r\n","// Header component sits at the top of App.js ast all times, holding the logo, user email, and AmplifySignOut button\r\n\r\nimport React from 'react';\r\nimport { makeStyles } from '@material-ui/core/styles';\r\nimport { AppBar, Toolbar, Typography, Hidden } from '@material-ui/core';\r\nimport Logo from '../assets/images/LogoTopLeft.png';\r\nimport { AmplifySignOut } from '@aws-amplify/ui-react';\r\n\r\nconst useStyles = makeStyles((theme) => ({\r\n appBar: {\r\n background: '#0272BC',\r\n zIndex: theme.zIndex.drawer + 1, // sets the zIndex of the AppBar as +1 to the drawer so the AppBar will always sit on top of the drawer\r\n },\r\n logo: {\r\n flexGrow: 10, // similar to the grid system, these properties make the logo take up as much space as possible relative to the email and signout button\r\n },\r\n userEmail: {\r\n flexGrow: 1,\r\n },\r\n signOutButton: {\r\n flexGrow: 1,\r\n },\r\n}));\r\n\r\n// this header is inside MeterVibeAppLayout.js'\r\nconst Header = ({ userEmail }) => {\r\n const classes = useStyles();\r\n return (\r\n <>\r\n \r\n \r\n \r\n
\r\n
\r\n {/* MUI component hides its contents on xs screens and below*/}\r\n \r\n {userEmail} \r\n \r\n {/* TODO: we need to make the signout button also push to / (right now pathname is the same when you leave it) */}\r\n \r\n \r\n \r\n >\r\n );\r\n};\r\n\r\nexport default Header;\r\n","// CameraDropdown.js holds the list of cameras so the user can select one and set it to state\r\n// JZ - there is a bug that after list of cameras comes back, the first camera (A101) will show in the dropdown\r\n// though that camera is not actually selected. It should stay with the disabled menu option, but it doesn't.\r\n\r\nimport React from 'react';\r\nimport { makeStyles } from '@material-ui/core/styles';\r\nimport InputLabel from '@material-ui/core/InputLabel';\r\nimport MenuItem from '@material-ui/core/MenuItem';\r\nimport FormControl from '@material-ui/core/FormControl';\r\nimport Select from '@material-ui/core/Select';\r\nimport { useHistory } from 'react-router-dom';\r\nimport { useLocation } from 'react-router-dom';\r\nimport { useDispatch, useSelector } from 'react-redux';\r\nimport { CAMERA_ACTIONS } from './../reducers/cameras.reducer';\r\nimport { getCameraObject } from './../reducers/cameras.reducer';\r\n\r\nconst useStyles = makeStyles((theme) => ({\r\n formControl: {\r\n margin: theme.spacing(1),\r\n minWidth: 120,\r\n },\r\n selectEmpty: {\r\n marginTop: theme.spacing(2),\r\n },\r\n}));\r\n\r\nconst { SET_USER_SELECTED_CAMERA } = CAMERA_ACTIONS;\r\n\r\nconst CameraDropDown = ({ showMore }) => {\r\n let history = useHistory();\r\n let { pathname } = useLocation();\r\n const dispatch = useDispatch();\r\n\r\n const { cameras, userSelectedCamera } = useSelector(\r\n ({ camerasReducer }) => camerasReducer\r\n );\r\n\r\n const userSelectedCameraId = userSelectedCamera?.cameraId;\r\n\r\n const classes = useStyles();\r\n\r\n const handleChange = (event) => {\r\n const selectedCameraId = event.target.value;\r\n\r\n const selectedCameraObject = getCameraObject(cameras, selectedCameraId);\r\n\r\n dispatch({ type: SET_USER_SELECTED_CAMERA, payload: selectedCameraObject });\r\n // don't push to /device if user is in /reports\r\n if (pathname === '/reports') return;\r\n history.push('/device');\r\n };\r\n\r\n /*\r\n \r\n Selected Device \r\n \r\n \r\n Select Camera\r\n \r\n {/* dropdown items mapped from cameras array, then selected camera is stored in state *---/}\r\n {cameras.map((camera, i) => (\r\n \r\n {camera.cameraId}\r\n \r\n ))}\r\n \r\n \r\n */\r\n\r\n return (\r\n \r\n Device \r\n \r\n {/* dropdown items mapped from cameras array, then selected camera is stored in state */}\r\n {cameras.map((camera, i) => (\r\n \r\n {!showMore ? (\r\n camera.cameraId\r\n ) : (\r\n <>\r\n {' (' +\r\n camera.cameraId +\r\n ') ' +\r\n \"'\" +\r\n camera.cameraName +\r\n \"'\" +\r\n ' located at ' +\r\n \"'\" +\r\n camera.cameraLocation +\r\n \"'\"}\r\n >\r\n )}\r\n \r\n ))}\r\n \r\n \r\n );\r\n};\r\n\r\nexport default CameraDropDown;\r\n","import React, { useEffect } from 'react';\r\nimport { Auth } from 'aws-amplify';\r\nimport { useHistory } from 'react-router-dom';\r\n\r\n/**\r\n * @param {React.FunctionComponent} Component // the component wrapped in the function when exported\r\n * @param {String} route // the route pushing to on-error (when un-authenticated), defaults to '/' when not provided.\r\n * @returns {React.FunctionComponent} // the component to render when authenticated\r\n */\r\nconst protectedRoute =\r\n (Component, route = '/') =>\r\n (props) => {\r\n const history = useHistory();\r\n async function checkAuthState() {\r\n try {\r\n // check if user is authenticated\r\n await Auth.currentAuthenticatedUser();\r\n } catch (err) {\r\n // if not authenticated, push to the route (defaults to '/')\r\n history.push(route);\r\n }\r\n }\r\n useEffect(() => {\r\n checkAuthState();\r\n // no dependency array, runs first on mount and then every re-render (acts as both componentDidMount and componentDidUpdate)\r\n });\r\n // if is authenticated, return the component as usual.\r\n return ;\r\n };\r\n\r\nexport default protectedRoute;\r\n","import React, { useState, useMemo } from 'react';\r\nimport { useSelector } from 'react-redux';\r\nimport { Grid, Toolbar, Typography } from '@material-ui/core';\r\nimport { makeStyles } from '@material-ui/core/styles';\r\nimport { NavLink } from 'react-router-dom';\r\nimport { ListItem } from '@material-ui/core';\r\nimport clsx from 'clsx';\r\nimport Drawer from '@material-ui/core/Drawer';\r\nimport List from '@material-ui/core/List';\r\nimport Divider from '@material-ui/core/Divider';\r\nimport Container from '@material-ui/core/Container';\r\nimport Paper from '@material-ui/core/Paper';\r\nimport Header from '../components/Header';\r\nimport CameraDropdown from '../components/CameraDropdown';\r\nimport { Link, useLocation } from 'react-router-dom';\r\nimport protectedRoute from '../utils/protectedRoute';\r\n\r\n// icons\r\nimport IconButton from '@material-ui/core/IconButton';\r\nimport DashboardIcon from '@material-ui/icons/Dashboard';\r\nimport ChevronLeftIcon from '@material-ui/icons/ChevronLeft';\r\nimport LinkedCameraIcon from '@material-ui/icons/LinkedCamera';\r\nimport ChevronRightIcon from '@material-ui/icons/ChevronRight';\r\nimport AddBoxIcon from '@material-ui/icons/AddBox';\r\nimport VpnKeyIcon from '@material-ui/icons/VpnKey';\r\nimport ReportsIcon from '@material-ui/icons/Assessment';\r\nimport AlertsIcon from '@material-ui/icons/ErrorOutline';\r\nimport PersonIcon from '@material-ui/icons/Person';\r\n\r\nconst drawerWidth = 240;\r\n\r\nconst useStyles = makeStyles((theme) => ({\r\n root: {\r\n display: 'flex',\r\n },\r\n toolbar: {\r\n paddingRight: 24, // keep right padding when drawer closed\r\n },\r\n toolbarIcon: {\r\n display: 'flex',\r\n alignItems: 'center',\r\n justifyContent: 'flex-end',\r\n padding: '0 8px',\r\n ...theme.mixins.toolbar,\r\n },\r\n iconButton: {\r\n '&:focus': {\r\n outline: 'none',\r\n },\r\n },\r\n appBar: {\r\n zIndex: theme.zIndex.drawer + 1,\r\n transition: theme.transitions.create(['width', 'margin'], {\r\n easing: theme.transitions.easing.sharp,\r\n duration: theme.transitions.duration.leavingScreen,\r\n }),\r\n },\r\n appBarShift: {\r\n marginLeft: drawerWidth,\r\n width: `calc(100% - ${drawerWidth}px)`,\r\n transition: theme.transitions.create(['width', 'margin'], {\r\n easing: theme.transitions.easing.sharp,\r\n duration: theme.transitions.duration.enteringScreen,\r\n }),\r\n },\r\n menuButton: {\r\n marginRight: 36,\r\n },\r\n menuButtonHidden: {\r\n display: 'none',\r\n },\r\n title: {\r\n flexGrow: 1,\r\n },\r\n drawerPaper: {\r\n position: 'relative',\r\n whiteSpace: 'nowrap',\r\n width: drawerWidth,\r\n background: '#015388',\r\n transition: theme.transitions.create('width', {\r\n easing: theme.transitions.easing.sharp,\r\n duration: theme.transitions.duration.enteringScreen,\r\n }),\r\n },\r\n drawerPaperClose: {\r\n overflowX: 'hidden',\r\n transition: theme.transitions.create('width', {\r\n easing: theme.transitions.easing.sharp,\r\n duration: theme.transitions.duration.leavingScreen,\r\n }),\r\n width: theme.spacing(7),\r\n [theme.breakpoints.up('sm')]: {\r\n width: theme.spacing(9),\r\n },\r\n },\r\n appBarSpacer: theme.mixins.toolbar,\r\n content: {\r\n // color currently defaults to white - commenting this back in makes it dark\r\n // backgroundColor:\r\n // theme.palette.mode === \"light\"\r\n // ? theme.palette.grey[100]\r\n // : theme.palette.grey[900],\r\n flexGrow: 1,\r\n // height: '100vmax', // this is what makes the page scroll way more down than the content that it has\r\n overflow: 'auto',\r\n },\r\n container: {\r\n paddingTop: theme.spacing(4),\r\n paddingBottom: theme.spacing(4),\r\n },\r\n paper: {\r\n padding: theme.spacing(2),\r\n display: 'flex',\r\n overflow: 'auto',\r\n flexDirection: 'column',\r\n // make it eggshell white in the dashboard page\r\n background: ({ pathname }) => pathname.includes('/dashboard') && '#fafafa',\r\n },\r\n fixedHeight: {\r\n height: 240,\r\n },\r\n boldText: {\r\n fontSize: '14px',\r\n fontWeight: 700,\r\n color: '#01070F',\r\n fontFamily: ['Montserrat', 'Roboto', 'Sans-Serif'].join(','),\r\n },\r\n}));\r\n\r\nconst navLinks = [\r\n {\r\n title: 'Dashboard',\r\n Icon: DashboardIcon,\r\n path: '/dashboard',\r\n },\r\n {\r\n title: 'Activate',\r\n Icon: AddBoxIcon,\r\n path: '/activate',\r\n isAdminLink: true,\r\n },\r\n {\r\n title: 'Device',\r\n Icon: LinkedCameraIcon,\r\n path: '/device',\r\n },\r\n {\r\n title: 'Admin',\r\n Icon: VpnKeyIcon,\r\n path: '/admin',\r\n isAdminLink: true,\r\n },\r\n {\r\n title: 'Reports',\r\n Icon: ReportsIcon,\r\n path: '/reports',\r\n },\r\n {\r\n title: 'Alerts',\r\n Icon: AlertsIcon,\r\n path: '/alerts',\r\n isAdminLink: true,\r\n },\r\n {\r\n title: 'User Settings',\r\n Icon: PersonIcon,\r\n path: '/user-settings',\r\n isAdminLink: true, // if is admin link, only admin can see this link\r\n },\r\n];\r\n\r\nconst customLayoutPaths = [/^\\/reports$/, /^\\/alerts$/, /^\\/user-settings$/];\r\n\r\nfunction MeterVibeAppLayout({ children }) {\r\n const [open, setOpen] = useState(true);\r\n const { pathname } = useLocation();\r\n const classes = useStyles({ pathname });\r\n\r\n const toggleDrawer = () => {\r\n setOpen(!open);\r\n };\r\n\r\n // you can destructure multiple state from store like this\r\n const { admin, userEmail, cameras, userSelectedCamera } = useSelector(\r\n ({ userReducer, camerasReducer }) => ({\r\n admin: userReducer.admin,\r\n userEmail: userReducer.userEmail,\r\n cameras: camerasReducer.cameras,\r\n userSelectedCamera: camerasReducer.userSelectedCamera,\r\n })\r\n );\r\n\r\n // can also do this\r\n // const [admin, userEmail] = useSelector(({ userReducer }) => [\r\n // userReducer.admin,\r\n // userReducer.userEmail,\r\n // ]);\r\n\r\n const renderPaperWrapper = useMemo(() => {\r\n for (let url of customLayoutPaths) {\r\n // this is to avoid the paper wrapper for components that have extra navbar\r\n if (url.test(pathname)) return false;\r\n }\r\n return true; // render the container and paper wrapper if its not in these paths array\r\n }, [pathname]);\r\n\r\n return (\r\n <>\r\n \r\n\r\n \r\n \r\n \r\n {open ? (\r\n \r\n ) : (\r\n \r\n )}\r\n \r\n
\r\n \r\n \r\n {navLinks.map(({ path, Icon, title, isAdminLink }, key) =>\r\n // if the link is an admin link, only render if user is admin\r\n isAdminLink ? (\r\n admin && (\r\n \r\n )\r\n ) : (\r\n // else if the link isn't an admin link, just render as it is\r\n \r\n )\r\n )}\r\n
\r\n \r\n \r\n \r\n {/* show something else in dashboard page */}\r\n {!pathname.includes('/dashboard') ? (\r\n \r\n ) : (\r\n \r\n \r\n Device List ({cameras.length || 0})\r\n \r\n \r\n )}\r\n \r\n\r\n {/* doing this because of the Container component making reports navbar not stick to top */}\r\n {renderPaperWrapper ? (\r\n {children} \r\n ) : (\r\n children\r\n )}\r\n \r\n >\r\n );\r\n}\r\n\r\n// wrap the parent layout with protectedRoute to check if user is authenticated, no need to wrap the children.\r\nexport default protectedRoute(MeterVibeAppLayout);\r\n\r\nconst DrawerListLink = ({ path, title, Icon }) => (\r\n \r\n \r\n \r\n \r\n \r\n {title}\r\n \r\n \r\n);\r\n\r\nconst PaperWrapper = ({ children, classes }) => (\r\n \r\n \r\n \r\n {/*
*/}\r\n {/* page content goes here */}\r\n {children} \r\n \r\n \r\n \r\n);\r\n","/* eslint-disable */\r\n\r\n// MeterVibe Terms of Service\r\n\r\n// To edit the Terms of Service please use markdown syntax. For more details visit https://www.markdownguide.org/cheat-sheet/\r\n\r\n// to convert doc or docx to markdown, you can use: https://word2md.com/\r\n\r\nconst LAST_MODIFIED_DATE = 'September 29, 2021';\r\n\r\nexport default `\r\n*Last modified: ${LAST_MODIFIED_DATE}*\r\n\r\n**TERMS OF USE**\r\n\r\nThese Terms of Service ("Terms") establish a binding contractual agreement between you (the visitor or user; hereinafter referred to as "you" or "your") and XRobotix LLC. "XRobotix LLC", "our", "us" or "we") according to which you may visit the XRobotix LLC website at [http://metervibe.com](http://metervibe.com/) ("Website", "Site" or "Service") as well as your view and use of any and all hosted software and applications including but not limited to the interactive drawing application, plug-ins, modules and hosting services (the "Hosted Services"; the Website together with the Hosted Services may hereinafter be collectively referred to the "Services"). The Website is owned and operated by XRobotix LLC and all Services are the intellectual property of XRobotix LLC.\r\n\r\n**Description of Service**\r\n\r\nXRobotix LLC located at Suite 100, 189 US 9, Englishtown NJ creates a platform through the Website for selling IOT devices and subscription service for that device, MeterVibe.\r\n\r\nThe Website is offered subject to your acceptance without modification of all of these Terms and all other operating rules, policies (including, without limitation, XRobotix LLC Privacy Policy) and procedures that may be published from time to time on this Website or on or through the Hosted Services by us.\r\n\r\nPlease read these Terms carefully before accessing or using the Services. By accessing or using any of the Services, you expressly agree to become bound by the Terms of Use of these Terms. If you do not agree to all the Terms of Use, then you may not access the Website or use any Hosted Services. If these Terms of Use are considered an offer by XRobotix LLC, acceptance is expressly limited to these Terms.\r\n\r\n**1. Your XRobotix LLC Account.**\r\n\r\nIf you create an account on the Website or as required to access and use the Hosted Services, you are responsible for maintaining the security of your account and its content, and you are fully responsible for all activities that occur under the account and any other actions taken in connection with the Services. You must not describe or assign content to your account in a misleading or unlawful manner, including in a manner intended to trade on the name or reputation of others, and XRobotix LLC may change or remove any content or images that it considers inappropriate or unlawful, or otherwise likely to cause XRobotix LLC liability. You must immediately notify XRobotix LLC of any unauthorized uses of your account any other breaches of security. XRobotix LLC will not be liable for any acts or omissions by you, including any damages of any kind incurred as a result of such acts or omissions.\r\n\r\n**2. Content**\r\n\r\nWithout limiting any of those representations or warranties, XRobotix LLC has the right (though not the obligation) to, in XRobotix LLC sole discretion (i) refuse or remove any content that, in XRobotix LLC reasonable opinion, violates any XRobotix LLC's policy or is in any way harmful or objectionable, or (ii) terminate or deny access to and use of the Services to any individual or entity for any reason, in XRobotix LLC sole discretion.\r\n\r\n**3. Intellectual Property**\r\n\r\nUnless otherwise indicated, the Service is our proprietary property and all source code, databases, functionality, software, website designs, audio, video, text, photographs, and graphics on the Site (collectively, the "Content") and the trademarks, service marks, and logos contained therein (the "Marks") are owned or controlled by us or licensed to us, and are protected by copyright and trademark laws and various other intellectual property rights and unfair competition laws of the United States of America, foreign jurisdictions, and international conventions. The Content and the Marks are provided on the Site "AS IS" for your information and personal use only. Except as expressly provided in these Terms of Use, no part of the Site and no Content or Marks may be copied, reproduced, aggregated, republished, uploaded, posted, publicly displayed, encoded, translated, transmitted, distributed, licensed, or otherwise exploited for any commercial purpose whatsoever, without our express prior written permission.\r\n\r\nProvided that you are eligible to use the Site, you are granted a limited license to access and use the Site and to download or print a copy of any portion of the Content to which you have properly gained access solely for your personal, non-commercial use. We reserve all rights not expressly granted to you in and to the Site, the Content, and the Marks.\r\n\r\n**4. Limitation of Liability**\r\n\r\nTo the extent permitted under applicable law, under no circumstances shall we, our officers, directors, employees, parents, affiliates, successors, assigns, or licensors be liable to you or any other third party for any indirect, special, incidental, or consequential, exemplary or punitive damages of any type including, without limitation, damages for loss of goodwill, service interruption, computer failure or malfunction, loss of business profits, loss of data or business information, loss of additional software or computer configurations or costs of procurement of substitute goods or services, damages arising in connection with any use of the website or any and all other commercial damages or losses, arising out of or in connection with these terms. Notwithstanding anything to the contrary contained herein, in no event shall our total liability (including our officers, directors, employees, parents, and affiliates) for any claim arising out of or related to these terms, to the fullest extent possible under applicable law, exceed the amount paid if any, by you for the use of the services\r\n\r\n**5. Notice**\r\n\r\nWe may provide notice to you by means of e-mail, a general notice on the site, or by other reliable method to the address you have provided to us.\r\n\r\n**6. Indemnification**\r\n\r\nYou agree to indemnify, defend, and hold harmless us, our officers, directors, employees, agents, licensors and suppliers (collectively the "Service Providers") from and against all losses, expenses, damages and costs, including reasonable attorneys' fees, resulting from any violation of these Terms of Use or any activity related to your account (including negligent or wrongful conduct) by you or any other person accessing the site using your Internet account.\r\n\r\n**7. Third-Party Links**\r\n\r\nIn an attempt to provide increased value to our visitors, we may link to sites operated by third parties. However, even if the third party is affiliated with us, we have no control over these linked sites, all of which have separate privacy and data collection practices, independent of us. These\r\n\r\n**8. Responsibility of Website Visitors.**\r\n\r\nXRobotix LLC has not reviewed, and cannot review, all of the material, including computer software, posted to the site, and cannot therefore be responsible for that material's content, use or effects. By operating the site, XRobotix LLC does not represent or imply that it endorses the material there posted, or that it believes such material to be accurate, useful or non- harmful. You are responsible for taking precautions as necessary to protect yourself and your computer systems from viruses, worms, Trojan horses, and other harmful or destructive content. The Website may contain content that is offensive, indecent, or otherwise objectionable, as well as content containing technical inaccuracies, typographical mistakes, and other errors. The site may also contain material that violates the privacy or publicity rights, or infringes the intellectual property and other proprietary rights, of third parties, or the downloading, copying or use of which is subject to additional Terms of Use, stated or unstated. We disclaim any and all responsibility for any harm resulting from the use by visitors of the site, or from any downloading by those visitors of content posted thereon.\r\n\r\n**9. Content Posted on Other Sites.**\r\n\r\nWe have not reviewed, and cannot review, all of the material, including computer software, made available through the sites and webpages to which we link, and that link to us. XRobotix LLC does not have any control over those non XRobotix LLC sites and webpages, and is not responsible for their contents or their use. By linking to a non-XRobotix LLC site or webpage, XRobotix LLC does not represent or imply that it endorses such website or webpage. You are responsible for taking precautions as necessary to protect yourself and your computer systems from viruses, worms, Trojan horses, and other harmful or destructive content. XRobotix LLC disclaims any and all responsibility for any harm resulting from your use of non- XRobotix LLC sites and webpages.\r\n\r\n**10. Changes.**\r\n\r\nXRobotix LLC reserves the right, at its sole discretion, to modify or replace any part of these Terms. It is your responsibility to check these Terms periodically for changes. Your continued use of or access to the Website or Hosted Services following the posting of any changes to these Terms constitutes express acceptance of those changes. XRobotix LLC may also, in the future, offer new services and/or features through the site (including, the release of new tools and resources). Such new features and/or services shall be subject to these Terms.\r\n\r\n**11. Termination.**\r\n\r\nWe may terminate your access to all or any part of the site or your access to any of the Hosted Services at any time, with or without cause, effective immediately. If you wish to terminate your agreement with us or your XRobotix LLC account (if you have one), you may simply discontinue using the Services. Notwithstanding the foregoing, if you have a paid Services account, such account can only be terminated by us if you materially breach these Terms and fail to cure such breach within thirty (30) days from XRobotix LLC notice to you thereof; provided that, we can terminate the Services immediately as part of a general shut down of our service. All provisions of these Terms which by their nature should survive termination shall survive termination, including, without limitation, ownership provisions, warranty disclaimers, indemnity and limitations of liability.\r\n\r\n**12. Disclaimer of Warranties.**\r\n\r\nThe Services are provided "as is". XRobotix LLC and its suppliers and licensors hereby disclaim all warranties of any kind, express or implied, including, without limitation, the warranties of merchantability, fitness for a particular purpose and non-infringement. Neither we nor its suppliers and licensors, makes any warranty that the Services will be error free or that access thereto will be continuous or uninterrupted. You understand that you download from, or otherwise obtain content or services through, the Website at your own discretion and risk.\r\n\r\n**13. General Representation and Warranty.**\r\n\r\nYou represent and warrant that (i) your use of the Services will be in strict accordance with the Privacy Policy, with these Terms and with all applicable laws and regulations (including without limitation any local laws or regulations in your country, state, city, or other governmental area, regarding online conduct and acceptable content, and including all applicable laws regarding the transmission of technical data exported from the United States of America or the country in which you reside) and (ii) your use of the Website or Hosted Services will not infringe or misappropriate the intellectual property rights of any third party.\r\n\r\n**14. Miscellaneous.**\r\n\r\nThese Terms constitute the entire agreement between XRobotix LLC and you concerning the subject matter hereof, and they may only be modified by a written amendment signed by an authorized executive of XRobotix LLC, or by the posting by XRobotix LLC of a revised version of these Terms. Except to the extent applicable law, if any, provides otherwise, these Terms, any access to or use of the Services will be governed by the laws of the United States of America, excluding its conflict of law provisions, and the proper venue for any disputes arising out of or relating to any of the same will be the judicial courts of the United States of America.\r\n `;\r\n","import React from 'react';\r\nimport { useHistory } from 'react-router-dom';\r\nimport { useMediaQuery, Grid, Box } from '@material-ui/core';\r\nimport { makeStyles, useTheme } from '@material-ui/core/styles';\r\nimport PublicLandingHeader from './PublicLandingHeader';\r\nimport PublicLandingFooter from './PublicLandingFooter';\r\nimport { PublicLandingText as PageTitle } from './shared/typography/PublicLandingText';\r\nimport MeterVibeButton from './shared/buttons/MeterVibeButton';\r\n\r\n// markdown\r\nimport markdown from '../markdown/TermsOfServiceMarkdown';\r\nimport ReactMarkdown from 'react-markdown';\r\n\r\nconst COLORS = {\r\n DK_TEXT: '#015388',\r\n DEFAULT_TEXT: '#0272bc',\r\n LIGHT_TEXT: '#3d93cc',\r\n};\r\n\r\nconst useStyles = makeStyles((theme) => ({\r\n pageContent: {\r\n flexGrow: 1,\r\n },\r\n\r\n toolbarDistance: theme.mixins.toolbar, // create distance from toolbar.\r\n\r\n innerColumn: {\r\n width: '98%',\r\n maxWidth: '1100px',\r\n marginRight: 'auto',\r\n marginLeft: 'auto',\r\n padding: '10px',\r\n },\r\n\r\n markdown: {\r\n flexGrow: 1,\r\n // minHeight: '100vh',\r\n maxWidth: '100%',\r\n color: COLORS.DK_TEXT,\r\n '& h1': {\r\n fontSize: '48px',\r\n fontWeight: 700,\r\n },\r\n '& h2': {\r\n fontSize: '32px',\r\n fontWeight: 700,\r\n },\r\n '& h3': {\r\n fontSize: '20px',\r\n fontWeight: 700,\r\n lineHeight: '25px',\r\n },\r\n '& h4': {\r\n fontSize: '20px',\r\n fontWeight: 700,\r\n letterSpacing: '1.6pt',\r\n },\r\n '& a, a:active, a:visited': {\r\n color: COLORS.LIGHT_TEXT,\r\n },\r\n },\r\n}));\r\n\r\nexport default function TermsOfService() {\r\n const classes = useStyles();\r\n const history = useHistory();\r\n const theme = useTheme();\r\n const matchesSm = useMediaQuery(theme.breakpoints.down('sm'));\r\n\r\n return (\r\n <>\r\n \r\n \r\n \r\n
\r\n
\r\n
\r\n MeterVibe Terms of Service\r\n \r\n
\r\n
\r\n\r\n
\r\n \r\n history.goBack()}\r\n />\r\n \r\n \r\n
\r\n \r\n \r\n \r\n >\r\n );\r\n}\r\n","/* eslint-disable */\r\n\r\n// MeterVibe Privacy Policy\r\n\r\n// To edit the Privacy Policy please use markdown syntax. For more details visit https://www.markdownguide.org/cheat-sheet/\r\n\r\n// to convert doc or docx to markdown, you can use: https://word2md.com/\r\n\r\nconst LAST_MODIFIED_DATE = 'Oct. 15, 2021';\r\n\r\nexport default `\r\n*Last modified: ${LAST_MODIFIED_DATE}*\r\n\r\n\r\n**PRIVACY POLICY**\r\n\r\nThis Privacy Policy describes our policies on the collection, use, and disclosure of information about you in connection with your use of our services (the "Service"). The term "MeterVibe" or "us" or "we" or "our" refers to XRobotix LLC which operates [http://metervibe.com](http://metervibe.com/) website (the "Service").\r\n\r\nWhen you use the Service, you consent to our collection, use, and disclosure of information about you as described in this Privacy Policy.\r\n\r\nThis privacy policy sets out how MeterVibe and our website, [http://metervibe.com](http://metervibe.com/) uses and protects any information that you give MeterVibe while using this website. MeterVibe is committed to ensuring that your privacy is protected. Should we ask you to provide certain information by which you can be identified when using this website, then you can be assured that it will only be used in accordance with this privacy statement. MeterVibe may change this policy from time to time by updating this page. You should check this page from time to time to ensure that you are happy with any changes. This page was last updated on (insert date of website once complete).\r\n\r\n**WHAT WE COLLECT**\r\n\r\nWe may collect the following information from you when you:\r\n\r\n- When registering we may collect your name and contact information including: email address, mailing address, phone number, credit card information, demographic information such as postcode, preferences and interests. However, You may visit our site anonymously.\r\n- Information about gift recipients to enable us fulfill the gift purchase.\r\n- other information relevant to customer surveys and/or offers.\r\n\r\n**HOW WE USE INFORMATION WE COLLECT**\r\n\r\nWe may use the information we collect from you when you register, purchase products, enter a contest or promotion, respond to a survey or marketing communication, surf the website, or use certain other site features in the following ways:\r\n\r\n- provide the Service's functionality,\r\n- fulfill your requests, improve the Service's quality, engage in research and analysis relating to the Service, personalize your experience,\r\n- quickly process your transactions,\r\n- personalize your site experience and to allow us to deliver the type of content and product offerings in which you are most interested,\r\n- display relevant advertising, market the Service,\r\n- track usage of the Service, provide feedback to third party businesses that are listed on the Service,\r\n- provide customer support, message you, back up our systems,\r\n- If you have opted-in to receive our e-mail newsletter, we may send you periodic e-mails. If you would no longer like to receive promotional e-mail from us, please refer to the "How can you opt-out, remove or modify information you have provided to us?" section below. If you have not opted-in to receive e-mail newsletters, you will not receive these e-mails. Visitors who register or participate in other site features such as marketing programs and 'members-only' content will be given a choice whether they would like to be on our e-mail list and receive e-mail communications from us.\r\n- allow for disaster recovery, enhance the security of the Service, and\r\n- comply with legal obligations.\r\n\r\nEven when we do not retain such information, it still must be transmitted to our servers initially and stored long enough to process.\r\n\r\n**SECURITY**\r\n\r\nWe use various safeguards to protect the personal information submitted to us, both during transmission and after we receive it. However, no method of transmission over the Internet or via mobile device, or method of electronic storage, is 100% secure. Therefore, while we strive to use commercially acceptable means to protect your personal information, we cannot guarantee its absolute security.\r\n\r\n**LINKS TO OTHER WEBSITES**\r\n\r\nIn an attempt to provide you with increased value, we may include third party links on our site. These linked sites contain independent and separate privacy policies that we do not have control over. Therefore, we cannot be responsible for the protection and privacy of any information which you provide while visiting such other sites and such sites are not governed by this privacy statement.\r\n\r\nWe therefore have no liability for the content and activities of these linked sites. Nonetheless, we seek to protect the integrity of our site and welcome any feedback about these linked sites. You should exercise caution and look at the privacy statement applicable to the website in question (including if a specific link does not work).\r\n\r\n**CONTROLLING YOUR PERSONAL INFORMATION**\r\n\r\nWe do not sell, trade, or otherwise transfer to outside parties your personally identifiable information unless we notify you in advance, except as described below. The term "outside parties" does not include MeterVibe. It also does not include website hosting partners and other parties who assist us in operating our website, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others' rights, property, or safety.\r\n\r\nHowever, non-personally identifiable visitor information may be provided to other parties for marketing, advertising, or other uses.\r\n\r\nHow can you opt-out, remove or modify information you have provided to us?\r\n\r\nTo modify your e-mail subscriptions, please let us know by modifying your preferences in the "My Account" section. Please note that due to email production schedules you may receive any emails already in production.\r\n\r\n**DATA RETENTION AND ACCOUNT TERMINATION**\r\n\r\nYou may also opt to delete your account. We will remove certain public content from view and/or dissociate them from your account profile, but we may retain information about you for the purposes authorized under this Privacy Policy unless prohibited by law. For example, we may retain information to prevent, investigate, or identify possible wrongdoing in connection with the Service or to comply with legal obligations. We may also maintain residual copies of your personal information in our backup systems. Please note that businesses cannot remove their business pages, ratings, or reviews by closing their accounts.\r\n\r\n**CONTACT INFORMATION**\r\n\r\nYou may contact us concerning our Privacy Policy, or write to us at the following address:\r\n\r\nSuite 100, 189 US 9, Englishtown NJ 07726\r\n\r\n**MODIFICATIONS TO THIS PRIVACY POLICY**\r\n\r\nWe may revise this Privacy Policy from time to time. The most current version of the Privacy Policy will govern our collection, use, and disclosure of information about you. If we make material changes to this Privacy Policy, we will notify you by email or by posting a notice on the Service prior to or on the effective date of the changes. By continuing to access or use the Service after those changes become effective, you acknowledge the revised Privacy Policy.\r\n\r\n**ONLINE POLICY ONLY**\r\n\r\nThis online privacy policy applies only to information collected through our website and not to information collected offline.\r\n `;\r\n","import React from 'react';\r\nimport { useHistory } from 'react-router-dom';\r\nimport { useMediaQuery, Grid, Box } from '@material-ui/core';\r\nimport { makeStyles, useTheme } from '@material-ui/core/styles';\r\nimport PublicLandingHeader from './PublicLandingHeader';\r\nimport PublicLandingFooter from './PublicLandingFooter';\r\nimport { PublicLandingText as PageTitle } from './shared/typography/PublicLandingText';\r\nimport MeterVibeButton from './shared/buttons/MeterVibeButton';\r\n\r\n// markdown\r\nimport markdown from '../markdown/PrivacyPolicyMarkdown';\r\nimport ReactMarkdown from 'react-markdown';\r\n\r\nconst COLORS = {\r\n DK_TEXT: '#015388',\r\n DEFAULT_TEXT: '#0272bc',\r\n LIGHT_TEXT: '#3d93cc',\r\n};\r\n\r\nconst useStyles = makeStyles((theme) => ({\r\n pageContent: {\r\n flexGrow: 1,\r\n },\r\n\r\n toolbarDistance: theme.mixins.toolbar, // create distance from toolbar.\r\n\r\n innerColumn: {\r\n width: '98%',\r\n maxWidth: '1100px',\r\n marginRight: 'auto',\r\n marginLeft: 'auto',\r\n padding: '10px',\r\n },\r\n\r\n markdown: {\r\n flexGrow: 1,\r\n // minHeight: '100vh',\r\n maxWidth: '100%',\r\n color: COLORS.DK_TEXT,\r\n '& h1': {\r\n fontSize: '48px',\r\n fontWeight: 700,\r\n },\r\n '& h2': {\r\n fontSize: '32px',\r\n fontWeight: 700,\r\n },\r\n '& h3': {\r\n fontSize: '20px',\r\n fontWeight: 700,\r\n lineHeight: '25px',\r\n },\r\n '& h4': {\r\n fontSize: '20px',\r\n fontWeight: 700,\r\n letterSpacing: '1.6pt',\r\n },\r\n '& a, a:active, a:visited': {\r\n color: COLORS.LIGHT_TEXT,\r\n },\r\n },\r\n}));\r\n\r\nexport default function TermsOfService() {\r\n const classes = useStyles();\r\n const history = useHistory();\r\n const theme = useTheme();\r\n const matchesSm = useMediaQuery(theme.breakpoints.down('sm'));\r\n\r\n return (\r\n <>\r\n \r\n \r\n \r\n
\r\n
\r\n
\r\n MeterVibe Privacy Policy\r\n \r\n
\r\n
\r\n\r\n
\r\n \r\n history.goBack()}\r\n />\r\n \r\n \r\n
\r\n \r\n \r\n \r\n >\r\n );\r\n}\r\n","import React from 'react';\r\nimport {\r\n Grid,\r\n Tab,\r\n Tabs,\r\n useTheme,\r\n useMediaQuery,\r\n makeStyles,\r\n} from '@material-ui/core';\r\n\r\nconst useStyles = makeStyles({\r\n container: {\r\n marginLeft: '0px',\r\n marginRight: '0px',\r\n paddingTop: '20px',\r\n backgroundColor: '#FAFAFA',\r\n borderBottom: '1px solid #999',\r\n marginBottom: '2em',\r\n },\r\n\r\n tab: {\r\n '&:focus': {\r\n outline: 'none',\r\n },\r\n },\r\n});\r\n\r\nexport default function ReportsNavbar({ changeToTab, activeTab }) {\r\n const theme = useTheme();\r\n const matchesXs = useMediaQuery(theme.breakpoints.down('xs'));\r\n const classes = useStyles();\r\n\r\n return (\r\n <>\r\n \r\n \r\n changeToTab('UsageOverview')}\r\n />\r\n\r\n changeToTab('Readings')}\r\n />\r\n\r\n {/* changeToTab('PerAppliances')}\r\n /> */}\r\n\r\n {/* changeToTab('CustomReports')}\r\n /> */}\r\n \r\n \r\n >\r\n );\r\n}\r\n","import moment from 'moment';\r\n\r\nexport const periodDates = (startPeriodDate, periodType = 'month') => {\r\n // TODO: do it for all period types, not just monthly.\r\n\r\n const startDateResult = moment(startPeriodDate).startOf(periodType);\r\n const endDateResult = moment(startPeriodDate).endOf(periodType);\r\n\r\n return [startDateResult, endDateResult];\r\n};\r\n\r\nexport const addRealMonth = (d) => {\r\n let futureMonth = moment(d).add(1, 'M');\r\n let futureMonthEnd = moment(futureMonth).endOf('month');\r\n return d.date() !== futureMonth.date() &&\r\n futureMonth.isSame(futureMonthEnd.format('YYYY-MM-DD'))\r\n ? futureMonth.add(1, 'd')\r\n : futureMonth;\r\n};\r\n\r\nexport const nextPeriodDates = (\r\n currentStartPeriodDate,\r\n currentEndPeriodDate,\r\n periodType = 'month'\r\n) => {\r\n let nextStartPeriodDate;\r\n let nextEndPeriodDate;\r\n\r\n if (periodType === 'month') {\r\n nextStartPeriodDate = addRealMonth(currentStartPeriodDate);\r\n nextEndPeriodDate = moment(nextStartPeriodDate).endOf('month');\r\n } else {\r\n nextStartPeriodDate = moment(currentStartPeriodDate).add(1, periodType);\r\n nextEndPeriodDate = moment(currentEndPeriodDate).add(1, periodType);\r\n }\r\n\r\n return [nextStartPeriodDate, nextEndPeriodDate];\r\n};\r\n\r\n// latestImagesAtBottom of arr.\r\n\r\n//use the point to the left and the right and take average delta\r\n//then add in first point, delta to right, and last point delta to left\r\n//ts = [[timestamp, value] ....[timestamp,value]]\r\nexport const computeDeltas = (tSeries) => {\r\n let result = [];\r\n const millisecsToHours = 1000 * 60 * 60;\r\n //first entry is (v(n+1) - v(n))/(t(n+1)-t(n))\r\n result.push([\r\n tSeries[0][0], //t(0)\r\n (millisecsToHours * (tSeries[1][1] - tSeries[0][1])) /\r\n (tSeries[1][0] - tSeries[0][0]), //\r\n ]);\r\n for (let idx = 1; idx < tSeries.length - 1; idx++) {\r\n result.push([\r\n tSeries[idx][0], //t(idx)\r\n (millisecsToHours * (tSeries[idx + 1][1] - tSeries[idx - 1][1])) /\r\n (tSeries[idx + 1][0] - tSeries[idx - 1][0]),\r\n ]);\r\n }\r\n //now the last entry is delta with next to l\r\n result.push([\r\n tSeries[tSeries.length - 1][0], //t(0)\r\n (millisecsToHours *\r\n (tSeries[tSeries.length - 1][1] - tSeries[tSeries.length - 2][1])) /\r\n (tSeries[tSeries.length - 1][0] - tSeries[tSeries.length - 2][0]), //\r\n ]);\r\n return result;\r\n};\r\n\r\n//remove outliers from this sublist\r\n//outlier is great than 2X the first and last quartile\r\n//[[timestamp,value].....[timestamp,value]]\r\nexport const removeOutliers = (sublistArray) => {\r\n const len = sublistArray.length;\r\n const lowerQuartileIdx = parseInt(len / 4);\r\n const upperQuartileIdx = parseInt((3 * len) / 4);\r\n //sort by the second 'value' element\r\n sublistArray.sort(function (a, b) {\r\n return a[1] - b[1];\r\n });\r\n const lowerBound = 0.5 * sublistArray[lowerQuartileIdx][1];\r\n const upperBound = 2 * sublistArray[upperQuartileIdx][1];\r\n // console.log(\r\n // 'sublist filtering, sublist, len, lowerBound, upperBound:',\r\n // sublistArray,\r\n // len,\r\n // lowerBound,\r\n // upperBound\r\n // );\r\n //sort back to timestamp order (works without function arg)\r\n sublistArray.sort(function (a, b) {\r\n return a[0] - b[0];\r\n });\r\n //remove outliers if not in bound range (2X)\r\n const filtered = sublistArray.filter(function (value, index, arr) {\r\n return value[1] > lowerBound && value[1] < upperBound;\r\n });\r\n return filtered;\r\n};\r\n\r\nexport const createDeltaProperties = (imgList) => {\r\n for (let i = 0; i < imgList.length - 1; i++) {\r\n //if (isNaN(imgList[i].digits)) { //looks like chart ignore null values\r\n // imgList[i].digits = 100; //temporary solution so no Null data\r\n //}\r\n imgList[i].deltaDigits = imgList[i].digits - imgList[i + 1].digits;\r\n imgList[i].deltaTimeMS =\r\n imgList[i].timestampSaved - imgList[i + 1].timestampSaved;\r\n }\r\n\r\n return imgList;\r\n};\r\n","import React, { useCallback, useState } from 'react';\r\nimport { useDispatch, useSelector } from 'react-redux';\r\n\r\n// components\r\nimport {\r\n Grid,\r\n makeStyles,\r\n Typography,\r\n // Button,\r\n useTheme,\r\n useMediaQuery,\r\n} from '@material-ui/core';\r\nimport ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';\r\nimport ToggleButton from '@material-ui/lab/ToggleButton';\r\nimport MeterVibeButtonPrimary from './../shared/buttons/MeterVibeButtonPrimary';\r\nimport MeterVibeButtonSecondary from './../shared/buttons/MeterVibeButtonSecondary';\r\n\r\n// services\r\nimport { deleteTimestamp } from './../../services/cameras.services';\r\n\r\n// utils\r\nimport { REPORTS_ACTIONS } from './../../reducers/reports.reducer';\r\nimport moment from 'moment';\r\nimport { addRealMonth } from '../../utils/Reports.utils';\r\n\r\nconst useStyles = makeStyles((theme) => ({\r\n buttonGroup: {\r\n // all children (buttons) inside button group\r\n '& > *': {\r\n fontSize: '12px !important',\r\n fontFamily: ['Public Sans', 'Roboto', 'Sans-Serif'].join(','), // fallbacks\r\n backgroundColor: '#F5F5F5',\r\n color: '#000',\r\n\r\n // have to do it this way to override Mui defaults.\r\n borderRight: '1px solid #3D93CC !important',\r\n borderLeft: '1px solid #3D93CC !important',\r\n borderTop: '1px solid #3D93CC !important',\r\n borderBottom: '1px solid #3D93CC !important',\r\n transition: 'all 250ms ease-in-out',\r\n\r\n '&:focus': {\r\n outline: 'none',\r\n },\r\n '&:hover': {\r\n background: '#fff',\r\n color: '#3D93CC',\r\n },\r\n '&.Mui-selected': {\r\n // selected button\r\n background: '#3D93CC',\r\n color: '#fff',\r\n\r\n '&:hover': {\r\n background: '#3D93CC',\r\n color: '#fff',\r\n },\r\n },\r\n },\r\n },\r\n sectionTitle: {\r\n userSelect: 'none',\r\n },\r\n updateInfoButton: {\r\n backgroundColor: 'rgb(2,114,188)',\r\n background:\r\n 'linear-gradient(90deg, rgba(2,114,188,1) 0%, rgba(1,83,136,1) 100%)',\r\n '&:focus': {\r\n outline: 'none',\r\n },\r\n },\r\n titleHidden: {\r\n visibility: 'hidden',\r\n userSelect: 'none',\r\n },\r\n}));\r\n\r\nconst {\r\n TOGGLE_SELECT_MODE_ENABLED,\r\n CLEAR_ALL_SELECTED_ROWS,\r\n SELECT_ALL_ROWS,\r\n DELETE_SELECTED_ROWS,\r\n SET_START_DATE,\r\n SET_END_DATE,\r\n SET_PERIOD_TYPE,\r\n RESET_TIME,\r\n} = REPORTS_ACTIONS;\r\n\r\nexport default function ReportsFilters({ cameraId }) {\r\n const [selectButtonsDisabled, setSelectButtonsDisabled] = useState(false);\r\n\r\n const [{ pageState, isSelectModeEnabled, selectedRows }, { admin }] =\r\n useSelector(({ reportsReducer, userReducer }) => [\r\n reportsReducer,\r\n userReducer,\r\n ]);\r\n\r\n const dispatch = useDispatch();\r\n\r\n const toggleSelectMode = useCallback(() => {\r\n dispatch({ type: TOGGLE_SELECT_MODE_ENABLED });\r\n }, [dispatch]);\r\n\r\n const onDeleteClick = async () => {\r\n setSelectButtonsDisabled(true);\r\n\r\n for await (const timestamp of selectedRows) {\r\n const result = await deleteTimestamp(cameraId, timestamp);\r\n\r\n if (result.status === true) {\r\n setTimeout(() => {\r\n dispatch({ type: DELETE_SELECTED_ROWS }); // delete rows on front end and render new state in UI\r\n }, 20);\r\n }\r\n }\r\n\r\n setSelectButtonsDisabled(false);\r\n };\r\n\r\n const selectAllItems = useCallback(() => {\r\n dispatch({ type: SELECT_ALL_ROWS });\r\n }, [dispatch]);\r\n\r\n const unSelectAllItems = useCallback(() => {\r\n dispatch({ type: CLEAR_ALL_SELECTED_ROWS });\r\n }, [dispatch]);\r\n\r\n // don't render if in readings tab and is not admin (admin can see select buttons in readings page)\r\n if (pageState.readingsTabSelected && !admin) return null;\r\n\r\n return (\r\n \r\n {/* first row */}\r\n\r\n \r\n \r\n {/* don't show date filters if in readings tab */}\r\n {!pageState.readingsTabSelected && }\r\n {/* only show period view buttons when there is a chart. */}\r\n {pageState.showChart && }\r\n \r\n\r\n {/* enable/disable select mode in readings page */}\r\n \r\n \r\n \r\n );\r\n}\r\n\r\nconst DateFilters = () => {\r\n const classes = useStyles();\r\n const { startDate, endDate, chartData } = useSelector(\r\n ({ reportsReducer }) => reportsReducer\r\n );\r\n const dispatch = useDispatch();\r\n\r\n const minimumDate = moment(chartData[1][0]).format('YYYY-MM-DD') ?? moment();\r\n\r\n return (\r\n \r\n \r\n Select time period\r\n \r\n \r\n \r\n \r\n dispatch({ type: SET_START_DATE, payload: event.target.value })\r\n }\r\n style={{ width: '100%', height: '36px' }}\r\n />\r\n \r\n \r\n \r\n dispatch({ type: SET_END_DATE, payload: event.target.value })\r\n }\r\n style={{ width: '100%', height: '36px' }}\r\n />\r\n \r\n \r\n \r\n );\r\n};\r\n\r\nconst SelectModeButtons = ({\r\n render,\r\n selectAllItems,\r\n unSelectAllItems,\r\n onDeleteClick,\r\n toggleSelectMode,\r\n isSelectModeEnabled,\r\n selectButtonsDisabled,\r\n}) => {\r\n const [allItemsSelected, setAllItemsSelected] = useState(false);\r\n\r\n if (!render) return null;\r\n return (\r\n \r\n {isSelectModeEnabled ? (\r\n <>\r\n \r\n \r\n {\r\n if (allItemsSelected) {\r\n unSelectAllItems();\r\n setAllItemsSelected(false);\r\n } else {\r\n selectAllItems();\r\n setAllItemsSelected(true);\r\n }\r\n }}\r\n />\r\n \r\n \r\n >\r\n ) : (\r\n \r\n )}\r\n \r\n );\r\n};\r\n\r\nconst PeriodTypeButtons = () => {\r\n const { currentPeriodType, startDate } = useSelector(\r\n ({ reportsReducer }) => reportsReducer\r\n );\r\n const dispatch = useDispatch();\r\n const classes = useStyles();\r\n const theme = useTheme();\r\n const matchesXs = useMediaQuery(theme.breakpoints.down('xs'));\r\n\r\n return (\r\n \r\n \r\n Period view\r\n \r\n\r\n {\r\n if (newValue === 'all') {\r\n dispatch({\r\n type: SET_END_DATE,\r\n payload: addRealMonth(moment(startDate)).format('YYYY-MM-DD'),\r\n });\r\n }\r\n\r\n if (newValue === 'reset') {\r\n return dispatch({ type: RESET_TIME });\r\n }\r\n\r\n dispatch({ type: SET_PERIOD_TYPE, payload: newValue });\r\n }}\r\n exclusive\r\n orientation={matchesXs ? 'vertical' : 'horizontal'}>\r\n \r\n All\r\n \r\n \r\n Daily\r\n \r\n \r\n Weekly\r\n \r\n \r\n Monthly\r\n \r\n\r\n \r\n Reset\r\n \r\n \r\n \r\n );\r\n};\r\n","import React from 'react';\r\nimport { Chart } from 'react-google-charts';\r\nimport { Grid } from '@material-ui/core';\r\n\r\nexport default function ReportsChart({ data, chartType, render, unitsLabel }) {\r\n return (\r\n \r\n \r\n {/* ****************** Bar Chart ****************** */}\r\n {render && (\r\n Loading Chart
}\r\n data={data}\r\n options={{\r\n colors: ['#3d93cc'],\r\n hAxis: {\r\n title: 'Date/Time',\r\n format: 'MM yyyy',\r\n },\r\n vAxis: {\r\n title: unitsLabel,\r\n },\r\n chart: {},\r\n }}\r\n />\r\n )}\r\n \r\n \r\n );\r\n}\r\n","import React, { memo, useMemo, useState, useCallback, useEffect } from 'react';\r\nimport { makeStyles } from '@material-ui/core';\r\nimport { useTable, usePagination } from 'react-table';\r\nimport { useDispatch, useSelector } from 'react-redux';\r\n\r\n// components\r\nimport Table from '@material-ui/core/Table';\r\nimport Tooltip from '@material-ui/core/Tooltip';\r\nimport TableBody from '@material-ui/core/TableBody';\r\nimport TableCell from '@material-ui/core/TableCell';\r\nimport TableContainer from '@material-ui/core/TableContainer';\r\nimport TableHead from '@material-ui/core/TableHead';\r\nimport TableRow from '@material-ui/core/TableRow';\r\nimport TableFooter from '@material-ui/core/TableFooter';\r\nimport TablePagination from '@material-ui/core/TablePagination';\r\nimport IconButton from '@material-ui/core/IconButton';\r\nimport ImageDetailsModal from '../../components/modals/camera_modals/ImageDetailsModal';\r\n\r\n// utils/services\r\nimport { putDigits } from './../../services/images.services';\r\nimport moment from 'moment';\r\nimport 'moment-timezone';\r\nimport { REPORTS_ACTIONS } from './../../reducers/reports.reducer';\r\n\r\n// icons\r\nimport FirstPageIcon from '@material-ui/icons/FirstPage';\r\nimport LastPageIcon from '@material-ui/icons/LastPage';\r\nimport KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft';\r\nimport KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight';\r\n\r\nconst useStyles = makeStyles((theme) => ({\r\n root: {\r\n width: '100%',\r\n },\r\n tableContainer: {\r\n maxHeight: 500,\r\n },\r\n tableFooterRow: {\r\n display: 'flex',\r\n },\r\n paginationSpacer: {\r\n flex: '1 1 100%',\r\n [theme.breakpoints.down('md')]: {\r\n flex: '0 0',\r\n },\r\n },\r\n paginationActions: {\r\n flexShrink: 0,\r\n marginLeft: theme.spacing(2.5),\r\n [theme.breakpoints.down('sm')]: {\r\n flexShrink: 1, // direction buttons in a column in small screen\r\n },\r\n },\r\n}));\r\n\r\nconst Linebreak = ({ value }) => {\r\n // add in the style to \\n containing string to have multiple lines in cell\r\n return {value} ;\r\n};\r\n\r\n// Create an editable cell renderer\r\n//https://stackoverflow.com/questions/57419197/react-table-with-hooks-looses-focus-on-input-inside-table\r\n// this component is the cell for editing the digits (admin only)\r\nconst EditDigitsCell = memo(\r\n ({\r\n value: initialValue,\r\n row,\r\n column,\r\n handleCellInputChange, // This is a custom function that we supplied to our table instance\r\n }) => {\r\n // We need to keep and update the state of the cell normally\r\n const [value, setValue] = useState(initialValue);\r\n\r\n const onChange = (e) => {\r\n setValue(e.target.value);\r\n };\r\n\r\n // We'll only update the external data when the input is blurred\r\n const onBlur = async () => {\r\n await putDigits(\r\n value,\r\n row.original.cameraId,\r\n row.original.timestampSaved\r\n ); // update data in db when input is blurred\r\n\r\n handleCellInputChange(row, column, value); // rerender the page with the new state.\r\n };\r\n\r\n return ;\r\n }\r\n);\r\n\r\nconst { ROW_SELECTED, CLEAR_ALL_SELECTED_ROWS, SET_READINGS_DATA } =\r\n REPORTS_ACTIONS;\r\n\r\n// TABLE\r\nexport default function ReportsReadingsTable() {\r\n const classes = useStyles();\r\n const [isImageModalOpen, setIsImageModalOpen] = useState(false);\r\n const dispatch = useDispatch();\r\n\r\n const [\r\n {\r\n imagesList,\r\n startDate,\r\n endDate,\r\n selectedRows,\r\n isSelectModeEnabled,\r\n readingsData,\r\n pageState,\r\n reportInfo,\r\n isLoading,\r\n userSelectedCamera,\r\n },\r\n { admin },\r\n ] = useSelector(({ reportsReducer, userReducer }) => [\r\n reportsReducer,\r\n userReducer,\r\n ]);\r\n\r\n useEffect(() => {\r\n // clear all selected rows when select mode is disabled (kinda like how gmail clears all selected rows when disabling).\r\n if (!isSelectModeEnabled) {\r\n dispatch({ type: CLEAR_ALL_SELECTED_ROWS });\r\n }\r\n }, [isSelectModeEnabled, dispatch]);\r\n\r\n const showImageModal = useCallback(\r\n (timestampSaved) => setIsImageModalOpen(timestampSaved),\r\n [setIsImageModalOpen]\r\n );\r\n const closeImageModal = useCallback(\r\n () => setIsImageModalOpen(false),\r\n [setIsImageModalOpen]\r\n );\r\n\r\n // When our cell renderer calls handleCellInputChange, we'll use\r\n // the rowIndex, columnID and new value to update the\r\n // original data\r\n const handleCellInputChange = useCallback(\r\n (row, column, value) => {\r\n const newReadingsData = [...readingsData].map((item, index) => {\r\n if (index === row.index) {\r\n return {\r\n ...readingsData[row.index],\r\n [column.id]: value,\r\n };\r\n }\r\n return item;\r\n });\r\n\r\n dispatch({ type: SET_READINGS_DATA, payload: newReadingsData });\r\n },\r\n\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n [readingsData]\r\n );\r\n\r\n useEffect(() => {\r\n // sorted imagesList that is filterable by date for readings table.\r\n\r\n const filteredReadingsData = imagesList\r\n .filter(({ timestampSaved }) => {\r\n let dateToCheck = moment(timestampSaved).format('YYYY-MM-DD');\r\n return moment(dateToCheck).isBetween(\r\n startDate,\r\n endDate,\r\n null, // can be year, month .... the granularity of your comaprison\r\n '[]' // inclusive in range\r\n );\r\n })\r\n .sort((a, b) => {\r\n // latest date first.\r\n // Sort the dates in descending order:\r\n return b.timestampSaved - a.timestampSaved;\r\n });\r\n\r\n dispatch({ type: SET_READINGS_DATA, payload: filteredReadingsData });\r\n\r\n // do it everytime these vars change.\r\n }, [startDate, endDate, imagesList, pageState, userSelectedCamera]); // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n const handleSelectRow = useCallback(\r\n (rowInfo) => {\r\n if (!isSelectModeEnabled) return;\r\n\r\n dispatch({ type: ROW_SELECTED, payload: rowInfo });\r\n },\r\n [isSelectModeEnabled, dispatch]\r\n );\r\n\r\n const rowFn = (rowInfo) => {\r\n return {\r\n onClick: (e, handleOriginal) => {\r\n console.log('It was in this row:', rowInfo);\r\n\r\n if (admin) {\r\n // only select row if user is an admin\r\n handleSelectRow(rowInfo);\r\n }\r\n // IMPORTANT! React-Table uses onClick internally to trigger\r\n // events like expanding SubComponents and pivots.\r\n // By default a custom 'onClick' handler will override this functionality.\r\n // If you want to fire the original onClick handler, call the\r\n // 'handleOriginal' function.\r\n\r\n // if (handleOriginal) {\r\n // handleOriginal();\r\n // }\r\n },\r\n };\r\n };\r\n\r\n const columns = useMemo(\r\n () => [\r\n // {\r\n // Header: \"Id\",\r\n // accessor: \"cameraId\", // String-based value accessors!\r\n // width: 70\r\n // },\r\n {\r\n Header: 'Date',\r\n width: 200,\r\n id: 'time',\r\n accessor: ({ timestampSaved }) =>\r\n moment(timestampSaved).format('hh:mm A MM/DD/YYYY'), //this gets the date formated\r\n\r\n Cell: (row) => , //format as 2 lines using \\n\r\n },\r\n {\r\n Header: 'Image',\r\n width: 280,\r\n Cell: ({ row }) => {\r\n let reading = (\r\n Number(row.original.digits) * Number(reportInfo.reportMult)\r\n ).toFixed(Math.max(0, -Math.log10(reportInfo.reportMult))); //set number of decimal places\r\n\r\n reading = Number(reading).toLocaleString(); // add commas with to localeString()\r\n\r\n return (\r\n <>\r\n \r\n \r\n
showImageModal(row.original.timestampSaved)}\r\n height={40}\r\n alt=\"SignedUrl\"\r\n src={row.original.SignedUrl}\r\n />\r\n
\r\n \r\n \r\n >\r\n );\r\n },\r\n id: 'Image',\r\n },\r\n {\r\n Header: 'Digits', //props => Friend Age , // Custom header components!\r\n width: 100,\r\n accessor: 'digits',\r\n Cell: ({ cell, handleCellInputChange, data }) => {\r\n // makes this editable (renderEditable)\r\n const { row, column } = cell;\r\n const cellValue = data[row?.index][column?.id] || ''; // or could use undefined\r\n\r\n return admin ? (\r\n \r\n ) : (\r\n {cellValue} \r\n );\r\n }, //allow user to edit this cell\r\n },\r\n {\r\n // Gallons\r\n Header: `${reportInfo.reportLabel}`, // Gallons\r\n width: 100,\r\n id: 'reading',\r\n //accessor: d => new Date(d.timestampSaved).toDateString()\r\n //\"timestampSaved\",\r\n accessor: (d) => {\r\n // d.digits comes back as a string in response...\r\n let gallons = (\r\n Number(d.digits) * Number(reportInfo.reportMult)\r\n ).toFixed(Math.max(0, -Math.log10(reportInfo.reportMult))); //set number of decimal places\r\n\r\n return Number(gallons).toLocaleString(); // add commas with to localeString()\r\n },\r\n\r\n //Cell: props => {props.value} // Custom cell components!\r\n },\r\n {\r\n Header: 'Delta',\r\n width: 70,\r\n id: 'delta',\r\n //accessor: d => new Date(d.timestampSaved).toDateString()\r\n //\"timestampSaved\",\r\n accessor: (d) => {\r\n return `${(d.deltaDigits * reportInfo.reportMult)\r\n .toFixed(Math.max(0, -Math.log10(reportInfo.reportMult))) //set number of decimal places\r\n .toLocaleString()}`;\r\n },\r\n //Cell: props => {props.value} // Custom cell components!\r\n },\r\n {\r\n Header: 'Mins',\r\n width: 70,\r\n id: 'mins',\r\n //accessor: d => new Date(d.timestampSaved).toDateString()\r\n //\"timestampSaved\",\r\n accessor: (d) =>\r\n `${(d.deltaTimeMS / 1000 / 60).toFixed(1).toLocaleString()}`,\r\n\r\n //Cell: props => {props.value} // Custom cell components!\r\n },\r\n {\r\n Header: 'Per Hour',\r\n width: 70,\r\n id: 'flow',\r\n //accessor: d =>\r\n // `${((60 * d.deltaDigits) / d.deltaTimeMS).toLocaleString()}`,\r\n Cell: ({ row }) => {\r\n return (\r\n \r\n {((row.values.delta * 60) / row.values.mins).toFixed(2)}\r\n \r\n ); // Custom cell components!\r\n },\r\n },\r\n ],\r\n // check for these dependencies changing to rerender the data.\r\n // eslint-disable-next-line\r\n [\r\n reportInfo,\r\n admin,\r\n closeImageModal,\r\n isImageModalOpen,\r\n showImageModal,\r\n readingsData,\r\n userSelectedCamera,\r\n ]\r\n );\r\n\r\n const {\r\n gotoPage,\r\n setPageSize,\r\n // canPreviousPage,\r\n // canNextPage,\r\n pageOptions,\r\n state: { pageIndex, pageSize },\r\n getTableProps,\r\n getTableBodyProps,\r\n headerGroups,\r\n page: rows, // Instead of using 'rows', we'll use page,\r\n // which has only the rows for the active page\r\n prepareRow,\r\n } = useTable(\r\n {\r\n columns,\r\n data: readingsData,\r\n handleCellInputChange,\r\n autoResetPage: false, // When making changes to the external data you want to disable automatic resets to the state of the table\r\n initialState: { pageIndex: 0, pageSize: 100 },\r\n },\r\n usePagination\r\n ); // react-table hooks\r\n\r\n return (\r\n <>\r\n \r\n \r\n \r\n {headerGroups.map((headerGroup) => (\r\n // table headers\r\n \r\n {headerGroup.headers.map((column) => {\r\n return (\r\n \r\n {column.render('Header')}\r\n \r\n );\r\n })}\r\n \r\n ))}\r\n \r\n \r\n {rows.map((row, i) => {\r\n prepareRow(row);\r\n // table rows\r\n const isSelected = selectedRows.includes(\r\n row.original.timestampSaved\r\n );\r\n\r\n const selectedRowColorPrimary = '#357DAD';\r\n const selectedRowColorSecondary = '#FFF';\r\n\r\n return (\r\n \r\n {row.cells.map((cell, i) => {\r\n return (\r\n // cell of row.\r\n \r\n {cell.render('Cell')}\r\n \r\n );\r\n })}\r\n \r\n );\r\n })}\r\n \r\n
\r\n \r\n\r\n \r\n \r\n \r\n {/* PAGINATION */}\r\n\r\n {/* Material UI TablePagination component */}\r\n {!isLoading && (\r\n \r\n `${pageIndex + 1}-${pageOptions.length} of ${\r\n pageOptions.length\r\n }`\r\n }\r\n classes={{ spacer: classes.paginationSpacer }}\r\n onRowsPerPageChange={(e) => setPageSize(Number(e.target.value))}\r\n ActionsComponent={(props) => (\r\n \r\n )}\r\n />\r\n )}\r\n \r\n \r\n
\r\n >\r\n );\r\n}\r\n\r\nconst TableActions = (props) => {\r\n const { /*count, */ page, /*rowsPerPage*/ onPageChange, pageOptions } = props;\r\n const classes = useStyles();\r\n\r\n const canGoNext = page < pageOptions.length - 1;\r\n const canGoBack = page > 0;\r\n\r\n const handleFirstPageButtonClick = () => {\r\n onPageChange(0);\r\n };\r\n\r\n const handleBackButtonClick = () => {\r\n // if (!canPreviousPage) return;\r\n if (!canGoBack) return;\r\n const previousPage = page - 1;\r\n onPageChange(previousPage);\r\n };\r\n\r\n const handleNextButtonClick = () => {\r\n // if (!canNextPage) return;\r\n if (!canGoNext) return;\r\n\r\n const nextPage = page + 1;\r\n onPageChange(nextPage);\r\n };\r\n\r\n const handleLastPageButtonClick = () => {\r\n // onPageChange(Math.max(0, Math.ceil(count / rowsPerPage) - 1));\r\n onPageChange(pageOptions.length - 1);\r\n };\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n = Math.ceil(count / rowsPerPage) - 1}\r\n disabled={!canGoNext}\r\n aria-label=\"next page\">\r\n \r\n \r\n = Math.ceil(count / rowsPerPage) - 1}\r\n disabled={!canGoNext}\r\n aria-label=\"last page\">\r\n \r\n \r\n
\r\n );\r\n};\r\n","import React, { useState, useEffect, useCallback } from 'react';\r\nimport { useSelector, useDispatch } from 'react-redux';\r\nimport {\r\n Paper,\r\n Container,\r\n makeStyles,\r\n Grid,\r\n CircularProgress,\r\n Typography,\r\n Box,\r\n} from '@material-ui/core';\r\nimport moment from 'moment';\r\n\r\n// core components\r\nimport ReportsNavbar from '../components/reports_components/ReportsNavbar';\r\nimport ReportsFilters from '../components/reports_components/ReportsFilters';\r\nimport ReportsChart from '../components/reports_components/ReportsChart';\r\nimport ReportsReadingsTable from '../components/reports_components/ReportsReadingsTable';\r\n\r\n// services\r\nimport { getReportOnlyParams } from './../services/reports.services';\r\nimport { getAllImagesListProc } from '../services/images.services';\r\n\r\n// utils\r\nimport { REPORTS_ACTIONS } from './../reducers/reports.reducer';\r\nimport {\r\n removeOutliers,\r\n computeDeltas,\r\n nextPeriodDates,\r\n periodDates,\r\n createDeltaProperties,\r\n} from '../utils/Reports.utils';\r\n\r\nconst useStyles = makeStyles((theme) => ({\r\n paper: {\r\n // padding: theme.spacing(2),\r\n display: 'flex',\r\n flexDirection: 'column',\r\n height: '100%',\r\n },\r\n container: {\r\n paddingTop: theme.spacing(4),\r\n paddingBottom: theme.spacing(4),\r\n },\r\n selectTable: {\r\n // this does nothing\r\n '&.rt-tr': {\r\n fontSize: '10px',\r\n fontWeight: 'bold',\r\n fontFamily: 'montserrat',\r\n },\r\n },\r\n}));\r\n\r\nconst PageSection = ({ children, marginBottom, gridItemStyles }) => {\r\n const classes = useStyles();\r\n return (\r\n \r\n \r\n \r\n {children} \r\n \r\n \r\n \r\n );\r\n};\r\n\r\nconst {\r\n SET_PAGE_STATE,\r\n SET_BAR_CHART_DATA,\r\n SET_IS_LOADING,\r\n SET_CHART_DATA,\r\n SET_CAMERA_DATA,\r\n SET_IMAGES_LIST,\r\n SET_REPORT_INFO,\r\n SET_START_DATE,\r\n} = REPORTS_ACTIONS;\r\n\r\nexport default function Reports() {\r\n const dispatch = useDispatch();\r\n\r\n const [userSelectedCamera, reportsState] = useSelector(\r\n ({ camerasReducer, reportsReducer }) => [\r\n camerasReducer.userSelectedCamera,\r\n reportsReducer,\r\n ]\r\n );\r\n\r\n // using a reports reducer to remove some clutter\r\n const {\r\n pageState,\r\n activeTab,\r\n barChartData,\r\n isLoading,\r\n chartData,\r\n cameraData,\r\n reportInfo,\r\n startDate,\r\n endDate,\r\n currentPeriodType,\r\n } = reportsState;\r\n\r\n const setIsLoading = useCallback(\r\n (payload) => dispatch({ type: SET_IS_LOADING, payload }),\r\n [dispatch]\r\n );\r\n\r\n const classes = useStyles();\r\n\r\n // const [flowData, setFlowData] = useState([]); // we're not using flowData for now.\r\n\r\n const [noData, setNoData] = useState(false);\r\n\r\n const handlePeriodicFilter = useCallback(\r\n (data, startDate, label = 'Gallons', periodType = 'month') => {\r\n /* \r\n data = [\r\n ['timestamp', 'value'],\r\n [Sat Jan 09 2021 22:00:22 GMT-0500 (Eastern Standard Time), 740161.93],\r\n [Sun Jan 10 2021 03:00:22 GMT-0500 (Eastern Standard Time), 740161.93]\r\n ]\r\n */\r\n const result = [['Date/Time', label, 'Deltas']];\r\n\r\n if (periodType === 'all') {\r\n for (let i = 1; i < data.length; i++) {\r\n let [currentTimestamp, currentDigits] = data[i];\r\n currentTimestamp = moment(currentTimestamp);\r\n\r\n if (currentTimestamp.isAfter(moment(endDate))) {\r\n break;\r\n }\r\n\r\n if (!currentTimestamp.isBetween(moment(startDate), moment(endDate))) {\r\n continue;\r\n }\r\n\r\n const [, previousDigits] = result[result.length - 1];\r\n const delta = currentDigits - (previousDigits ?? 0);\r\n\r\n result.push([new Date(currentTimestamp), currentDigits, delta]);\r\n }\r\n\r\n console.log('periodType all result', result);\r\n\r\n // so we get an expected result but for some reason it's not being reflected on the graph\r\n return result;\r\n }\r\n\r\n let [startPeriodDate, endPeriodDate] = periodDates(\r\n moment(startDate),\r\n periodType\r\n );\r\n\r\n for (let i = 1; i < data.length; i++) {\r\n let [currentTimestamp, currentDigits] = data[i];\r\n currentTimestamp = moment(currentTimestamp);\r\n\r\n // past the end of our date range so return the answer\r\n if (currentTimestamp.isAfter(moment(endDate))) {\r\n // return result;\r\n break;\r\n }\r\n\r\n // found a date to use in this period\r\n if (currentTimestamp.isBetween(startPeriodDate, endPeriodDate)) {\r\n // result.push([new Date(currentTimestamp), currentDigits]);\r\n\r\n const [, previousDigits] = result[result.length - 1];\r\n\r\n const delta = currentDigits - (previousDigits ?? 0);\r\n\r\n result.push([new Date(startPeriodDate), currentDigits, delta]);\r\n\r\n [startPeriodDate, endPeriodDate] = nextPeriodDates(\r\n startPeriodDate,\r\n endPeriodDate,\r\n periodType\r\n );\r\n\r\n continue;\r\n }\r\n\r\n // no date date found within our current period, advanced into the next period\r\n if (currentTimestamp.isAfter(endPeriodDate)) {\r\n if (i - 1 > 0) {\r\n const [, prevResultDigits] = result[result.length - 1];\r\n\r\n const delta = currentDigits - (prevResultDigits ?? 0);\r\n\r\n const [, previousDigits] = data[i - 1];\r\n // result.push(data[i - 1]); // push previous timestamp and digits\r\n result.push([new Date(startPeriodDate), previousDigits, delta]);\r\n }\r\n\r\n [startPeriodDate, endPeriodDate] = nextPeriodDates(\r\n startPeriodDate,\r\n endPeriodDate,\r\n periodType\r\n ); // next time through loop look for the next period entry\r\n }\r\n }\r\n\r\n return result;\r\n },\r\n\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n [endDate, barChartData, currentPeriodType]\r\n );\r\n\r\n //removes outlier data before diplaying the chart\r\n const cleanData = useCallback((imagesList) => {\r\n // console.log('cleaning data', imagesList, 'length list:', imagesList.length);\r\n let noZerosList = imagesList\r\n //reverse the order to earliest time to latest\r\n //also reverses input list imagesList\r\n .reverse()\r\n //filter out no digits value, or value=0\r\n .filter((elt) => 'digits' in elt && parseInt(elt.digits) > 0)\r\n //replce the digits string with integer\r\n .map((elt) => [elt.timestampSaved, parseInt(elt.digits)]);\r\n\r\n // if (noZerosList <= 0) {\r\n // setNoData(true);\r\n // return imagesList;\r\n // }\r\n\r\n console.log('with no Zeros, length is:', noZerosList.length);\r\n //group in groups of 20 data points\r\n let lIdx = 0;\r\n let cleanedData = [];\r\n for (let rIdx = 20; rIdx < noZerosList.length - 20; rIdx = rIdx + 20) {\r\n // console.log('concatenating from/to:', lIdx, rIdx);\r\n cleanedData = cleanedData.concat(\r\n removeOutliers(noZerosList.slice(lIdx, rIdx))\r\n );\r\n lIdx += 20;\r\n }\r\n //do last one with left over elemnts at end >=20 in length\r\n // console.log('concatenating from/to:', lIdx, noZerosList.length);\r\n cleanedData = cleanedData.concat(\r\n removeOutliers(noZerosList.slice(lIdx, noZerosList.length))\r\n );\r\n // console.log('cleanedData', cleanedData);\r\n\r\n return cleanedData;\r\n }, []);\r\n\r\n //user chose a camera, so set state, and also list of images for that camera from server\r\n //also get and set the report params for this camera\r\n const handleDeviceSelected = useCallback(async () => {\r\n setIsLoading(true);\r\n setNoData(false);\r\n\r\n // const eventKey = event.target.value;\r\n if (!userSelectedCamera?.cameraId) return;\r\n const { cameraId } = userSelectedCamera;\r\n\r\n dispatch({ type: SET_CAMERA_DATA, payload: userSelectedCamera });\r\n\r\n //get report params from ddb, and save them to state, and locally for use in this function\r\n let digitsMultiplier;\r\n\r\n let label;\r\n\r\n getReportOnlyParams(cameraId).then((params) => {\r\n // console.log('getting params for selected camera:', params);\r\n dispatch({\r\n type: SET_REPORT_INFO,\r\n payload: {\r\n reportCount: params.costPerUnit,\r\n reportLabel: params.units,\r\n reportMult: params.multiplier,\r\n },\r\n });\r\n digitsMultiplier = params.multiplier;\r\n\r\n label = params.units;\r\n });\r\n\r\n getAllImagesListProc(cameraId).then(async (imgList) => {\r\n try {\r\n console.log(\r\n 'before saving off images list, lets modify it with calculations for gpm'\r\n );\r\n\r\n imgList = createDeltaProperties(imgList);\r\n\r\n console.log('length', imgList.length, imgList);\r\n\r\n dispatch({ type: SET_IMAGES_LIST, payload: imgList });\r\n\r\n console.log('updated to add in deltaDigits and deltaTimeMS', imgList);\r\n\r\n const hasDigitsList = imgList.filter(({ digits }) => Boolean(digits));\r\n\r\n const cleanedData = cleanData(hasDigitsList);\r\n\r\n console.log('cleanedData', cleanedData);\r\n let chartD = cleanedData.map((elt) => [\r\n new Date(parseInt(elt[0])),\r\n parseInt(elt[1]) * digitsMultiplier,\r\n ]);\r\n\r\n let flowD = computeDeltas(chartD);\r\n console.log('flowData:', flowD);\r\n\r\n flowD.unshift(['timestamp', 'delta/time']); //add to front of list\r\n chartD.unshift(['timestamp', 'value']); //add to front of list\r\n // console.log('imagesList', imagesList);\r\n // console.log('chartData:', chartD);\r\n // console.log('flowData:', flowD);\r\n dispatch({ type: SET_CHART_DATA, payload: chartD }); // the original unfiltered chart data\r\n // setFlowData(flowD);\r\n\r\n let startDate = chartD[1][0];\r\n\r\n dispatch({\r\n type: SET_START_DATE,\r\n payload: moment(startDate).format('YYYY-MM-DD'),\r\n });\r\n\r\n const result = handlePeriodicFilter(chartD, startDate, label, 'month');\r\n\r\n dispatch({ type: SET_BAR_CHART_DATA, payload: result });\r\n setIsLoading(false);\r\n } catch (err) {\r\n setIsLoading(false);\r\n setNoData(true);\r\n }\r\n });\r\n\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [userSelectedCamera, reportInfo.reportLabel]);\r\n\r\n // Handle transfer between tabs\r\n const changeToTab = useCallback(\r\n (tab) => {\r\n dispatch({ type: SET_PAGE_STATE, payload: tab });\r\n },\r\n [dispatch]\r\n );\r\n\r\n // prevent the re-creation of a function when passed as dep to a useEffect with useCallback\r\n const onDateFilterChange = useCallback(\r\n (periodType) => {\r\n if (!startDate) return;\r\n let result = handlePeriodicFilter(\r\n chartData,\r\n startDate,\r\n reportInfo.reportLabel,\r\n periodType\r\n );\r\n dispatch({ type: SET_BAR_CHART_DATA, payload: result });\r\n },\r\n\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n [chartData, startDate, endDate, reportInfo.reportLabel]\r\n );\r\n\r\n useEffect(() => {\r\n // this will also run on mount\r\n const onDeviceSelected = async () => {\r\n if (userSelectedCamera?.cameraId) {\r\n // run this function if there is a device selected when going to this page\r\n handleDeviceSelected();\r\n }\r\n };\r\n onDeviceSelected();\r\n /* run this function everytime the user selects a different camera on the dropdown\r\n no need for a different dropdown component here. */\r\n\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [userSelectedCamera]);\r\n\r\n useEffect(() => {\r\n // filter barchart data by time everytime user changes it\r\n onDateFilterChange(currentPeriodType);\r\n // run this everytime these change.\r\n }, [startDate, endDate, currentPeriodType, onDateFilterChange]);\r\n\r\n if (isLoading) {\r\n return (\r\n <>\r\n \r\n \r\n \r\n \r\n \r\n Loading...\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n >\r\n );\r\n }\r\n\r\n if (noData) {\r\n return (\r\n <>\r\n \r\n \r\n No data\r\n \r\n >\r\n );\r\n }\r\n\r\n return (\r\n <>\r\n {/* ****************** Report Top Navigation Bar ****************** */}\r\n \r\n\r\n {/* ****************** Filter Card ****************** */}\r\n {pageState.showFilter && (\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n )}\r\n\r\n \r\n \r\n {/* ****************** Usage Overview START ****************** */}\r\n {pageState.showChart && (\r\n \r\n {/* Bar chart */}\r\n \r\n \r\n )}\r\n \r\n\r\n {/* readings table */}\r\n {pageState.readingsTabSelected && (\r\n \r\n {cameraData.cameraId !== '' && }\r\n \r\n )}\r\n \r\n >\r\n );\r\n}\r\n","import React from 'react';\r\nimport {\r\n Grid,\r\n Tab,\r\n Tabs,\r\n useTheme,\r\n useMediaQuery,\r\n makeStyles,\r\n} from '@material-ui/core';\r\n\r\nconst useStyles = makeStyles((theme) => ({\r\n container: {\r\n marginLeft: '0px',\r\n marginRight: '0px',\r\n paddingTop: '20px',\r\n backgroundColor: '#FAFAFA',\r\n borderBottom: '1px solid #999',\r\n marginBottom: '2em',\r\n },\r\n\r\n tab: {\r\n '&:focus': {\r\n outline: 'none',\r\n },\r\n },\r\n}));\r\n\r\nexport default function AlertsNavbar({ changeToTab, activeTab }) {\r\n const theme = useTheme();\r\n const classes = useStyles();\r\n const matchesXs = useMediaQuery(theme.breakpoints.down('xs'));\r\n\r\n return (\r\n <>\r\n \r\n \r\n \r\n\r\n \r\n \r\n \r\n >\r\n );\r\n}\r\n","import React from 'react';\r\nimport Container from '@material-ui/core/Container';\r\nimport Grid from '@material-ui/core/Grid';\r\nimport Paper from '@material-ui/core/Paper';\r\nimport makeStyles from '@material-ui/core/styles/makeStyles';\r\n\r\nconst useStyles = makeStyles((theme) => ({\r\n paper: {\r\n // padding: theme.spacing(2),\r\n display: 'flex',\r\n flexDirection: 'column',\r\n height: '100%',\r\n },\r\n container: {\r\n paddingTop: theme.spacing(2),\r\n paddingBottom: theme.spacing(4),\r\n },\r\n}));\r\n\r\nexport default function PageSection({\r\n children,\r\n marginBottom,\r\n gridItemStyles,\r\n}) {\r\n const classes = useStyles();\r\n return (\r\n \r\n \r\n \r\n {children} \r\n \r\n \r\n \r\n );\r\n}\r\n","import React from 'react';\r\nimport makeStyles from '@material-ui/core/styles/makeStyles';\r\n\r\n// components\r\nimport MeterVibeButtonPrimary from '../../shared/buttons/MeterVibeButtonPrimary';\r\nimport MeterVibeButtonSecondary from '../../shared/buttons/MeterVibeButtonSecondary';\r\nimport Typography from '@material-ui/core/Typography';\r\nimport Grid from '@material-ui/core/Grid';\r\n\r\n// icons\r\nimport AlertsIcon from '@material-ui/icons/ErrorTwoTone';\r\n\r\nconst IssueFoundSection = ({\r\n issueFound,\r\n closeIssueFound,\r\n openDetailsModal,\r\n}) => {\r\n const classes = useStyles();\r\n\r\n const { message, type } = issueFound;\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n \r\n\r\n \r\n \r\n \r\n {type}\r\n \r\n \r\n\r\n \r\n \r\n Potential issue found\r\n \r\n \r\n \r\n \r\n\r\n \r\n \r\n {/* message = 'Water has been running for 3 hours . You have used 240 gallons of water' */}\r\n {message}\r\n \r\n \r\n\r\n \r\n \r\n openDetailsModal(issueFound)}\r\n />\r\n \r\n\r\n \r\n \r\n \r\n \r\n \r\n );\r\n};\r\n\r\nconst useStyles = makeStyles({\r\n issueTypeText: {\r\n fontSize: '14px',\r\n color: '#7A7A7A',\r\n fontWeight: 700,\r\n fontFamily: ['Montserrat', 'Roboto', 'Sans-Serif'].join(','),\r\n },\r\n\r\n issueHeaderText: {\r\n fontSize: '18px',\r\n color: '#363636',\r\n fontWeight: 700,\r\n fontFamily: ['Montserrat', 'Roboto', 'Sans-Serif'].join(','),\r\n },\r\n\r\n issueMessageText: {\r\n fontFamily: ['Public Sans', 'Roboto', 'Sans-Serif'].join(','),\r\n fontSize: '14px',\r\n color: '#363636',\r\n },\r\n\r\n textBold: {\r\n fontWeight: 700,\r\n },\r\n});\r\n\r\nexport default React.memo(IssueFoundSection);\r\n","import React from 'react';\r\n\r\n// components\r\nimport Typography from '@material-ui/core/Typography';\r\nimport Grid from '@material-ui/core/Grid';\r\nimport Moment from 'react-moment';\r\n\r\nimport makeStyles from '@material-ui/core/styles/makeStyles';\r\n\r\nconst AlertsSection = ({ alerts, handleClearAlerts, openDetailsModal }) => {\r\n const classes = useStyles();\r\n\r\n return (\r\n <>\r\n \r\n Alerts \r\n \r\n\r\n
\r\n\r\n {alerts.length ? (\r\n alerts.map((alert, key) => (\r\n // AlertRow defined in line 57\r\n \r\n ))\r\n ) : (\r\n \r\n No Alerts\r\n \r\n )}\r\n\r\n
\r\n\r\n \r\n {alerts.length ? (\r\n \r\n Clear all\r\n \r\n ) : null}\r\n \r\n >\r\n );\r\n};\r\n\r\nconst AlertRow = ({ alert, openDetailsModal }) => {\r\n const { severity, message, timestamp } = alert;\r\n\r\n const classes = useStyles({ alertSeverity: severity });\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n\r\n {message} \r\n\r\n openDetailsModal(alert)}\r\n className={classes.viewDetailsButton}>\r\n View details\r\n \r\n \r\n\r\n \r\n \r\n {timestamp}\r\n \r\n \r\n \r\n \r\n );\r\n};\r\n\r\nconst ALERT_SEVERITIES = {\r\n info: '#3D93CC',\r\n success: '#5FB200',\r\n warning: '#FF9F00',\r\n danger: '#D0021B',\r\n};\r\n\r\nconst buttonStyles = {\r\n border: 0,\r\n padding: 0,\r\n transition: 'all 250ms ease-in-out',\r\n background: 'none',\r\n cursor: 'pointer',\r\n color: '#0272BC',\r\n\r\n '&:hover': {\r\n transform: 'scale(1.01)',\r\n },\r\n\r\n '&:focus': {\r\n outline: 'none',\r\n },\r\n\r\n '&:active': {\r\n color: '#00224B',\r\n },\r\n};\r\n\r\nconst useStyles = makeStyles({\r\n alertsSectionHeader: {\r\n borderBottom: '1px solid #DBDBDB',\r\n padding: '10px 16px',\r\n\r\n '& > h1': {\r\n color: '#01070F',\r\n fontSize: '14px',\r\n fontWeight: 700,\r\n fontFamily: ['Montserrat', 'Roboto', 'Sans-Serif'].join(','),\r\n },\r\n },\r\n\r\n verticalSpacer: {\r\n display: 'flex',\r\n padding: '10px',\r\n flexGrow: 1,\r\n },\r\n\r\n alertsSectionFooter: {\r\n padding: '10px',\r\n backgroundColor: '#FAFAFA',\r\n },\r\n\r\n alertsClearAllButton: {\r\n ...buttonStyles,\r\n fontSize: '14px',\r\n fontWeight: 700,\r\n fontFamily: ['Montserrat', 'Roboto', 'Sans-Serif'].join(','),\r\n border: 0,\r\n padding: 0,\r\n transition: 'all 250ms ease-in-out',\r\n },\r\n\r\n alertRowItemsContainer: {\r\n backgroundColor: '#FAFAFA',\r\n margin: '10px 20px',\r\n padding: '10px',\r\n },\r\n\r\n alertRowSeverityCircle: {\r\n backgroundColor: ({ alertSeverity }) => ALERT_SEVERITIES[alertSeverity],\r\n borderRadius: '50%',\r\n width: '16px',\r\n height: '16px',\r\n marginRight: '10px',\r\n },\r\n\r\n alertRowMessage: {\r\n fontSize: '12px',\r\n fontFamily: ['Public Sans', 'Roboto', 'Sans-Serif'].join(','),\r\n color: '#363636',\r\n padding: 0,\r\n marginRight: '10px',\r\n },\r\n\r\n alertRowTimestamp: {\r\n fontSize: '12px',\r\n fontFamily: ['Public Sans', 'Roboto', 'Sans-Serif'].join(','),\r\n color: '#363636',\r\n },\r\n\r\n viewDetailsButton: {\r\n ...buttonStyles,\r\n fontSize: '12px',\r\n fontFamily: ['Public Sans', 'Roboto', 'Sans-Serif'].join(','),\r\n },\r\n});\r\n\r\nexport default React.memo(AlertsSection);\r\n","import React from 'react';\r\nimport Typography from '@material-ui/core/Typography';\r\nimport Grid from '@material-ui/core/Grid';\r\nimport MeterVibeButtonSecondary from '../../shared/buttons/MeterVibeButtonSecondary';\r\nimport MeterVibeButtonPrimary from '../../shared/buttons/MeterVibeButtonPrimary';\r\nimport {\r\n DialogTitle,\r\n DialogContent,\r\n DialogActions,\r\n DraggableModal,\r\n} from '../../shared/modal/ModalComponents';\r\nimport moment from 'moment';\r\nimport 'moment-timezone';\r\nimport {\r\n makeStyles,\r\n Table,\r\n TableCell,\r\n TableHead,\r\n TableRow,\r\n TableBody,\r\n} from '@material-ui/core';\r\n\r\nconst useStyles = makeStyles({\r\n imageSectionRoot: {\r\n overflow: 'hidden',\r\n padding: '20px',\r\n },\r\n\r\n breakdownSectionRoot: {\r\n overflow: 'hidden',\r\n padding: '20px',\r\n },\r\n\r\n altText: {\r\n color: '#363636',\r\n fontSize: '14px',\r\n fontWeight: 400,\r\n fontFamily: ['Public Sans', 'Roboto', 'Sans-Serif'].join(','),\r\n },\r\n\r\n snapshotImg: {\r\n width: '100%',\r\n },\r\n\r\n digitsText: {\r\n fontSize: '16px',\r\n color: '#363636',\r\n fontFamily: ['Montserrat', 'Roboto', 'Sans-Serif'].join(','),\r\n fontWeight: 700,\r\n marginTop: '10px',\r\n },\r\n\r\n breakdownTableHead: {\r\n '& > th': {\r\n fontWeight: 700,\r\n color: '#242424',\r\n padding: '5px 10px',\r\n },\r\n },\r\n\r\n breakdownTableRow: {\r\n ' & > td': {\r\n color: '#363636',\r\n padding: '15px 10px',\r\n },\r\n },\r\n\r\n buttonsContainer: {\r\n padding: '20px',\r\n display: 'flex',\r\n justifyContent: 'space-evenly',\r\n },\r\n\r\n closeBtn: {\r\n width: '50%',\r\n '&:focus': {\r\n outline: 'none',\r\n },\r\n },\r\n\r\n resolveBtn: {\r\n width: '100%',\r\n '&:focus': {\r\n outline: 'none',\r\n },\r\n },\r\n});\r\n\r\nconst SEVERITY_TITLES = {\r\n info: 'Info',\r\n danger: 'Potential issue found',\r\n warning: 'Potential issue found',\r\n success: 'Success',\r\n};\r\n\r\nconst SEVERITY_RATINGS = {\r\n info: 0,\r\n success: 0,\r\n warning: 1,\r\n danger: 2,\r\n};\r\n\r\nfunction AlertDetailsModal({\r\n open,\r\n onClose,\r\n imageData,\r\n previousImageData,\r\n label,\r\n message,\r\n severity,\r\n resolveIssue,\r\n}) {\r\n const currentDigits = imageData?.digits;\r\n const previousDigits = previousImageData?.digits;\r\n\r\n const classes = useStyles();\r\n\r\n // danger || warning = \"Issue Details\" : \"Info || Success Details\"\r\n const modalTitle =\r\n SEVERITY_RATINGS[severity] >= 1\r\n ? 'Issue Details'\r\n : `${SEVERITY_TITLES[severity]} Details`;\r\n\r\n return (\r\n \r\n {modalTitle} \r\n\r\n \r\n \r\n \r\n {SEVERITY_TITLES[severity]}\r\n \r\n {message} \r\n \r\n\r\n {/* if the severity is warning or danger, show the images, else don't */}\r\n {SEVERITY_RATINGS[severity] >= 1 && (\r\n <>\r\n {previousDigits && (\r\n \r\n )}\r\n {currentDigits && (\r\n \r\n )}\r\n\r\n \r\n >\r\n )}\r\n \r\n\r\n \r\n \r\n\r\n \r\n \r\n \r\n );\r\n}\r\n\r\n// a section with the image, timestamp and the digits\r\nconst ImageSection = ({ imageSource, timeStampSaved, digits, category }) => {\r\n const classes = useStyles();\r\n\r\n return (\r\n \r\n {/* recent snapshot: 10-20-2021 */}\r\n \r\n \r\n {category} snapshot:{' '}\r\n {moment(timeStampSaved).format('hh:mmA MM/DD/yyyy')}\r\n \r\n \r\n\r\n \r\n \r\n \r\n\r\n \r\n \r\n {digits}\r\n \r\n \r\n \r\n );\r\n};\r\n\r\nconst BreakdownSection = ({ label, timestamp, digits }) => {\r\n const classes = useStyles();\r\n const formattedTime = moment(timestamp).format('hh:mmA MM/DD/yyyy');\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n Breakdown per minute \r\n {[1, 2].map((_emptyCell, key) => (\r\n \r\n ))}\r\n \r\n \r\n\r\n \r\n \r\n {[formattedTime, digits, `0 ${label}`].map((cellText, key) => (\r\n {cellText} \r\n ))}\r\n \r\n \r\n
\r\n \r\n );\r\n};\r\n\r\nexport default React.memo(AlertDetailsModal);\r\n","import React, { useState, useCallback } from 'react';\r\n\r\n// components\r\nimport PageSection from '../../shared/containers/PageSection';\r\nimport IssueFoundSection from './IssueFoundSection';\r\n\r\n// utils\r\nimport AlertsSection from './AlertsSection';\r\nimport AlertDetailsModal from './AlertDetailsModal';\r\n\r\nexport default function StatusAndAlerts({\r\n alerts,\r\n issueFound,\r\n closeIssueFound,\r\n handleClearAlerts,\r\n clearOneAlert,\r\n}) {\r\n const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);\r\n const [modalData, setModalData] = useState(null);\r\n\r\n const openDetailsModal = useCallback(\r\n (alert) => {\r\n setIsDetailsModalOpen(alert.timestamp);\r\n setModalData(alert);\r\n },\r\n [setIsDetailsModalOpen, setModalData]\r\n );\r\n\r\n const closeDetailsModal = useCallback(() => {\r\n setIsDetailsModalOpen(false);\r\n }, [setIsDetailsModalOpen]);\r\n\r\n const onIssueResolved = useCallback(() => {\r\n clearOneAlert(modalData);\r\n closeDetailsModal();\r\n setModalData(null);\r\n }, [closeDetailsModal, clearOneAlert, modalData]);\r\n\r\n return (\r\n <>\r\n {issueFound && (\r\n \r\n \r\n \r\n )}\r\n\r\n \r\n \r\n \r\n\r\n \r\n >\r\n );\r\n}\r\n","import React, { useState, useEffect } from 'react';\r\n\r\nimport {\r\n Grid,\r\n Checkbox,\r\n FormGroup,\r\n FormControlLabel,\r\n Select,\r\n MenuItem,\r\n} from '@material-ui/core';\r\nimport makeStyles from '@material-ui/core/styles/makeStyles';\r\n\r\nconst useStyles = makeStyles({\r\n separator: {\r\n backgroundColor: '#DBDBDB',\r\n height: '1px',\r\n width: '100%',\r\n },\r\n});\r\n\r\nexport default function CheckboxesSection() {\r\n const classes = useStyles();\r\n\r\n // this state is for the very left side of each row, it decideds if to enable the right side.\r\n const [enabledCheckboxes, setEnabledCheckboxes] = useState({\r\n potentialLeakageAlerts: false,\r\n dailyWaterUsageAlerts: [false, '15% more than average'],\r\n dailyWaterUsageReport: false,\r\n monthlyWaterUsageReport: false,\r\n });\r\n\r\n const initialCheckboxesState = {\r\n onEmail: false,\r\n onMobilePhone: false,\r\n inWebApplication: false,\r\n };\r\n\r\n // these states are for the right side of each row\r\n const [sendPotentialLeakageAlerts, setSendPotentialLeakageAlerts] = useState(\r\n initialCheckboxesState\r\n );\r\n const [sendAlertDaily, setSendAlertDaily] = useState(initialCheckboxesState);\r\n const [sendDailyWaterUsageReport, setSendDailyWaterUsageReport] = useState(\r\n initialCheckboxesState\r\n );\r\n const [sendMonthlyWaterUsageReport, setSendMonthlyWaterUsageReport] =\r\n useState(initialCheckboxesState);\r\n\r\n const toggleEnabledCheckboxes = (e) => {\r\n const { name, value } = e.target;\r\n\r\n // the left side of daily water usage has 2 things, so it's an array in the state, 0 is a boolean and 1 is the avg value\r\n if (name === 'dailyWaterUsageAlerts.bool') {\r\n setEnabledCheckboxes((prevState) => ({\r\n ...prevState,\r\n dailyWaterUsageAlerts: [\r\n !prevState.dailyWaterUsageAlerts[0],\r\n prevState.dailyWaterUsageAlerts[1],\r\n ],\r\n }));\r\n\r\n return;\r\n }\r\n\r\n if (name === 'dailyWaterUsageAlerts.average') {\r\n setEnabledCheckboxes((prevState) => ({\r\n ...prevState,\r\n dailyWaterUsageAlerts: [prevState.dailyWaterUsageAlerts[0], value],\r\n }));\r\n\r\n return;\r\n }\r\n\r\n // toggle the boolean\r\n setEnabledCheckboxes((prevState) => ({\r\n ...prevState,\r\n [name]: !prevState[name],\r\n }));\r\n };\r\n\r\n const handleSubmit = async () => {\r\n let reqBody = {\r\n potentialLeakageAlerts: {\r\n // do the same thing for the other sections\r\n send: enabledCheckboxes.potentialLeakageAlerts, // back-end should prioritize send, if other values are true but send is false, just don't send\r\n ...sendPotentialLeakageAlerts, // onEmail, onMobilePhone, inWebApplication\r\n },\r\n\r\n dailyAlerts: {\r\n send: enabledCheckboxes.dailyWaterUsageAlerts[0],\r\n average: enabledCheckboxes.dailyWaterUsageAlerts[1],\r\n ...sendAlertDaily, // onEmail, onMobilePhone, inWebApplication\r\n },\r\n };\r\n\r\n console.log({ reqBody });\r\n return;\r\n };\r\n\r\n useEffect(() => {\r\n window._testSubmit = () => handleSubmit(); // test handleSubmit\r\n });\r\n\r\n return (\r\n \r\n {/* potential leakage alerts */}\r\n \r\n\r\n
\r\n\r\n {/* Daily Water Alerts */}\r\n \r\n
\r\n {/* End of daily water alerts */}\r\n\r\n {/* Send daily water usage report */}\r\n \r\n\r\n
\r\n {/* End of daily usage report */}\r\n\r\n {/* Send monthly water usage report */}\r\n \r\n {/* end of Send monthly water usage report */}\r\n \r\n );\r\n}\r\n\r\nconst checkboxAliases = {\r\n onEmail: 'On email',\r\n onMobilePhone: 'On mobile phone',\r\n inWebApplication: 'In Web Application',\r\n};\r\n\r\n// the checkbox group at the right side of every row\r\nconst CheckboxGroup = ({ state, setState, disabled }) => (\r\n \r\n {['onEmail', 'onMobilePhone', 'inWebApplication'].map((name, key) => {\r\n const labelText = checkboxAliases[name];\r\n\r\n return (\r\n \r\n setState((prevState) => ({\r\n ...prevState,\r\n [name]: !prevState[name],\r\n }))\r\n }\r\n checked={state[name]}\r\n />\r\n }\r\n label={labelText}\r\n />\r\n );\r\n })}\r\n \r\n);\r\n\r\nconst CheckboxRow = ({\r\n state,\r\n setState,\r\n stateName,\r\n onChange,\r\n enabledCheckboxes,\r\n label,\r\n marginTop,\r\n}) => (\r\n \r\n \r\n \r\n }\r\n label={label}\r\n />\r\n \r\n\r\n {/* CheckboxGroup contains the 'On email', 'On Mobile Phone', 'In Web Application' checkboxes */}\r\n \r\n \r\n);\r\n\r\nconst DailyWaterAlertsRow = ({\r\n enabledCheckboxes,\r\n onChange,\r\n sendAlertDaily,\r\n setSendAlertDaily,\r\n}) => (\r\n \r\n \r\n \r\n \r\n }\r\n label=\"Send alert if daily water usage is\"\r\n />\r\n \r\n\r\n \r\n \r\n {['15% more than average', '35% more than average'].map(\r\n (value, key) => (\r\n \r\n {value}\r\n \r\n )\r\n )}\r\n \r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n);\r\n","import React, { useState } from 'react';\r\nimport { Typography, Grid, TextField } from '@material-ui/core';\r\nimport makeStyles from '@material-ui/core/styles/makeStyles';\r\nimport moment from 'moment';\r\n\r\nconst useStyles = makeStyles({\r\n defaultParagraph: {\r\n fontSize: '14px',\r\n color: '#363636',\r\n fontFamily: ['Public Sans', 'Roboto', 'Sans-Serif'].join(','),\r\n marginBottom: 0,\r\n },\r\n});\r\n\r\n// leakage system working hours\r\nexport default function HoursSection() {\r\n const classes = useStyles();\r\n\r\n // browser time inputs don't take hh:mm A, they take HH:mm which means 1 PM would be 13:00 (military time)\r\n const [hours, setHours] = useState({\r\n min: moment().format('HH:mm'), // left input\r\n max: moment(Date.now() + 1000 * 60 * 60).format('HH:mm'), // right input\r\n });\r\n\r\n const handleChange = (e) => {\r\n const { name, value } = e.target;\r\n\r\n setHours((prevState) => ({\r\n ...prevState,\r\n [name]: value,\r\n }));\r\n };\r\n\r\n return (\r\n \r\n \r\n Leakage system working hours. This is the time when our system will do\r\n regular leakage test. You can change this time periods to your\r\n preference.\r\n \r\n\r\n \r\n \r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n \r\n );\r\n}\r\n","import React from 'react';\r\nimport PageWrapper from '../../shared/containers/PageSection';\r\nimport { Typography, Grid } from '@material-ui/core';\r\nimport makeStyles from '@material-ui/core/styles/makeStyles';\r\n\r\n// sections\r\nimport CheckboxesSection from './CheckboxesSection';\r\nimport HoursSection from './HoursSection';\r\n\r\nconst useStyles = makeStyles({\r\n defaultParagraph: {\r\n fontSize: '14px',\r\n color: '#363636',\r\n fontFamily: ['Public Sans', 'Roboto', 'Sans-Serif'].join(','),\r\n marginBottom: 0,\r\n },\r\n\r\n link: {\r\n color: '#0272BC',\r\n cursor: 'pointer',\r\n },\r\n\r\n verticalSpacer: {\r\n display: 'flex',\r\n flexGrow: 1,\r\n padding: '10px',\r\n },\r\n\r\n separator: {\r\n backgroundColor: '#DBDBDB',\r\n height: '1px',\r\n width: '100%',\r\n },\r\n});\r\n\r\nexport default function AlertsSettings() {\r\n return (\r\n \r\n {[IntroSection, HoursSection, CheckboxesSection].map(\r\n (SectionComponent, idx, arr) => (\r\n \r\n \r\n {idx !== arr.length - 1 ? : null}\r\n \r\n )\r\n )}\r\n \r\n );\r\n}\r\n\r\nconst SectionSeparator = () => {\r\n const classes = useStyles();\r\n\r\n return (\r\n \r\n \r\n \r\n );\r\n};\r\n\r\nconst IntroSection = () => {\r\n const classes = useStyles();\r\n\r\n return (\r\n \r\n \r\n Here you can configure what types of alerts you want to recieve. Each\r\n type of alert can be send to different channels (Email, Phone, Web\r\n application). You can change email or mobile phone number in{' '}\r\n User settings .\r\n \r\n \r\n );\r\n};\r\n","import mockSnapshotSRC from '../assets/mock_images/cam32_concat.jpg';\r\n\r\n// this is just for creating mock dates\r\nconst SECONDS_IN_DAY = 60 * 60 * 24; // not in MS\r\nconst ONE_DAY_IN_MS = 1000 * SECONDS_IN_DAY; // one day in milliseconds\r\nconst ONE_HOUR_IN_MS = 1000 * 60 * 60; // one hour in milliseconds\r\n\r\nconst mockSnapshotRecent = {\r\n signedUrl: mockSnapshotSRC,\r\n digits: '0,000,292.88',\r\n timestampSaved: Date.now() - ONE_HOUR_IN_MS,\r\n};\r\n\r\nconst mockSnapshotPrior = {\r\n signedUrl: mockSnapshotSRC,\r\n digits: '140,149,222.88',\r\n timestampSaved: Date.now() - ONE_HOUR_IN_MS * 4,\r\n};\r\n\r\nexport const mockAlerts = [\r\n {\r\n type: 'Leakage',\r\n message:\r\n 'Potential leakage: Water have been running for 3 hours. You have used 240 gallons of water',\r\n timestamp: Date.now(),\r\n isCleared: false,\r\n severity: 'danger', // severity is used for the color of circles at the AlertRow component (inside AlertsSection.js)\r\n\r\n // snapshots for details modal\r\n firstSnapshot: mockSnapshotPrior,\r\n recentSnapshot: mockSnapshotRecent,\r\n },\r\n {\r\n type: 'Leakage',\r\n\r\n message: 'You used 45% more water today compared to your average daily use',\r\n timestamp: Date.now() - ONE_HOUR_IN_MS,\r\n isCleared: false,\r\n severity: 'warning',\r\n\r\n firstSnapshot: mockSnapshotPrior,\r\n recentSnapshot: mockSnapshotRecent,\r\n },\r\n {\r\n type: 'Leakage',\r\n\r\n message: 'You used 15% more water today compared to your average daily use',\r\n timestamp: Date.now() - ONE_DAY_IN_MS,\r\n isCleared: false,\r\n severity: 'info',\r\n\r\n firstSnapshot: mockSnapshotPrior,\r\n recentSnapshot: mockSnapshotRecent,\r\n },\r\n {\r\n type: 'Leakage',\r\n\r\n message: 'New device (code 235235252) was successfuly activated',\r\n timestamp: Date.now() - ONE_DAY_IN_MS * 2,\r\n isCleared: false,\r\n severity: 'success',\r\n\r\n firstSnapshot: mockSnapshotPrior,\r\n recentSnapshot: mockSnapshotRecent,\r\n },\r\n];\r\n","import React, { useState, useEffect, useCallback } from 'react';\r\n\r\n// components\r\nimport AlertsNavbar from '../components/alerts_components/AlertsNavbar';\r\nimport StatusAndAlerts from '../components/alerts_components/StatusAndAlerts/StatusAndAlerts';\r\nimport AlertsSettings from '../components/alerts_components/AlertsSettings/AlertsSettings';\r\n\r\nimport { mockAlerts } from '../mocks/alerts.mock';\r\n\r\nconst SEVERITY_RATINGS = {\r\n info: 0,\r\n success: 0,\r\n warning: 1,\r\n danger: 2,\r\n};\r\n\r\nconst toggleAlertCleared = (alert) => {\r\n return {\r\n ...alert,\r\n isCleared: !alert.isCleared,\r\n };\r\n};\r\n\r\nconst checkNewIssueFound = (alerts) => {\r\n const newIssueFound = alerts.find(\r\n ({ severity }) => SEVERITY_RATINGS[severity] >= 1 // warning || danger\r\n );\r\n\r\n return newIssueFound ?? false;\r\n};\r\n\r\nexport default function Alerts() {\r\n const [activeTab, setActiveTab] = useState('StatusAndAlerts');\r\n const [issueFound, setIssueFound] = useState(null);\r\n const [allVisibleAlerts, setAllVisibleAlerts] = useState([]);\r\n\r\n useEffect(() => {\r\n // API.get('/user/allAlerts')\r\n let responseData = {\r\n alerts: mockAlerts,\r\n };\r\n\r\n const { alerts } = responseData;\r\n\r\n const unclearedAlerts = alerts.filter((alert) => !alert.isCleared);\r\n\r\n setAllVisibleAlerts(unclearedAlerts);\r\n }, []);\r\n\r\n const changeToTab = useCallback(\r\n (_e, newTab) => {\r\n setActiveTab(newTab);\r\n },\r\n [setActiveTab]\r\n );\r\n\r\n const closeIssueFound = useCallback(() => {\r\n // API.post(\"/alerts/issues/issueid\")\r\n setIssueFound(false);\r\n }, [setIssueFound]);\r\n\r\n const handleClearAlerts = useCallback(() => {\r\n // API.post(\"/alerts/clearAll\")\r\n setAllVisibleAlerts([]);\r\n }, [setAllVisibleAlerts]);\r\n\r\n // this runs everytime an alert is cleared.\r\n // toggleAlertCleared will change the isCleared value (so if it's false it'll be true)\r\n const clearOneAlert = useCallback(\r\n (alert) => {\r\n // API.post(alerts/clear/alert.timestamp)\r\n\r\n // toggleAlertCleared will change the isCleared value (so if it's false it'll be true)\r\n const updatedAlert = toggleAlertCleared(alert);\r\n\r\n // map though the state and if timestamp is even that means thats the alert we want to update\r\n const updatedAlerts = [...allVisibleAlerts].map((mappedAlert) =>\r\n mappedAlert.timestamp === alert.timestamp ? updatedAlert : mappedAlert\r\n );\r\n\r\n // get the alerts that aren't cleared\r\n const unclearedAlerts = updatedAlerts.filter((alert) => !alert.isCleared);\r\n\r\n setAllVisibleAlerts(unclearedAlerts);\r\n },\r\n [setAllVisibleAlerts, allVisibleAlerts]\r\n );\r\n\r\n useEffect(() => {\r\n // check if there's a new issue found everytime alerts change\r\n const newIssueFound = checkNewIssueFound(allVisibleAlerts);\r\n setIssueFound(newIssueFound);\r\n }, [allVisibleAlerts]);\r\n\r\n return (\r\n <>\r\n \r\n\r\n {activeTab === 'StatusAndAlerts' ? (\r\n \r\n ) : (\r\n \r\n )}\r\n >\r\n );\r\n}\r\n","import React, { useState, useEffect, useCallback } from 'react';\r\nimport { useSelector } from 'react-redux';\r\nimport { makeStyles } from '@material-ui/core/styles';\r\n\r\n// components\r\nimport Grid from '@material-ui/core/Grid';\r\nimport Paper from '@material-ui/core/Paper';\r\nimport Typography from '@material-ui/core/Typography';\r\nimport FormGroup from '@material-ui/core/FormGroup';\r\nimport FormControl from '@material-ui/core/FormControl';\r\nimport InputLabel from '@material-ui/core/InputLabel';\r\nimport TextField from '@material-ui/core/TextField';\r\nimport Select from '@material-ui/core/Select';\r\nimport Box from '@material-ui/core/Box';\r\n\r\nimport MenuItem from '@material-ui/core/MenuItem';\r\nimport FormHelperText from '@material-ui/core/FormHelperText';\r\nimport MeterVibeButtonPrimary from '../components/shared/buttons/MeterVibeButtonPrimary';\r\n\r\nexport default function UserSettings() {\r\n const { user } = useSelector(({ userReducer }) => userReducer);\r\n\r\n if (!user?.id) return null; // or loading\r\n\r\n return (\r\n \r\n \r\n\r\n \r\n \r\n\r\n \r\n \r\n \r\n );\r\n}\r\n\r\nconst PersonalDetails = () => {\r\n const { userEmail } = useSelector(({ userReducer }) => userReducer);\r\n\r\n const [personalDetails, setPersonalDetails] = useState({\r\n // name: user.name, // doesn't exist\r\n name: '',\r\n email: '',\r\n phoneNumber: '',\r\n });\r\n\r\n const classes = useStyles();\r\n\r\n const handleChange = (e) => {\r\n const { name, value } = e.target;\r\n setPersonalDetails((prevState) => ({\r\n ...prevState,\r\n [name]: value,\r\n }));\r\n };\r\n\r\n useEffect(() => {\r\n setPersonalDetails((prevState) => ({\r\n ...prevState,\r\n email: userEmail,\r\n }));\r\n // set the email to what is the userEmail when mounted\r\n\r\n return () => {\r\n setPersonalDetails((prevState) => ({\r\n ...prevState,\r\n email: userEmail,\r\n }));\r\n };\r\n }, [userEmail]);\r\n\r\n const onSavePersonalDetails = useCallback(() => {\r\n alert(JSON.stringify(personalDetails));\r\n }, [personalDetails]);\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n Personal details\r\n \r\n\r\n \r\n \r\n \r\n \r\n\r\n \r\n \r\n \r\n\r\n \r\n \r\n \r\n Will be used to send alerts on your mobile phone. You can\r\n customize what you want to receive in Alerts settings.\r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n );\r\n};\r\n\r\nconst ChangePassword = () => {\r\n const [passwordData, setPasswordData] = useState({\r\n oldPassword: '',\r\n newPassword: '',\r\n });\r\n\r\n const classes = useStyles();\r\n\r\n const handleChange = (e) => {\r\n const { name, value } = e.target;\r\n\r\n setPasswordData((prevState) => ({\r\n ...prevState,\r\n [name]: value,\r\n }));\r\n };\r\n\r\n const onSaveNewPassword = useCallback(() => {\r\n alert(JSON.stringify(passwordData));\r\n }, [passwordData]);\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n Change password\r\n \r\n\r\n \r\n \r\n \r\n \r\n\r\n \r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n );\r\n};\r\n\r\nconst DefaultPreferences = () => {\r\n const classes = useStyles();\r\n\r\n const [defaultPreferences, setDefaultPreferences] = useState({\r\n showDataType: 'Gallons',\r\n gallonPrice: '',\r\n });\r\n\r\n const handleChange = (e) => {\r\n const { name, value } = e.target;\r\n\r\n setDefaultPreferences((prevState) => ({\r\n ...prevState,\r\n [name]: value,\r\n }));\r\n };\r\n\r\n const onSaveDefaultPreferences = useCallback(() => {\r\n alert(JSON.stringify(defaultPreferences));\r\n }, [defaultPreferences]);\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n Default preferences\r\n \r\n\r\n \r\n \r\n \r\n Show data in\r\n \r\n\r\n \r\n Gallons \r\n Cubic Feet \r\n \r\n \r\n\r\n \r\n \r\n Gallon Price\r\n \r\n\r\n \r\n \r\n \r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n );\r\n};\r\n\r\nconst useStyles = makeStyles((theme) => ({\r\n sectionItems: {\r\n padding: '30px',\r\n },\r\n\r\n sectionTitle: {\r\n fontWeight: 700,\r\n color: '#01070F',\r\n fontSize: '18px',\r\n fontFamily: ['Montserrat', 'Roboto', 'Sans-Serif'].join(','),\r\n },\r\n\r\n inputLabel: {\r\n color: '#363636',\r\n fontFamily: ['Public Sans', 'Roboto', 'Sans-Serif'].join(','),\r\n fontSize: '14px',\r\n },\r\n\r\n saveButton: {\r\n maxWidth: '40%',\r\n marginTop: '10px',\r\n [theme.breakpoints.down('md')]: {\r\n maxWidth: '100%',\r\n },\r\n },\r\n}));\r\n","import React from 'react';\r\nimport { Route, Switch } from 'react-router-dom';\r\nimport Dashboard from './containers/Dashboard';\r\nimport Activate from './containers/Activate';\r\nimport Admin from './containers/Admin';\r\nimport Device from './containers/Device';\r\nimport NotFound from './components/NotFound';\r\nimport PublicLanding from './components/PublicLanding';\r\nimport MeterVibeAppLayout from './layouts/MeterVibeAppLayout';\r\nimport TermsOfService from './components/TermsOfService';\r\nimport PrivacyPolicy from './components/PrivacyPolicy';\r\nimport Reports from './containers/Reports';\r\nimport Alerts from './containers/Alerts';\r\nimport UserSettings from './containers/UserSettings';\r\n\r\nimport { useSelector } from 'react-redux';\r\n\r\nexport default () => {\r\n const user = useSelector(({ userReducer }) => userReducer.user);\r\n\r\n return (\r\n \r\n {/* commenting out this because now after changes everytime going to '/' redirects to dashboard, doesn't seem needed... */}\r\n {/* */}\r\n {/* redirects root path to /dashboard so Dashboard NavLink always shows bold in drawer */}\r\n\r\n \r\n \r\n \r\n\r\n {/* wrap the authenticated-section of the app with the app layout */}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n {/*\r\n doing this because when the user isn't authenticated the useScript hook in Admin.js will still run\r\n so I'm blocking this route from existing if we don't have a user\r\n */}\r\n {user && }\r\n \r\n\r\n {/* Finally, catch all unmatched routes */}\r\n \r\n \r\n );\r\n};\r\n","import { useEffect } from 'react';\r\nimport { useLocation } from 'react-router-dom';\r\n// https://reactrouter.com/web/guides/scroll-restoration\r\nexport default function ScrollToTopOnMount() {\r\n const { pathname } = useLocation();\r\n // scroll to top on mount or page change\r\n useEffect(() => {\r\n window.scrollTo(0, 0);\r\n }, [pathname]);\r\n\r\n return null;\r\n}\r\n","import React, { useEffect } from 'react';\r\nimport { useHistory } from 'react-router-dom';\r\nimport { useDispatch } from 'react-redux';\r\nimport { onAuthUIStateChange } from '@aws-amplify/ui-components';\r\nimport { Auth } from 'aws-amplify';\r\nimport { Hub } from '@aws-amplify/core';\r\n\r\nimport Routes from './Routes';\r\nimport './App.css';\r\nimport { makeStyles } from '@material-ui/core/styles';\r\nimport CssBaseline from '@material-ui/core/CssBaseline';\r\nimport ScrollToTopOnMount from './components/shared/helper_components/ScrollToTopOnMount';\r\n\r\n// reducer actions\r\nimport { USER_ACTIONS } from './reducers/user.reducer';\r\nimport { AUTH_ACTIONS } from './reducers/auth.reducer';\r\nimport { CAMERA_ACTIONS } from './reducers/cameras.reducer';\r\nimport { API_ACTIONS } from './reducers/general.reducer';\r\n\r\n// services\r\nimport { apiCameras } from './services/cameras.services';\r\n\r\nconst useStyles = makeStyles({\r\n root: {\r\n display: 'flex',\r\n minHeight: '100vh', // doing this so MUI paper reaches from top to bottom and doesn't get cut off\r\n },\r\n});\r\n\r\nconst { SET_USER, SET_USER_EMAIL, SET_ADMIN } = USER_ACTIONS;\r\nconst { SET_AUTH_STATE } = AUTH_ACTIONS;\r\nconst { SET_CAMERAS, SET_USER_SELECTED_CAMERA } = CAMERA_ACTIONS;\r\nconst { SET_AWAITING_API } = API_ACTIONS;\r\n\r\nexport default function App() {\r\n const history = useHistory();\r\n\r\n const dispatch = useDispatch();\r\n\r\n const classes = useStyles();\r\n\r\n //add in Hub listener to dispatch on Auth events\r\n //this way we can clear out user data on logout\r\n //and set up user data on login\r\n useEffect(() => {\r\n //run this after the auth user login event to set the list of devices\r\n //set this users Admin level\r\n //set the currently selected device\r\n const getCameras = async () => {\r\n try {\r\n const apiCamerasResponse = await apiCameras();\r\n console.log('apiCamerasResponse:', apiCamerasResponse);\r\n // setAdmin in App.js to whatever value comes back from API\r\n dispatch({ type: SET_ADMIN, payload: apiCamerasResponse.admin });\r\n // sort the cameras and set them to state\r\n const sortedCameras = apiCamerasResponse.data.sort((a, b) =>\r\n a.cameraId.localeCompare(b.cameraId)\r\n );\r\n\r\n dispatch({ type: SET_CAMERAS, payload: sortedCameras });\r\n\r\n // console.log('sortedCameras:', sortedCameras);\r\n\r\n dispatch({\r\n type: SET_USER_SELECTED_CAMERA,\r\n payload: sortedCameras[0],\r\n });\r\n // console.log('userSelectedCamera:', userSelectedCamera);\r\n // allow Dashboard.js to map the cameras\r\n dispatch({ type: SET_AWAITING_API, payload: false });\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n };\r\n const authListener = (data) => {\r\n //callback function dispatched by auth publish when authentication event\r\n switch (data.payload.event) {\r\n case 'signIn':\r\n console.log('user signed in');\r\n console.log('data:', data);\r\n console.log('email:', data.payload.data.attributes.email);\r\n\r\n dispatch({ type: SET_USER, payload: data.payload.data });\r\n\r\n dispatch({\r\n type: SET_USER_EMAIL,\r\n payload: data.payload.data.attributes.email,\r\n });\r\n\r\n getCameras();\r\n\r\n history.push('/dashboard'); // push the user to the app after signing in\r\n break;\r\n case 'signUp':\r\n console.log('user signed up');\r\n break;\r\n case 'signOut':\r\n console.log('user signed out');\r\n console.log('data:', data);\r\n console.log('email:', data.payload.data.attributes.email);\r\n history.push('/'); //logging out, so go to / on login.\r\n break;\r\n case 'signIn_failure':\r\n console.log('user sign in failed');\r\n break;\r\n case 'configured':\r\n console.log('the Auth module is configured');\r\n break;\r\n default: //get rid of compiler warning of expected default case\r\n }\r\n };\r\n\r\n //on page reload, there is no signIn or Signout, but the state is reset\r\n //const userInfo = await Auth.currentUserInfo();\r\n Auth.currentUserInfo().then((data) => {\r\n console.log('auth current credentials:', data);\r\n if (data) {\r\n // when this is run at start, there is no currentUserInfo, so data.attributs.email undefined and errored\r\n dispatch({ type: SET_USER, payload: data });\r\n\r\n dispatch({\r\n type: SET_USER_EMAIL,\r\n payload: data.attributes.email,\r\n });\r\n }\r\n });\r\n getCameras();\r\n\r\n //create a listener that runs on auth changes\r\n Hub.listen('auth', authListener); //subscribe to the auth events\r\n return () => {\r\n Hub.remove('auth', authListener); //useEffect cleanup, remove the listener\r\n };\r\n\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, []);\r\n\r\n // this useEffect handles authentication changes and the history array\r\n useEffect(() => {\r\n //return onAuthUIStateChange((nextAuthState, authData) => {\r\n onAuthUIStateChange((nextAuthState, authData) => {\r\n dispatch({ type: SET_AUTH_STATE, payload: nextAuthState });\r\n dispatch({\r\n type: SET_USER,\r\n payload: authData,\r\n });\r\n });\r\n\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, []); // Only re-run the effect if user or history changes\r\n\r\n return (\r\n <>\r\n \r\n \r\n \r\n \r\n
\r\n >\r\n );\r\n}\r\n","// In production, we register a service worker to serve assets from local cache.\r\n\r\n// This lets the app load faster on subsequent visits in production, and gives\r\n// it offline capabilities. However, it also means that developers (and users)\r\n// will only see deployed updates on the \"N+1\" visit to a page, since previously\r\n// cached resources are updated in the background.\r\n\r\n// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.\r\n// This link also includes instructions on opting out of this behavior.\r\n\r\nconst isLocalhost = Boolean(\r\n window.location.hostname === 'localhost' ||\r\n // [::1] is the IPv6 localhost address.\r\n window.location.hostname === '[::1]' ||\r\n // 127.0.0.1/8 is considered localhost for IPv4.\r\n window.location.hostname.match(\r\n /^127(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/\r\n )\r\n);\r\n\r\nexport default function register() {\r\n if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {\r\n // The URL constructor is available in all browsers that support SW.\r\n const publicUrl = new URL(process.env.PUBLIC_URL, window.location);\r\n if (publicUrl.origin !== window.location.origin) {\r\n // Our service worker won't work if PUBLIC_URL is on a different origin\r\n // from what our page is served on. This might happen if a CDN is used to\r\n // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374\r\n return;\r\n }\r\n\r\n window.addEventListener('load', () => {\r\n const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;\r\n\r\n if (isLocalhost) {\r\n // This is running on localhost. Lets check if a service worker still exists or not.\r\n checkValidServiceWorker(swUrl);\r\n\r\n // Add some additional logging to localhost, pointing developers to the\r\n // service worker/PWA documentation.\r\n navigator.serviceWorker.ready.then(() => {\r\n console.log(\r\n 'This web app is being served cache-first by a service ' +\r\n 'worker. To learn more, visit https://goo.gl/SC7cgQ'\r\n );\r\n });\r\n } else {\r\n // Is not local host. Just register service worker\r\n registerValidSW(swUrl);\r\n }\r\n });\r\n }\r\n}\r\n\r\nfunction registerValidSW(swUrl) {\r\n navigator.serviceWorker\r\n .register(swUrl)\r\n .then(registration => {\r\n registration.onupdatefound = () => {\r\n const installingWorker = registration.installing;\r\n installingWorker.onstatechange = () => {\r\n if (installingWorker.state === 'installed') {\r\n if (navigator.serviceWorker.controller) {\r\n // At this point, the old content will have been purged and\r\n // the fresh content will have been added to the cache.\r\n // It's the perfect time to display a \"New content is\r\n // available; please refresh.\" message in your web app.\r\n console.log('New content is available; please refresh.');\r\n } else {\r\n // At this point, everything has been precached.\r\n // It's the perfect time to display a\r\n // \"Content is cached for offline use.\" message.\r\n console.log('Content is cached for offline use.');\r\n }\r\n }\r\n };\r\n };\r\n })\r\n .catch(error => {\r\n console.error('Error during service worker registration:', error);\r\n });\r\n}\r\n\r\nfunction checkValidServiceWorker(swUrl) {\r\n // Check if the service worker can be found. If it can't reload the page.\r\n fetch(swUrl)\r\n .then(response => {\r\n // Ensure service worker exists, and that we really are getting a JS file.\r\n if (\r\n response.status === 404 ||\r\n response.headers.get('content-type').indexOf('javascript') === -1\r\n ) {\r\n // No service worker found. Probably a different app. Reload the page.\r\n navigator.serviceWorker.ready.then(registration => {\r\n registration.unregister().then(() => {\r\n window.location.reload();\r\n });\r\n });\r\n } else {\r\n // Service worker found. Proceed as normal.\r\n registerValidSW(swUrl);\r\n }\r\n })\r\n .catch(() => {\r\n console.log(\r\n 'No internet connection found. App is running in offline mode.'\r\n );\r\n });\r\n}\r\n\r\nexport function unregister() {\r\n if ('serviceWorker' in navigator) {\r\n navigator.serviceWorker.ready.then(registration => {\r\n registration.unregister();\r\n });\r\n }\r\n}\r\n","import React from 'react';\r\nimport ReactDOM from 'react-dom';\r\nimport { BrowserRouter as Router } from 'react-router-dom';\r\nimport Amplify from 'aws-amplify';\r\nimport config from './config';\r\nimport App from './App';\r\nimport registerServiceWorker from './registerServiceWorker';\r\nimport { Provider as ReduxStoreProvider } from 'react-redux';\r\nimport configureStore from './store'; // index.js\r\n\r\nconst reduxStore = configureStore();\r\n\r\nAmplify.configure({\r\n Auth: {\r\n mandatorySignIn: true,\r\n region: config.cognito.REGION,\r\n userPoolId: config.cognito.USER_POOL_ID,\r\n identityPoolId: config.cognito.IDENTITY_POOL_ID,\r\n userPoolWebClientId: config.cognito.APP_CLIENT_ID,\r\n },\r\n Storage: {\r\n region: config.s3.REGION,\r\n bucket: config.s3.BUCKET,\r\n identityPoolId: config.cognito.IDENTITY_POOL_ID,\r\n },\r\n API: {\r\n endpoints: [\r\n {\r\n name: 'dev-iotvid-app',\r\n endpoint: config.apiGateway.URL,\r\n region: config.apiGateway.REGION,\r\n },\r\n {\r\n name: 'dev-iotvid-ml',\r\n endpoint: config.apiGateway.URL_ML,\r\n region: config.apiGateway.REGION,\r\n },\r\n ],\r\n },\r\n});\r\n\r\nReactDOM.render(\r\n \r\n \r\n \r\n \r\n ,\r\n document.getElementById('root')\r\n);\r\nregisterServiceWorker();\r\n","import { combineReducers } from 'redux';\r\n\r\nimport userReducer from './user.reducer';\r\nimport authReducer from './auth.reducer';\r\nimport camerasReducer from './cameras.reducer';\r\nimport generalReducer from './general.reducer';\r\nimport reportsReducer from './reports.reducer';\r\n\r\n// this reducer combines all other specific reducers.\r\nexport default combineReducers({\r\n userReducer,\r\n authReducer,\r\n camerasReducer,\r\n generalReducer,\r\n reportsReducer,\r\n});\r\n","import { createStore } from 'redux';\r\nimport rootReducer from '../reducers/root.reducer';\r\n\r\nexport default function configureStore() {\r\n const store = createStore(\r\n rootReducer // will give us access to reducer and initial state.\r\n );\r\n\r\n return store;\r\n}\r\n","export const CAMERA_ACTIONS = {\r\n SET_USER_SELECTED_CAMERA: 'SET_USER_SELECTED_CAMERA',\r\n SET_CAMERAS: 'SET_CAMERAS',\r\n};\r\n\r\nconst { SET_USER_SELECTED_CAMERA, SET_CAMERAS } = CAMERA_ACTIONS;\r\n\r\nconst initialState = {\r\n cameras: [],\r\n userSelectedCamera: null,\r\n};\r\n\r\nexport default function camerasReducer(state = initialState, action) {\r\n const { type, payload } = action;\r\n\r\n switch (type) {\r\n case SET_CAMERAS: {\r\n return {\r\n ...state,\r\n cameras: payload,\r\n };\r\n }\r\n\r\n case SET_USER_SELECTED_CAMERA: {\r\n return {\r\n ...state,\r\n userSelectedCamera: payload,\r\n };\r\n }\r\n\r\n default:\r\n return state;\r\n }\r\n}\r\n\r\n// returns the cameraObject by passing selectedCameraId (for CameraDropdown.js)\r\nexport const getCameraObject = (cameras, selectedCameraId) =>\r\n cameras.find(({ cameraId }) => cameraId === selectedCameraId);\r\n"],"sourceRoot":""}