diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..fa90a744 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,27 @@ +{ + "bracketSpacing": true, + "printWidth": 80, + "proseWrap": "preserve", + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "tabWidth": 4, + "useTabs": true, + "arrowParens": "always", + "endOfLine": "lf", + "overrides": [ + { + "files": "*.json", + "options": { + "singleQuote": false + } + }, + { + "files": ".*rc", + "options": { + "singleQuote": false, + "parser": "json" + } + } + ] +} \ No newline at end of file diff --git a/src/App.css b/src/App.css index 1697733b..76c915de 100644 --- a/src/App.css +++ b/src/App.css @@ -1,100 +1,100 @@ :root { - --navbar-height: 70px; - --navbar-height: calc(56px + max(env(safe-area-inset-bottom), 14px)); + --navbar-height: 70px; + --navbar-height: calc(56px + max(env(safe-area-inset-bottom), 14px)); } html, body, #root, .App { - height: 100%; + height: 100%; } html, body { - background-color: #2c2f33; - margin: 0; - padding: 0; - font-size: 14px; - font-family: - "Inter", - -apple-system, - BlinkMacSystemFont, - "Segoe UI", - "Roboto", - "Oxygen", - "Ubuntu", - "Cantarell", - "Fira Sans", - "Droid Sans", - "Helvetica Neue", - sans-serif; - text-rendering: optimizeLegibility; - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - -webkit-text-size-adjust: 100%; + background-color: #2c2f33; + margin: 0; + padding: 0; + font-size: 14px; + font-family: + 'Inter', + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + 'Roboto', + 'Oxygen', + 'Ubuntu', + 'Cantarell', + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + text-rendering: optimizeLegibility; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + -webkit-text-size-adjust: 100%; } .App { - text-align: left; - background-color: #2c2f33; - display: flex; - flex-direction: column; + text-align: left; + background-color: #2c2f33; + display: flex; + flex-direction: column; } .App-logo { - height: 40vmin; - pointer-events: none; + height: 40vmin; + pointer-events: none; } .MainContent { - overflow: auto; - flex: 1; + overflow: auto; + flex: 1; } @media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } + .App-logo { + animation: App-logo-spin infinite 20s linear; + } } .App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; } .App-link { - color: #61dafb; + color: #61dafb; } .Container { - padding: 1.5rem 3rem 0; - flex-grow: 1; + padding: 1.5rem 3rem 0; + flex-grow: 1; } .footer { - margin-top: 80px; - background-color: #23272a; - padding: 40px; - min-height: 130px; + margin-top: 80px; + background-color: #23272a; + padding: 40px; + min-height: 130px; } .MuiCard-root { - display: flex; - flex-direction: column; - justify-content: space-between; + display: flex; + flex-direction: column; + justify-content: space-between; } .announcement { - padding: 1rem; - font-size: 1.2em; - color: white; - text-align: center; - background-color: #23272a; + padding: 1rem; + font-size: 1.2em; + color: white; + text-align: center; + background-color: #23272a; } diff --git a/src/App.tsx b/src/App.tsx index a914dc18..6d833004 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,89 +1,95 @@ -import React, { useEffect, useState } from "react"; -import { BrowserRouter, Route, Routes } from "react-router-dom"; -import { DateTime } from "luxon"; +import React, { useEffect, useState } from 'react'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import { DateTime } from 'luxon'; -import Navbar from "./components/Navbar"; -import ListPage from "./pages/ListPage"; -import MapPage from "./pages/MapPage"; -import NotFoundPage from "./pages/NotFoundPage"; -import { queryLocations, getLocationStatus } from "./util/queryLocations"; -import "./App.css"; +import Navbar from './components/Navbar'; +import ListPage from './pages/ListPage'; +import MapPage from './pages/MapPage'; +import NotFoundPage from './pages/NotFoundPage'; +import { queryLocations, getLocationStatus } from './util/queryLocations'; +import './App.css'; import { - IReadOnlyExtendedLocation, - IReadOnlyLocation, -} from "./types/locationTypes"; + IReadOnlyExtendedLocation, + IReadOnlyLocation, +} from './types/locationTypes'; -const CMU_EATS_API_URL = "https://dining.apis.scottylabs.org/locations"; +const CMU_EATS_API_URL = 'https://dining.apis.scottylabs.org/locations'; // const CMU_EATS_API_URL = 'http://localhost:5173/example-response.json'; // for debugging purposes (note that you need an example-response.json file in the /public folder) // const CMU_EATS_API_URL = 'http://localhost:5010/locations'; // for debugging purposes (note that you need an example-response.json file in the /public folder) function App() { - // Load locations - const [locations, setLocations] = useState(); - const [extendedLocationData, setExtendedLocationData] = - useState(); - useEffect(() => { - queryLocations(CMU_EATS_API_URL).then((parsedLocations) => { - setLocations(parsedLocations); - }); - }, []); + // Load locations + const [locations, setLocations] = useState(); + const [extendedLocationData, setExtendedLocationData] = + useState(); + useEffect(() => { + queryLocations(CMU_EATS_API_URL).then((parsedLocations) => { + setLocations(parsedLocations); + }); + }, []); - useEffect(() => { - const intervalId = setInterval( - (function updateExtendedLocationData() { - if (locations !== undefined) { - // Remove .setZone('America/New_York') and change time in computer settings when testing - // Alternatively, simply set now = DateTime.local(2023, 12, 22, 18, 33); where the parameters are Y,M,D,H,M - const now = DateTime.now().setZone("America/New_York"); - setExtendedLocationData( - locations.map((location) => ({ - ...location, - ...getLocationStatus(location.times, now), // populate location with more detailed info relevant to current time - })), - ); - } - return updateExtendedLocationData; // returns itself here - })(), // self-invoking function - 1 * 1000, // updates every second - ); - return () => clearInterval(intervalId); - }, [locations]); + useEffect(() => { + const intervalId = setInterval( + (function updateExtendedLocationData() { + if (locations !== undefined) { + // Remove .setZone('America/New_York') and change time in computer settings when testing + // Alternatively, simply set now = DateTime.local(2023, 12, 22, 18, 33); where the parameters are Y,M,D,H,M + const now = DateTime.now().setZone('America/New_York'); + setExtendedLocationData( + locations.map((location) => ({ + ...location, + ...getLocationStatus(location.times, now), // populate location with more detailed info relevant to current time + })), + ); + } + return updateExtendedLocationData; // returns itself here + })(), // self-invoking function + 1 * 1000, // updates every second + ); + return () => clearInterval(intervalId); + }, [locations]); - // Auto-refresh the page when the user goes online after previously being offline - useEffect(() => { - function handleOnline() { - if (navigator.onLine) { - // Refresh the page - window.location.reload(); - } - } + // Auto-refresh the page when the user goes online after previously being offline + useEffect(() => { + function handleOnline() { + if (navigator.onLine) { + // Refresh the page + window.location.reload(); + } + } - window.addEventListener("online", handleOnline); + window.addEventListener('online', handleOnline); - return () => window.removeEventListener("online", handleOnline); - }, []); + return () => window.removeEventListener('online', handleOnline); + }, []); - return ( - - -
-
- - } - /> - } - /> - } /> - -
- -
-
-
- ); + return ( + + +
+
+ + + } + /> + + } + /> + } /> + +
+ +
+
+
+ ); } export default App; diff --git a/src/components/EateryCard.tsx b/src/components/EateryCard.tsx index 58867d0c..ec0beb6e 100644 --- a/src/components/EateryCard.tsx +++ b/src/components/EateryCard.tsx @@ -1,281 +1,318 @@ -import { useState } from "react"; +import { useState } from 'react'; import { - Card, - CardHeader, - Typography, - Link, - styled, - Grid, - Button, - Accordion, - AccordionSummary, - AccordionDetails, - CardContent, - CardActions, - Avatar, - Dialog, -} from "@mui/material"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; + Card, + CardHeader, + Typography, + Link, + styled, + Grid, + Button, + Accordion, + AccordionSummary, + AccordionDetails, + CardContent, + CardActions, + Avatar, + Dialog, +} from '@mui/material'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import TextProps from "../types/interfaces"; +import TextProps from '../types/interfaces'; import { - IReadOnlyExtendedLocation, - LocationState, -} from "../types/locationTypes"; + IReadOnlyExtendedLocation, + LocationState, +} from '../types/locationTypes'; const colors: Record = { - [LocationState.OPEN]: "#19b875", - [LocationState.CLOSED]: "#dd3c18", - [LocationState.CLOSED_LONG_TERM]: "#dd3c18", - [LocationState.OPENS_SOON]: "#f6cc5d", - [LocationState.CLOSES_SOON]: "#f3f65d", + [LocationState.OPEN]: '#19b875', + [LocationState.CLOSED]: '#dd3c18', + [LocationState.CLOSED_LONG_TERM]: '#dd3c18', + [LocationState.OPENS_SOON]: '#f6cc5d', + [LocationState.CLOSES_SOON]: '#f3f65d', }; const StyledCard = styled(Card)({ - backgroundColor: "#23272A", - border: "2px solid rgba(0, 0, 0, 0.2)", - textAlign: "left", - borderRadius: 7, - height: "100%", - justifyContent: "flex-start", + backgroundColor: '#23272A', + border: '2px solid rgba(0, 0, 0, 0.2)', + textAlign: 'left', + borderRadius: 7, + height: '100%', + justifyContent: 'flex-start', }); const StyledCardHeader = styled(CardHeader)({ - fontWeight: 500, - backgroundColor: "#1D1F21", + fontWeight: 500, + backgroundColor: '#1D1F21', }); const CustomLink = styled(Link)({ - color: "white", - textDecoration: "underline", + color: 'white', + textDecoration: 'underline', }); const NameText = styled(Typography)({ - color: "white", - padding: 0, - fontFamily: - '"Zilla Slab", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", ' + - '"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", ' + - '"Droid Sans", "Helvetica Neue", sans-serif', - textTransform: "capitalize", + color: 'white', + padding: 0, + fontFamily: + '"Zilla Slab", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", ' + + '"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", ' + + '"Droid Sans", "Helvetica Neue", sans-serif', + textTransform: 'capitalize', }); const LocationText = styled(Typography)({ - color: "#8D979F", - marginBottom: "10px", - fontWeight: 500, - fontSize: 14, + color: '#8D979F', + marginBottom: '10px', + fontWeight: 500, + fontSize: 14, }); const DescriptionText = styled(Typography)({ - color: "white", + color: 'white', }); const OpenText = styled(Typography)(({ changesSoon }) => ({ - color: changesSoon - ? colors[LocationState.CLOSES_SOON] - : colors[LocationState.OPEN], - fontSize: 14, - fontWeight: 500, - fontFamily: - '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", ' + - '"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", ' + - '"Helvetica Neue", sans-serif', + color: changesSoon + ? colors[LocationState.CLOSES_SOON] + : colors[LocationState.OPEN], + fontSize: 14, + fontWeight: 500, + fontFamily: + '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", ' + + '"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", ' + + '"Helvetica Neue", sans-serif', })); const ClosedText = styled(Typography)(({ changesSoon }) => ({ - color: changesSoon - ? colors[LocationState.OPENS_SOON] - : colors[LocationState.CLOSED], - fontSize: 14, - fontWeight: 500, - fontFamily: - '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", ' + - '"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", ' + - '"Helvetica Neue", sans-serif', + color: changesSoon + ? colors[LocationState.OPENS_SOON] + : colors[LocationState.CLOSED], + fontSize: 14, + fontWeight: 500, + fontFamily: + '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", ' + + '"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", ' + + '"Helvetica Neue", sans-serif', })); const ActionButton = styled(Button)({ - fontWeight: 600, - fontFamily: - '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", ' + - '"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", ' + - '"Helvetica Neue", sans-serif', - color: "white", - backgroundColor: "#1D1F21", - elevation: 30, + fontWeight: 600, + fontFamily: + '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", ' + + '"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", ' + + '"Helvetica Neue", sans-serif', + color: 'white', + backgroundColor: '#1D1F21', + elevation: 30, }); const blinkingAnimation = { - "@keyframes blinking": { - "0%": { - opacity: 0, - }, - "50%": { - opacity: 1, - }, - "75%": { - opacity: 1, - }, - "100%": { - opacity: 0, - }, - }, + '@keyframes blinking': { + '0%': { + opacity: 0, + }, + '50%': { + opacity: 1, + }, + '75%': { + opacity: 1, + }, + '100%': { + opacity: 0, + }, + }, }; const Dot = styled(Card)( - ({ state, changesSoon }: { state: LocationState; changesSoon: boolean }) => ({ - background: colors[state], - width: "100%", - height: "100%", - borderRadius: "50%", - foregroundColor: colors[state], - ...(changesSoon && blinkingAnimation), - animationName: changesSoon ? "blinking" : undefined, - animationDuration: "1s", - animationIterationCount: "infinite", - }), + ({ + state, + changesSoon, + }: { + state: LocationState; + changesSoon: boolean; + }) => ({ + background: colors[state], + width: '100%', + height: '100%', + borderRadius: '50%', + foregroundColor: colors[state], + ...(changesSoon && blinkingAnimation), + animationName: changesSoon ? 'blinking' : undefined, + animationDuration: '1s', + animationIterationCount: 'infinite', + }), ); const SpecialsContent = styled(Accordion)({ - backgroundColor: "#23272A", + backgroundColor: '#23272A', }); function EateryCard({ location }: { location: IReadOnlyExtendedLocation }) { - const { - name, - location: locationText, - url, - shortDescription, - menu, - todaysSpecials = [], - statusMsg, - todaysSoups = [], - } = location; - const changesSoon = !location.closedLongTerm && location.changesSoon; - const isOpen = !location.closedLongTerm && location.isOpen; + const { + name, + location: locationText, + url, + shortDescription, + menu, + todaysSpecials = [], + statusMsg, + todaysSoups = [], + } = location; + const changesSoon = !location.closedLongTerm && location.changesSoon; + const isOpen = !location.closedLongTerm && location.isOpen; - const [modalOpen, setModalOpen] = useState(false); + const [modalOpen, setModalOpen] = useState(false); - return ( - <> - - - - {statusMsg} - - ) : ( - - {statusMsg} - - ) - } - avatar={ - - - - } - /> - - - - {name} - - - {locationText} - {shortDescription} - - - {menu && ( - { - window.open(menu, "_blank"); - }} - > - Menu - - )} - {(todaysSpecials.length !== 0 || todaysSoups.length !== 0) && ( - { - setModalOpen(true); - }} - > - Specials - - )} - - - + return ( + <> + + + + {statusMsg} + + ) : ( + + {statusMsg} + + ) + } + avatar={ + + + + } + /> + + + + {name} + + + + {locationText} + + {shortDescription} + + + {menu && ( + { + window.open(menu, '_blank'); + }} + > + Menu + + )} + {(todaysSpecials.length !== 0 || + todaysSoups.length !== 0) && ( + { + setModalOpen(true); + }} + > + Specials + + )} + + + - { - setModalOpen(false); - }} - PaperProps={{ - style: { - backgroundColor: "#23272A", - }, - }} - > - - - {statusMsg} - - ) : ( - - {statusMsg} - - ) - } - avatar={ - - - - } - /> - - - {name} - - {locationText} - - {todaysSpecials.concat(todaysSoups).map((special) => ( - - } - aria-controls="panel1a-content" - id="panel1a-header" - > - {special.title} - - - {special.description} - - - ))} - - - - ); + { + setModalOpen(false); + }} + PaperProps={{ + style: { + backgroundColor: '#23272A', + }, + }} + > + + + {statusMsg} + + ) : ( + + {statusMsg} + + ) + } + avatar={ + + + + } + /> + + + {name} + + + {locationText} + + + {todaysSpecials.concat(todaysSoups).map((special) => ( + + + } + aria-controls="panel1a-content" + id="panel1a-header" + > + + {special.title} + + + + + {special.description} + + + + ))} + + + + ); } export default EateryCard; diff --git a/src/components/Navbar.css b/src/components/Navbar.css index 5f026bc8..9869b447 100644 --- a/src/components/Navbar.css +++ b/src/components/Navbar.css @@ -1,60 +1,60 @@ .Navbar { - height: var(--navbar-height); - background-color: #1e1e1e; - padding: 14px; - box-sizing: border-box; - border-top: 2px solid #31373e; + height: var(--navbar-height); + background-color: #1e1e1e; + padding: 14px; + box-sizing: border-box; + border-top: 2px solid #31373e; } .Navbar-links { - position: relative; - display: grid; - grid-auto-columns: 1fr; - grid-auto-flow: column; - height: 40px; - margin: 0 auto; - max-width: 500px; + position: relative; + display: grid; + grid-auto-columns: 1fr; + grid-auto-flow: column; + height: 40px; + margin: 0 auto; + max-width: 500px; } .Navbar-links a { - position: relative; - z-index: 5; - - display: flex; - align-items: center; - justify-content: center; - - font-family: "Zilla Slab", sans-serif; - font-weight: 500; - color: white; - font-size: 16px; - text-decoration: none; + position: relative; + z-index: 5; + + display: flex; + align-items: center; + justify-content: center; + + font-family: 'Zilla Slab', sans-serif; + font-weight: 500; + color: white; + font-size: 16px; + text-decoration: none; } .Navbar-links svg { - width: 24px; - height: 24px; - margin-right: 0.4em; + width: 24px; + height: 24px; + margin-right: 0.4em; } .Navbar-active { - position: absolute; - z-index: 2; - left: 0; - top: 0; - width: 50%; - height: 100%; - - background-color: #2b2f33; - border-radius: 999px; + position: absolute; + z-index: 2; + left: 0; + top: 0; + width: 50%; + height: 100%; + + background-color: #2b2f33; + border-radius: 999px; } @media (prefers-reduced-motion: no-preference) { - .Navbar-active { - transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1) transform; - } + .Navbar-active { + transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1) transform; + } } .Navbar-active_map { - transform: translateX(100%); + transform: translateX(100%); } diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index c0284d22..218fe4b6 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,55 +1,57 @@ -import { Link, useLocation } from "react-router-dom"; -import "./Navbar.css"; +import { Link, useLocation } from 'react-router-dom'; +import './Navbar.css'; function Navbar() { - const isMap = useLocation().pathname === "/map"; + const isMap = useLocation().pathname === '/map'; - return ( - - ); + /> + + Map + +
+
+ + ); } export default Navbar; diff --git a/src/components/NoResultsError.tsx b/src/components/NoResultsError.tsx index 9ace35f6..a35623df 100644 --- a/src/components/NoResultsError.tsx +++ b/src/components/NoResultsError.tsx @@ -1,16 +1,17 @@ -import { Box } from "@mui/material"; -import { ErrorButton, ErrorText, ErrorTitle } from "../style"; +import { Box } from '@mui/material'; +import { ErrorButton, ErrorText, ErrorTitle } from '../style'; function EateryCard({ onClear }: { onClear: () => unknown }) { - return ( - - No results found - - Try searching for a name (e.g. “Schatz”) or location (e.g. “Cohon”). - - Clear search - - ); + return ( + + No results found + + Try searching for a name (e.g. “Schatz”) or location (e.g. + “Cohon”). + + Clear search + + ); } export default EateryCard; diff --git a/src/index.css b/src/index.css index 4a1df4db..279fbeaf 100644 --- a/src/index.css +++ b/src/index.css @@ -1,13 +1,13 @@ body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', + 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', + 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", - monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; } diff --git a/src/index.tsx b/src/index.tsx index 8232f758..1828353d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,15 +1,15 @@ -import React from "react"; -import { createRoot } from "react-dom/client"; -import "./index.css"; +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import './index.css'; -import App from "./App"; +import App from './App'; -const rootElement = document.getElementById("root"); +const rootElement = document.getElementById('root'); if (rootElement) { - createRoot(rootElement).render( - - - , - ); + createRoot(rootElement).render( + + + , + ); } diff --git a/src/pages/ListPage.css b/src/pages/ListPage.css index c8bdd881..dbd64618 100644 --- a/src/pages/ListPage.css +++ b/src/pages/ListPage.css @@ -1,59 +1,59 @@ .ListPage { - display: flex; - flex-direction: column; - min-height: 100%; /* So footer (the contact info one) actually stays at the bottom */ + display: flex; + flex-direction: column; + min-height: 100%; /* So footer (the contact info one) actually stays at the bottom */ } .badge-accent { - color: #105c03; - background: #19b875; - padding: 10px; - border-radius: 30; + color: #105c03; + background: #19b875; + padding: 10px; + border-radius: 30; } .Locations-header { - display: grid; - grid-gap: 1rem; - padding: 3rem 0; + display: grid; + grid-gap: 1rem; + padding: 3rem 0; } @media screen and (min-width: 900px) { - .Locations-header { - grid-template-columns: 1fr 300px; - align-items: center; - } + .Locations-header { + grid-template-columns: 1fr 300px; + align-items: center; + } } .Locations-search { - display: block; - width: 100%; - padding: 0.8rem 1rem; - padding-left: 3rem; - border-radius: 1rem; - background: #23272a; - - outline: none; - border: none; - box-shadow: 0 0 0 2px rgba(255, 255, 255, 0); - transition: 0.1s ease-in-out box-shadow; - - font-family: inherit; - font-size: 1.2rem; - - /* Heroicons v2.0.12 by Refactoring UI Inc., used under MIT license */ - background-image: url("data: image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='rgba(255, 255, 255, .6)' class='w-5 h-5' %3E%3Cpath fill-rule='evenodd' d='M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z' clip-rule='evenodd' /%3E%3C/svg%3E"); - background-size: 20px; - background-repeat: no-repeat; - background-position: 1rem center; - - color: white; - font-weight: 500; + display: block; + width: 100%; + padding: 0.8rem 1rem; + padding-left: 3rem; + border-radius: 1rem; + background: #23272a; + + outline: none; + border: none; + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0); + transition: 0.1s ease-in-out box-shadow; + + font-family: inherit; + font-size: 1.2rem; + + /* Heroicons v2.0.12 by Refactoring UI Inc., used under MIT license */ + background-image: url("data: image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='rgba(255, 255, 255, .6)' class='w-5 h-5' %3E%3Cpath fill-rule='evenodd' d='M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z' clip-rule='evenodd' /%3E%3C/svg%3E"); + background-size: 20px; + background-repeat: no-repeat; + background-position: 1rem center; + + color: white; + font-weight: 500; } .Locations-search::-webkit-search-decoration { - display: none; + display: none; } .Locations-search:focus { - box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.4); + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.4); } diff --git a/src/pages/ListPage.tsx b/src/pages/ListPage.tsx index 2d65c7d4..c3708d8e 100644 --- a/src/pages/ListPage.tsx +++ b/src/pages/ListPage.tsx @@ -1,238 +1,267 @@ -import { Typography, Grid, Alert, styled } from "@mui/material"; -import { useEffect, useMemo, useState, useLayoutEffect } from "react"; -import Fuse from "fuse.js"; -import EateryCard from "../components/EateryCard"; -import NoResultsError from "../components/NoResultsError"; -import getGreeting from "../util/greeting"; -import "./ListPage.css"; +import { Typography, Grid, Alert, styled } from '@mui/material'; +import { useEffect, useMemo, useState, useLayoutEffect } from 'react'; +import Fuse from 'fuse.js'; +import EateryCard from '../components/EateryCard'; +import NoResultsError from '../components/NoResultsError'; +import getGreeting from '../util/greeting'; +import './ListPage.css'; import { - IReadOnlyExtendedLocation, - LocationState, -} from "../types/locationTypes"; -import assert from "../util/assert"; + IReadOnlyExtendedLocation, + LocationState, +} from '../types/locationTypes'; +import assert from '../util/assert'; // Typography const HeaderText = styled(Typography)({ - color: "white", - padding: 0, - fontFamily: - '"Zilla Slab", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", ' + - '"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", ' + - '"Droid Sans", "Helvetica Neue", sans-serif', - fontWeight: 800, - fontSize: "3em", + color: 'white', + padding: 0, + fontFamily: + '"Zilla Slab", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", ' + + '"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", ' + + '"Droid Sans", "Helvetica Neue", sans-serif', + fontWeight: 800, + fontSize: '3em', }); const ErrorText = styled(Typography)({ - color: "white", - padding: 0, - fontFamily: - '"Zilla Slab", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", ' + - '"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", ' + - '"Droid Sans", "Helvetica Neue", sans-serif', + color: 'white', + padding: 0, + fontFamily: + '"Zilla Slab", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", ' + + '"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", ' + + '"Droid Sans", "Helvetica Neue", sans-serif', }); const LogoText = styled(Typography)({ - color: "#dd3c18", - padding: 0, - fontFamily: - '"Zilla Slab", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", ' + - '"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", ' + - '"Droid Sans", "Helvetica Neue", sans-serif', - fontWeight: 800, + color: '#dd3c18', + padding: 0, + fontFamily: + '"Zilla Slab", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", ' + + '"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", ' + + '"Droid Sans", "Helvetica Neue", sans-serif', + fontWeight: 800, }); const FooterText = styled(Typography)({ - color: "white", - marginBottom: 20, - fontSize: 16, + color: 'white', + marginBottom: 20, + fontSize: 16, }); const StyledAlert = styled(Alert)({ - backgroundColor: "#23272a", - color: "#ffffff", + backgroundColor: '#23272a', + color: '#ffffff', }); function ListPage({ - locations, + locations, }: { - locations: IReadOnlyExtendedLocation[] | undefined; + locations: IReadOnlyExtendedLocation[] | undefined; }) { - const greeting = useMemo(() => getGreeting(new Date().getHours()), []); - - // Fuzzy search options - const fuseOptions = { - // keys to perform the search on - keys: ["name", "location", "shortDescription"], - threshold: 0.3, - }; - - const [fuse, setFuse] = useState | null>( - null, - ); - - // Search query processing - const [searchQuery, setSearchQuery] = useState(""); - - const [filteredLocations, setFilteredLocations] = useState< - IReadOnlyExtendedLocation[] - >([]); - - useEffect(() => { - if (locations) { - const fuseInstance = new Fuse(locations, fuseOptions); - setFuse(fuseInstance); - } - }, [locations]); - - useLayoutEffect(() => { - if (locations === undefined || fuse === null) return; - const processedSearchQuery = searchQuery.trim().toLowerCase(); - - // Fuzzy search. If there's no search query, it returns all locations. - setFilteredLocations( - processedSearchQuery.length === 0 - ? locations - : fuse.search(processedSearchQuery).map((result) => result.item), - ); - }, [searchQuery, fuse, locations]); - - // const [showAlert, setShowAlert] = useState(true); - const [showOfflineAlert, setShowOfflineAlert] = useState(!navigator.onLine); - - // Load the search query from the URL, if any - useEffect(() => { - const urlQuery = new URLSearchParams(window.location.search).get("search"); - if (urlQuery) { - setSearchQuery(urlQuery); - } - }, []); - - // Monitor for the user being online - useEffect(() => { - const handleOnlineStatus = () => { - setShowOfflineAlert(!navigator.onLine); - }; - - window.addEventListener("online", handleOnlineStatus); - window.addEventListener("offline", handleOnlineStatus); - - return () => { - window.removeEventListener("online", handleOnlineStatus); - window.removeEventListener("offline", handleOnlineStatus); - }; - }, []); - - return ( -
- {/* showAlert && + const greeting = useMemo(() => getGreeting(new Date().getHours()), []); + + // Fuzzy search options + const fuseOptions = { + // keys to perform the search on + keys: ['name', 'location', 'shortDescription'], + threshold: 0.3, + }; + + const [fuse, setFuse] = useState | null>( + null, + ); + + // Search query processing + const [searchQuery, setSearchQuery] = useState(''); + + const [filteredLocations, setFilteredLocations] = useState< + IReadOnlyExtendedLocation[] + >([]); + + useEffect(() => { + if (locations) { + const fuseInstance = new Fuse(locations, fuseOptions); + setFuse(fuseInstance); + } + }, [locations]); + + useLayoutEffect(() => { + if (locations === undefined || fuse === null) return; + const processedSearchQuery = searchQuery.trim().toLowerCase(); + + // Fuzzy search. If there's no search query, it returns all locations. + setFilteredLocations( + processedSearchQuery.length === 0 + ? locations + : fuse + .search(processedSearchQuery) + .map((result) => result.item), + ); + }, [searchQuery, fuse, locations]); + + // const [showAlert, setShowAlert] = useState(true); + const [showOfflineAlert, setShowOfflineAlert] = useState(!navigator.onLine); + + // Load the search query from the URL, if any + useEffect(() => { + const urlQuery = new URLSearchParams(window.location.search).get( + 'search', + ); + if (urlQuery) { + setSearchQuery(urlQuery); + } + }, []); + + // Monitor for the user being online + useEffect(() => { + const handleOnlineStatus = () => { + setShowOfflineAlert(!navigator.onLine); + }; + + window.addEventListener('online', handleOnlineStatus); + window.addEventListener('offline', handleOnlineStatus); + + return () => { + window.removeEventListener('online', handleOnlineStatus); + window.removeEventListener('offline', handleOnlineStatus); + }; + }, []); + + return ( +
+ {/* showAlert && setShowAlert(false)}> 🚧 [Issue Description] Please remain patient while we work on a fix. Thank you. 🚧 */} - {showOfflineAlert && ( - setShowOfflineAlert(false)} - > - 🚫🌐 We are temporarily unable to provide the latest available dining - information or the map while you are offline. We apologize for any - inconvenience. 🌐🚫 - - )} -
-
- - {locations === undefined ? "Loading..." : greeting} - - setSearchQuery(e.target.value)} - placeholder="Search" - /> -
- {(() => { - if (locations === undefined) return undefined; // still loading - if (locations.length === 0) - return ( - - Oops! We received an invalid API response (or no data at all). - If this problem persists, please let us know. - - ); - if (filteredLocations.length === 0) - return setSearchQuery("")} />; - return ( - - {[...filteredLocations] - .sort((location1, location2) => { - const state1 = location1.locationState; - const state2 = location2.locationState; - if (state1 !== state2) return state1 - state2; - // this if statement is janky but otherwise TS won't - // realize that the timeUntil property exists on both l1 and l2 - if (location1.closedLongTerm || location2.closedLongTerm) { - assert( - location1.closedLongTerm && location2.closedLongTerm, - ); - return location1.name.localeCompare(location2.name); - } - // flip sorting order if locations are both open or opening soon - return ( - (state1 === LocationState.OPEN || - state1 === LocationState.OPENS_SOON - ? -1 - : 1) * - (location1.timeUntil - location2.timeUntil) - ); - }) - .map((location) => ( - - ))} - - ); - })()} -
-
- - All times displayed in Pittsburgh local time (ET). - - {/* eslint-disable */} - - Contact{" "} - - Jaisal - - ,{" "} - - Josef - - , or{" "} - - Aaron - {" "} - with any problems. - - - Made with 🩷 by{" "} - - ScottyLabs - - . - - {/* eslint-enable */} - - cmu:eats - -
-
- ); + {showOfflineAlert && ( + setShowOfflineAlert(false)} + > + 🚫🌐 We are temporarily unable to provide the latest + available dining information or the map while you are + offline. We apologize for any inconvenience. 🌐🚫 + + )} +
+
+ + {locations === undefined ? 'Loading...' : greeting} + + setSearchQuery(e.target.value)} + placeholder="Search" + /> +
+ {(() => { + if (locations === undefined) return undefined; // still loading + if (locations.length === 0) + return ( + + Oops! We received an invalid API response (or no + data at all). If this problem persists, please + let us know. + + ); + if (filteredLocations.length === 0) + return ( + setSearchQuery('')} + /> + ); + return ( + + {[...filteredLocations] + .sort((location1, location2) => { + const state1 = location1.locationState; + const state2 = location2.locationState; + if (state1 !== state2) + return state1 - state2; + // this if statement is janky but otherwise TS won't + // realize that the timeUntil property exists on both l1 and l2 + if ( + location1.closedLongTerm || + location2.closedLongTerm + ) { + assert( + location1.closedLongTerm && + location2.closedLongTerm, + ); + return location1.name.localeCompare( + location2.name, + ); + } + // flip sorting order if locations are both open or opening soon + return ( + (state1 === LocationState.OPEN || + state1 === LocationState.OPENS_SOON + ? -1 + : 1) * + (location1.timeUntil - + location2.timeUntil) + ); + }) + .map((location) => ( + + ))} + + ); + })()} +
+
+ + All times displayed in Pittsburgh local time (ET). + + {/* eslint-disable */} + + Contact{' '} + + Jaisal + + ,{' '} + + Josef + + , or{' '} + + Aaron + {' '} + with any problems. + + + Made with 🩷 by{' '} + + ScottyLabs + + . + + {/* eslint-enable */} + + cmu:eats + +
+
+ ); } export default ListPage; diff --git a/src/pages/MapPage.css b/src/pages/MapPage.css index 3fe3bf21..e1460a6b 100644 --- a/src/pages/MapPage.css +++ b/src/pages/MapPage.css @@ -1,33 +1,33 @@ .MapPage { - background-color: #2c2d2f; - display: grid; - grid-template-rows: 1fr auto; - height: 100%; + background-color: #2c2d2f; + display: grid; + grid-template-rows: 1fr auto; + height: 100%; } .MapDrawer { - padding: 14px; - background: #2d2f33; - border-radius: 14px 14px 0 0; - overflow: hidden; - box-sizing: content-box; + padding: 14px; + background: #2d2f33; + border-radius: 14px 14px 0 0; + overflow: hidden; + box-sizing: content-box; } @media (prefers-reduced-motion: no-preference) { - .MapDrawer { - transition: max-height ease-in 300ms; - } + .MapDrawer { + transition: max-height ease-in 300ms; + } } .DrawerTransition-exit { - max-height: 300px; + max-height: 300px; } .DrawerTransition-enter, .DrawerTransition-exit-active { - max-height: 0; + max-height: 0; } .DrawerTransition-enter-active { - max-height: 300px; + max-height: 300px; } diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx index aff7e040..d8515d2a 100644 --- a/src/pages/MapPage.tsx +++ b/src/pages/MapPage.tsx @@ -1,111 +1,114 @@ -import { useMemo, useState, useRef } from "react"; +import { useMemo, useState, useRef } from 'react'; import { - Map, - Marker, - ColorScheme, - PointOfInterestCategory, -} from "mapkit-react"; -import { CSSTransition } from "react-transition-group"; -import EateryCard from "../components/EateryCard"; -import "./MapPage.css"; -import { IReadOnlyExtendedLocation } from "../types/locationTypes"; + Map, + Marker, + ColorScheme, + PointOfInterestCategory, +} from 'mapkit-react'; +import { CSSTransition } from 'react-transition-group'; +import EateryCard from '../components/EateryCard'; +import './MapPage.css'; +import { IReadOnlyExtendedLocation } from '../types/locationTypes'; const token = process.env.VITE_MAPKITJS_TOKEN; function abbreviate(longName: string) { - const importantPart = longName.split(/(-|\(|'|&| at )/i)[0].trim(); - return importantPart - .split(" ") - .map((word) => word.charAt(0)) - .join(""); + const importantPart = longName.split(/(-|\(|'|&| at )/i)[0].trim(); + return importantPart + .split(' ') + .map((word) => word.charAt(0)) + .join(''); } function MapPage({ - locations, + locations, }: { - locations: IReadOnlyExtendedLocation[] | undefined; + locations: IReadOnlyExtendedLocation[] | undefined; }) { - const [selectedLocationIndex, setSelectedLocationIndex] = useState(); - const [isDrawerVisible, setDrawerVisible] = useState(false); - const drawerRef = useRef(null); + const [selectedLocationIndex, setSelectedLocationIndex] = + useState(); + const [isDrawerVisible, setDrawerVisible] = useState(false); + const drawerRef = useRef(null); - const cameraBoundary = useMemo( - () => ({ - centerLatitude: 40.444, - centerLongitude: -79.945, - latitudeDelta: 0.006, - longitudeDelta: 0.01, - }), - [], - ); + const cameraBoundary = useMemo( + () => ({ + centerLatitude: 40.444, + centerLongitude: -79.945, + latitudeDelta: 0.006, + longitudeDelta: 0.01, + }), + [], + ); - const initialRegion = useMemo( - () => ({ - centerLatitude: 40.44316701238923, - centerLongitude: -79.9431147637379, - latitudeDelta: 0.006337455593801167, - longitudeDelta: 0.011960061265583022, - }), - [], - ); - if (!locations) return undefined; + const initialRegion = useMemo( + () => ({ + centerLatitude: 40.44316701238923, + centerLongitude: -79.9431147637379, + latitudeDelta: 0.006337455593801167, + longitudeDelta: 0.011960061265583022, + }), + [], + ); + if (!locations) return undefined; - return ( -
- - {locations.map((location, locationIndex) => { - if (!location.coordinates) return undefined; - return ( - { - setSelectedLocationIndex(locationIndex); - setDrawerVisible(true); - }} - onDeselect={() => { - if (selectedLocationIndex === locationIndex) { - setDrawerVisible(false); - } - }} - /> - ); - })} - + return ( +
+ + {locations.map((location, locationIndex) => { + if (!location.coordinates) return undefined; + return ( + { + setSelectedLocationIndex(locationIndex); + setDrawerVisible(true); + }} + onDeselect={() => { + if (selectedLocationIndex === locationIndex) { + setDrawerVisible(false); + } + }} + /> + ); + })} + - -
- {selectedLocationIndex !== undefined && ( - - )} -
-
-
- ); + +
+ {selectedLocationIndex !== undefined && ( + + )} +
+
+
+ ); } export default MapPage; diff --git a/src/pages/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx index a7d50a92..ad765d19 100644 --- a/src/pages/NotFoundPage.tsx +++ b/src/pages/NotFoundPage.tsx @@ -1,23 +1,25 @@ -import { useNavigate } from "react-router-dom"; -import { Box } from "@mui/material"; -import { ErrorTitle, ErrorText, ErrorButton } from "../style"; +import { useNavigate } from 'react-router-dom'; +import { Box } from '@mui/material'; +import { ErrorTitle, ErrorText, ErrorButton } from '../style'; function NotFoundPage() { - const navigate = useNavigate(); + const navigate = useNavigate(); - return ( - - Oops! - We couldn’t find the page you are looking for. - { - navigate("/"); - }} - > - Home page - - - ); + return ( + + Oops! + + We couldn’t find the page you are looking for. + + { + navigate('/'); + }} + > + Home page + + + ); } export default NotFoundPage; diff --git a/src/setupTests.ts b/src/setupTests.ts index 1dd407a6..8f2609b7 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -2,4 +2,4 @@ // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom -import "@testing-library/jest-dom"; +import '@testing-library/jest-dom'; diff --git a/src/style.ts b/src/style.ts index 366addba..98e6d25e 100644 --- a/src/style.ts +++ b/src/style.ts @@ -1,31 +1,31 @@ -import { Button, styled, Typography } from "@mui/material"; +import { Button, styled, Typography } from '@mui/material'; const ErrorTitle = styled(Typography)({ - color: "white", - marginBottom: 12, - fontSize: 24, - fontFamily: - '"Zilla Slab", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", ' + - '"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", ' + - '"Droid Sans", "Helvetica Neue", sans-serif', - fontWeight: 600, + color: 'white', + marginBottom: 12, + fontSize: 24, + fontFamily: + '"Zilla Slab", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", ' + + '"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", ' + + '"Droid Sans", "Helvetica Neue", sans-serif', + fontWeight: 600, }); const ErrorText = styled(Typography)({ - color: "#d4d4d8", - marginBottom: 20, - fontSize: 16, + color: '#d4d4d8', + marginBottom: 20, + fontSize: 16, }); const ErrorButton = styled(Button)({ - fontWeight: 600, - fontFamily: - '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", ' + - '"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", ' + - '"Helvetica Neue", sans-serif', - color: "white", - backgroundColor: "#1D1F21", - elevation: 30, + fontWeight: 600, + fontFamily: + '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", ' + + '"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", ' + + '"Helvetica Neue", sans-serif', + color: 'white', + backgroundColor: '#1D1F21', + elevation: 30, }); export { ErrorTitle, ErrorText, ErrorButton }; diff --git a/src/types/interfaces.ts b/src/types/interfaces.ts index 8e5786fd..97b93e3c 100644 --- a/src/types/interfaces.ts +++ b/src/types/interfaces.ts @@ -1,5 +1,5 @@ export default interface TextProps { - variant: "subtitle1"; - changesSoon: boolean; - children: React.ReactNode; + variant: 'subtitle1'; + changesSoon: boolean; + children: React.ReactNode; } diff --git a/src/types/joiLocationTypes.ts b/src/types/joiLocationTypes.ts index 0786c995..bc387130 100644 --- a/src/types/joiLocationTypes.ts +++ b/src/types/joiLocationTypes.ts @@ -1,48 +1,48 @@ -import Joi from "joi"; -import { isValidTimeSlotArray } from "../util/time"; -import { IReadOnlyAPILocation } from "./locationTypes"; -import assert from "../util/assert"; +import Joi from 'joi'; +import { isValidTimeSlotArray } from '../util/time'; +import { IReadOnlyAPILocation } from './locationTypes'; +import assert from '../util/assert'; const { string, number, boolean } = Joi.types(); const ITimeSlotTimeJoiSchema = Joi.object({ - day: number.min(0).max(6).required(), - hour: number.min(0).max(23).required(), - minute: number.min(0).max(59).required(), + day: number.min(0).max(6).required(), + hour: number.min(0).max(23).required(), + minute: number.min(0).max(59).required(), }); const ITimeSlotJoiSchema = Joi.object({ - start: ITimeSlotTimeJoiSchema.required(), - end: ITimeSlotTimeJoiSchema.required(), + start: ITimeSlotTimeJoiSchema.required(), + end: ITimeSlotTimeJoiSchema.required(), }); const ISpecialJoiSchema = Joi.object({ - title: string.required(), - description: string, + title: string.required(), + description: string, }); // Note: Keys without .required() are optional by default export const ILocationAPIJoiSchema = Joi.object({ - conceptId: number.required(), - name: string, - shortDescription: string, - description: string.required(), - url: string.required(), - menu: string, - location: string.required(), - coordinates: { - lat: number.required(), - lng: number.required(), - }, - acceptsOnlineOrders: boolean.required(), - times: Joi.array() - .items(ITimeSlotJoiSchema) - .required() - .custom((val) => { - assert(isValidTimeSlotArray(val)); - return val; - }) - .message("Received invalid (probably improperly sorted) time slots!"), - todaysSpecials: Joi.array().items(ISpecialJoiSchema), - todaysSoups: Joi.array().items(ISpecialJoiSchema), + conceptId: number.required(), + name: string, + shortDescription: string, + description: string.required(), + url: string.required(), + menu: string, + location: string.required(), + coordinates: { + lat: number.required(), + lng: number.required(), + }, + acceptsOnlineOrders: boolean.required(), + times: Joi.array() + .items(ITimeSlotJoiSchema) + .required() + .custom((val) => { + assert(isValidTimeSlotArray(val)); + return val; + }) + .message('Received invalid (probably improperly sorted) time slots!'), + todaysSpecials: Joi.array().items(ISpecialJoiSchema), + todaysSoups: Joi.array().items(ISpecialJoiSchema), }); export const IAPIResponseJoiSchema = Joi.object<{ locations: any[] }>({ - locations: Joi.array().required(), + locations: Joi.array().required(), }); // shallow validation to make sure we have the locations field. That's it. diff --git a/src/types/locationTypes.ts b/src/types/locationTypes.ts index 2a6a6709..fbb54c73 100644 --- a/src/types/locationTypes.ts +++ b/src/types/locationTypes.ts @@ -1,21 +1,21 @@ /** Note that everything being exported here is readonly */ export type RecursiveReadonly = T extends object - ? { - readonly [P in keyof T]: RecursiveReadonly; - } - : T; + ? { + readonly [P in keyof T]: RecursiveReadonly; + } + : T; /** * Describes either start or end time in any given ITimeSlot */ export interface ITimeSlotTime { - /** 0-6 (0 is Sunday, 6 is Saturday) */ - readonly day: number; - /** 0-23 - 0 means 12AM */ - readonly hour: number; - /** 0-59 */ - readonly minute: number; + /** 0-6 (0 is Sunday, 6 is Saturday) */ + readonly day: number; + /** 0-23 - 0 means 12AM */ + readonly hour: number; + /** 0-59 */ + readonly minute: number; } /** @@ -33,8 +33,8 @@ export interface ITimeSlotTime { * through the commit history of the Dining API) */ export interface ITimeSlot { - readonly start: ITimeSlotTime; - readonly end: ITimeSlotTime; + readonly start: ITimeSlotTime; + readonly end: ITimeSlotTime; } /** @@ -45,17 +45,17 @@ export interface ITimeSlot { export type ITimeSlots = ReadonlyArray; interface ISpecial { - title: string; - description?: string; + title: string; + description?: string; } // Ordered by priority - affects how tiles are displayed in the grid (first to last) export enum LocationState { - OPEN, - CLOSES_SOON, - OPENS_SOON, - CLOSED, - CLOSED_LONG_TERM, + OPEN, + CLOSES_SOON, + OPENS_SOON, + CLOSED, + CLOSED_LONG_TERM, } /** @@ -64,48 +64,48 @@ export enum LocationState { * update the Joi Schema in joiLocationTypes.ts as well) */ interface IAPILocation { - conceptId: number; - name?: string; - shortDescription?: string; - description: string; - url: string; - /** Menu link */ - menu?: string; - location: string; - coordinates?: { - lat: number; - lng: number; - }; - acceptsOnlineOrders: boolean; - times: ITimeSlot[]; - todaysSpecials?: ISpecial[]; - todaysSoups?: ISpecial[]; + conceptId: number; + name?: string; + shortDescription?: string; + description: string; + url: string; + /** Menu link */ + menu?: string; + location: string; + coordinates?: { + lat: number; + lng: number; + }; + acceptsOnlineOrders: boolean; + times: ITimeSlot[]; + todaysSpecials?: ISpecial[]; + todaysSoups?: ISpecial[]; } // All of the following are extended from the base API type // Base type interface ILocation extends IAPILocation { - name: string; // This field is now guaranteed to be defined + name: string; // This field is now guaranteed to be defined } // 'Closed' here refers to closed for the near future (no timeslots available) interface ILocationStatusBase { - /** No forseeable opening times after *now* */ - closedLongTerm: boolean; - statusMsg: string; - locationState: LocationState; + /** No forseeable opening times after *now* */ + closedLongTerm: boolean; + statusMsg: string; + locationState: LocationState; } interface ILocationStatusOpen extends ILocationStatusBase { - isOpen: boolean; - timeUntil: number; - closedLongTerm: false; - changesSoon: boolean; - locationState: Exclude; + isOpen: boolean; + timeUntil: number; + closedLongTerm: false; + changesSoon: boolean; + locationState: Exclude; } interface ILocationStatusClosed extends ILocationStatusBase { - closedLongTerm: true; - locationState: LocationState.CLOSED_LONG_TERM; + closedLongTerm: true; + locationState: LocationState.CLOSED_LONG_TERM; } interface IExtendedLocationOpen extends ILocation, ILocationStatusOpen {} interface IExtendedLocationClosed extends ILocation, ILocationStatusClosed {} diff --git a/src/util/assert.ts b/src/util/assert.ts index ec0d4162..22b6e656 100644 --- a/src/util/assert.ts +++ b/src/util/assert.ts @@ -1,5 +1,5 @@ export default function assert(condition: boolean, message?: string) { - if (!condition) { - throw new Error(message || "Assertion failed"); - } + if (!condition) { + throw new Error(message || 'Assertion failed'); + } } diff --git a/src/util/greeting.ts b/src/util/greeting.ts index f6a36a3d..5c43225d 100644 --- a/src/util/greeting.ts +++ b/src/util/greeting.ts @@ -1,56 +1,56 @@ -import assert from "./assert"; -import bounded from "./misc"; +import assert from './assert'; +import bounded from './misc'; const graveyard = [ - "Staying up all night?", - "Want a late-night snack?", - "Don't stay up too late!", - "Delivery too expensive?", + 'Staying up all night?', + 'Want a late-night snack?', + "Don't stay up too late!", + 'Delivery too expensive?', ]; const morning = [ - "Fancy some breakfast?", - "Is breakfast really the most important meal of the day?", - "What do you want to eat?", - "Have a good morning!", - "Start your day with a delicious meal!", - "Time to refuel for the day ahead!", - "Breakfast is calling your name!", + 'Fancy some breakfast?', + 'Is breakfast really the most important meal of the day?', + 'What do you want to eat?', + 'Have a good morning!', + 'Start your day with a delicious meal!', + 'Time to refuel for the day ahead!', + 'Breakfast is calling your name!', ]; const afternoon = [ - "What do you want for lunch?", - "What do you want to eat?", - "Have a good afternoon!", - "Use those blocks!", - "Fuel up for the afternoon!", - "Lunch options galore!", - "Satisfy your midday hunger!", - "Craving something savory for lunch?", + 'What do you want for lunch?', + 'What do you want to eat?', + 'Have a good afternoon!', + 'Use those blocks!', + 'Fuel up for the afternoon!', + 'Lunch options galore!', + 'Satisfy your midday hunger!', + 'Craving something savory for lunch?', ]; const evening = [ - "What do you want for dinner?", - "What do you want to eat?", - "Have a good evening!", - "Grab a bite to eat!", - "Hungry night owl?", - "Midnight munchies? We've got you!", + 'What do you want for dinner?', + 'What do you want to eat?', + 'Have a good evening!', + 'Grab a bite to eat!', + 'Hungry night owl?', + "Midnight munchies? We've got you!", ]; const getGreeting = (hours: number) => { - assert(bounded(hours, 0, 24)); - if (hours < 6) { - return graveyard[Math.floor(Math.random() * graveyard.length)]; - } - if (hours < 12) { - return morning[Math.floor(Math.random() * morning.length)]; - } - if (hours < 17) { - return afternoon[Math.floor(Math.random() * afternoon.length)]; - } - if (hours < 24) { - return evening[Math.floor(Math.random() * evening.length)]; - } + assert(bounded(hours, 0, 24)); + if (hours < 6) { + return graveyard[Math.floor(Math.random() * graveyard.length)]; + } + if (hours < 12) { + return morning[Math.floor(Math.random() * morning.length)]; + } + if (hours < 17) { + return afternoon[Math.floor(Math.random() * afternoon.length)]; + } + if (hours < 24) { + return evening[Math.floor(Math.random() * evening.length)]; + } - return "Welcome to CMUEats!"; + return 'Welcome to CMUEats!'; }; export default getGreeting; diff --git a/src/util/misc.ts b/src/util/misc.ts index 293247bf..3fc0ab1e 100644 --- a/src/util/misc.ts +++ b/src/util/misc.ts @@ -1,9 +1,9 @@ -import assert from "./assert"; +import assert from './assert'; /** * @returns true iff n (int) is in [a,b) */ export default function bounded(n: number, a: number, b: number) { - assert(a <= b); - return n >= a && n < b; + assert(a <= b); + return n >= a && n < b; } diff --git a/src/util/queryLocations.ts b/src/util/queryLocations.ts index eedcb032..b9c2634c 100644 --- a/src/util/queryLocations.ts +++ b/src/util/queryLocations.ts @@ -1,41 +1,41 @@ -import axios from "axios"; +import axios from 'axios'; -import { DateTime } from "luxon"; +import { DateTime } from 'luxon'; import { - LocationState, - ITimeSlotTime, - IReadOnlyLocation, - IReadOnlyLocationStatus, - ITimeSlots, - IReadOnlyAPILocation, -} from "../types/locationTypes"; + LocationState, + ITimeSlotTime, + IReadOnlyLocation, + IReadOnlyLocationStatus, + ITimeSlots, + IReadOnlyAPILocation, +} from '../types/locationTypes'; import { - diffInMinutes, - currentlyOpen, - getNextTimeSlot, - isTimeSlotTime, - isValidTimeSlotArray, - getTimeString, - minutesSinceSundayTimeSlotTime, - minutesSinceSundayDateTime, - getApproximateTimeStringFromMinutes, -} from "./time"; -import toTitleCase from "./string"; -import assert from "./assert"; + diffInMinutes, + currentlyOpen, + getNextTimeSlot, + isTimeSlotTime, + isValidTimeSlotArray, + getTimeString, + minutesSinceSundayTimeSlotTime, + minutesSinceSundayDateTime, + getApproximateTimeStringFromMinutes, +} from './time'; +import toTitleCase from './string'; +import assert from './assert'; import { - IAPIResponseJoiSchema, - ILocationAPIJoiSchema, -} from "../types/joiLocationTypes"; + IAPIResponseJoiSchema, + ILocationAPIJoiSchema, +} from '../types/joiLocationTypes'; const WEEKDAYS = [ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', ]; /** * Return the status message for a dining location, given the current or next available @@ -45,35 +45,36 @@ const WEEKDAYS = [ * @returns {string} The status message for the location */ export function getStatusMessage( - isOpen: boolean, - nextTime: ITimeSlotTime, - now: DateTime, + isOpen: boolean, + nextTime: ITimeSlotTime, + now: DateTime, ): string { - assert(isTimeSlotTime(nextTime)); - const diff = diffInMinutes(nextTime, now); - const weekdayDiff = - nextTime.day - - (now.weekday % 7) + // now.weekday returns 1-7 [mon-sun] instead of 0-6 [sun-sat] - (minutesSinceSundayTimeSlotTime(nextTime) < minutesSinceSundayDateTime(now) - ? 7 - : 0); // nextTime wraps around to next week? Add 7 days to nextTime.day + assert(isTimeSlotTime(nextTime)); + const diff = diffInMinutes(nextTime, now); + const weekdayDiff = + nextTime.day - + (now.weekday % 7) + // now.weekday returns 1-7 [mon-sun] instead of 0-6 [sun-sat] + (minutesSinceSundayTimeSlotTime(nextTime) < + minutesSinceSundayDateTime(now) + ? 7 + : 0); // nextTime wraps around to next week? Add 7 days to nextTime.day - const time = getTimeString(nextTime); + const time = getTimeString(nextTime); - const action = isOpen ? "Closes" : "Opens"; - let day = WEEKDAYS[nextTime.day]; + const action = isOpen ? 'Closes' : 'Opens'; + let day = WEEKDAYS[nextTime.day]; - if (weekdayDiff === 1) { - day = "tomorrow"; - } else if (weekdayDiff === 0) { - day = "today"; - } + if (weekdayDiff === 1) { + day = 'tomorrow'; + } else if (weekdayDiff === 0) { + day = 'today'; + } - const relTimeDiff = getApproximateTimeStringFromMinutes(diff); - if (relTimeDiff === "0 minutes") { - return `${action} now (${day} at ${time})`; - } - return `${action} in ${relTimeDiff} (${day} at ${time})`; + const relTimeDiff = getApproximateTimeStringFromMinutes(diff); + if (relTimeDiff === '0 minutes') { + return `${action} now (${day} at ${time})`; + } + return `${action} in ${relTimeDiff} (${day} at ${time})`; } /** @@ -83,76 +84,76 @@ export function getStatusMessage( * @returns */ export function getLocationStatus( - timeSlots: ITimeSlots, - now: DateTime, + timeSlots: ITimeSlots, + now: DateTime, ): IReadOnlyLocationStatus { - assert( - isValidTimeSlotArray(timeSlots), - `${JSON.stringify(timeSlots)} is invalid!`, - ); - const nextTimeSlot = getNextTimeSlot(timeSlots, now); - if (nextTimeSlot === null) - return { - statusMsg: "Closed until further notice", - closedLongTerm: true, - locationState: LocationState.CLOSED_LONG_TERM, - }; - const isOpen = currentlyOpen(nextTimeSlot, now); - const relevantTime = isOpen ? nextTimeSlot.end : nextTimeSlot.start; // when will the next closing/opening event happen? - const timeUntil = diffInMinutes(relevantTime, now); - const statusMsg = getStatusMessage(isOpen, relevantTime, now); - const changesSoon = timeUntil <= 60; - // eslint-disable-next-line no-nested-ternary - const locationState = isOpen - ? changesSoon - ? LocationState.CLOSES_SOON - : LocationState.OPEN - : changesSoon - ? LocationState.OPENS_SOON - : LocationState.CLOSED; + assert( + isValidTimeSlotArray(timeSlots), + `${JSON.stringify(timeSlots)} is invalid!`, + ); + const nextTimeSlot = getNextTimeSlot(timeSlots, now); + if (nextTimeSlot === null) + return { + statusMsg: 'Closed until further notice', + closedLongTerm: true, + locationState: LocationState.CLOSED_LONG_TERM, + }; + const isOpen = currentlyOpen(nextTimeSlot, now); + const relevantTime = isOpen ? nextTimeSlot.end : nextTimeSlot.start; // when will the next closing/opening event happen? + const timeUntil = diffInMinutes(relevantTime, now); + const statusMsg = getStatusMessage(isOpen, relevantTime, now); + const changesSoon = timeUntil <= 60; + // eslint-disable-next-line no-nested-ternary + const locationState = isOpen + ? changesSoon + ? LocationState.CLOSES_SOON + : LocationState.OPEN + : changesSoon + ? LocationState.OPENS_SOON + : LocationState.CLOSED; - return { - closedLongTerm: false, - isOpen, - statusMsg, - timeUntil, - changesSoon, - locationState, - }; + return { + closedLongTerm: false, + isOpen, + statusMsg, + timeUntil, + changesSoon, + locationState, + }; } export async function queryLocations( - cmuEatsAPIUrl: string, + cmuEatsAPIUrl: string, ): Promise { - try { - // Query locations - const { data } = await axios.get(cmuEatsAPIUrl); - if (!data) { - return []; - } - const { locations: rawLocations } = - await IAPIResponseJoiSchema.validateAsync(data); + try { + // Query locations + const { data } = await axios.get(cmuEatsAPIUrl); + if (!data) { + return []; + } + const { locations: rawLocations } = + await IAPIResponseJoiSchema.validateAsync(data); - // Check for invalid location data - const validLocations = rawLocations.filter((location) => { - const { error } = ILocationAPIJoiSchema.validate(location); - if (error !== undefined) { - console.error("Validation error!", error.details); - // eslint-disable-next-line no-underscore-dangle - console.error("original obj", error._original); - // eslint-disable-next-line no-alert - alert( - `${location.name} has invalid corresponding data! Ignoring location and continuing validation`, - ); - } - return error === undefined; - }) as IReadOnlyAPILocation[]; + // Check for invalid location data + const validLocations = rawLocations.filter((location) => { + const { error } = ILocationAPIJoiSchema.validate(location); + if (error !== undefined) { + console.error('Validation error!', error.details); + // eslint-disable-next-line no-underscore-dangle + console.error('original obj', error._original); + // eslint-disable-next-line no-alert + alert( + `${location.name} has invalid corresponding data! Ignoring location and continuing validation`, + ); + } + return error === undefined; + }) as IReadOnlyAPILocation[]; - return validLocations.map((location) => ({ - ...location, - name: toTitleCase(location.name ?? "Untitled"), // Convert names to title case - })); - } catch (err: any) { - console.error(err); - return []; - } + return validLocations.map((location) => ({ + ...location, + name: toTitleCase(location.name ?? 'Untitled'), // Convert names to title case + })); + } catch (err: any) { + console.error(err); + return []; + } } diff --git a/src/util/string.ts b/src/util/string.ts index 76035e42..c60c38bb 100644 --- a/src/util/string.ts +++ b/src/util/string.ts @@ -4,16 +4,16 @@ * @returns the same string, but in title case (single characters aren't upper-cased) */ export default function toTitleCase(str: string) { - return str - .trim() - .toLowerCase() - .split(" ") - .map((word) => { - if (word === "ii") return "II"; // special case - if (word.length > 1) { - return word[0].toUpperCase() + word.slice(1); - } - return word; - }) - .join(" "); + return str + .trim() + .toLowerCase() + .split(' ') + .map((word) => { + if (word === 'ii') return 'II'; // special case + if (word.length > 1) { + return word[0].toUpperCase() + word.slice(1); + } + return word; + }) + .join(' '); } diff --git a/src/util/time.ts b/src/util/time.ts index 97b2129f..ffc410d0 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -1,11 +1,11 @@ /** Pure utility functions that process location (restaurant) timeslots */ -import { DateTime } from "luxon"; -import { ITimeSlotTime, ITimeSlot, ITimeSlots } from "../types/locationTypes"; +import { DateTime } from 'luxon'; +import { ITimeSlotTime, ITimeSlot, ITimeSlots } from '../types/locationTypes'; -import assert from "./assert"; -import bounded from "./misc"; +import assert from './assert'; +import bounded from './misc'; const WEEK_MINUTES = 7 * 24 * 60; @@ -16,14 +16,14 @@ const WEEK_MINUTES = 7 * 24 * 60; * @returns Raw minutes since Sun 12am */ export function minutesSinceSunday(day: number, hour: number, minute: number) { - assert( - bounded(day, 0, 7) && bounded(hour, 0, 24) && bounded(minute, 0, 60), - "Invalid minutesSinceSunday input!", - ); - return day * 60 * 24 + hour * 60 + minute; + assert( + bounded(day, 0, 7) && bounded(hour, 0, 24) && bounded(minute, 0, 60), + 'Invalid minutesSinceSunday input!', + ); + return day * 60 * 24 + hour * 60 + minute; } export function minutesSinceSundayDateTime(now: DateTime) { - return minutesSinceSunday(now.weekday % 7, now.hour, now.minute); + return minutesSinceSunday(now.weekday % 7, now.hour, now.minute); } /** @@ -32,20 +32,20 @@ export function minutesSinceSundayDateTime(now: DateTime) { * @returns Whether or not the timeslot has valid/expected values (all property values must be integers) */ export function isTimeSlotTime(timeSlotTime: ITimeSlotTime) { - const { day, hour, minute } = timeSlotTime; - return ( - Number.isInteger(day) && - Number.isInteger(minute) && - Number.isInteger(hour) && - bounded(day, 0, 7) && - bounded(hour, 0, 24) && - bounded(minute, 0, 60) - ); + const { day, hour, minute } = timeSlotTime; + return ( + Number.isInteger(day) && + Number.isInteger(minute) && + Number.isInteger(hour) && + bounded(day, 0, 7) && + bounded(hour, 0, 24) && + bounded(minute, 0, 60) + ); } export function minutesSinceSundayTimeSlotTime(timeSlot: ITimeSlotTime) { - assert(isTimeSlotTime(timeSlot)); - return minutesSinceSunday(timeSlot.day, timeSlot.hour, timeSlot.minute); + assert(isTimeSlotTime(timeSlot)); + return minutesSinceSunday(timeSlot.day, timeSlot.hour, timeSlot.minute); } /** * @@ -54,13 +54,13 @@ export function minutesSinceSundayTimeSlotTime(timeSlot: ITimeSlotTime) { * @returns true/false */ export function isTimeSlot(timeSlot: ITimeSlot, allowWrapAround?: boolean) { - return ( - isTimeSlotTime(timeSlot.start) && - isTimeSlotTime(timeSlot.end) && - (allowWrapAround || - minutesSinceSundayTimeSlotTime(timeSlot.start) <= - minutesSinceSundayTimeSlotTime(timeSlot.end)) - ); + return ( + isTimeSlotTime(timeSlot.start) && + isTimeSlotTime(timeSlot.end) && + (allowWrapAround || + minutesSinceSundayTimeSlotTime(timeSlot.start) <= + minutesSinceSundayTimeSlotTime(timeSlot.end)) + ); } /** @@ -69,20 +69,20 @@ export function isTimeSlot(timeSlot: ITimeSlot, allowWrapAround?: boolean) { * @returns ("1 day", "32 minutes", "3 hours", etc.) */ export function getApproximateTimeStringFromMinutes(minutes: number) { - assert(minutes >= 0, "Minutes must be positive!"); + assert(minutes >= 0, 'Minutes must be positive!'); - const pluralTag = (strings: TemplateStringsArray, amt: number) => - `${strings[0]}${amt}${strings[1]}${amt === 1 ? "" : "s"}`; + const pluralTag = (strings: TemplateStringsArray, amt: number) => + `${strings[0]}${amt}${strings[1]}${amt === 1 ? '' : 's'}`; - let diff = minutes; - const minuteCount = diff % 60; - diff = Math.floor(diff / 60); - const hourCount = diff % 24; - diff = Math.floor(diff / 24); - const dayCount = diff; - if (dayCount !== 0) return pluralTag`${dayCount} day`; - if (hourCount !== 0) return pluralTag`${hourCount} hour`; - return pluralTag`${minuteCount} minute`; + let diff = minutes; + const minuteCount = diff % 60; + diff = Math.floor(diff / 60); + const hourCount = diff % 24; + diff = Math.floor(diff / 24); + const dayCount = diff; + if (dayCount !== 0) return pluralTag`${dayCount} day`; + if (hourCount !== 0) return pluralTag`${hourCount} hour`; + return pluralTag`${minuteCount} minute`; } /** @@ -91,12 +91,12 @@ export function getApproximateTimeStringFromMinutes(minutes: number) { * @returns HH:MM (AM/PM) */ export function getTimeString(time: ITimeSlotTime) { - assert(isTimeSlotTime(time)); - const { hour, minute } = time; - const hour12H = hour % 12 === 0 ? 12 : hour % 12; - const ampm = hour >= 12 ? "PM" : "AM"; - const minutePadded = minute < 10 ? `0${minute}` : minute; - return `${hour12H}:${minutePadded} ${ampm}`; + assert(isTimeSlotTime(time)); + const { hour, minute } = time; + const hour12H = hour % 12 === 0 ? 12 : hour % 12; + const ampm = hour >= 12 ? 'PM' : 'AM'; + const minutePadded = minute < 10 ? `0${minute}` : minute; + return `${hour12H}:${minutePadded} ${ampm}`; } /** @@ -111,20 +111,20 @@ export function getTimeString(time: ITimeSlotTime) { * rely on that assumption. Oh well. It's legacy code, am I right? */ export function isValidTimeSlotArray(timeSlots: ITimeSlots) { - for (let i = 0; i < timeSlots.length; i += 1) { - const allowWrapAround = i === timeSlots.length - 1; - if (!isTimeSlot(timeSlots[i], allowWrapAround)) return false; - if (i > 0) { - const { start } = timeSlots[i]; - const prevEnd = timeSlots[i - 1].end; - if ( - minutesSinceSundayTimeSlotTime(prevEnd) >= - minutesSinceSundayTimeSlotTime(start) - ) - return false; - } - } - return true; + for (let i = 0; i < timeSlots.length; i += 1) { + const allowWrapAround = i === timeSlots.length - 1; + if (!isTimeSlot(timeSlots[i], allowWrapAround)) return false; + if (i > 0) { + const { start } = timeSlots[i]; + const prevEnd = timeSlots[i - 1].end; + if ( + minutesSinceSundayTimeSlotTime(prevEnd) >= + minutesSinceSundayTimeSlotTime(start) + ) + return false; + } + } + return true; } /** @@ -134,14 +134,14 @@ export function isValidTimeSlotArray(timeSlots: ITimeSlots) { * @returns (smallest non-negative) time in minutes to get from now to timeSlot */ export function diffInMinutes(timeSlotTime: ITimeSlotTime, now: DateTime) { - assert(isTimeSlotTime(timeSlotTime)); - const diff = - (minutesSinceSundayTimeSlotTime(timeSlotTime) - - minutesSinceSundayDateTime(now) + - WEEK_MINUTES) % - WEEK_MINUTES; - assert(diff >= 0); - return diff; + assert(isTimeSlotTime(timeSlotTime)); + const diff = + (minutesSinceSundayTimeSlotTime(timeSlotTime) - + minutesSinceSundayDateTime(now) + + WEEK_MINUTES) % + WEEK_MINUTES; + assert(diff >= 0); + return diff; } /** @@ -151,14 +151,14 @@ export function diffInMinutes(timeSlotTime: ITimeSlotTime, now: DateTime) { * @returns true if the location is open, false otherwise */ export function currentlyOpen(timeSlot: ITimeSlot, now: DateTime) { - assert(isTimeSlot(timeSlot, true)); - const start = minutesSinceSundayTimeSlotTime(timeSlot.start); - const nowMinutes = minutesSinceSundayDateTime(now); - const end = minutesSinceSundayTimeSlotTime(timeSlot.end); - if (end < start) { - return start <= nowMinutes || nowMinutes <= end; // we're more flexible with the bounds because time is wrapping around - } - return start <= nowMinutes && nowMinutes <= end; + assert(isTimeSlot(timeSlot, true)); + const start = minutesSinceSundayTimeSlotTime(timeSlot.start); + const nowMinutes = minutesSinceSundayDateTime(now); + const end = minutesSinceSundayTimeSlotTime(timeSlot.end); + if (end < start) { + return start <= nowMinutes || nowMinutes <= end; // we're more flexible with the bounds because time is wrapping around + } + return start <= nowMinutes && nowMinutes <= end; } /** @@ -168,19 +168,19 @@ export function currentlyOpen(timeSlot: ITimeSlot, now: DateTime) { * then it returns that slot). If there are no available slots, it returns null */ export function getNextTimeSlot(times: ITimeSlots, now: DateTime) { - assert(isValidTimeSlotArray(times)); - if (times.length === 0) return null; - const nowMinutes = minutesSinceSundayDateTime(now); - // Find the first time slot that opens after now - const nextTimeSlot = times.find( - (time) => - currentlyOpen(time, now) || - minutesSinceSundayTimeSlotTime(time.start) > nowMinutes, - ); + assert(isValidTimeSlotArray(times)); + if (times.length === 0) return null; + const nowMinutes = minutesSinceSundayDateTime(now); + // Find the first time slot that opens after now + const nextTimeSlot = times.find( + (time) => + currentlyOpen(time, now) || + minutesSinceSundayTimeSlotTime(time.start) > nowMinutes, + ); - if (nextTimeSlot === undefined) { - // End of the week. Return the first time slot instead. - return times[0]; - } - return nextTimeSlot; + if (nextTimeSlot === undefined) { + // End of the week. Return the first time slot instead. + return times[0]; + } + return nextTimeSlot; } diff --git a/src/vite.env.d.ts b/src/vite.env.d.ts index 5c5a1fad..043fc8f6 100644 --- a/src/vite.env.d.ts +++ b/src/vite.env.d.ts @@ -3,12 +3,12 @@ /// /// -import { FC, SVGProps } from "react"; +import { FC, SVGProps } from 'react'; -declare module "*.png"; -declare module "*.jpeg"; -declare module "*.jpg"; -declare module "*.svg" { - const content: FC>; - export default content; +declare module '*.png'; +declare module '*.jpeg'; +declare module '*.jpg'; +declare module '*.svg' { + const content: FC>; + export default content; }