From 4ba6dd4021a68608cd38832099a0a74953215be0 Mon Sep 17 00:00:00 2001 From: Deep Date: Sat, 3 Aug 2024 14:59:46 +0530 Subject: [PATCH] APIs for search, route, and stop --- index.html | 6 +- package.json | 3 + src/assets/icon_green_circle.svg | 22 ++ src/assets/icon_location.svg | 25 +- src/components/bottom_tray.jsx | 153 ++++++++++++ src/components/circle_loader.jsx | 21 ++ src/components/history_list.jsx | 8 + src/components/page_back_button.jsx | 22 ++ src/favourites/index.jsx | 61 +++-- src/index.scss | 355 +++++++++++++++++++++++++--- src/landing/index.jsx | 127 +++++----- src/landing/sidebar.jsx | 21 +- src/main.jsx | 10 + src/route/index.jsx | 303 ++++++++++++++++++++++++ src/search/index.jsx | 119 +++++----- src/search/search_result_item.jsx | 19 +- src/search/search_results.jsx | 87 +++++++ src/stop/bus-stop-route-item.jsx | 39 +++ src/stop/index.jsx | 185 +++++++++++++++ src/utils/api.js | 8 + src/utils/constants.js | 15 +- src/utils/index.js | 47 ++++ yarn.lock | 67 ++++++ 23 files changed, 1517 insertions(+), 206 deletions(-) create mode 100644 src/assets/icon_green_circle.svg create mode 100644 src/components/bottom_tray.jsx create mode 100644 src/components/circle_loader.jsx create mode 100644 src/components/history_list.jsx create mode 100644 src/components/page_back_button.jsx create mode 100644 src/route/index.jsx create mode 100644 src/search/search_results.jsx create mode 100644 src/stop/bus-stop-route-item.jsx create mode 100644 src/stop/index.jsx create mode 100644 src/utils/api.js create mode 100644 src/utils/index.js diff --git a/index.html b/index.html index 55585e9..f87b33d 100644 --- a/index.html +++ b/index.html @@ -17,11 +17,7 @@ crossorigin="anonymous" href="https://fonts.gstatic.com" /> - + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/icon_location.svg b/src/assets/icon_location.svg index b08c7a9..f33deeb 100644 --- a/src/assets/icon_location.svg +++ b/src/assets/icon_location.svg @@ -1,3 +1,22 @@ - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/bottom_tray.jsx b/src/components/bottom_tray.jsx new file mode 100644 index 0000000..e2ccc39 --- /dev/null +++ b/src/components/bottom_tray.jsx @@ -0,0 +1,153 @@ +import React from "react"; +import classNames from "classnames"; + +const COLLAPSED_HEIGHT = 200; + +const getCoordinatesFromEvent = (e) => { + let [x, y] = [0, 0]; + if (e.clientX) { + x = e.clientX; + y = e.clientY; + } else if (e.touches) { + x = e.touches[0].clientX; + y = e.touches[0].clientY; + } + return [x, y]; + }; + +class BottomTray extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + x: null, + y: null, + move: 0, + resized: 0, + bodyHeight: null, + }; + this.mouseTime = 0; + this.secondInterval = null; + } + + componentDidMount() { + (window.visualViewport || window).addEventListener("resize", this.onResize); + this.onResize(); + window.addEventListener("mousemove", this.onPointerMove, { passive: true }); + window.addEventListener("touchmove", this.onPointerMove, { passive: true }); + window.addEventListener("mouseup", this.onPointerUp, { passive: true }); + window.addEventListener("touchend", this.onPointerUp, { passive: true }); + } + + componentWillUnmount() { + clearInterval(this.secondInterval); + (window.visualViewport || window).removeEventListener("resize", this.onResize); + window.removeEventListener("mousemove", this.onPointerMove, { + passive: true, + }); + window.removeEventListener("touchmove", this.onPointerMove, { + passive: true, + }); + window.removeEventListener("mouseup", this.onPointerUp, { passive: true }); + window.removeEventListener("touchend", this.onPointerUp, { passive: true }); + } + + componentDidUpdate(prevProps) { + const { bodyHeight: newBodyHeight } = this.props; + const { bodyHeight } = prevProps; + const { move } = this.state; + + // Handle resize for bottom tray sizing + const delta = bodyHeight ? newBodyHeight - bodyHeight : 0; + this.setState({ + move: move < -COLLAPSED_HEIGHT ? move - delta : move, + }); + + } + + onPointerDown = (e) => { + if (window.innerWidth > 999) { + return; + } + const [x, y] = getCoordinatesFromEvent(e); + this.mouseTime = performance.now(); + this.setState({ x, y }); + }; + + onPointerMove = (e) => { + e.stopPropagation(); + if (this.state.x === null) { + return; + } + const [x, y] = getCoordinatesFromEvent(e); + this.setState(({ move }) => ({ + move: Math.max( + move + y - this.state.y, + -window.innerHeight + COLLAPSED_HEIGHT, + ), + y, + })); + }; + + onPointerUp = () => { + if (this.state.x === null) { + return; + } + this.setState({ + x: null, + y: null, + }); + if (this.state.move < -COLLAPSED_HEIGHT - 50) { + // Touch moved up by a significant value above the midway height + this.setState({ + move: -window.innerHeight + COLLAPSED_HEIGHT, + }); + } + else { + // Touch moved up but not significant + this.setState({ + move: 0, + }); + } + }; + + onResize = () => { + this.setState({ + bodyHeight: window.visualViewport?.height || window.innerHeight, + }); + }; + render() { + const { + children, + headerContent, + } = this.props; + const { move, bodyHeight } = this.state; + return ( +
+
+
+ { headerContent } +
+
+ { children } +
+
+ ) + } +}; + +export default BottomTray; diff --git a/src/components/circle_loader.jsx b/src/components/circle_loader.jsx new file mode 100644 index 0000000..59b29ca --- /dev/null +++ b/src/components/circle_loader.jsx @@ -0,0 +1,21 @@ +import React from "react"; + +const CircleLoader = ({ text }) => { + return ( +
+
{ text || "Loading..." } +
+ ); +}; + +export const CircleLoaderBlock = ({ text }) => { + return ( +
+ +
+ ); +} + +export default CircleLoader; diff --git a/src/components/history_list.jsx b/src/components/history_list.jsx new file mode 100644 index 0000000..b74a43e --- /dev/null +++ b/src/components/history_list.jsx @@ -0,0 +1,8 @@ +import React from "react"; + +const HistoryList = () => { + + return ""; +}; + +export default HistoryList; diff --git a/src/components/page_back_button.jsx b/src/components/page_back_button.jsx new file mode 100644 index 0000000..5b8af97 --- /dev/null +++ b/src/components/page_back_button.jsx @@ -0,0 +1,22 @@ +import React from "react"; +import { Link, useLocation } from "react-router-dom"; +import {Icon} from "@iconify/react/dist/iconify.js"; +import { ROUTES } from "../utils/constants"; + +const PageBackButton = () => { + const location = useLocation(); + // console.log(location.state); + const onBackClick = () => { + if(!!location.state) { + window.history.back(); + } + }; + + return ( + + + + ); +}; + +export default PageBackButton; diff --git a/src/favourites/index.jsx b/src/favourites/index.jsx index 7e3aa15..875637d 100644 --- a/src/favourites/index.jsx +++ b/src/favourites/index.jsx @@ -1,38 +1,25 @@ -import React from "react"; +import React, { useState } from "react"; +import _ from "lodash"; import {Icon} from "@iconify/react/dist/iconify.js"; import SearchResultItem from "../search/search_result_item"; -import {ROUTES, SEARCH_RESULT_TYPES} from "../utils/constants.js"; +import {MAX_HISTORY_LENGTH, ROUTES} from "../utils/constants.js"; import {Link} from "react-router-dom"; -const FAVOURITES = [ - { - type: SEARCH_RESULT_TYPES.location, - text: "Mayur Paradise", - favourite: true, - }, - { - type: SEARCH_RESULT_TYPES.bus_stop, - text: "Tippasandra Market Bus Stop", - favourite: true, - }, - { - type: SEARCH_RESULT_TYPES.metro_station_purple, - text: "Nadaprabhu Kempegowda Metro Station, Majestic", - favourite: true, - }, - { - type: SEARCH_RESULT_TYPES.bus_number, - text: "314", - favourite: true, - }, - { - type: SEARCH_RESULT_TYPES.bus_number, - text: "333G", - favourite: true, - }, -]; - const FavouritesPage = () => { + const [favourites, setFavouritesItems] = useState( + JSON.parse(localStorage.getItem("bpt_favourites") || "[]") + ); + + const removeFromFavourites = (e, info) => { + e.stopPropagation(); + e.preventDefault(); + + const { id, type } = info; + const newFavourites = _.filter(favourites, f => !(f.id === id && f.type === type)); + setFavouritesItems(newFavourites); + localStorage.setItem("bpt_favourites", JSON.stringify(newFavourites)); + } + return ( <>
{ - FAVOURITES.map(i => ) + _.size(favourites) > 0 ? ( + favourites.map(i => ( + + )) + ) : ( + "You do not have any favourites added" + ) }
diff --git a/src/index.scss b/src/index.scss index f572faa..aab161d 100644 --- a/src/index.scss +++ b/src/index.scss @@ -16,10 +16,13 @@ html { font-family: "IBM Plex Sans", sans-serif; font-size: 20px; + overscroll-behavior-y: none; + height: -webkit-fill-available; } body { margin: 0; + overscroll-behavior-y: none; font-family: "IBM Plex Sans", sans-serif; } @@ -28,22 +31,20 @@ body { height: 100vh; } -#page-header { - background-color: #2D2D2D; - border-radius: 0 0 16px 16px; - padding: 18px 16px; +#page-back { + position: fixed; + top: 28px; + left: 24px; + border-radius: 50%; + width: 36px; + height: 36px; display: flex; align-items: center; + justify-content: center; + background-color: #ffffff; box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25); } -#header-back { - background-color: transparent; - border: 0; - margin: 0 8px 0 0; - padding: 0; -} - #header-text { color: #ffffff; font-size: 0.8rem; @@ -57,11 +58,113 @@ body { } } +#page-header { + background-color: #2D2D2D; + border-radius: 0 0 16px 16px; + padding: 18px 16px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25); +} + + +#header-back { + background-color: transparent; + border: 0; + margin: 0 8px 0 0; + padding: 0; +} + +#header-icon { + margin: 0 8px -2px 0; +} + .subheading { + text-transform: uppercase; font-size: 0.7rem; color: #999999; } +.circle-loader { + border: 4px solid #f3f3f3; + border-top: 4px solid #3498db; + border-radius: 50%; + width: 36px; + height: 36px; + animation: spin 1s linear infinite; + margin-right: 10px;; +} + +.circle-loader-wrapper { + display: flex; + align-items: center; + padding: 10px; +} + +.circle-loader-block { + display: flex; + align-items: center; + justify-content: center; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + + +#bottom-tray { + position: fixed; + left: 0; + right: 0; + top: 0; + //top: calc(100vh - 90px); + z-index: 1000; + background-color: #ffffff; + width: 100%; + height: 100vh; + border-radius: 8px 8px 0 0; + box-shadow: 0px -3px 6px #00000029; + transition: border-radius 0.3s; + display: flex; + flex-direction: column; + &.sharp-corners { + border-radius: 0; + } +} + +#bottom-tray-header { + user-select: none; + border-bottom: 1px solid #cccccc; +} + +#bottom-tray-drag-indicator { + position: absolute; + width: 30px; + top: 6px; + left: calc(50% - 15px); + height: 4px; + border-radius: 2px; + background-color: #cccccc; +} + +#bottom-tray-content { + flex-grow: 1; + height: 10px; + border-bottom: 1px solid #cccccc; + display: flex; + flex-direction: column; + overflow: auto; +} + +#page-heading { + display: flex; + align-items: center; + justify-content: space-between; + padding: 5px 20px; +} + /******************** Landing page ********************/ @@ -71,13 +174,8 @@ body { height: 100%; } -#landing-bottom { - position: fixed; - bottom: 40px; - left: 0; - right: 0; - z-index: 1000; - padding: 24px 24px 0; +#landing-contents { + padding: 32px 24px 0; } #landing-input { @@ -94,11 +192,16 @@ body { align-items: center; font-size: 0.9rem; text-decoration: none; + margin-bottom: 32px;; svg { margin-right: 10px; } } +.landing-section { + margin-bottom: 36px; +} + #landing-icons { display: flex; align-items: center; @@ -106,6 +209,9 @@ body { } .landing-button { + display: inline-flex; + align-items: center; + justify-content: center; background-color: #2D2D2D; width: 48px; height: 48px; @@ -113,11 +219,21 @@ body { border: 0; } +#landing-header { + display: flex; + flex-direction: row-reverse; + justify-content: space-between; + align-items: center; + margin-bottom: 32px; + h1 { + font-size: 1.6rem; + font-weight: 500; + margin: 0; + } +} + #landing-hamburger { - position: fixed; - top: 28px; - left: 24px; - border: 0; + border: 0; background: transparent; margin: 0; padding: 0; @@ -208,6 +324,8 @@ body { margin: 0 0 10px; align-items: flex-start; padding: 5px 0 10px; + color: inherit; + text-decoration: none; } .search-item-icon { @@ -222,18 +340,191 @@ body { flex-shrink: 1; margin-top: -4px; line-height: 1.6; - .bus_number { - background-color: #1A73E9; - border: 2px solid #1967D3; - border-radius: 4px; - color: #ffffff; - font-weight: bold; - font-size: 0.7rem; - padding: 0 6px; - } + color: inherit; + text-decoration: none; +} + +.bus_number { + background-color: #1A73E9; + border: 2px solid #1967D3; + border-radius: 4px; + color: #ffffff; + font-weight: bold; + font-size: 0.7rem; + padding: 0 6px; } .search-result-favourite { background-color: transparent; border: 0; -} \ No newline at end of file +} + +/******************** + Bus stop page +********************/ +.bus-stop-page-list { + padding: 10px 20px; +} + +.bus-stop-item { + padding: 19px 0 17px; + border-bottom: 1px solid #D9D9D9; + &:last-child { + border-bottom: 0; + } +} +.bus-item-header { + display: flex; + align-items: center; + margin-bottom: 8px; +} + +.bus-item-icon { + margin-right: 8px; + width: 16px; + height: 16px; +} + +.bus-item-trips { + color: #666666; + font-size: 0.8rem; +} + +.bus-stop-item-row { + display: flex; + font-size: 0.7rem; + margin-bottom: 4px; +} + +.bus-stop-item-fromto { + width: 50px; + color: #999999; + flex-grow: 0; + flex-shrink: 0; +} + +.bus-stop-item-desc { + font-size: 0.7rem; + color: #999999; + font-style: italic; +} + +#bus-page-flip { + height: 48px; + display: flex; + align-items: center; + position: fixed; + right: 16px; + bottom: 48px; + background-color: #2D2D2D; + border: 0; + border-radius: 18px; + padding: 0 14px 0 20px; + color: #ffffff; + font: inherit; + font-size: 0.8rem; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.25); +} + +/******************** + Bus route page +********************/ +#bus-route-page { + padding: 24px 20px; +} + +#bus-route-fromto { + display: flex; + justify-content: space-between; + margin-bottom: 34px; +} + +#bus-route-flip { + height: 48px; + width: 48px; + display: flex; + align-items: center; + justify-content: center; + background-color: #2D2D2D; + border: 0; + border-radius: 18px; + color: #ffffff; + font: inherit; + font-size: 0.8rem; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.25); +} + +#bus-routes-stops-heading { + display: flex; + font-size: 0.7rem; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid #D9D9D9; + padding: 0 0 4px 0; + margin-bottom: 10px; + h4 { + margin: 0; + } + span { + color: #999999; + } +} + +#bus-route-stops-list { + position: relative; +} + +.bus-route-stop-item { + display: flex; + align-items: center; + padding: 12px 0; +} + +.bus-route-stop-icon { + margin-right: 8px; + width: 16px; + height: 16px; +} + +.bus-route-stop-text { + font-size: 0.8rem; + flex-grow: 1; + color: inherit; + text-decoration: none; +} + +.bus-route-stop-time { + font-size: 0.7rem; + color: #666666; +} + +#bus-route-stops-line { + position: absolute; + width: 4px; + background-color: #1967D3; + top: 30px; + bottom: 31px; + left: 6px; + z-index: -1; +} + +.source-marker { + background-image: url(./assets/icon_green_circle.svg); + background-size: cover; + width: 30px; + height: 30px; + border-radius: 50%; + cursor: pointer; + transform: translate(0, -15px); +} + +.destination-marker { + background-image: url(./assets/icon_location.svg); + background-size: cover; + width: 30px; + height: 30px; + border-radius: 50%; + cursor: pointer; + transform: translate(0, -15px); +} + diff --git a/src/landing/index.jsx b/src/landing/index.jsx index db073d6..f2a4d63 100644 --- a/src/landing/index.jsx +++ b/src/landing/index.jsx @@ -1,10 +1,12 @@ import React from "react"; import mapboxgl from "mapbox-gl"; -import {MAPBOX_TOKEN, ROUTES} from "../utils/constants.js"; +import _ from "lodash"; +import {MAPBOX_TOKEN, MAX_HISTORY_LENGTH, ROUTES} from "../utils/constants.js"; import { Icon } from '@iconify/react'; import Sidebar from "./sidebar.jsx"; -import {Link} from "react-router-dom"; +import {Link, useLocation} from "react-router-dom"; +import SearchResultItem from "../search/search_result_item.jsx"; mapboxgl.accessToken = MAPBOX_TOKEN; @@ -12,75 +14,90 @@ class LandingPage extends React.PureComponent { constructor(props) { super(props); this.state = { - lat: 12.977529081680132, - lng: 77.57247169985196, - zoom: 11, - supported: mapboxgl.supported(), + historyItems: _.take(JSON.parse(localStorage.getItem("bpt_history") || "[]"), 3), + favourites: JSON.parse(localStorage.getItem("bpt_favourites") || "[]"), }; - this.mapContainer = React.createRef(); } + + onFavouriteClick = (e, info) => { + e.stopPropagation(); + e.preventDefault(); - initMap = () => { - if(!this.mapContainer.current) { - return; + const { favourites } = this.state; + const { id, text, type } = info; + let newFavourites =[]; + if(_.some(favourites, f => f.id === id && f.type === type)) { + newFavourites = _.filter(favourites, f => !(f.id === id && f.type === type)); + } else { + newFavourites = [ + { id, text, type }, + ...favourites + ]; } - const { lng, lat, zoom } = this.state; - const map = new mapboxgl.Map({ - container: this.mapContainer.current, - style: "mapbox://styles/mapbox/streets-v11", - center: [lng, lat], - zoom: zoom, - minZoom: 10, - maxZoom: 18, + this.setState({ + favourites: newFavourites }); - map.dragRotate.disable(); - map.touchZoomRotate.disableRotation(); - - map.on("move", () => { - this.setState({ - lng: map.getCenter().lng.toFixed(4), - lat: map.getCenter().lat.toFixed(4), - zoom: map.getZoom().toFixed(2), - }); - }); - this.map = map; - }; - - componentDidMount() { - if (this.state.supported) { - this.initMap(); - this.map?.on("load", () => { - - }); - } - } - - componentWillUnmount() { - this.map?.remove(); + localStorage.setItem("bpt_favourites", JSON.stringify(newFavourites)); } render() { + const { historyItems, favourites } = this.state; return ( - <> -
+
-
Where to? -
- - -
+ { + _.size(historyItems) > 0 && ( +
+

Recent

+ { + historyItems.map(i => { + const isFavourite = _.some(favourites, f => f.id === i.id && f.type === i.type); + return ( + + ); + }) + } +
+ ) + } + { + _.size(favourites) > 0 && ( +
+

Favourites

+ { + favourites.map(i => ( + + )) + } +
+ ) + }
- ); } } -export default LandingPage; +const LandingPageWithRouter = () => { + const location = useLocation(); + console.log(location); + return ( + + ) +}; + + +export default LandingPageWithRouter; diff --git a/src/landing/sidebar.jsx b/src/landing/sidebar.jsx index 6cc4abd..608ff7c 100644 --- a/src/landing/sidebar.jsx +++ b/src/landing/sidebar.jsx @@ -32,13 +32,19 @@ const Sidebar = () => { Language
{ LANGUAGES.map(l => ( - + )) } -
  • + {/*
  • All BMTC bus routes -
  • + */}
  • Favourites
  • @@ -49,9 +55,12 @@ const Sidebar = () => {
    ) : ( - +
    + +

    Plan your trip

    +
    ); }; diff --git a/src/main.jsx b/src/main.jsx index 46c3c11..8c2bb7b 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -9,6 +9,8 @@ import {ROUTES} from "./utils/constants.js"; import AboutPage from "./about/index.jsx"; import AllBusesPage from "./all-buses"; import FavouritesPage from "./favourites/index.jsx"; +import RoutePage from "./route"; +import StopPage from "./stop"; const router = createBrowserRouter([ { @@ -31,6 +33,14 @@ const router = createBrowserRouter([ path: ROUTES.favourites, element: }, + { + path: ROUTES.route, + element: + }, + { + path: ROUTES.stop, + element: + }, ]); ReactDOM.createRoot(document.getElementById('root')).render( diff --git a/src/route/index.jsx b/src/route/index.jsx new file mode 100644 index 0000000..f00b104 --- /dev/null +++ b/src/route/index.jsx @@ -0,0 +1,303 @@ +import React from "react"; +import _ from "lodash"; +import { useParams, Link } from "react-router-dom"; +import { MAPBOX_TOKEN, MAX_HISTORY_LENGTH, ROUTES, SEARCH_RESULT_TYPES } from "../utils/constants.js"; +import {Icon} from "@iconify/react/dist/iconify.js"; +import mapboxgl from "mapbox-gl"; + +import IconBusNumber from "../assets/icon_bus_number.svg"; +import IconBusStop from "../assets/icon_bus_stop.svg"; +import IconGreenCircle from "../assets/icon_green_circle.svg"; +import IconLocation from "../assets/icon_location.svg"; +import { getRouteDetailsApi } from "../utils/api.js"; +import PageBackButton from "../components/page_back_button.jsx"; +import BottomTray from "../components/bottom_tray.jsx"; +import { afterMapLoad } from "../utils/index.js"; + +mapboxgl.accessToken = MAPBOX_TOKEN; + +const STOP_TYPES = { + source: "source", + stop: "stop", + destination: "destination", +}; + +const ICON_FOR_STOP_TYPE = { + [STOP_TYPES.source]: IconGreenCircle, + [STOP_TYPES.destination]: IconLocation, + [STOP_TYPES.stop]: IconBusStop, +} + +class RoutePage extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + routeDetails: null, + // lat: 12.977529081680132, + // lng: 77.57247169985196, + // zoom: 11, + supported: mapboxgl.supported(), + isFavourited: false, + }; + this.mapContainer = React.createRef(); + } + + initMap = () => { + if(!this.mapContainer.current) { + return; + } + // const { lng, lat, zoom } = this.state; + const map = new mapboxgl.Map({ + container: this.mapContainer.current, + style: "mapbox://styles/mapbox/streets-v11", + center: [77.57247169985196, 12.977529081680132], + zoom: 11, + minZoom: 10, + maxZoom: 18, + }); + map.dragRotate.disable(); + map.touchZoomRotate.disableRotation(); + + // map.on("move", () => { + // this.setState({ + // lng: map.getCenter().lng.toFixed(4), + // lat: map.getCenter().lat.toFixed(4), + // zoom: map.getZoom().toFixed(2), + // }); + // }); + this.map = map; + }; + + componentDidMount() { + this.getRouteDetails(); + if (this.state.supported) { + this.initMap(); + afterMapLoad(this.map, () => { + this.map.addSource('route', { + 'type': 'geojson', + 'data': { + 'type': 'Feature', + 'properties': {}, + 'geometry': { + 'type': 'LineString', + 'coordinates': [] + } + } + }); + this.map.addLayer({ + 'id': 'route', + 'type': 'line', + 'source': 'route', + 'layout': { + 'line-join': 'round', + 'line-cap': 'round' + }, + 'paint': { + 'line-color': '#4264fb', + 'line-width': 6 + } + }); + }) + } + } + + componentWillUnmount() { + this.map?.remove(); + } + + componentDidUpdate(prevProps) { + if(prevProps.routeId !== this.props.routeId) { + this.getRouteDetails(); + } + }; + + getRouteDetails = async () => { + const { routeId } = this.props; + const { data: { data: routeDetails }} = await getRouteDetailsApi(routeId); + const favourites = JSON.parse(localStorage.getItem("bpt_favourites") || "[]"); + this.setState({ + routeDetails, + isFavourited: _.some(favourites, f => f.type === 'bus_number' && f.id === routeDetails.route_id), + }); + + + // Update route line data + const coordinates = routeDetails.shapeInformation.line; + afterMapLoad(this.map, () => { + this.map.getSource('route').setData({ + 'type': 'Feature', + 'properties': {}, + 'geometry': { + 'type': 'LineString', + 'coordinates': coordinates + } + }); + + // Set map boundaries to the line + const bounds = new mapboxgl.LngLatBounds(coordinates[0],coordinates[0]); + for (const coord of coordinates) { bounds.extend(coord); } + this.map.fitBounds(bounds, { padding: 20 }); + + // Add start and end markers + const sourceDetails = _.find( + routeDetails.stopInformation, + { + stop_id: _.first(routeDetails.route_trips[0].timings).stop_id + } + ); + const sourceEl = document.createElement('div'); + sourceEl.className = 'source-marker'; + new mapboxgl.Marker(sourceEl).setLngLat(sourceDetails.stop_loc).addTo(this.map); + + const destinationDetails = _.find( + routeDetails.stopInformation, + { + stop_id: _.last(routeDetails.route_trips[0].timings).stop_id + } + ); + const destinationEl = document.createElement('div'); + destinationEl.className = 'destination-marker'; + new mapboxgl.Marker(destinationEl).setLngLat(destinationDetails.stop_loc).addTo(this.map); + }); + + + // Add this bus route to history + const historyItems = JSON.parse(localStorage.getItem("bpt_history") || "[]"); + const newHistory = _.take( + _.uniqBy( + [ + { id: routeDetails.route_id, text: routeDetails.route_short_name, type: "bus_number" }, + ...historyItems + ], + s => `${s.type}-${s.id}`, + ), + MAX_HISTORY_LENGTH, + ); + localStorage.setItem("bpt_history", JSON.stringify(newHistory)); + + } + + toggleFavourite = () => { + const { routeDetails, isFavourited } = this.state; + const currentFavourites = JSON.parse(localStorage.getItem("bpt_favourites") || "[]"); + let newFavourites = []; + if(isFavourited) { + newFavourites = _.filter(currentFavourites, f => !(f.type === "bus_number" && f.id === routeDetails.route_id)); + this.setState({ + isFavourited: false, + }); + } else { + newFavourites = [ + { id: routeDetails.route_id, text: routeDetails.route_short_name, type: SEARCH_RESULT_TYPES.bus_number }, + ...currentFavourites, + ] + this.setState({ + isFavourited: true, + }); + } + localStorage.setItem("bpt_favourites", JSON.stringify(newFavourites)); + }; + + render() { + const { routeDetails, isFavourited } = this.state; + const [from, to] = (routeDetails?.route_long_name || "").split("→"); + + return ( + <> +
    + { + !!routeDetails && ( + <> + + +

    + + + {routeDetails.route_short_name} + +

    + +
    + )} + > +
    +
    +
    +
    +
    From
    + {_.trim(from)} +
    +
    +
    To
    + {_.trim(to)} +
    +
    + {/* */} +
    + +
    +

    + Stops on route +

    + Timing at the stop +
    +
    + { + _.map( + routeDetails.route_trips[0].timings, + (s, index) => { + const stopDetails = _.find(routeDetails.stopInformation, { + stop_id: s.stop_id, + }); + let stopType = STOP_TYPES.stop; + if(index === 0) { + stopType = STOP_TYPES.source; + } else if(index === _.size(routeDetails.route_trips[0].timings) - 1) { + stopType = STOP_TYPES.destination; + } + return ( +
    + + {stopDetails.stop_name} + + { + s.arrival_time.substring(0, 5) + } + +
    + ); + } + ) + } +
    +
    +
    + + + ) + } + + ); + } +}; + +const RoutePageWithParams = () => { + const { route_id: routeId } = useParams(); + return ( + + ); +}; + +export default RoutePageWithParams; diff --git a/src/search/index.jsx b/src/search/index.jsx index 65042e8..b7fbb23 100644 --- a/src/search/index.jsx +++ b/src/search/index.jsx @@ -1,57 +1,54 @@ -import React, {useState} from "react"; +import React, {useEffect, useState} from "react"; +import _ from "lodash"; import {Icon} from "@iconify/react/dist/iconify.js"; -import SearchResultItem from "./search_result_item"; -import {ROUTES, SEARCH_RESULT_TYPES} from "../utils/constants.js"; +import {API_CALL_STATUSES, ROUTES, SEARCH_RESULT_TYPES} from "../utils/constants.js"; import {Link} from "react-router-dom"; +import { getSearchResultsApi } from "../utils/api.js"; +import { deleteUrlParameter, getUrlParameter, setUrlParameter, useDebouncedValue } from "../utils/index.js"; +import SearchResults from "./search_results.jsx"; -const HISTORY_ITEMS = [ - { - type: SEARCH_RESULT_TYPES.location, - text: "Mayur Paradise", - favourite: false, - }, - { - type: SEARCH_RESULT_TYPES.bus_stop, - text: "Tippasandra Market Bus Stop", - favourite: true, - }, - { - type: SEARCH_RESULT_TYPES.metro_station_purple, - text: "Nadaprabhu Kempegowda Metro Station, Majestic", - favourite: false, - }, - { - type: SEARCH_RESULT_TYPES.bus_number, - text: "314", - favourite: false, - }, - { - type: SEARCH_RESULT_TYPES.bus_number, - text: "333G", - favourite: false, - }, -]; +const SearchPage = () => { + const [searchText, setSearchText] = useState(getUrlParameter("q") || ""); + const [apiStatus, setApiStatus] = useState(API_CALL_STATUSES.INITIAL); + const [searchResults, setSearchResults] = useState([]); + const debouncedSearchText = useDebouncedValue(searchText, 500); -const SEARCH_RESULTS = [ - { - type: SEARCH_RESULT_TYPES.location, - text: "Indiranagar Cafe Grill", - favourite: false, - }, - { - type: SEARCH_RESULT_TYPES.bus_stop, - text: "Indiranagar 100 Feet Road", - favourite: true, - }, - { - type: SEARCH_RESULT_TYPES.metro_station_purple, - text: "Indiranagar Metro Station", - favourite: false, - }, -] + useEffect(() => { + if(!searchText) { + setSearchResults([]); + return; + } + const apiCall = async () => { + const results = await getSearchResultsApi(searchText); + setSearchResults(_.map(results.data.data, r => ( + { + type: r.type === "route" ? SEARCH_RESULT_TYPES.bus_number : SEARCH_RESULT_TYPES.bus_stop, + text: r.name, + id: r.id, + } + ))); + setApiStatus(API_CALL_STATUSES.SUCCESS); + } + apiCall(); + }, [debouncedSearchText]); + + useEffect(() => { + let newParams = ""; + if(searchText) { + newParams = setUrlParameter("q", searchText); + setApiStatus(API_CALL_STATUSES.PROGRESS); + } else { + newParams = deleteUrlParameter("q"); + setApiStatus(API_CALL_STATUSES.INITIAL); + } + const newParamString = newParams.toString(); + if(newParamString) { + history.replaceState(null, null, `?${newParamString}`); + } else { + history.replaceState(null, null, window.location.href.split("?")[0]); + } + }, [searchText]); -const SearchPage = () => { - const [searchText, setSearchText] = useState(""); return ( <> - { - searchText ? ( -
    - { - SEARCH_RESULTS.map(i => ) - } -
    - ) : ( -
    -

    History

    - { - HISTORY_ITEMS.map(i => ) - } -
    - ) - } + + ); }; diff --git a/src/search/search_result_item.jsx b/src/search/search_result_item.jsx index f6779e7..d319d34 100644 --- a/src/search/search_result_item.jsx +++ b/src/search/search_result_item.jsx @@ -1,4 +1,5 @@ import React from "react"; +import { Link } from "react-router-dom"; import IconBusStop from "../assets/icon_bus_stop.svg"; import IconBusNumber from "../assets/icon_bus_number.svg"; @@ -16,11 +17,17 @@ const IconForResultType = { [SEARCH_RESULT_TYPES.location]: IconLocation, }; -const SearchResultItem = ({ info }) => { +const getLink = (info) => { + if(info.type === SEARCH_RESULT_TYPES.bus_stop) { + return `/stop/${info.id}`; + } + return `/route/${info.id}`; +} + +const SearchResultItem = ({ info, isFavourite, linkState, onItemClick = () => {}, onFavouriteClick }) => { const IconForInfo = IconForResultType[info.type]; return ( - // TODO: Change the key. Text could be common between two items -
    + onItemClick(info)}>
    @@ -29,16 +36,16 @@ const SearchResultItem = ({ info }) => { }
    - -
    + ); }; diff --git a/src/search/search_results.jsx b/src/search/search_results.jsx new file mode 100644 index 0000000..0b4354d --- /dev/null +++ b/src/search/search_results.jsx @@ -0,0 +1,87 @@ +import React, { useState, useEffect } from "react"; +import _ from "lodash"; + +import SearchResultItem from "./search_result_item"; +import { API_CALL_STATUSES, MAX_HISTORY_LENGTH, ROUTES } from "../utils/constants"; +import { CircleLoaderBlock } from "../components/circle_loader"; + +const SearchResults = ({ apiStatus, searchText, searchResults }) => { + const [historyItems] = useState( + JSON.parse(localStorage.getItem("bpt_history") || "[]") + ); + const [favourites, setFavouritesItems] = useState( + JSON.parse(localStorage.getItem("bpt_favourites") || "[]") + ); + + const onFavouriteClick = (e, info) => { + e.stopPropagation(); + e.preventDefault(); + + const { id, text, type } = info; + let newFavourites =[]; + if(_.some(favourites, f => f.id === id && f.type === type)) { + newFavourites = _.filter(favourites, f => !(f.id === id && f.type === type)); + } else { + newFavourites = [ + { id, text, type }, + ...favourites + ]; + } + setFavouritesItems(newFavourites); + localStorage.setItem("bpt_favourites", JSON.stringify(newFavourites)); + } + + if(apiStatus === API_CALL_STATUSES.PROGRESS) { + return ( + + ); + } + + if(!searchText && _.size(historyItems) > 0) { + return ( +
    +

    Recent

    + { + historyItems.map(i => { + const isFavourite = _.some(favourites, f => f.id === i.id && f.type === i.type); + return ( + + ); + }) + } +
    + ); + } + + if(!searchText) { + return ""; + } + + if(_.size(searchResults) === 0) { + return "No results found"; + } + + return ( +
    + { + searchResults.map(i => ( + + )) + } +
    + ) + +}; + +export default SearchResults; diff --git a/src/stop/bus-stop-route-item.jsx b/src/stop/bus-stop-route-item.jsx new file mode 100644 index 0000000..047b642 --- /dev/null +++ b/src/stop/bus-stop-route-item.jsx @@ -0,0 +1,39 @@ +import React from "react"; +import _ from "lodash"; +import { Link } from "react-router-dom"; +import IconBusNumber from "../assets/icon_bus_number.svg"; +import {ROUTES, SEARCH_RESULT_TYPES} from "../utils/constants.js"; + +const BusStopRouteItem = ({ info }) => { + const [from, to] = info.route_long_name.split("→"); + return ( +
    +
    + + + + {info.route_short_name } + + +
    + { info.trip_count} trips +
    +
    + {/* TODO: Change layout to CSS grid later and remove hardcoded width */} +
    +
    From
    + {_.trim(from)} +
    +
    +
    To
    + {_.trim(to)} +
    +
    + {info.desc} +
    + +
    + ) +}; + +export default BusStopRouteItem; diff --git a/src/stop/index.jsx b/src/stop/index.jsx new file mode 100644 index 0000000..ccb88c0 --- /dev/null +++ b/src/stop/index.jsx @@ -0,0 +1,185 @@ +import React from "react"; +import _ from "lodash"; +import { useParams} from "react-router-dom"; +import {Icon} from "@iconify/react/dist/iconify.js"; +import mapboxgl from "mapbox-gl"; +import { MAPBOX_TOKEN, MAX_HISTORY_LENGTH, SEARCH_RESULT_TYPES } from "../utils/constants.js"; + +import IconBusStop from "../assets/icon_bus_stop.svg"; +import BusStopRouteItem from "./bus-stop-route-item.jsx"; +import { getStopDetailsApi } from "../utils/api.js"; +import BottomTray from "../components/bottom_tray.jsx"; +import PageBackButton from "../components/page_back_button.jsx"; +import { afterMapLoad } from "../utils/index.js"; + +mapboxgl.accessToken = MAPBOX_TOKEN; + +class StopPage extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + stopDetails: null, + // lat: 12.977529081680132, + // lng: 77.57247169985196, + // zoom: 11, + supported: mapboxgl.supported(), + isFavourited: false, + }; + this.mapContainer = React.createRef(); + } + + initMap = () => { + if(!this.mapContainer.current) { + return; + } + const { lng, lat, zoom } = this.state; + const map = new mapboxgl.Map({ + container: this.mapContainer.current, + style: "mapbox://styles/mapbox/streets-v11", + center: [77.57247169985196, 12.977529081680132], + zoom: 11, + minZoom: 10, + maxZoom: 18, + }); + map.dragRotate.disable(); + map.touchZoomRotate.disableRotation(); + + // map.on("move", () => { + // this.setState({ + // lng: map.getCenter().lng.toFixed(4), + // lat: map.getCenter().lat.toFixed(4), + // zoom: map.getZoom().toFixed(2), + // }); + // }); + this.map = map; + }; + + componentDidMount() { + this.getStopDetails(); + if (this.state.supported) { + this.initMap(); + this.map?.on("load", () => { + + }); + } + } + + componentWillUnmount() { + this.map?.remove(); + } + + componentDidUpdate(prevProps) { + if(prevProps.stopId !== this.props.stopId) { + this.getStopDetails(); + } + }; + + getStopDetails = async () => { + const { stopId } = this.props; + const { data: { data: stopDetails }} = await getStopDetailsApi(stopId); + const favourites = JSON.parse(localStorage.getItem("bpt_favourites") || "[]"); + this.setState({ + stopDetails, + isFavourited: _.some(favourites, f => f.type === 'bus_stop' && f.id === stopDetails.stop_id), + }); + + // Add marker for the stop on the map + afterMapLoad(this.map, () => { + const sourceEl = document.createElement('div'); + sourceEl.className = 'destination-marker'; + new mapboxgl.Marker(sourceEl).setLngLat(stopDetails.stop_loc).addTo(this.map); + this.map.flyTo({ + center: stopDetails.stop_loc, + zoom: 17, + }); + }); + + // Add this bus stop to history + const historyItems = JSON.parse(localStorage.getItem("bpt_history") || "[]"); + const newHistory = _.take( + _.uniqBy( + [ + { id: stopDetails.stop_id, text: stopDetails.stop_name, type: SEARCH_RESULT_TYPES.bus_stop }, + ...historyItems + ], + s => `${s.type}-${s.id}`, + ), + MAX_HISTORY_LENGTH, + ); + localStorage.setItem("bpt_history", JSON.stringify(newHistory)); + + } + + toggleFavourite = () => { + const { stopDetails, isFavourited } = this.state; + const currentFavourites = JSON.parse(localStorage.getItem("bpt_favourites") || "[]"); + let newFavourites = []; + if(isFavourited) { + newFavourites = _.filter(currentFavourites, f => !(f.type === "bus_stop" && f.id === stopDetails.stop_id)); + this.setState({ + isFavourited: false, + }); + } else { + newFavourites = [ + { id: stopDetails.stop_id, text: stopDetails.stop_name, type: "bus_stop" }, + ...currentFavourites, + ] + this.setState({ + isFavourited: true, + }); + } + localStorage.setItem("bpt_favourites", JSON.stringify(newFavourites)); + }; + + render() { + const { stopDetails, isFavourited } = this.state; + + return ( + <> +
    + { + !!stopDetails && ( + <> + + +

    + + { stopDetails.stop_name } +

    + +
    + )} + > +
    +

    Buses through this stop

    + { + stopDetails.trips.map(b => ) + } +
    + + + ) + } + + ); + } +}; + +const StopPageWithParams = () => { + const { stop_id: stopId } = useParams(); + return ( + + ) +} + +export default StopPageWithParams; diff --git a/src/utils/api.js b/src/utils/api.js new file mode 100644 index 0000000..e30a4e1 --- /dev/null +++ b/src/utils/api.js @@ -0,0 +1,8 @@ +import axios from "axios"; +import { BACKEND_HOST } from "./constants"; + +export const getSearchResultsApi = (searchText) => axios.get(`${BACKEND_HOST}/enroute/search/?q=${searchText}`); + +export const getRouteDetailsApi = (routeId) => axios.get(`${BACKEND_HOST}/enroute/route/${routeId}`); + +export const getStopDetailsApi = (stopId) => axios.get(`${BACKEND_HOST}/enroute/stop/${stopId}`); diff --git a/src/utils/constants.js b/src/utils/constants.js index ed4e298..31159dd 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -1,14 +1,18 @@ export const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_TOKEN; export const GOOGLE_API_KEY = import.meta.env.VITE_GOOGLE_API_KEY; +export const BACKEND_HOST = import.meta.env.VITE_PUBLIC_BACKEND_HOST; + +export const MAX_HISTORY_LENGTH = 20; export const ROUTES = { home: "/", search: "/search", favourites: "/favourites", + route: "/route/:route_id", + stop: "/stop/:stop_id", + all_buses: "/all-buses", about: "/about", - bus_route: "/bus-route/:route_id", - bus_stop: "/bus-stop/:stop_id", }; export const SEARCH_RESULT_TYPES = { @@ -17,4 +21,11 @@ export const SEARCH_RESULT_TYPES = { metro_station_purple: "metro_station_purple", metro_station_green: "metro_station_green", bus_number: "bus_number", +}; + +export const API_CALL_STATUSES = { + INITIAL: "INITIAL", + PROGRESS: "PROGRESS", + SUCCESS: "SUCCESS", + ERROR: "ERROR", }; \ No newline at end of file diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..d34b10e --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,47 @@ +import { useEffect, useState } from "react"; + +export const useDebouncedValue = (inputValue, delay) => { + const [debouncedValue, setDebouncedValue] = useState(inputValue); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(inputValue); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [inputValue, delay]); + + return debouncedValue; +}; + +export const getUrlParameter = (paramName) => { + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + return urlParams.get(paramName); +}; + +export const setUrlParameter = (paramName, paramValue) => { + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + urlParams.set(paramName, paramValue); + return urlParams; +}; + +export const deleteUrlParameter = (paramName) => { + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + urlParams.delete(paramName); + return urlParams; +}; + +export const afterMapLoad = (map, fn) => { + if(map._loaded) { + fn(); + } else { + map?.on("load", () => { + fn(); + }); + } +}; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index d354d8f..5281c81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -746,11 +746,25 @@ asynciterator.prototype@^1.0.0: dependencies: has-symbols "^1.0.3" +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + available-typed-arrays@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +axios@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.3.tgz#a1125f2faf702bc8e8f2104ec3a76fab40257d85" + integrity sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -837,6 +851,11 @@ chalk@^4.0.0: optionalDependencies: fsevents "~2.3.2" +classnames@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -861,6 +880,13 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -920,6 +946,11 @@ define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0, de has-property-descriptors "^1.0.0" object-keys "^1.1.1" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -1257,6 +1288,11 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -1264,6 +1300,15 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1761,6 +1806,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -1803,6 +1853,18 @@ mapbox-gl@^2.15.0: tinyqueue "^2.0.3" vt-pbf "^3.1.3" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -2016,6 +2078,11 @@ protocol-buffers-schema@^3.3.1: resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03" integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw== +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"