├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── Readme.md ├── frontend ├── .gitignore ├── index.html ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.tsx │ ├── Pages │ │ ├── Home.tsx │ │ ├── SimulatorDashboard.tsx │ │ ├── UserDashboard.tsx │ │ └── dashboard.css │ ├── api.ts │ ├── assets │ │ ├── icon_delivery_associate.svg │ │ ├── icon_drop.svg │ │ ├── icon_pickup.svg │ │ ├── react.svg │ │ └── truck.svg │ ├── components │ │ ├── AppBar.tsx │ │ ├── DraggableMarker.tsx │ │ ├── DriverDashboard.tsx │ │ ├── ShipmentDashboard.tsx │ │ ├── ShipmentInfo.tsx │ │ ├── ShipmentRequest.tsx │ │ └── UserInfo.tsx │ ├── constants.ts │ ├── main.tsx │ ├── types.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock ├── package.json ├── src ├── app.ts ├── config.ts ├── constants.ts ├── controller │ ├── ping.ts │ ├── shipment.ts │ └── user.ts ├── createIndex.ts ├── dbClient.ts ├── models │ ├── DeliveryAssociate.ts │ ├── Shipment.ts │ └── User.ts ├── routes │ └── index.ts ├── seed.ts ├── server.ts ├── services │ ├── deliveryAssociates │ │ ├── createOne.ts │ │ ├── findByEmail.ts │ │ └── updateLocation.ts │ ├── shipments │ │ ├── createOne.ts │ │ ├── updateDA.ts │ │ └── updateStatus.ts │ └── users │ │ ├── createOne.ts │ │ ├── findByEmail.ts │ │ └── findById.ts ├── socketHandler.ts ├── types │ ├── AppRequest.ts │ ├── AppResponse.ts │ ├── IDeliveryAssociate.ts │ ├── IShipment.ts │ ├── IUser.ts │ └── IUserJWTPayload.ts └── watchers │ ├── deliveryAssociates.ts │ └── shipment.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | results 3 | tmp 4 | 5 | # Coverage reports 6 | coverage 7 | 8 | # API keys and secrets 9 | .env 10 | 11 | # Dependency directory 12 | node_modules 13 | 14 | # OS metadata 15 | .DS_Store 16 | Thumbs.db 17 | 18 | # Ignore built ts files 19 | dist/**/* 20 | 21 | # ignore yarn.lock 22 | package-lock.json 23 | 24 | # build artifacts 25 | dist -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/src/server.ts", 15 | "preLaunchTask": "tsc: build - tsconfig.json", 16 | "outFiles": [ 17 | "${workspaceFolder}/dist/**/*.js" 18 | ] 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.semi": true, 3 | "prettier.singleQuote": true, 4 | "prettier.tabWidth": 2, 5 | "prettier.trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Real-time Delivery Service Using MongoDB Change Streams 2 | The project demonstrates the use of MongoDB Change Streams in a real-time location tracking application. The application is a local package delivery service similar to Uber. Check out the entire article on the [MongoDB Developer Center](https://www.mongodb.com/developer/languages/javascript/real-time-tracking-change-streams-socketio/) 3 | 4 | ## Project Description 5 | The Real-time Delivery Service application allows customers to track their package deliveries in real-time. The application utilizes MongoDB Change Streams to listen for document updates like location and shipment status and uses Socket.io to broadcast them to the connected clients. 6 | 7 | ## How to Run the Application 8 | - Clone the repository to your local machine. 9 | - Create a .env file in the root of the project. 10 | - Add the MONGODB_CONNECTION_STRING 11 | - You can easily generate the connection from MongoDB Atlas UI (incase of local instance check this [link](https://www.mongodb.com/docs/drivers/node/current/fundamentals/connection/connect/)) 12 | - Run `npm install` in the root folder to install the required dependencies for the server. 13 | - Run `npm run dev` in the root folder to start the server. 14 | - Open a new terminal and navigate to the `/frontend` folder. 15 | - Run `npm install` to install the required dependencies for the frontend. 16 | - Run `npm run dev` to start the React application. 17 | - The server will be running on http://localhost:5050 and the frontend will be running on http://localhost:5173/ 18 | 19 | ## How to Test the Application 20 | - Make sure you have the application running on your local machine as described in the "How to Run the Application" section above. 21 | - For testing purposes, a user and a driver account have been seeded in the database. 22 | - Open the frontend of the application in one browser window and choose to login as a user. 23 | - In another separate browser window, open the frontend and choose to login as a driver. 24 | - In the user window, create a new shipment and choose the pickup and delivery locations. 25 | - The driver should receive a notification of the new shipment and be able to accept it. 26 | - You can then track the shipment in real-time using the location updates provided by MongoDB Change Streams and broadcasted via Socket.io. 27 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "^11.10.0", 13 | "@emotion/styled": "^11.10.0", 14 | "@fontsource/roboto": "^4.5.8", 15 | "@mui/icons-material": "^5.8.4", 16 | "@mui/material": "^5.10.1", 17 | "axios": "^0.27.2", 18 | "leaflet": "^1.9.3", 19 | "lodash": "^4.17.21", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "react-leaflet": "^4.1.0", 23 | "react-router-dom": "^6.3.0", 24 | "socket.io-client": "^4.5.1" 25 | }, 26 | "devDependencies": { 27 | "@types/leaflet": "^1.9.0", 28 | "@types/lodash": "^4.14.191", 29 | "@types/react": "^18.0.27", 30 | "@types/react-dom": "^18.0.10", 31 | "@types/react-router": "^5.1.20", 32 | "@types/react-router-dom": "^5.3.3", 33 | "@vitejs/plugin-react": "^3.0.1", 34 | "typescript": "^4.9.3", 35 | "vite": "^4.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; 2 | import AppBar from './components/AppBar'; 3 | import UserDashboard from './Pages/UserDashboard'; 4 | import SimulatorDashboard from './Pages/SimulatorDashboard'; 5 | import Home from './Pages/Home'; 6 | 7 | function App() { 8 | return ( 9 | 10 | 11 | 12 | } /> 13 | } /> 14 | } /> 15 | 404 Page Not Found} /> 16 | 17 | 18 | ); 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /frontend/src/Pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import Card from '@mui/material/Card'; 3 | import CardContent from '@mui/material/CardContent'; 4 | import Typography from '@mui/material/Typography'; 5 | import { CardActionArea } from '@mui/material'; 6 | import Stack from '@mui/material/Stack'; 7 | import PersonIcon from '@mui/icons-material/Person'; 8 | import LocalShippingIcon from '@mui/icons-material/LocalShipping'; 9 | 10 | export default function Home() { 11 | const navigate = useNavigate(); 12 | 13 | function CardButton({ 14 | pageName, 15 | title, 16 | Icon, 17 | }: { 18 | pageName: string; 19 | title: string; 20 | Icon: any; 21 | }) { 22 | return ( 23 | 24 | navigate(`/${pageName}`)}> 25 | 26 | 27 | 28 | 29 | {title} 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | 38 | return ( 39 | <> 40 |
49 |
50 | 51 | Welcome to Delivery Service 52 | 53 |
54 |
55 | 60 |
61 |
62 | 67 |
68 |
69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /frontend/src/Pages/SimulatorDashboard.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef, useMemo } from 'react'; 2 | import io from 'socket.io-client'; 3 | import { useParams } from 'react-router-dom'; 4 | import L, { LatLng } from 'leaflet'; 5 | import 'leaflet/dist/leaflet.css'; 6 | import throttle from 'lodash/throttle'; 7 | import { 8 | MapContainer, 9 | TileLayer, 10 | Marker, 11 | Popup, 12 | useMapEvents, 13 | } from 'react-leaflet'; 14 | 15 | import { DRIVER_EMAIL_DEFAULT, API_URL, socketEvents } from '../constants'; 16 | 17 | import { 18 | IShipment, 19 | ShipmentStatus, 20 | IDeliveryAssociate, 21 | IUpdateDALocation, 22 | } from '../types'; 23 | 24 | import iconDeliveryAssociate from '../assets/icon_delivery_associate.svg'; 25 | import iconPickup from '../assets/icon_pickup.svg'; 26 | import iconDrop from '../assets/icon_drop.svg'; 27 | 28 | import DriverDashboard from '../components/DriverDashboard'; 29 | import ShipmentDashboard from '../components/ShipmentDashboard'; 30 | 31 | import './dashboard.css'; 32 | 33 | const initialValues: { 34 | zoom: number; 35 | center: [number, number]; 36 | scrollWheelZoom: boolean; 37 | } = { 38 | zoom: 15, 39 | center: [13.092123232608643, 80.28222309087568], 40 | scrollWheelZoom: true, 41 | }; 42 | const mapContainerStyle = { 43 | width: '100%', 44 | height: '99vh', 45 | }; 46 | const THROTTLE_DELAY = 50; 47 | const socket = io(API_URL); 48 | 49 | const SimulatorDashboard = () => { 50 | const [deliveryAssociate, setDeliveryAssociate] = 51 | // @ts-ignore 52 | useState({}); 53 | // @ts-ignore 54 | const [shipmentData, setShipmentData] = useState({}); 55 | const [isConnected, setIsConnected] = useState(socket.connected); 56 | const [draggable, setDraggable] = useState(true); 57 | const [position, setPosition] = useState(initialValues.center); 58 | 59 | useEffect(() => { 60 | // Setting the seeded driver email to identify a logged in driver 61 | // Modify this part as per your needs like setting an auth token 62 | sessionStorage.setItem('driverEmail', DRIVER_EMAIL_DEFAULT); 63 | 64 | // Establish Socket 65 | socket.on('connect', () => { 66 | setIsConnected(true); 67 | }); 68 | 69 | socket.on('disconnect', () => { 70 | setIsConnected(false); 71 | }); 72 | 73 | // on Unmount 74 | return () => { 75 | socket.off('connect'); 76 | socket.off('disconnect'); 77 | socket.off('pong'); 78 | }; 79 | }, []); 80 | 81 | const gpsUpdate = (position: any) => { 82 | if (position instanceof LatLng) { 83 | const email = sessionStorage.getItem('driverEmail') || ''; 84 | const data: IUpdateDALocation = { 85 | email: email, 86 | location: { type: 'Point', coordinates: [position.lng, position.lat] }, 87 | }; 88 | socket.emit(socketEvents.UPDATE_DA_LOCATION, data); 89 | } 90 | }; 91 | 92 | // position side effects 93 | useEffect(() => { 94 | gpsUpdate(position); 95 | }, [position]); 96 | 97 | const throttledPositionUpdate = throttle(function (position) { 98 | console.log('throttled position', position); 99 | gpsUpdate(position); 100 | }, THROTTLE_DELAY); 101 | 102 | function DraggableMarker() { 103 | const markerIcon = L.icon({ 104 | iconUrl: iconDeliveryAssociate, 105 | iconSize: [35, 35], // size of the icon 106 | popupAnchor: [-3, -20], // point from which the popup should open relative to the iconAnchor 107 | className: 'marker', 108 | }); 109 | const markerRef = useRef(null); 110 | const eventHandlers = useMemo( 111 | () => ({ 112 | dragend() { 113 | const marker = markerRef.current; 114 | if (marker != null) { 115 | // @ts-ignore 116 | setPosition(marker.getLatLng()); 117 | } 118 | }, 119 | drag() { 120 | const marker = markerRef.current; 121 | if (marker != null) { 122 | // @ts-ignore 123 | throttledPositionUpdate(marker.getLatLng()); 124 | } 125 | }, 126 | }), 127 | [] 128 | ); 129 | 130 | return ( 131 | 138 | ); 139 | } 140 | const PickUpMarker = () => { 141 | const markerIcon = L.icon({ 142 | iconUrl: iconPickup, 143 | iconSize: [70, 50], // size of the icon 144 | popupAnchor: [-3, -20], // point from which the popup should open relative to the iconAnchor 145 | }); 146 | try { 147 | const shipment = shipmentData as IShipment; 148 | const coordinates = shipment?.pickupLocation?.coordinates; 149 | return Array.isArray(coordinates) ? ( 150 | 151 | Pickup location 152 | 153 | ) : null; 154 | } catch (error) { 155 | console.error(error); 156 | return null; 157 | } 158 | }; 159 | const DropLocationMarker = () => { 160 | const markerIcon = L.icon({ 161 | iconUrl: iconDrop, 162 | iconSize: [50, 40], // size of the icon 163 | popupAnchor: [-3, -20], // point from which the popup should open relative to the iconAnchor 164 | }); 165 | try { 166 | const shipment = shipmentData as IShipment; 167 | const coordinates = shipment?.dropLocation?.coordinates; 168 | return Array.isArray(coordinates) ? ( 169 | 170 | Delivery location 171 | 172 | ) : null; 173 | } catch (error) { 174 | console.error(error); 175 | return null; 176 | } 177 | }; 178 | 179 | return ( 180 |
181 |
182 |
183 | 184 | {shipmentData._id ? ( 185 | 189 | ) : null} 190 |
191 |
192 |
193 |
194 | 195 | 199 | 200 | {shipmentData._id && 201 | shipmentData.status !== ShipmentStatus.delivered ? ( 202 | 203 | ) : null} 204 | {shipmentData._id && 205 | shipmentData.status !== ShipmentStatus.delivered ? ( 206 | 207 | ) : null} 208 | 209 |
210 |
211 |
212 | ); 213 | }; 214 | export default SimulatorDashboard; 215 | -------------------------------------------------------------------------------- /frontend/src/Pages/UserDashboard.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useReducer } from 'react'; 2 | import io from 'socket.io-client'; 3 | import { LatLng } from 'leaflet'; 4 | import { Point } from 'geojson'; 5 | import { MapContainer, TileLayer } from 'react-leaflet'; 6 | import Button from '@mui/material/Button'; 7 | import 'leaflet/dist/leaflet.css'; 8 | 9 | import UserInfo from '../components/UserInfo'; 10 | import DraggableMarker from '../components/DraggableMarker'; 11 | import ShipmentInfo from '../components/ShipmentInfo'; 12 | import { 13 | DashboardStatus, 14 | State, 15 | IAction, 16 | IShipment, 17 | ShipmentStatus, 18 | } from '../types'; 19 | import { 20 | USER_EMAIL_DEFAULT, 21 | pickupMarkerIcon, 22 | dropMarkerIcon, 23 | driverMarkerIcon, 24 | API_URL, 25 | socketEvents, 26 | mapInitialViewProps, 27 | ACTIONS, 28 | } from '../constants'; 29 | import { createShipment } from '../api'; 30 | import './dashboard.css'; 31 | 32 | const socket = io(API_URL); 33 | 34 | const initialState: State = { 35 | pickupLocation: new LatLng(13.102971824499635, 80.27971744537354), 36 | isPickupDraggable: false, 37 | isShowPickupMarker: false, 38 | dropLocation: new LatLng(13.092123232608643, 80.28222309087568), 39 | isDropDraggable: false, 40 | isShowDropMarker: false, 41 | driverLocation: null, 42 | dashboardStatus: DashboardStatus.NO_SHIPMENT, 43 | }; 44 | 45 | function reducer(state: State, action: IAction): State { 46 | switch (action.type) { 47 | case ACTIONS.NEW_DELIVERY_CLICKED: 48 | return { 49 | ...state, 50 | isPickupDraggable: true, 51 | isShowPickupMarker: true, 52 | dashboardStatus: DashboardStatus.SHIPMENT_INITIATED, 53 | }; 54 | case ACTIONS.SET_DRIVER_LOCATION: 55 | return { 56 | ...state, 57 | driverLocation: action.payload.position, // position should be Leaflet LatLng 58 | }; 59 | case ACTIONS.SET_PICKUP_LOCATION: 60 | return { 61 | ...state, 62 | pickupLocation: action.payload.position, // position should be Leaflet LatLng 63 | }; 64 | case ACTIONS.SET_DROP_LOCATION: 65 | return { 66 | ...state, 67 | dropLocation: action.payload.position, // position should be Leaflet LatLng 68 | }; 69 | case ACTIONS.PICKUP_SELECTED: 70 | return { 71 | ...state, 72 | isPickupDraggable: false, 73 | isDropDraggable: true, 74 | isShowDropMarker: true, 75 | dashboardStatus: DashboardStatus.PICKUP_SELECTED, 76 | }; 77 | case ACTIONS.DROP_SELECTED: 78 | return { 79 | ...state, 80 | isDropDraggable: false, 81 | dashboardStatus: DashboardStatus.DROP_SELECTED, 82 | }; 83 | case ACTIONS.ASSOCIATE_ASSIGNED: 84 | return { 85 | ...state, 86 | dashboardStatus: DashboardStatus.ASSOCIATE_ASSIGNED, 87 | }; 88 | case ACTIONS.PICKUP_LOCATION_REACHED: 89 | return { 90 | ...state, 91 | dashboardStatus: DashboardStatus.PICKUP_LOCATION_REACHED, 92 | }; 93 | case ACTIONS.TRANSPORTING: 94 | return { 95 | ...state, 96 | dashboardStatus: DashboardStatus.TRANSPORTING, 97 | }; 98 | case ACTIONS.DROP_LOCATION_REACHED: 99 | return { 100 | ...state, 101 | dashboardStatus: DashboardStatus.DROP_LOCATION_REACHED, 102 | }; 103 | case ACTIONS.DELIVERED: 104 | return { 105 | ...state, 106 | dashboardStatus: DashboardStatus.DELIVERED, 107 | }; 108 | default: 109 | console.log('default action'); 110 | return state; 111 | } 112 | } 113 | 114 | const shipmentStatusActionMapper: Record = { 115 | requested: { type: 'Default' }, 116 | deliveryAssociateAssigned: { type: ACTIONS.ASSOCIATE_ASSIGNED }, 117 | pickupLocationReached: { type: ACTIONS.PICKUP_LOCATION_REACHED }, 118 | transporting: { type: ACTIONS.TRANSPORTING }, 119 | dropLocationReached: { type: ACTIONS.DROP_LOCATION_REACHED }, 120 | delivered: { type: ACTIONS.DELIVERED }, 121 | cancelled: { type: ACTIONS.CANCELLED }, 122 | }; 123 | 124 | const UserDashboard = () => { 125 | const [isConnected, setIsConnected] = useState(socket.connected); 126 | const [state, dispatch] = useReducer(reducer, initialState); 127 | 128 | useEffect(() => { 129 | // Setting seeded user's email as logged in user 130 | // You can change this to update logged in user email or id 131 | sessionStorage.setItem('userEmail', USER_EMAIL_DEFAULT); 132 | 133 | // Establish Socket 134 | socket.on('connect', () => { 135 | setIsConnected(true); 136 | }); 137 | 138 | socket.on('disconnect', () => { 139 | setIsConnected(false); 140 | }); 141 | socket.on(socketEvents.DA_LOCATION_CHANGED, (data) => { 142 | try { 143 | const coorArr = data?.currentLocation?.coordinates; 144 | const isNumberType = (value: any) => typeof value === 'number'; 145 | if ( 146 | Array.isArray(coorArr) && 147 | coorArr.length === 2 && 148 | isNumberType(coorArr[0]) && 149 | isNumberType(coorArr[1]) 150 | ) { 151 | const lat = coorArr[1]; 152 | const lng = coorArr[0]; 153 | const newLocation = new LatLng(lat, lng); 154 | const action = { 155 | type: ACTIONS.SET_DRIVER_LOCATION, 156 | payload: { position: newLocation }, 157 | }; 158 | dispatch(action); 159 | } 160 | } catch (error) { 161 | console.error(error); 162 | } 163 | }); 164 | 165 | // Listens to Shipment updates once subscribed 166 | socket.on(socketEvents.SHIPMENT_UPDATED, (data: IShipment) => { 167 | try { 168 | console.log({ data }); 169 | // Subscribe to delivery associate 170 | if (data.deliveryAssociateId) { 171 | socket.emit(socketEvents.SUBSCRIBE_TO_DA, { 172 | deliveryAssociateId: data.deliveryAssociateId, 173 | }); 174 | } 175 | // Dispatch Action on Shipment status change 176 | if (data.status) { 177 | dispatch(shipmentStatusActionMapper[data.status]); 178 | } 179 | } catch (error) { 180 | console.error(error); 181 | } 182 | }); 183 | 184 | // on Unmount 185 | return () => { 186 | socket.off('connect'); 187 | socket.off('disconnect'); 188 | socket.off('pong'); 189 | }; 190 | }, []); 191 | 192 | const setPickupLocation = (position: LatLng) => { 193 | const action = { 194 | type: ACTIONS.SET_PICKUP_LOCATION, 195 | payload: { position }, 196 | }; 197 | dispatch(action); 198 | }; 199 | const setDropLocation = (position: LatLng) => { 200 | const action = { 201 | type: ACTIONS.SET_DROP_LOCATION, 202 | payload: { position }, 203 | }; 204 | dispatch(action); 205 | }; 206 | const onNewDeliveryClick = () => { 207 | const action = { 208 | type: ACTIONS.NEW_DELIVERY_CLICKED, 209 | payload: {}, 210 | }; 211 | dispatch(action); 212 | }; 213 | const onPickupSelected = () => { 214 | const action = { type: ACTIONS.PICKUP_SELECTED, payload: {} }; 215 | dispatch(action); 216 | }; 217 | const onDropSelected = async () => { 218 | try { 219 | const action = { type: ACTIONS.DROP_SELECTED, payload: {} }; 220 | await dispatch(action); 221 | const pickupPoint: Point = { 222 | type: 'Point', 223 | coordinates: [state.pickupLocation.lng, state.pickupLocation.lat], 224 | }; 225 | const dropPoint: Point = { 226 | type: 'Point', 227 | coordinates: [state.dropLocation.lng, state.dropLocation.lat], 228 | }; 229 | // Call API to Create new Shipment 230 | const createShipmentOp = await createShipment(pickupPoint, dropPoint); 231 | const shipment = createShipmentOp.data; 232 | // Subscribe to MongoDB Change Stream via Socket io for the created Shipment 233 | socket.emit(socketEvents.SUBSCRIBE_TO_SHIPMENT, { 234 | shipmentId: shipment._id, 235 | }); 236 | } catch (error) { 237 | console.error(error); 238 | } 239 | }; 240 | 241 | const ButtonNewDelivery = () => { 242 | return ( 243 | 251 | ); 252 | }; 253 | const ButtonConfirmPickUp = () => { 254 | return ( 255 | 263 | ); 264 | }; 265 | const ButtonConfirmDrop = () => { 266 | return ( 267 | 275 | ); 276 | }; 277 | 278 | return ( 279 |
280 |
281 | 282 | {/* Shipment info */} 283 |
284 | 285 |
286 | {/* Action button */} 287 |
288 | {state.dashboardStatus === DashboardStatus.NO_SHIPMENT && ( 289 | 290 | )} 291 | {state.dashboardStatus === DashboardStatus.SHIPMENT_INITIATED && ( 292 | 293 | )} 294 | {state.dashboardStatus === DashboardStatus.PICKUP_SELECTED && ( 295 | 296 | )} 297 |
298 |
299 |
300 | 304 | 308 | {/* Pickup Marker */} 309 | {state.isShowPickupMarker ? ( 310 | 319 | ) : null} 320 | {/* Drop Location Marker */} 321 | {state.isShowDropMarker ? ( 322 | 331 | ) : null} 332 | {/* Driver Location Marker */} 333 | {state.driverLocation && ( 334 | 341 | )} 342 | 343 |
344 |
345 | ); 346 | }; 347 | export default UserDashboard; 348 | -------------------------------------------------------------------------------- /frontend/src/Pages/dashboard.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | } 4 | 5 | .col-1 { 6 | flex: 1; 7 | padding: 15px 20px; 8 | } 9 | 10 | .col-2 { 11 | flex: 1; 12 | border: 2px solid black; 13 | margin: 10px; 14 | } 15 | 16 | .marker { 17 | z-index: 10000 !important; 18 | } 19 | 20 | .flex-center { 21 | display: flex; 22 | justify-content: center; 23 | margin: 20px 0; 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Point } from 'geojson'; 3 | import { API_URL, USER_EMAIL_DEFAULT } from './constants'; 4 | import { ShipmentStatus, ShipmentRes } from './types'; 5 | 6 | export const createShipment = async ( 7 | pickupLocation: Point, 8 | dropLocation: Point 9 | ): Promise => { 10 | const email = sessionStorage.getItem('userEmail') || USER_EMAIL_DEFAULT; 11 | const data = { pickupLocation, dropLocation, email }; 12 | const config = { 13 | method: 'post', 14 | url: `${API_URL}/shipment`, 15 | data: data, 16 | }; 17 | const response = await axios(config); 18 | return response.data as any; 19 | }; 20 | 21 | export const updateShipmentStatus = async ( 22 | id: string, 23 | status: ShipmentStatus 24 | ): Promise => { 25 | const requestBody = { status }; 26 | try { 27 | const response = await axios.patch( 28 | `${API_URL}/shipment/${id}/status`, 29 | requestBody 30 | ); 31 | return response.data as ShipmentRes; 32 | } catch (error) { 33 | throw error; 34 | } 35 | }; 36 | 37 | export const updateShipmentDeliveryAssociate = async ( 38 | id: string, 39 | email: string 40 | ): Promise => { 41 | const requestBody = { email }; 42 | try { 43 | const response = await axios.patch( 44 | `${API_URL}/shipment/${id}/delivery-associate`, 45 | requestBody 46 | ); 47 | return response.data as ShipmentRes; 48 | } catch (error) { 49 | throw error; 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /frontend/src/assets/icon_delivery_associate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /frontend/src/assets/icon_drop.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | 13 | 15 | 17 | 19 | -------------------------------------------------------------------------------- /frontend/src/assets/icon_pickup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/truck.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /frontend/src/components/AppBar.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import AppBar from '@mui/material/AppBar'; 3 | import Toolbar from '@mui/material/Toolbar'; 4 | import Typography from '@mui/material/Typography'; 5 | 6 | export default function MainAppBar() { 7 | const navigate = useNavigate(); 8 | return ( 9 | <> 10 | 11 | 12 | { 17 | navigate('/'); 18 | }} 19 | style={{ 20 | cursor: 'pointer', 21 | maxWidth: 'fit-content', 22 | }} 23 | > 24 | Delivery Service 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/components/DraggableMarker.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useMemo, useEffect } from 'react'; 2 | import { Marker, Popup } from 'react-leaflet'; 3 | import { LatLng } from 'leaflet'; 4 | import isEqual from 'lodash/isEqual'; 5 | 6 | type PropTypes = { 7 | markerIcon: L.Icon; 8 | isDraggable: boolean; 9 | position: LatLng; 10 | setPosition?: Function; 11 | markerName: string; 12 | popupMsg?: string; 13 | }; 14 | function DraggableMarker(props: PropTypes) { 15 | const markerRef = useRef(null); 16 | 17 | useEffect(() => { 18 | const marker = markerRef.current; 19 | // @ts-ignore 20 | if (marker != null) marker.openPopup(); 21 | }, []); 22 | 23 | const eventHandlers = useMemo( 24 | () => ({ 25 | dragend() { 26 | const marker = markerRef.current; 27 | if (marker != null) { 28 | // @ts-ignore 29 | props.setPosition(marker.getLatLng()); 30 | } 31 | }, 32 | }), 33 | [] 34 | ); 35 | return ( 36 | 43 | {props.popupMsg && {props.popupMsg}} 44 | 45 | ); 46 | } 47 | export default React.memo(DraggableMarker, (prev, next) => { 48 | if ( 49 | prev.markerName === 'Driver-marker' || 50 | next.markerName === 'Driver-marker' 51 | ) { 52 | return false; 53 | } 54 | return ( 55 | isEqual(prev.position, next.position) || 56 | prev.isDraggable === next.isDraggable 57 | ); 58 | }); 59 | -------------------------------------------------------------------------------- /frontend/src/components/DriverDashboard.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import Typography from '@mui/material/Typography'; 3 | import Card from '@mui/material/Card'; 4 | import CardContent from '@mui/material/CardContent'; 5 | import Box from '@mui/material/Box'; 6 | import Stack from '@mui/material/Stack'; 7 | 8 | import ShipmentRequest from './ShipmentRequest'; 9 | import { socketEvents } from '../constants'; 10 | import { IDeliveryAssociate, IShipment, ShipmentStatus } from '../types'; 11 | import { updateShipmentStatus, updateShipmentDeliveryAssociate } from '../api'; 12 | 13 | type Props = { 14 | socket: any; 15 | setShipmentData: any; 16 | }; 17 | 18 | const DriverDashboard = (props: Props) => { 19 | // @ts-ignore 20 | const [newShipmentRequest, setNewShipmentRequest] = useState({}); 21 | 22 | const email = sessionStorage.getItem('driverEmail') || ''; 23 | const name = email.substring(0, email.indexOf('@')); 24 | const nameCaseCorrected = name.charAt(0).toUpperCase() + name.slice(1); 25 | 26 | useEffect(() => { 27 | props.socket.on(socketEvents.SHIPMENT_CREATED, (data: any) => { 28 | setNewShipmentRequest(data); 29 | }); 30 | }, []); 31 | 32 | const onAccept = async () => { 33 | await updateShipmentStatus( 34 | newShipmentRequest?._id, 35 | ShipmentStatus.deliveryAssociateAssigned 36 | ); 37 | const email = sessionStorage.getItem('driverEmail') || ''; 38 | const shipmentData = await updateShipmentDeliveryAssociate( 39 | newShipmentRequest?._id, 40 | email 41 | ); 42 | props.setShipmentData(shipmentData.data); 43 | // @ts-ignore 44 | setNewShipmentRequest({}); 45 | }; 46 | const onReject = () => { 47 | // @ts-ignore 48 | setNewShipmentRequest({}); 49 | }; 50 | return ( 51 |
56 | 57 | 58 | 59 | Associate Details 60 | 61 | 66 | 67 | 68 | Email: 69 | {email} 70 | 71 | 72 | Name: 73 | {nameCaseCorrected} 74 | 75 | 76 | 77 | 78 | 79 |
80 | {/* New Shipment Notification */} 81 | {newShipmentRequest._id ? ( 82 | 83 | ) : null} 84 |
85 |
86 | ); 87 | }; 88 | 89 | export default DriverDashboard; 90 | -------------------------------------------------------------------------------- /frontend/src/components/ShipmentDashboard.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@mui/material/Button'; 2 | import Typography from '@mui/material/Typography'; 3 | import Card from '@mui/material/Card'; 4 | import CardContent from '@mui/material/CardContent'; 5 | import Box from '@mui/material/Box'; 6 | import Stack from '@mui/material/Stack'; 7 | import Divider from '@mui/material/Divider'; 8 | import { IShipment, ShipmentStatus } from '../types'; 9 | import { updateShipmentStatus } from '../api'; 10 | 11 | type Props = { 12 | shipmentData: IShipment; 13 | setShipmentData: any; 14 | }; 15 | const statusDisplayName: Record = { 16 | [ShipmentStatus.deliveryAssociateAssigned]: 'Delivery Associate Assigned', 17 | [ShipmentStatus.pickupLocationReached]: 'Reached Pick up location', 18 | [ShipmentStatus.dropLocationReached]: 'Reached Drop location', 19 | [ShipmentStatus.transporting]: 'Transporting', 20 | [ShipmentStatus.delivered]: 'Delivered', 21 | [ShipmentStatus.requested]: 'Requested', 22 | [ShipmentStatus.cancelled]: 'Cancelled', 23 | }; 24 | 25 | type UpdateAction = { 26 | actionName: string; 27 | statusToUpdate: ShipmentStatus; 28 | }; 29 | 30 | const ShipmentDashboard = (props: Props) => { 31 | const { shipmentData, setShipmentData } = props; 32 | // Function to determine the next status action based on current status 33 | const updateAction = (): UpdateAction => { 34 | const currentStatus: ShipmentStatus = shipmentData.status; 35 | const pickupLocationReached = { 36 | actionName: statusDisplayName[ShipmentStatus.pickupLocationReached], 37 | statusToUpdate: ShipmentStatus.pickupLocationReached, 38 | }; 39 | const transporting = { 40 | actionName: statusDisplayName[ShipmentStatus.transporting], 41 | statusToUpdate: ShipmentStatus.transporting, 42 | }; 43 | const dropLocationReached = { 44 | actionName: statusDisplayName[ShipmentStatus.dropLocationReached], 45 | statusToUpdate: ShipmentStatus.dropLocationReached, 46 | }; 47 | const delivered = { 48 | actionName: statusDisplayName[ShipmentStatus.delivered], 49 | statusToUpdate: ShipmentStatus.delivered, 50 | }; 51 | let returnObj: UpdateAction; 52 | switch (currentStatus) { 53 | case ShipmentStatus.deliveryAssociateAssigned: 54 | returnObj = pickupLocationReached; 55 | break; 56 | case ShipmentStatus.pickupLocationReached: 57 | returnObj = transporting; 58 | break; 59 | case ShipmentStatus.transporting: 60 | returnObj = dropLocationReached; 61 | break; 62 | case ShipmentStatus.dropLocationReached: 63 | returnObj = delivered; 64 | break; 65 | default: 66 | returnObj = delivered; 67 | break; 68 | } 69 | return returnObj; 70 | }; 71 | 72 | const onShipmentStatusUpdate = async (statusToUpdate: ShipmentStatus) => { 73 | const updatedShipmentData = await updateShipmentStatus( 74 | shipmentData._id, 75 | statusToUpdate 76 | ); 77 | setShipmentData(updatedShipmentData.data); 78 | }; 79 | return ( 80 | <> 81 | {shipmentData._id ? ( 82 |
83 |
88 | 89 | 90 | 91 | Shipment details 92 | 93 | 98 | 99 | 100 | Id: {shipmentData?._id} 101 | 102 | 103 | 104 | Status: {statusDisplayName[shipmentData.status]} 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 |
113 |
120 | 129 |
130 |
131 | ) : null} 132 | 133 | ); 134 | }; 135 | export default ShipmentDashboard; 136 | -------------------------------------------------------------------------------- /frontend/src/components/ShipmentInfo.tsx: -------------------------------------------------------------------------------- 1 | import Alert from '@mui/material/Alert'; 2 | import AlertTitle from '@mui/material/AlertTitle'; 3 | import Stack from '@mui/material/Stack'; 4 | import { DashboardStatus } from '../types'; 5 | import { infoMsgs } from '../constants'; 6 | 7 | type PropType = { 8 | dashboardStatus: DashboardStatus; 9 | }; 10 | export default function ShipmentInfo(props: PropType) { 11 | const { title, msg } = infoMsgs[props.dashboardStatus]; 12 | const severity = 13 | props.dashboardStatus === DashboardStatus.DELIVERED ? 'success' : 'info'; 14 | return ( 15 | <> 16 | {title && msg ? ( 17 | 18 | 19 | 20 | {title} 21 | 22 | {msg} 23 | 24 | 25 | ) : null} 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/components/ShipmentRequest.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@mui/material/Button'; 2 | import Typography from '@mui/material/Typography'; 3 | import Card from '@mui/material/Card'; 4 | import CardContent from '@mui/material/CardContent'; 5 | 6 | const style = { 7 | display: 'flex', 8 | justifyContent: 'space-evenly', 9 | }; 10 | 11 | type Props = { 12 | onAccept: any; 13 | onReject: any; 14 | }; 15 | const ShipmentRequest = (props: Props) => { 16 | return ( 17 |
18 | 19 | 20 | 21 | New Shipment Available 22 | 23 |
24 | 32 | 40 |
41 |
42 |
43 |
44 | ); 45 | }; 46 | export default ShipmentRequest; 47 | -------------------------------------------------------------------------------- /frontend/src/components/UserInfo.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import Card from '@mui/material/Card'; 3 | import Box from '@mui/material/Box'; 4 | import Avatar from '@mui/material/Avatar'; 5 | import Typography from '@mui/material/Typography'; 6 | import Stack from '@mui/material/Stack'; 7 | import Divider from '@mui/material/Divider'; 8 | 9 | const initialUserData = { 10 | email: '', 11 | name: '', 12 | }; 13 | 14 | const UserInfo = () => { 15 | const [userData, setUserData] = useState(initialUserData); 16 | 17 | useEffect(() => { 18 | const email = sessionStorage.getItem('userEmail') || ''; 19 | const name = email.substring(0, email.indexOf('@')); 20 | const nameCaseCorrected = name.charAt(0).toUpperCase() + name.slice(1); 21 | setUserData({ email, name: nameCaseCorrected }); 22 | }, []); 23 | 24 | return ( 25 | 26 | 27 | 32 | 33 | {userData.name} 34 | {userData.email} 35 | 36 | 37 | 38 | 39 | 45 | 46 | ); 47 | }; 48 | export default UserInfo; 49 | -------------------------------------------------------------------------------- /frontend/src/constants.ts: -------------------------------------------------------------------------------- 1 | import L from 'leaflet'; 2 | import iconPickup from './assets/icon_pickup.svg'; 3 | import iconDrop from './assets/icon_drop.svg'; 4 | import iconDriver from './assets/truck.svg'; 5 | import { IInfo, DashboardStatus } from './types'; 6 | 7 | export const DRIVER_EMAIL_DEFAULT = 'john@example.com'; 8 | export const USER_EMAIL_DEFAULT = 'adam@example.com'; 9 | 10 | export const API_URL = 'http://localhost:5050'; 11 | 12 | export const pickupMarkerIcon = L.icon({ 13 | iconUrl: iconPickup, 14 | iconSize: [50, 50], // size of the icon 15 | popupAnchor: [-3, -20], // point from which the popup should open relative to the iconAnchor 16 | className: 'marker', 17 | }); 18 | 19 | export const dropMarkerIcon = L.icon({ 20 | iconUrl: iconDrop, 21 | iconSize: [50, 50], // size of the icon 22 | popupAnchor: [-3, -20], // point from which the popup should open relative to the iconAnchor 23 | className: 'marker', 24 | }); 25 | 26 | export const driverMarkerIcon = L.icon({ 27 | iconUrl: iconDriver, 28 | iconSize: [35, 35], // size of the icon 29 | popupAnchor: [-3, -20], // point from which the popup should open relative to the iconAnchor 30 | className: 'marker', 31 | }); 32 | 33 | export const socketEvents = { 34 | SUBSCRIBE_TO_SHIPMENT: 'SUBSCRIBE_TO_SHIPMENT', 35 | SUBSCRIBE_TO_DA: 'SUBSCRIBE_TO_DA', 36 | DA_LOCATION_CHANGED: 'DA_LOCATION_CHANGED', 37 | LEAVE_ROOM: 'LEAVE_ROOM', 38 | SHIPMENT_UPDATED: 'SHIPMENT_UPDATED', 39 | SHIPMENT_CREATED: 'SHIPMENT_CREATED', 40 | UPDATE_DA_LOCATION: 'UPDATE_DA_LOCATION', 41 | }; 42 | 43 | export const infoMsgs: Record = { 44 | SHIPMENT_INITIATED: { 45 | title: 'Select Pickup Location', 46 | msg: 'Move the Pickup marker on the map to choose your pickup location. Click confirm to continue.', 47 | }, 48 | PICKUP_SELECTED: { 49 | title: 'Select Delivery Location', 50 | msg: 'Move the Flag marker on the map to choose your Delivery location. Click confirm to continue.', 51 | }, 52 | DROP_SELECTED: { 53 | title: 'Searching for delivery Associates', 54 | msg: 'Please wait, we are looking for associates to handle your delivery', 55 | }, 56 | NO_SHIPMENT: { title: '', msg: '' }, 57 | SEARCHING_ASSOCIATES: { title: '', msg: '' }, 58 | ASSOCIATE_ASSIGNED: { 59 | title: 'Delivery Associate Assigned', 60 | msg: 'An Associate has been assigned to handle your delivery and will soon reach your pickup location', 61 | }, 62 | PICKUP_LOCATION_REACHED: { 63 | title: 'Associate reached Pickup location', 64 | msg: 'Our Associate has arrived at the pickup location', 65 | }, 66 | TRANSPORTING: { 67 | title: 'Transporting', 68 | msg: 'Your package is getting delivered', 69 | }, 70 | DROP_LOCATION_REACHED: { 71 | title: 'Reached Delivery location', 72 | msg: 'Associate reached delivery location', 73 | }, 74 | DELIVERED: { 75 | title: 'Delivered', 76 | msg: 'The package has been delivered successfully', 77 | }, 78 | CANCELLED: { title: '', msg: '' }, 79 | }; 80 | 81 | export const ACTIONS = { 82 | FIRST_LOAD: 'FIRST_LOAD', 83 | NEW_DELIVERY_CLICKED: 'NEW_DELIVERY_CLICKED', 84 | SET_DRIVER_LOCATION: 'SET_DRIVER_LOCATION', 85 | SET_PICKUP_LOCATION: 'SET_PICKUP_LOCATION', 86 | SET_DROP_LOCATION: 'SET_DROP_LOCATION', 87 | ...DashboardStatus, 88 | }; 89 | 90 | export const mapInitialViewProps: { 91 | zoom: number; 92 | center: [number, number]; 93 | scrollWheelZoom: boolean; 94 | } = { 95 | zoom: 15, 96 | center: [13.092123232608643, 80.28222309087568], 97 | scrollWheelZoom: true, 98 | }; 99 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | 5 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /frontend/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Point } from 'geojson'; 2 | import { LatLng } from 'leaflet'; 3 | 4 | export interface AppRes { 5 | data: any; 6 | isError: boolean; 7 | errMsg?: string; 8 | } 9 | 10 | export enum ShipmentStatus { 11 | requested = 'requested', 12 | deliveryAssociateAssigned = 'deliveryAssociateAssigned', 13 | pickupLocationReached = 'pickupLocationReached', 14 | transporting = 'transporting', 15 | dropLocationReached = 'dropLocationReached', 16 | delivered = 'delivered', 17 | cancelled = 'cancelled', 18 | } 19 | export interface IShipment { 20 | _id: string; 21 | pickupLocation: Point; 22 | dropLocation: Point; 23 | userId: string; 24 | status: ShipmentStatus; 25 | deliveryAssociateId?: string; 26 | } 27 | export interface ShipmentRes extends AppRes { 28 | data: IShipment; 29 | } 30 | 31 | export interface IUser { 32 | _id: string; 33 | email: string; 34 | name: string; 35 | organization: string; 36 | roles: Array; 37 | } 38 | 39 | export interface UserRes extends AppRes { 40 | data: IUser; 41 | } 42 | 43 | export interface TokenRes extends AppRes { 44 | data: { token: string }; 45 | } 46 | 47 | export enum DashboardStatus { 48 | NO_SHIPMENT = 'NO_SHIPMENT', 49 | SHIPMENT_INITIATED = 'SHIPMENT_INITIATED', 50 | PICKUP_SELECTED = 'PICKUP_SELECTED', 51 | DROP_SELECTED = 'DROP_SELECTED', 52 | SEARCHING_ASSOCIATES = 'SEARCHING_ASSOCIATES', 53 | ASSOCIATE_ASSIGNED = 'ASSOCIATE_ASSIGNED', 54 | PICKUP_LOCATION_REACHED = 'PICKUP_LOCATION_REACHED', 55 | TRANSPORTING = 'TRANSPORTING', 56 | DROP_LOCATION_REACHED = 'DROP_LOCATION_REACHED', 57 | DELIVERED = 'DELIVERED', 58 | CANCELLED = 'CANCELLED', 59 | } 60 | export interface IInfo { 61 | title: string; 62 | msg: string; 63 | } 64 | export interface State { 65 | pickupLocation: LatLng; 66 | isPickupDraggable: boolean; 67 | isShowPickupMarker: boolean; 68 | dropLocation: LatLng; 69 | isDropDraggable: boolean; 70 | isShowDropMarker: boolean; 71 | driverLocation: LatLng | null; 72 | dashboardStatus: DashboardStatus; 73 | } 74 | export interface IAction { 75 | type: string; 76 | payload?: any; 77 | } 78 | 79 | export enum DeliveryAssociateStatus { 80 | available = 'available', 81 | delivering = 'delivering', 82 | off = 'off', 83 | } 84 | export interface IDeliveryAssociate { 85 | _id: string; 86 | email: string; 87 | name: string; 88 | status: DeliveryAssociateStatus; 89 | currentLocation: Point; 90 | } 91 | export interface IUpdateDALocation { 92 | email: string; 93 | location: Point; 94 | } 95 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /frontend/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@ampproject/remapping@^2.1.0": 6 | version "2.2.0" 7 | resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" 8 | integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== 9 | dependencies: 10 | "@jridgewell/gen-mapping" "^0.1.0" 11 | "@jridgewell/trace-mapping" "^0.3.9" 12 | 13 | "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.18.6": 14 | version "7.18.6" 15 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" 16 | integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== 17 | dependencies: 18 | "@babel/highlight" "^7.18.6" 19 | 20 | "@babel/compat-data@^7.20.5": 21 | version "7.20.10" 22 | resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.10.tgz#9d92fa81b87542fff50e848ed585b4212c1d34ec" 23 | integrity sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg== 24 | 25 | "@babel/core@^7.20.7": 26 | version "7.20.12" 27 | resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d" 28 | integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg== 29 | dependencies: 30 | "@ampproject/remapping" "^2.1.0" 31 | "@babel/code-frame" "^7.18.6" 32 | "@babel/generator" "^7.20.7" 33 | "@babel/helper-compilation-targets" "^7.20.7" 34 | "@babel/helper-module-transforms" "^7.20.11" 35 | "@babel/helpers" "^7.20.7" 36 | "@babel/parser" "^7.20.7" 37 | "@babel/template" "^7.20.7" 38 | "@babel/traverse" "^7.20.12" 39 | "@babel/types" "^7.20.7" 40 | convert-source-map "^1.7.0" 41 | debug "^4.1.0" 42 | gensync "^1.0.0-beta.2" 43 | json5 "^2.2.2" 44 | semver "^6.3.0" 45 | 46 | "@babel/generator@^7.20.7": 47 | version "7.20.7" 48 | resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.7.tgz#f8ef57c8242665c5929fe2e8d82ba75460187b4a" 49 | integrity sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw== 50 | dependencies: 51 | "@babel/types" "^7.20.7" 52 | "@jridgewell/gen-mapping" "^0.3.2" 53 | jsesc "^2.5.1" 54 | 55 | "@babel/helper-compilation-targets@^7.20.7": 56 | version "7.20.7" 57 | resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz#a6cd33e93629f5eb473b021aac05df62c4cd09bb" 58 | integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ== 59 | dependencies: 60 | "@babel/compat-data" "^7.20.5" 61 | "@babel/helper-validator-option" "^7.18.6" 62 | browserslist "^4.21.3" 63 | lru-cache "^5.1.1" 64 | semver "^6.3.0" 65 | 66 | "@babel/helper-environment-visitor@^7.18.9": 67 | version "7.18.9" 68 | resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" 69 | integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== 70 | 71 | "@babel/helper-function-name@^7.19.0": 72 | version "7.19.0" 73 | resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" 74 | integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== 75 | dependencies: 76 | "@babel/template" "^7.18.10" 77 | "@babel/types" "^7.19.0" 78 | 79 | "@babel/helper-hoist-variables@^7.18.6": 80 | version "7.18.6" 81 | resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" 82 | integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== 83 | dependencies: 84 | "@babel/types" "^7.18.6" 85 | 86 | "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6": 87 | version "7.18.6" 88 | resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" 89 | integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== 90 | dependencies: 91 | "@babel/types" "^7.18.6" 92 | 93 | "@babel/helper-module-transforms@^7.20.11": 94 | version "7.20.11" 95 | resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz#df4c7af713c557938c50ea3ad0117a7944b2f1b0" 96 | integrity sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg== 97 | dependencies: 98 | "@babel/helper-environment-visitor" "^7.18.9" 99 | "@babel/helper-module-imports" "^7.18.6" 100 | "@babel/helper-simple-access" "^7.20.2" 101 | "@babel/helper-split-export-declaration" "^7.18.6" 102 | "@babel/helper-validator-identifier" "^7.19.1" 103 | "@babel/template" "^7.20.7" 104 | "@babel/traverse" "^7.20.10" 105 | "@babel/types" "^7.20.7" 106 | 107 | "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.19.0": 108 | version "7.20.2" 109 | resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" 110 | integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== 111 | 112 | "@babel/helper-simple-access@^7.20.2": 113 | version "7.20.2" 114 | resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" 115 | integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== 116 | dependencies: 117 | "@babel/types" "^7.20.2" 118 | 119 | "@babel/helper-split-export-declaration@^7.18.6": 120 | version "7.18.6" 121 | resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" 122 | integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== 123 | dependencies: 124 | "@babel/types" "^7.18.6" 125 | 126 | "@babel/helper-string-parser@^7.19.4": 127 | version "7.19.4" 128 | resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" 129 | integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== 130 | 131 | "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": 132 | version "7.19.1" 133 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" 134 | integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== 135 | 136 | "@babel/helper-validator-option@^7.18.6": 137 | version "7.18.6" 138 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" 139 | integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== 140 | 141 | "@babel/helpers@^7.20.7": 142 | version "7.20.7" 143 | resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.7.tgz#04502ff0feecc9f20ecfaad120a18f011a8e6dce" 144 | integrity sha512-PBPjs5BppzsGaxHQCDKnZ6Gd9s6xl8bBCluz3vEInLGRJmnZan4F6BYCeqtyXqkk4W5IlPmjK4JlOuZkpJ3xZA== 145 | dependencies: 146 | "@babel/template" "^7.20.7" 147 | "@babel/traverse" "^7.20.7" 148 | "@babel/types" "^7.20.7" 149 | 150 | "@babel/highlight@^7.18.6": 151 | version "7.18.6" 152 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" 153 | integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== 154 | dependencies: 155 | "@babel/helper-validator-identifier" "^7.18.6" 156 | chalk "^2.0.0" 157 | js-tokens "^4.0.0" 158 | 159 | "@babel/parser@^7.20.7": 160 | version "7.20.7" 161 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.7.tgz#66fe23b3c8569220817d5feb8b9dcdc95bb4f71b" 162 | integrity sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg== 163 | 164 | "@babel/plugin-syntax-jsx@^7.17.12": 165 | version "7.18.6" 166 | resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" 167 | integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== 168 | dependencies: 169 | "@babel/helper-plugin-utils" "^7.18.6" 170 | 171 | "@babel/plugin-transform-react-jsx-self@^7.18.6": 172 | version "7.18.6" 173 | resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.18.6.tgz#3849401bab7ae8ffa1e3e5687c94a753fc75bda7" 174 | integrity sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig== 175 | dependencies: 176 | "@babel/helper-plugin-utils" "^7.18.6" 177 | 178 | "@babel/plugin-transform-react-jsx-source@^7.19.6": 179 | version "7.19.6" 180 | resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.19.6.tgz#88578ae8331e5887e8ce28e4c9dc83fb29da0b86" 181 | integrity sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ== 182 | dependencies: 183 | "@babel/helper-plugin-utils" "^7.19.0" 184 | 185 | "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": 186 | version "7.20.7" 187 | resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd" 188 | integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ== 189 | dependencies: 190 | regenerator-runtime "^0.13.11" 191 | 192 | "@babel/template@^7.18.10", "@babel/template@^7.20.7": 193 | version "7.20.7" 194 | resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" 195 | integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== 196 | dependencies: 197 | "@babel/code-frame" "^7.18.6" 198 | "@babel/parser" "^7.20.7" 199 | "@babel/types" "^7.20.7" 200 | 201 | "@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.7": 202 | version "7.20.12" 203 | resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.12.tgz#7f0f787b3a67ca4475adef1f56cb94f6abd4a4b5" 204 | integrity sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ== 205 | dependencies: 206 | "@babel/code-frame" "^7.18.6" 207 | "@babel/generator" "^7.20.7" 208 | "@babel/helper-environment-visitor" "^7.18.9" 209 | "@babel/helper-function-name" "^7.19.0" 210 | "@babel/helper-hoist-variables" "^7.18.6" 211 | "@babel/helper-split-export-declaration" "^7.18.6" 212 | "@babel/parser" "^7.20.7" 213 | "@babel/types" "^7.20.7" 214 | debug "^4.1.0" 215 | globals "^11.1.0" 216 | 217 | "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.20.2", "@babel/types@^7.20.7": 218 | version "7.20.7" 219 | resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f" 220 | integrity sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg== 221 | dependencies: 222 | "@babel/helper-string-parser" "^7.19.4" 223 | "@babel/helper-validator-identifier" "^7.19.1" 224 | to-fast-properties "^2.0.0" 225 | 226 | "@emotion/babel-plugin@^11.10.5": 227 | version "11.10.5" 228 | resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz#65fa6e1790ddc9e23cc22658a4c5dea423c55c3c" 229 | integrity sha512-xE7/hyLHJac7D2Ve9dKroBBZqBT7WuPQmWcq7HSGb84sUuP4mlOWoB8dvVfD9yk5DHkU1m6RW7xSoDtnQHNQeA== 230 | dependencies: 231 | "@babel/helper-module-imports" "^7.16.7" 232 | "@babel/plugin-syntax-jsx" "^7.17.12" 233 | "@babel/runtime" "^7.18.3" 234 | "@emotion/hash" "^0.9.0" 235 | "@emotion/memoize" "^0.8.0" 236 | "@emotion/serialize" "^1.1.1" 237 | babel-plugin-macros "^3.1.0" 238 | convert-source-map "^1.5.0" 239 | escape-string-regexp "^4.0.0" 240 | find-root "^1.1.0" 241 | source-map "^0.5.7" 242 | stylis "4.1.3" 243 | 244 | "@emotion/cache@^11.10.5": 245 | version "11.10.5" 246 | resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.5.tgz#c142da9351f94e47527ed458f7bbbbe40bb13c12" 247 | integrity sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA== 248 | dependencies: 249 | "@emotion/memoize" "^0.8.0" 250 | "@emotion/sheet" "^1.2.1" 251 | "@emotion/utils" "^1.2.0" 252 | "@emotion/weak-memoize" "^0.3.0" 253 | stylis "4.1.3" 254 | 255 | "@emotion/hash@^0.9.0": 256 | version "0.9.0" 257 | resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.0.tgz#c5153d50401ee3c027a57a177bc269b16d889cb7" 258 | integrity sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ== 259 | 260 | "@emotion/is-prop-valid@^1.2.0": 261 | version "1.2.0" 262 | resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz#7f2d35c97891669f7e276eb71c83376a5dc44c83" 263 | integrity sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg== 264 | dependencies: 265 | "@emotion/memoize" "^0.8.0" 266 | 267 | "@emotion/memoize@^0.8.0": 268 | version "0.8.0" 269 | resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f" 270 | integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA== 271 | 272 | "@emotion/react@^11.10.0": 273 | version "11.10.5" 274 | resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.5.tgz#95fff612a5de1efa9c0d535384d3cfa115fe175d" 275 | integrity sha512-TZs6235tCJ/7iF6/rvTaOH4oxQg2gMAcdHemjwLKIjKz4rRuYe1HJ2TQJKnAcRAfOUDdU8XoDadCe1rl72iv8A== 276 | dependencies: 277 | "@babel/runtime" "^7.18.3" 278 | "@emotion/babel-plugin" "^11.10.5" 279 | "@emotion/cache" "^11.10.5" 280 | "@emotion/serialize" "^1.1.1" 281 | "@emotion/use-insertion-effect-with-fallbacks" "^1.0.0" 282 | "@emotion/utils" "^1.2.0" 283 | "@emotion/weak-memoize" "^0.3.0" 284 | hoist-non-react-statics "^3.3.1" 285 | 286 | "@emotion/serialize@^1.1.1": 287 | version "1.1.1" 288 | resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.1.tgz#0595701b1902feded8a96d293b26be3f5c1a5cf0" 289 | integrity sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA== 290 | dependencies: 291 | "@emotion/hash" "^0.9.0" 292 | "@emotion/memoize" "^0.8.0" 293 | "@emotion/unitless" "^0.8.0" 294 | "@emotion/utils" "^1.2.0" 295 | csstype "^3.0.2" 296 | 297 | "@emotion/sheet@^1.2.1": 298 | version "1.2.1" 299 | resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.1.tgz#0767e0305230e894897cadb6c8df2c51e61a6c2c" 300 | integrity sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA== 301 | 302 | "@emotion/styled@^11.10.0": 303 | version "11.10.5" 304 | resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.10.5.tgz#1fe7bf941b0909802cb826457e362444e7e96a79" 305 | integrity sha512-8EP6dD7dMkdku2foLoruPCNkRevzdcBaY6q0l0OsbyJK+x8D9HWjX27ARiSIKNF634hY9Zdoedh8bJCiva8yZw== 306 | dependencies: 307 | "@babel/runtime" "^7.18.3" 308 | "@emotion/babel-plugin" "^11.10.5" 309 | "@emotion/is-prop-valid" "^1.2.0" 310 | "@emotion/serialize" "^1.1.1" 311 | "@emotion/use-insertion-effect-with-fallbacks" "^1.0.0" 312 | "@emotion/utils" "^1.2.0" 313 | 314 | "@emotion/unitless@^0.8.0": 315 | version "0.8.0" 316 | resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.0.tgz#a4a36e9cbdc6903737cd20d38033241e1b8833db" 317 | integrity sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw== 318 | 319 | "@emotion/use-insertion-effect-with-fallbacks@^1.0.0": 320 | version "1.0.0" 321 | resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz#ffadaec35dbb7885bd54de3fa267ab2f860294df" 322 | integrity sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A== 323 | 324 | "@emotion/utils@^1.2.0": 325 | version "1.2.0" 326 | resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561" 327 | integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw== 328 | 329 | "@emotion/weak-memoize@^0.3.0": 330 | version "0.3.0" 331 | resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb" 332 | integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg== 333 | 334 | "@esbuild/android-arm64@0.16.17": 335 | version "0.16.17" 336 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz#cf91e86df127aa3d141744edafcba0abdc577d23" 337 | integrity sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg== 338 | 339 | "@esbuild/android-arm@0.16.17": 340 | version "0.16.17" 341 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.16.17.tgz#025b6246d3f68b7bbaa97069144fb5fb70f2fff2" 342 | integrity sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw== 343 | 344 | "@esbuild/android-x64@0.16.17": 345 | version "0.16.17" 346 | resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.16.17.tgz#c820e0fef982f99a85c4b8bfdd582835f04cd96e" 347 | integrity sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ== 348 | 349 | "@esbuild/darwin-arm64@0.16.17": 350 | version "0.16.17" 351 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz#edef4487af6b21afabba7be5132c26d22379b220" 352 | integrity sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w== 353 | 354 | "@esbuild/darwin-x64@0.16.17": 355 | version "0.16.17" 356 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz#42829168730071c41ef0d028d8319eea0e2904b4" 357 | integrity sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg== 358 | 359 | "@esbuild/freebsd-arm64@0.16.17": 360 | version "0.16.17" 361 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz#1f4af488bfc7e9ced04207034d398e793b570a27" 362 | integrity sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw== 363 | 364 | "@esbuild/freebsd-x64@0.16.17": 365 | version "0.16.17" 366 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz#636306f19e9bc981e06aa1d777302dad8fddaf72" 367 | integrity sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug== 368 | 369 | "@esbuild/linux-arm64@0.16.17": 370 | version "0.16.17" 371 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz#a003f7ff237c501e095d4f3a09e58fc7b25a4aca" 372 | integrity sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g== 373 | 374 | "@esbuild/linux-arm@0.16.17": 375 | version "0.16.17" 376 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz#b591e6a59d9c4fe0eeadd4874b157ab78cf5f196" 377 | integrity sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ== 378 | 379 | "@esbuild/linux-ia32@0.16.17": 380 | version "0.16.17" 381 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz#24333a11027ef46a18f57019450a5188918e2a54" 382 | integrity sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg== 383 | 384 | "@esbuild/linux-loong64@0.16.17": 385 | version "0.16.17" 386 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz#d5ad459d41ed42bbd4d005256b31882ec52227d8" 387 | integrity sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ== 388 | 389 | "@esbuild/linux-mips64el@0.16.17": 390 | version "0.16.17" 391 | resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz#4e5967a665c38360b0a8205594377d4dcf9c3726" 392 | integrity sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw== 393 | 394 | "@esbuild/linux-ppc64@0.16.17": 395 | version "0.16.17" 396 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz#206443a02eb568f9fdf0b438fbd47d26e735afc8" 397 | integrity sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g== 398 | 399 | "@esbuild/linux-riscv64@0.16.17": 400 | version "0.16.17" 401 | resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz#c351e433d009bf256e798ad048152c8d76da2fc9" 402 | integrity sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw== 403 | 404 | "@esbuild/linux-s390x@0.16.17": 405 | version "0.16.17" 406 | resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz#661f271e5d59615b84b6801d1c2123ad13d9bd87" 407 | integrity sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w== 408 | 409 | "@esbuild/linux-x64@0.16.17": 410 | version "0.16.17" 411 | resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz#e4ba18e8b149a89c982351443a377c723762b85f" 412 | integrity sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw== 413 | 414 | "@esbuild/netbsd-x64@0.16.17": 415 | version "0.16.17" 416 | resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz#7d4f4041e30c5c07dd24ffa295c73f06038ec775" 417 | integrity sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA== 418 | 419 | "@esbuild/openbsd-x64@0.16.17": 420 | version "0.16.17" 421 | resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz#970fa7f8470681f3e6b1db0cc421a4af8060ec35" 422 | integrity sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg== 423 | 424 | "@esbuild/sunos-x64@0.16.17": 425 | version "0.16.17" 426 | resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz#abc60e7c4abf8b89fb7a4fe69a1484132238022c" 427 | integrity sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw== 428 | 429 | "@esbuild/win32-arm64@0.16.17": 430 | version "0.16.17" 431 | resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz#7b0ff9e8c3265537a7a7b1fd9a24e7bd39fcd87a" 432 | integrity sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw== 433 | 434 | "@esbuild/win32-ia32@0.16.17": 435 | version "0.16.17" 436 | resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz#e90fe5267d71a7b7567afdc403dfd198c292eb09" 437 | integrity sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig== 438 | 439 | "@esbuild/win32-x64@0.16.17": 440 | version "0.16.17" 441 | resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz#c5a1a4bfe1b57f0c3e61b29883525c6da3e5c091" 442 | integrity sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q== 443 | 444 | "@fontsource/roboto@^4.5.8": 445 | version "4.5.8" 446 | resolved "https://registry.yarnpkg.com/@fontsource/roboto/-/roboto-4.5.8.tgz#56347764786079838faf43f0eeda22dd7328437f" 447 | integrity sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA== 448 | 449 | "@jridgewell/gen-mapping@^0.1.0": 450 | version "0.1.1" 451 | resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" 452 | integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== 453 | dependencies: 454 | "@jridgewell/set-array" "^1.0.0" 455 | "@jridgewell/sourcemap-codec" "^1.4.10" 456 | 457 | "@jridgewell/gen-mapping@^0.3.2": 458 | version "0.3.2" 459 | resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" 460 | integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== 461 | dependencies: 462 | "@jridgewell/set-array" "^1.0.1" 463 | "@jridgewell/sourcemap-codec" "^1.4.10" 464 | "@jridgewell/trace-mapping" "^0.3.9" 465 | 466 | "@jridgewell/resolve-uri@3.1.0": 467 | version "3.1.0" 468 | resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" 469 | integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== 470 | 471 | "@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": 472 | version "1.1.2" 473 | resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" 474 | integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== 475 | 476 | "@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13": 477 | version "1.4.14" 478 | resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" 479 | integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== 480 | 481 | "@jridgewell/trace-mapping@^0.3.9": 482 | version "0.3.17" 483 | resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" 484 | integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== 485 | dependencies: 486 | "@jridgewell/resolve-uri" "3.1.0" 487 | "@jridgewell/sourcemap-codec" "1.4.14" 488 | 489 | "@mui/base@5.0.0-alpha.114": 490 | version "5.0.0-alpha.114" 491 | resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.114.tgz#19125f28b7d09d1cc60550872440ecba699d8374" 492 | integrity sha512-ZpsG2I+zTOAnVTj3Un7TxD2zKRA2OhEPGMcWs/9ylPlS6VuGQSXowPooZiqarjT7TZ0+1bOe8titk/t8dLFiGw== 493 | dependencies: 494 | "@babel/runtime" "^7.20.7" 495 | "@emotion/is-prop-valid" "^1.2.0" 496 | "@mui/types" "^7.2.3" 497 | "@mui/utils" "^5.11.2" 498 | "@popperjs/core" "^2.11.6" 499 | clsx "^1.2.1" 500 | prop-types "^15.8.1" 501 | react-is "^18.2.0" 502 | 503 | "@mui/core-downloads-tracker@^5.11.5": 504 | version "5.11.5" 505 | resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.5.tgz#473c9b918d974f03acc07d29ce467bb91eba13c6" 506 | integrity sha512-MIuWGjitOsugpRhp64CQY3ZEVMIu9M/L9ioql6QLSkz73+bGIlC9FEhfi670/GZ8pQIIGmtiGGwofYzlwEWjig== 507 | 508 | "@mui/icons-material@^5.8.4": 509 | version "5.11.0" 510 | resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.11.0.tgz#9ea6949278b2266d2683866069cd43009eaf6464" 511 | integrity sha512-I2LaOKqO8a0xcLGtIozC9xoXjZAto5G5gh0FYUMAlbsIHNHIjn4Xrw9rvjY20vZonyiGrZNMAlAXYkY6JvhF6A== 512 | dependencies: 513 | "@babel/runtime" "^7.20.6" 514 | 515 | "@mui/material@^5.10.1": 516 | version "5.11.5" 517 | resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.11.5.tgz#b4867b4a6f3289e41f70b4393c075a799be8d24b" 518 | integrity sha512-5fzjBbRYaB5MoEpvA32oalAWltOZ3/kSyuovuVmPc6UF6AG42lTtbdMLpdCygurFSGUMZYTg4Cjij52fKlDDgg== 519 | dependencies: 520 | "@babel/runtime" "^7.20.7" 521 | "@mui/base" "5.0.0-alpha.114" 522 | "@mui/core-downloads-tracker" "^5.11.5" 523 | "@mui/system" "^5.11.5" 524 | "@mui/types" "^7.2.3" 525 | "@mui/utils" "^5.11.2" 526 | "@types/react-transition-group" "^4.4.5" 527 | clsx "^1.2.1" 528 | csstype "^3.1.1" 529 | prop-types "^15.8.1" 530 | react-is "^18.2.0" 531 | react-transition-group "^4.4.5" 532 | 533 | "@mui/private-theming@^5.11.2": 534 | version "5.11.2" 535 | resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.11.2.tgz#93eafb317070888a988efa8d6a9ec1f69183a606" 536 | integrity sha512-qZwMaqRFPwlYmqwVKblKBGKtIjJRAj3nsvX93pOmatsXyorW7N/0IPE/swPgz1VwChXhHO75DwBEx8tB+aRMNg== 537 | dependencies: 538 | "@babel/runtime" "^7.20.7" 539 | "@mui/utils" "^5.11.2" 540 | prop-types "^15.8.1" 541 | 542 | "@mui/styled-engine@^5.11.0": 543 | version "5.11.0" 544 | resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.11.0.tgz#79afb30c612c7807c4b77602cf258526d3997c7b" 545 | integrity sha512-AF06K60Zc58qf0f7X+Y/QjaHaZq16znliLnGc9iVrV/+s8Ln/FCoeNuFvhlCbZZQ5WQcJvcy59zp0nXrklGGPQ== 546 | dependencies: 547 | "@babel/runtime" "^7.20.6" 548 | "@emotion/cache" "^11.10.5" 549 | csstype "^3.1.1" 550 | prop-types "^15.8.1" 551 | 552 | "@mui/system@^5.11.5": 553 | version "5.11.5" 554 | resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.11.5.tgz#c880199634708c866063396f88d3fdd4c1dfcb48" 555 | integrity sha512-KNVsJ0sgRRp2XBqhh4wPS5aacteqjwxgiYTVwVnll2fgkgunZKo3DsDiGMrFlCg25ZHA3Ax58txWGE9w58zp0w== 556 | dependencies: 557 | "@babel/runtime" "^7.20.7" 558 | "@mui/private-theming" "^5.11.2" 559 | "@mui/styled-engine" "^5.11.0" 560 | "@mui/types" "^7.2.3" 561 | "@mui/utils" "^5.11.2" 562 | clsx "^1.2.1" 563 | csstype "^3.1.1" 564 | prop-types "^15.8.1" 565 | 566 | "@mui/types@^7.2.3": 567 | version "7.2.3" 568 | resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.3.tgz#06faae1c0e2f3a31c86af6f28b3a4a42143670b9" 569 | integrity sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw== 570 | 571 | "@mui/utils@^5.11.2": 572 | version "5.11.2" 573 | resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.11.2.tgz#29764311acb99425159b159b1cb382153ad9be1f" 574 | integrity sha512-AyizuHHlGdAtH5hOOXBW3kriuIwUIKUIgg0P7LzMvzf6jPhoQbENYqY6zJqfoZ7fAWMNNYT8mgN5EftNGzwE2w== 575 | dependencies: 576 | "@babel/runtime" "^7.20.7" 577 | "@types/prop-types" "^15.7.5" 578 | "@types/react-is" "^16.7.1 || ^17.0.0" 579 | prop-types "^15.8.1" 580 | react-is "^18.2.0" 581 | 582 | "@popperjs/core@^2.11.6": 583 | version "2.11.6" 584 | resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" 585 | integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== 586 | 587 | "@react-leaflet/core@^2.1.0": 588 | version "2.1.0" 589 | resolved "https://registry.yarnpkg.com/@react-leaflet/core/-/core-2.1.0.tgz#383acd31259d7c9ae8fb1b02d5e18fe613c2a13d" 590 | integrity sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg== 591 | 592 | "@remix-run/router@1.3.0": 593 | version "1.3.0" 594 | resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.3.0.tgz#b6ee542c7f087b73b3d8215b9bf799f648be71cb" 595 | integrity sha512-nwQoYb3m4DDpHTeOwpJEuDt8lWVcujhYYSFGLluC+9es2PyLjm+jjq3IeRBQbwBtPLJE/lkuHuGHr8uQLgmJRA== 596 | 597 | "@socket.io/component-emitter@~3.1.0": 598 | version "3.1.0" 599 | resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" 600 | integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== 601 | 602 | "@types/geojson@*": 603 | version "7946.0.10" 604 | resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249" 605 | integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA== 606 | 607 | "@types/history@^4.7.11": 608 | version "4.7.11" 609 | resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" 610 | integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== 611 | 612 | "@types/leaflet@^1.9.0": 613 | version "1.9.0" 614 | resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.9.0.tgz#8caf452255e16cb15e0eabcb0d2a26793da0a6a2" 615 | integrity sha512-7LeOSj7EloC5UcyOMo+1kc3S1UT3MjJxwqsMT1d2PTyvQz53w0Y0oSSk9nwZnOZubCmBvpSNGceucxiq+ZPEUw== 616 | dependencies: 617 | "@types/geojson" "*" 618 | 619 | "@types/lodash@^4.14.191": 620 | version "4.14.191" 621 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" 622 | integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== 623 | 624 | "@types/parse-json@^4.0.0": 625 | version "4.0.0" 626 | resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" 627 | integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== 628 | 629 | "@types/prop-types@*", "@types/prop-types@^15.7.5": 630 | version "15.7.5" 631 | resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" 632 | integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== 633 | 634 | "@types/react-dom@^18.0.10": 635 | version "18.0.10" 636 | resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.10.tgz#3b66dec56aa0f16a6cc26da9e9ca96c35c0b4352" 637 | integrity sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg== 638 | dependencies: 639 | "@types/react" "*" 640 | 641 | "@types/react-is@^16.7.1 || ^17.0.0": 642 | version "17.0.3" 643 | resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a" 644 | integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw== 645 | dependencies: 646 | "@types/react" "*" 647 | 648 | "@types/react-router-dom@^5.3.3": 649 | version "5.3.3" 650 | resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" 651 | integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== 652 | dependencies: 653 | "@types/history" "^4.7.11" 654 | "@types/react" "*" 655 | "@types/react-router" "*" 656 | 657 | "@types/react-router@*", "@types/react-router@^5.1.20": 658 | version "5.1.20" 659 | resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" 660 | integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== 661 | dependencies: 662 | "@types/history" "^4.7.11" 663 | "@types/react" "*" 664 | 665 | "@types/react-transition-group@^4.4.5": 666 | version "4.4.5" 667 | resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416" 668 | integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA== 669 | dependencies: 670 | "@types/react" "*" 671 | 672 | "@types/react@*", "@types/react@^18.0.27": 673 | version "18.0.27" 674 | resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.27.tgz#d9425abe187a00f8a5ec182b010d4fd9da703b71" 675 | integrity sha512-3vtRKHgVxu3Jp9t718R9BuzoD4NcQ8YJ5XRzsSKxNDiDonD2MXIT1TmSkenxuCycZJoQT5d2vE8LwWJxBC1gmA== 676 | dependencies: 677 | "@types/prop-types" "*" 678 | "@types/scheduler" "*" 679 | csstype "^3.0.2" 680 | 681 | "@types/scheduler@*": 682 | version "0.16.2" 683 | resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" 684 | integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== 685 | 686 | "@vitejs/plugin-react@^3.0.1": 687 | version "3.0.1" 688 | resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-3.0.1.tgz#ad21fb81377970dd4021a31cd95a03eb6f5c4c48" 689 | integrity sha512-mx+QvYwIbbpOIJw+hypjnW1lAbKDHtWK5ibkF/V1/oMBu8HU/chb+SnqJDAsLq1+7rGqjktCEomMTM5KShzUKQ== 690 | dependencies: 691 | "@babel/core" "^7.20.7" 692 | "@babel/plugin-transform-react-jsx-self" "^7.18.6" 693 | "@babel/plugin-transform-react-jsx-source" "^7.19.6" 694 | magic-string "^0.27.0" 695 | react-refresh "^0.14.0" 696 | 697 | ansi-styles@^3.2.1: 698 | version "3.2.1" 699 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 700 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 701 | dependencies: 702 | color-convert "^1.9.0" 703 | 704 | asynckit@^0.4.0: 705 | version "0.4.0" 706 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 707 | integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== 708 | 709 | axios@^0.27.2: 710 | version "0.27.2" 711 | resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" 712 | integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== 713 | dependencies: 714 | follow-redirects "^1.14.9" 715 | form-data "^4.0.0" 716 | 717 | babel-plugin-macros@^3.1.0: 718 | version "3.1.0" 719 | resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" 720 | integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== 721 | dependencies: 722 | "@babel/runtime" "^7.12.5" 723 | cosmiconfig "^7.0.0" 724 | resolve "^1.19.0" 725 | 726 | browserslist@^4.21.3: 727 | version "4.21.4" 728 | resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" 729 | integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== 730 | dependencies: 731 | caniuse-lite "^1.0.30001400" 732 | electron-to-chromium "^1.4.251" 733 | node-releases "^2.0.6" 734 | update-browserslist-db "^1.0.9" 735 | 736 | callsites@^3.0.0: 737 | version "3.1.0" 738 | resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" 739 | integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== 740 | 741 | caniuse-lite@^1.0.30001400: 742 | version "1.0.30001446" 743 | resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001446.tgz#6d4ba828ab19f49f9bcd14a8430d30feebf1e0c5" 744 | integrity sha512-fEoga4PrImGcwUUGEol/PoFCSBnSkA9drgdkxXkJLsUBOnJ8rs3zDv6ApqYXGQFOyMPsjh79naWhF4DAxbF8rw== 745 | 746 | chalk@^2.0.0: 747 | version "2.4.2" 748 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" 749 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== 750 | dependencies: 751 | ansi-styles "^3.2.1" 752 | escape-string-regexp "^1.0.5" 753 | supports-color "^5.3.0" 754 | 755 | clsx@^1.2.1: 756 | version "1.2.1" 757 | resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" 758 | integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== 759 | 760 | color-convert@^1.9.0: 761 | version "1.9.3" 762 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 763 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 764 | dependencies: 765 | color-name "1.1.3" 766 | 767 | color-name@1.1.3: 768 | version "1.1.3" 769 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 770 | integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== 771 | 772 | combined-stream@^1.0.8: 773 | version "1.0.8" 774 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 775 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 776 | dependencies: 777 | delayed-stream "~1.0.0" 778 | 779 | convert-source-map@^1.5.0, convert-source-map@^1.7.0: 780 | version "1.9.0" 781 | resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" 782 | integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== 783 | 784 | cosmiconfig@^7.0.0: 785 | version "7.1.0" 786 | resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" 787 | integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== 788 | dependencies: 789 | "@types/parse-json" "^4.0.0" 790 | import-fresh "^3.2.1" 791 | parse-json "^5.0.0" 792 | path-type "^4.0.0" 793 | yaml "^1.10.0" 794 | 795 | csstype@^3.0.2, csstype@^3.1.1: 796 | version "3.1.1" 797 | resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" 798 | integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== 799 | 800 | debug@^4.1.0, debug@~4.3.1, debug@~4.3.2: 801 | version "4.3.4" 802 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 803 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 804 | dependencies: 805 | ms "2.1.2" 806 | 807 | delayed-stream@~1.0.0: 808 | version "1.0.0" 809 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 810 | integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== 811 | 812 | dom-helpers@^5.0.1: 813 | version "5.2.1" 814 | resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" 815 | integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== 816 | dependencies: 817 | "@babel/runtime" "^7.8.7" 818 | csstype "^3.0.2" 819 | 820 | electron-to-chromium@^1.4.251: 821 | version "1.4.284" 822 | resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" 823 | integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== 824 | 825 | engine.io-client@~6.2.3: 826 | version "6.2.3" 827 | resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.2.3.tgz#a8cbdab003162529db85e9de31575097f6d29458" 828 | integrity sha512-aXPtgF1JS3RuuKcpSrBtimSjYvrbhKW9froICH4s0F3XQWLxsKNxqzG39nnvQZQnva4CMvUK63T7shevxRyYHw== 829 | dependencies: 830 | "@socket.io/component-emitter" "~3.1.0" 831 | debug "~4.3.1" 832 | engine.io-parser "~5.0.3" 833 | ws "~8.2.3" 834 | xmlhttprequest-ssl "~2.0.0" 835 | 836 | engine.io-parser@~5.0.3: 837 | version "5.0.6" 838 | resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45" 839 | integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== 840 | 841 | error-ex@^1.3.1: 842 | version "1.3.2" 843 | resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" 844 | integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== 845 | dependencies: 846 | is-arrayish "^0.2.1" 847 | 848 | esbuild@^0.16.3: 849 | version "0.16.17" 850 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.16.17.tgz#fc2c3914c57ee750635fee71b89f615f25065259" 851 | integrity sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg== 852 | optionalDependencies: 853 | "@esbuild/android-arm" "0.16.17" 854 | "@esbuild/android-arm64" "0.16.17" 855 | "@esbuild/android-x64" "0.16.17" 856 | "@esbuild/darwin-arm64" "0.16.17" 857 | "@esbuild/darwin-x64" "0.16.17" 858 | "@esbuild/freebsd-arm64" "0.16.17" 859 | "@esbuild/freebsd-x64" "0.16.17" 860 | "@esbuild/linux-arm" "0.16.17" 861 | "@esbuild/linux-arm64" "0.16.17" 862 | "@esbuild/linux-ia32" "0.16.17" 863 | "@esbuild/linux-loong64" "0.16.17" 864 | "@esbuild/linux-mips64el" "0.16.17" 865 | "@esbuild/linux-ppc64" "0.16.17" 866 | "@esbuild/linux-riscv64" "0.16.17" 867 | "@esbuild/linux-s390x" "0.16.17" 868 | "@esbuild/linux-x64" "0.16.17" 869 | "@esbuild/netbsd-x64" "0.16.17" 870 | "@esbuild/openbsd-x64" "0.16.17" 871 | "@esbuild/sunos-x64" "0.16.17" 872 | "@esbuild/win32-arm64" "0.16.17" 873 | "@esbuild/win32-ia32" "0.16.17" 874 | "@esbuild/win32-x64" "0.16.17" 875 | 876 | escalade@^3.1.1: 877 | version "3.1.1" 878 | resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" 879 | integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== 880 | 881 | escape-string-regexp@^1.0.5: 882 | version "1.0.5" 883 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 884 | integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== 885 | 886 | escape-string-regexp@^4.0.0: 887 | version "4.0.0" 888 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" 889 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== 890 | 891 | find-root@^1.1.0: 892 | version "1.1.0" 893 | resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" 894 | integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== 895 | 896 | follow-redirects@^1.14.9: 897 | version "1.15.2" 898 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" 899 | integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== 900 | 901 | form-data@^4.0.0: 902 | version "4.0.0" 903 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" 904 | integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== 905 | dependencies: 906 | asynckit "^0.4.0" 907 | combined-stream "^1.0.8" 908 | mime-types "^2.1.12" 909 | 910 | fsevents@~2.3.2: 911 | version "2.3.2" 912 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 913 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 914 | 915 | function-bind@^1.1.1: 916 | version "1.1.1" 917 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 918 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 919 | 920 | gensync@^1.0.0-beta.2: 921 | version "1.0.0-beta.2" 922 | resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" 923 | integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== 924 | 925 | globals@^11.1.0: 926 | version "11.12.0" 927 | resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" 928 | integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== 929 | 930 | has-flag@^3.0.0: 931 | version "3.0.0" 932 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 933 | integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== 934 | 935 | has@^1.0.3: 936 | version "1.0.3" 937 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 938 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 939 | dependencies: 940 | function-bind "^1.1.1" 941 | 942 | hoist-non-react-statics@^3.3.1: 943 | version "3.3.2" 944 | resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" 945 | integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== 946 | dependencies: 947 | react-is "^16.7.0" 948 | 949 | import-fresh@^3.2.1: 950 | version "3.3.0" 951 | resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" 952 | integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== 953 | dependencies: 954 | parent-module "^1.0.0" 955 | resolve-from "^4.0.0" 956 | 957 | is-arrayish@^0.2.1: 958 | version "0.2.1" 959 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" 960 | integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== 961 | 962 | is-core-module@^2.9.0: 963 | version "2.11.0" 964 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" 965 | integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== 966 | dependencies: 967 | has "^1.0.3" 968 | 969 | "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: 970 | version "4.0.0" 971 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 972 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 973 | 974 | jsesc@^2.5.1: 975 | version "2.5.2" 976 | resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" 977 | integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== 978 | 979 | json-parse-even-better-errors@^2.3.0: 980 | version "2.3.1" 981 | resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" 982 | integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== 983 | 984 | json5@^2.2.2: 985 | version "2.2.3" 986 | resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" 987 | integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== 988 | 989 | leaflet@^1.9.3: 990 | version "1.9.3" 991 | resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.9.3.tgz#52ec436954964e2d3d39e0d433da4b2500d74414" 992 | integrity sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ== 993 | 994 | lines-and-columns@^1.1.6: 995 | version "1.2.4" 996 | resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" 997 | integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== 998 | 999 | lodash@^4.17.21: 1000 | version "4.17.21" 1001 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 1002 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 1003 | 1004 | loose-envify@^1.1.0, loose-envify@^1.4.0: 1005 | version "1.4.0" 1006 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" 1007 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== 1008 | dependencies: 1009 | js-tokens "^3.0.0 || ^4.0.0" 1010 | 1011 | lru-cache@^5.1.1: 1012 | version "5.1.1" 1013 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" 1014 | integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== 1015 | dependencies: 1016 | yallist "^3.0.2" 1017 | 1018 | magic-string@^0.27.0: 1019 | version "0.27.0" 1020 | resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" 1021 | integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA== 1022 | dependencies: 1023 | "@jridgewell/sourcemap-codec" "^1.4.13" 1024 | 1025 | mime-db@1.52.0: 1026 | version "1.52.0" 1027 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 1028 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 1029 | 1030 | mime-types@^2.1.12: 1031 | version "2.1.35" 1032 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 1033 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 1034 | dependencies: 1035 | mime-db "1.52.0" 1036 | 1037 | ms@2.1.2: 1038 | version "2.1.2" 1039 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 1040 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 1041 | 1042 | nanoid@^3.3.4: 1043 | version "3.3.4" 1044 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" 1045 | integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== 1046 | 1047 | node-releases@^2.0.6: 1048 | version "2.0.8" 1049 | resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae" 1050 | integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A== 1051 | 1052 | object-assign@^4.1.1: 1053 | version "4.1.1" 1054 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 1055 | integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== 1056 | 1057 | parent-module@^1.0.0: 1058 | version "1.0.1" 1059 | resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" 1060 | integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== 1061 | dependencies: 1062 | callsites "^3.0.0" 1063 | 1064 | parse-json@^5.0.0: 1065 | version "5.2.0" 1066 | resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" 1067 | integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== 1068 | dependencies: 1069 | "@babel/code-frame" "^7.0.0" 1070 | error-ex "^1.3.1" 1071 | json-parse-even-better-errors "^2.3.0" 1072 | lines-and-columns "^1.1.6" 1073 | 1074 | path-parse@^1.0.7: 1075 | version "1.0.7" 1076 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" 1077 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 1078 | 1079 | path-type@^4.0.0: 1080 | version "4.0.0" 1081 | resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" 1082 | integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== 1083 | 1084 | picocolors@^1.0.0: 1085 | version "1.0.0" 1086 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" 1087 | integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== 1088 | 1089 | postcss@^8.4.20: 1090 | version "8.4.21" 1091 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" 1092 | integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== 1093 | dependencies: 1094 | nanoid "^3.3.4" 1095 | picocolors "^1.0.0" 1096 | source-map-js "^1.0.2" 1097 | 1098 | prop-types@^15.6.2, prop-types@^15.8.1: 1099 | version "15.8.1" 1100 | resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" 1101 | integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== 1102 | dependencies: 1103 | loose-envify "^1.4.0" 1104 | object-assign "^4.1.1" 1105 | react-is "^16.13.1" 1106 | 1107 | react-dom@^18.2.0: 1108 | version "18.2.0" 1109 | resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" 1110 | integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== 1111 | dependencies: 1112 | loose-envify "^1.1.0" 1113 | scheduler "^0.23.0" 1114 | 1115 | react-is@^16.13.1, react-is@^16.7.0: 1116 | version "16.13.1" 1117 | resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" 1118 | integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== 1119 | 1120 | react-is@^18.2.0: 1121 | version "18.2.0" 1122 | resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" 1123 | integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== 1124 | 1125 | react-leaflet@^4.1.0: 1126 | version "4.2.0" 1127 | resolved "https://registry.yarnpkg.com/react-leaflet/-/react-leaflet-4.2.0.tgz#6c2ee4b576209d9fe40e78006cfa37196c216eaa" 1128 | integrity sha512-9d8T7hzYrQA5GLe3vn0qtRLJzQKgjr080NKa45yArGwuSl1nH/6aK9gp7DeYdktpdO1vKGSUTGW5AsUS064X0A== 1129 | dependencies: 1130 | "@react-leaflet/core" "^2.1.0" 1131 | 1132 | react-refresh@^0.14.0: 1133 | version "0.14.0" 1134 | resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" 1135 | integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== 1136 | 1137 | react-router-dom@^6.3.0: 1138 | version "6.7.0" 1139 | resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.7.0.tgz#0249f4ca4eb704562b8b0ff29caeb928c3a6ed38" 1140 | integrity sha512-jQtXUJyhso3kFw430+0SPCbmCmY1/kJv8iRffGHwHy3CkoomGxeYzMkmeSPYo6Egzh3FKJZRAL22yg5p2tXtfg== 1141 | dependencies: 1142 | "@remix-run/router" "1.3.0" 1143 | react-router "6.7.0" 1144 | 1145 | react-router@6.7.0: 1146 | version "6.7.0" 1147 | resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.7.0.tgz#db262684c13b5c2970694084ae9e8531718a0681" 1148 | integrity sha512-KNWlG622ddq29MAM159uUsNMdbX8USruoKnwMMQcs/QWZgFUayICSn2oB7reHce1zPj6CG18kfkZIunSSRyGHg== 1149 | dependencies: 1150 | "@remix-run/router" "1.3.0" 1151 | 1152 | react-transition-group@^4.4.5: 1153 | version "4.4.5" 1154 | resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" 1155 | integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== 1156 | dependencies: 1157 | "@babel/runtime" "^7.5.5" 1158 | dom-helpers "^5.0.1" 1159 | loose-envify "^1.4.0" 1160 | prop-types "^15.6.2" 1161 | 1162 | react@^18.2.0: 1163 | version "18.2.0" 1164 | resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" 1165 | integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== 1166 | dependencies: 1167 | loose-envify "^1.1.0" 1168 | 1169 | regenerator-runtime@^0.13.11: 1170 | version "0.13.11" 1171 | resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" 1172 | integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== 1173 | 1174 | resolve-from@^4.0.0: 1175 | version "4.0.0" 1176 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" 1177 | integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== 1178 | 1179 | resolve@^1.19.0, resolve@^1.22.1: 1180 | version "1.22.1" 1181 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" 1182 | integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== 1183 | dependencies: 1184 | is-core-module "^2.9.0" 1185 | path-parse "^1.0.7" 1186 | supports-preserve-symlinks-flag "^1.0.0" 1187 | 1188 | rollup@^3.7.0: 1189 | version "3.10.1" 1190 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.10.1.tgz#56278901ed11fc2898421e8e3e2c8155bc7b40b4" 1191 | integrity sha512-3Er+yel3bZbZX1g2kjVM+FW+RUWDxbG87fcqFM5/9HbPCTpbVp6JOLn7jlxnNlbu7s/N/uDA4EV/91E2gWnxzw== 1192 | optionalDependencies: 1193 | fsevents "~2.3.2" 1194 | 1195 | scheduler@^0.23.0: 1196 | version "0.23.0" 1197 | resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" 1198 | integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== 1199 | dependencies: 1200 | loose-envify "^1.1.0" 1201 | 1202 | semver@^6.3.0: 1203 | version "6.3.0" 1204 | resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" 1205 | integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== 1206 | 1207 | socket.io-client@^4.5.1: 1208 | version "4.5.4" 1209 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.5.4.tgz#d3cde8a06a6250041ba7390f08d2468ccebc5ac9" 1210 | integrity sha512-ZpKteoA06RzkD32IbqILZ+Cnst4xewU7ZYK12aS1mzHftFFjpoMz69IuhP/nL25pJfao/amoPI527KnuhFm01g== 1211 | dependencies: 1212 | "@socket.io/component-emitter" "~3.1.0" 1213 | debug "~4.3.2" 1214 | engine.io-client "~6.2.3" 1215 | socket.io-parser "~4.2.1" 1216 | 1217 | socket.io-parser@~4.2.1: 1218 | version "4.2.2" 1219 | resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206" 1220 | integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw== 1221 | dependencies: 1222 | "@socket.io/component-emitter" "~3.1.0" 1223 | debug "~4.3.1" 1224 | 1225 | source-map-js@^1.0.2: 1226 | version "1.0.2" 1227 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" 1228 | integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== 1229 | 1230 | source-map@^0.5.7: 1231 | version "0.5.7" 1232 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" 1233 | integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== 1234 | 1235 | stylis@4.1.3: 1236 | version "4.1.3" 1237 | resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7" 1238 | integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== 1239 | 1240 | supports-color@^5.3.0: 1241 | version "5.5.0" 1242 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 1243 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 1244 | dependencies: 1245 | has-flag "^3.0.0" 1246 | 1247 | supports-preserve-symlinks-flag@^1.0.0: 1248 | version "1.0.0" 1249 | resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" 1250 | integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 1251 | 1252 | to-fast-properties@^2.0.0: 1253 | version "2.0.0" 1254 | resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" 1255 | integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== 1256 | 1257 | typescript@^4.9.3: 1258 | version "4.9.4" 1259 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" 1260 | integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== 1261 | 1262 | update-browserslist-db@^1.0.9: 1263 | version "1.0.10" 1264 | resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" 1265 | integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== 1266 | dependencies: 1267 | escalade "^3.1.1" 1268 | picocolors "^1.0.0" 1269 | 1270 | vite@^4.0.0: 1271 | version "4.0.4" 1272 | resolved "https://registry.yarnpkg.com/vite/-/vite-4.0.4.tgz#4612ce0b47bbb233a887a54a4ae0c6e240a0da31" 1273 | integrity sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw== 1274 | dependencies: 1275 | esbuild "^0.16.3" 1276 | postcss "^8.4.20" 1277 | resolve "^1.22.1" 1278 | rollup "^3.7.0" 1279 | optionalDependencies: 1280 | fsevents "~2.3.2" 1281 | 1282 | ws@~8.2.3: 1283 | version "8.2.3" 1284 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" 1285 | integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== 1286 | 1287 | xmlhttprequest-ssl@~2.0.0: 1288 | version "2.0.0" 1289 | resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" 1290 | integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== 1291 | 1292 | yallist@^3.0.2: 1293 | version "3.1.1" 1294 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" 1295 | integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== 1296 | 1297 | yaml@^1.10.0: 1298 | version "1.10.2" 1299 | resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" 1300 | integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== 1301 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "location-tracker", 3 | "version": "1.0.0", 4 | "description": "location tracker application using MongoDB Change Stream", 5 | "main": "index.js", 6 | "author": "ashiqsultan", 7 | "license": "MIT", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "dev": "ts-node-dev --respawn src/server.ts", 11 | "build": "tsc", 12 | "start": "node dist/server.js", 13 | "watch-ts": "tsc -w" 14 | }, 15 | "devDependencies": { 16 | "@types/bcryptjs": "^2.4.2", 17 | "@types/cors": "^2.8.12", 18 | "@types/express": "^4.17.13", 19 | "@types/jsonwebtoken": "^8.5.8", 20 | "@types/morgan": "^1.9.3", 21 | "@types/node": "^18.11.9", 22 | "ts-node-dev": "^2.0.0", 23 | "typescript": "^4.8.3" 24 | }, 25 | "dependencies": { 26 | "@types/geojson": "^7946.0.10", 27 | "bcryptjs": "^2.4.3", 28 | "cors": "^2.8.5", 29 | "dotenv": "^16.0.1", 30 | "express": "^4.18.1", 31 | "jsonwebtoken": "^8.5.1", 32 | "mongodb": "4.13", 33 | "morgan": "^1.10.0", 34 | "socket.io": "^4.5.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import morgan from 'morgan'; 3 | import config from './config'; 4 | import AppResponse from './types/AppResponse'; 5 | import routes from './routes'; 6 | import cors from 'cors'; 7 | 8 | // Create Express server 9 | const app = express(); 10 | 11 | // Set PORT 12 | app.set('port', config.port); 13 | 14 | // parse json request body 15 | app.use(express.json()); 16 | // parse urlencoded request body 17 | app.use(express.urlencoded({ extended: true })); 18 | 19 | // Logger 20 | app.use(morgan('dev')); 21 | 22 | // Enable CORS 23 | app.use(cors(config.corsOptions)); 24 | 25 | // Route Handlers 26 | app.use('/', routes); 27 | 28 | // 404 Handler 29 | app.use(function (req, res, next) { 30 | const status = 404; 31 | const message = 'Resource not found'; 32 | const errorResponse: AppResponse = { 33 | data: [], 34 | isError: true, 35 | errMsg: message, 36 | }; 37 | res.status(status).send(errorResponse); 38 | }); 39 | 40 | // Server Error 500 Handler 41 | // Calling next(error) in any of the routes will call this function 42 | app.use( 43 | ( 44 | error: Error, 45 | req: express.Request, 46 | res: express.Response, 47 | next: express.NextFunction 48 | ) => { 49 | // Incase of 500 Server Error 50 | // The Error is only logged in server and not sent in response to restrict error details being known in the frontend 51 | console.error(error); 52 | const status = 500; 53 | const message = 54 | process.env.NODE_ENV === 'development' 55 | ? error.message 56 | : 'API Server Error'; 57 | const errorResponse: AppResponse = { 58 | data: [], 59 | isError: true, 60 | errMsg: message, 61 | }; 62 | res.status(status).send(errorResponse); 63 | } 64 | ); 65 | 66 | export default app; 67 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | interface IConfig { 2 | port: string; 3 | mongoConnString: string; 4 | secretKey: string; 5 | corsOptions: any; 6 | } 7 | const config: IConfig = { 8 | port: process.env.PORT || '5050', 9 | mongoConnString: process.env.MONGODB_CONNECTION_STRING || '', 10 | secretKey: process.env.SECRET_KEY, 11 | corsOptions: { origin: '*' }, 12 | }; 13 | 14 | export default config; 15 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | const socketEvents = { 2 | // DA means DELIVERY_ASSOCIATE 3 | UPDATE_DA_LOCATION: 'UPDATE_DA_LOCATION', 4 | DA_LOCATION_CHANGED: 'DA_LOCATION_CHANGED', 5 | 6 | SHIPMENT_CREATED: 'SHIPMENT_CREATED', 7 | SHIPMENT_UPDATED: 'SHIPMENT_UPDATED', 8 | 9 | SUBSCRIBE_TO_SHIPMENT: 'SUBSCRIBE_TO_SHIPMENT', 10 | SUBSCRIBE_TO_DA: 'SUBSCRIBE_TO_DA', 11 | 12 | LEAVE_ROOM: 'LEAVE_ROOM', 13 | }; 14 | 15 | const dbCollections = { 16 | users: 'users', 17 | shipments: 'shipments', 18 | deliveryAssociates: 'deliveryAssociates', 19 | }; 20 | 21 | export { socketEvents, dbCollections }; 22 | -------------------------------------------------------------------------------- /src/controller/ping.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import AppResponse from '../types/AppResponse'; 3 | 4 | const pong = async (req: Request, res: Response, next: NextFunction) => { 5 | try { 6 | const response: AppResponse = { data: 'pong', isError: false }; 7 | res.send(response); 8 | } catch (error) { 9 | next(error); 10 | } 11 | }; 12 | 13 | export { pong }; 14 | -------------------------------------------------------------------------------- /src/controller/shipment.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import AppRequest from 'AppRequest'; 3 | import IShipment, { ShipmentStatus } from '../types/IShipment'; 4 | import AppResponse from '../types/AppResponse'; 5 | import createOne from '../services/shipments/createOne'; 6 | import updateDA from '../services/shipments/updateDA'; 7 | import updateStatus from '../services/shipments/updateStatus'; 8 | import findUserByEmail from '../services/users/findByEmail'; 9 | import findDAByEmail from '../services/deliveryAssociates/findByEmail'; 10 | 11 | export const createShipment = async ( 12 | req: AppRequest, 13 | res: Response, 14 | next: NextFunction 15 | ) => { 16 | try { 17 | const userEmail = req.body.email; 18 | const userData = await findUserByEmail(userEmail); 19 | const userId = userData._id.toString(); 20 | const newShipment: IShipment = { 21 | pickupLocation: req.body.pickupLocation, 22 | dropLocation: req.body.dropLocation, 23 | status: ShipmentStatus.requested, 24 | userId, 25 | }; 26 | const createdShipment = await createOne(newShipment); 27 | console.log('createdShipment'); 28 | console.log(createdShipment); 29 | const response: AppResponse = { 30 | data: createdShipment, 31 | isError: false, 32 | }; 33 | res.send(response); 34 | } catch (error) { 35 | next(error); 36 | } 37 | }; 38 | 39 | export const patchDeliveryAssociate = async ( 40 | req: AppRequest, 41 | res: Response, 42 | next: NextFunction 43 | ) => { 44 | try { 45 | const shipmentId = req.params.id; 46 | const daEmail = req.body.email; 47 | const daData = await findDAByEmail(daEmail); 48 | const deliveryAssociateId = daData._id.toString(); 49 | const shipmentWithDeliveryAssociate = await updateDA( 50 | shipmentId, 51 | deliveryAssociateId 52 | ); 53 | console.log('shipmentWithDeliveryAssociate'); 54 | console.log(shipmentWithDeliveryAssociate); 55 | const response: AppResponse = { 56 | data: shipmentWithDeliveryAssociate, 57 | isError: false, 58 | }; 59 | res.send(response); 60 | } catch (error) { 61 | next(error); 62 | } 63 | }; 64 | 65 | export const patchStatus = async ( 66 | req: AppRequest, 67 | res: Response, 68 | next: NextFunction 69 | ) => { 70 | try { 71 | const shipmentId = req.params.id; 72 | const status = req.body.status; 73 | const shipment = await updateStatus(shipmentId, status); 74 | const response: AppResponse = { 75 | data: shipment, 76 | isError: false, 77 | }; 78 | res.send(response); 79 | } catch (error) { 80 | next(error); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /src/controller/user.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import AppResponse from '../types/AppResponse'; 3 | import createOne from '../services/users/createOne'; 4 | 5 | // Express route handler for creating new user 6 | // The json body should contain IUser interface 7 | export const createUser = async ( 8 | req: Request, 9 | res: Response, 10 | next: NextFunction 11 | ) => { 12 | try { 13 | // validate the request body based on IUser interface 14 | const email = req.body.email; 15 | const password = req.body.password; 16 | const name = req.body.name; 17 | if (!email || !password || !name) { 18 | const status = 400; 19 | const message = 20 | 'Invalid request body. Email, password and name are required.'; 21 | const errorResponse: AppResponse = { 22 | data: [], 23 | isError: true, 24 | errMsg: message, 25 | }; 26 | res.status(status).send(errorResponse); 27 | return; 28 | } 29 | const result = await createOne(req.body); 30 | res.status(201).json(result); 31 | } catch (error) { 32 | next(error); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/createIndex.ts: -------------------------------------------------------------------------------- 1 | import UserCollection from './models/User'; 2 | 3 | const indexUserCollection = async () => { 4 | try { 5 | const usersCollection = await UserCollection(); 6 | // createIndex only creates an index if it doesn't already exist 7 | await usersCollection.createIndex({ email: 1 }, { unique: true }); 8 | } catch (error) { 9 | console.log('Error creating index for users collection'); 10 | console.error(error); 11 | } 12 | }; 13 | 14 | const createAllIndexes = async () => { 15 | await indexUserCollection(); 16 | }; 17 | 18 | export default createAllIndexes; 19 | -------------------------------------------------------------------------------- /src/dbClient.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file exports an instance of the MongoDB client 3 | * The client exported is a singleton, so it will only be created once and reused for all subsequent calls 4 | * */ 5 | import { MongoClient } from 'mongodb'; 6 | import config from './config'; 7 | 8 | const uri = config.mongoConnString; 9 | let db: MongoClient; 10 | 11 | /** 12 | * Singleton function to connect to the database and return the client. 13 | * If a client is already connected, it will return the existing client 14 | * */ 15 | const dbConnect = async (): Promise => { 16 | try { 17 | if (db) { 18 | return db; 19 | } 20 | console.log('Connecting to MongoDB...'); 21 | const client = new MongoClient(uri); 22 | await client.connect(); 23 | console.log('Connected to db'); 24 | db = client; 25 | return db; 26 | } catch (error) { 27 | console.error('Error connecting to MongoDB', error); 28 | throw error; 29 | } 30 | }; 31 | 32 | export default dbConnect; 33 | -------------------------------------------------------------------------------- /src/models/DeliveryAssociate.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId, Collection, Document } from 'mongodb'; 2 | import IDeliveryAssociate from '../types/IDeliveryAssociate'; 3 | import dbClient from '../dbClient'; 4 | import { dbCollections } from '../constants'; 5 | 6 | export interface IDeliveryAssociateDocument 7 | extends IDeliveryAssociate, 8 | Document { 9 | _id?: ObjectId; 10 | } 11 | 12 | const DeliveryAssociateCollection = async (): Promise< 13 | Collection 14 | > => { 15 | const mongoClient = await dbClient(); 16 | const collection: Collection = mongoClient 17 | .db() 18 | .collection(dbCollections.deliveryAssociates); 19 | return collection; 20 | }; 21 | 22 | export default DeliveryAssociateCollection; 23 | -------------------------------------------------------------------------------- /src/models/Shipment.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId, Collection, Document } from 'mongodb'; 2 | import IShipment from '../types/IShipment'; 3 | import dbClient from '../dbClient'; 4 | import { dbCollections } from '../constants'; 5 | 6 | export interface IShipmentDocument 7 | extends IShipment, 8 | Document { 9 | _id?: ObjectId; 10 | } 11 | 12 | const ShipmentCollection = async (): Promise< 13 | Collection 14 | > => { 15 | const mongoClient = await dbClient(); 16 | const collection: Collection = mongoClient 17 | .db() 18 | .collection(dbCollections.shipments); 19 | return collection; 20 | }; 21 | 22 | export default ShipmentCollection; 23 | -------------------------------------------------------------------------------- /src/models/User.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId, Collection, Document } from 'mongodb'; 2 | import IUser from '../types/IUser'; 3 | import dbClient from '../dbClient'; 4 | import { dbCollections } from '../constants'; 5 | 6 | export interface IUserDocument 7 | extends IUser, 8 | Document { 9 | _id?: ObjectId; 10 | } 11 | 12 | const UserCollection = async (): Promise< 13 | Collection 14 | > => { 15 | const mongoClient = await dbClient(); 16 | const collection: Collection = mongoClient 17 | .db() 18 | .collection(dbCollections.users); 19 | return collection; 20 | }; 21 | 22 | export default UserCollection; 23 | -------------------------------------------------------------------------------- /src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { pong } from '../controller/ping'; 3 | import * as shipment from '../controller/shipment'; 4 | 5 | let routes = Router(); 6 | 7 | /** 8 | * Health check route. 9 | * Always returns 200 OK 10 | */ 11 | routes.get('/ping', pong); 12 | 13 | // Shipment 14 | routes.post('/shipment', shipment.createShipment); 15 | routes.patch( 16 | '/shipment/:id/delivery-associate', 17 | shipment.patchDeliveryAssociate 18 | ); 19 | routes.patch('/shipment/:id/status', shipment.patchStatus); 20 | export default routes; 21 | -------------------------------------------------------------------------------- /src/seed.ts: -------------------------------------------------------------------------------- 1 | import dbClient from './dbClient'; 2 | import createAllIndexes from './createIndex'; 3 | import IUser from 'IUser'; 4 | import IDeliveryAssociate, { 5 | DeliveryAssociateStatus, 6 | } from './types/IDeliveryAssociate'; 7 | import createOneUser from './services/users/createOne'; 8 | import createOneDA from './services/deliveryAssociates/createOne'; 9 | import findUserByEmail from './services/users/findByEmail'; 10 | import findDAByEmail from './services/deliveryAssociates/findByEmail'; 11 | 12 | const daJohn: IDeliveryAssociate = { 13 | name: 'John', 14 | email: 'john@example.com', 15 | status: DeliveryAssociateStatus.available, 16 | currentLocation: { coordinates: [0, 0], type: 'Point' }, 17 | }; 18 | 19 | const userAdam: IUser = { 20 | name: 'Adam', 21 | email: 'adam@example.com', 22 | password: 'password123', 23 | }; 24 | 25 | const main = async () => { 26 | await dbClient(); 27 | await createAllIndexes(); // Checking indexes on seeders might be required if seeders are executed separately. 28 | const adam = await findUserByEmail(userAdam.email); 29 | if (!adam) { 30 | await createOneUser(userAdam); 31 | } 32 | const john = await findDAByEmail(daJohn.email); 33 | if (!john) { 34 | await createOneDA(daJohn); 35 | } 36 | }; 37 | 38 | export default main; 39 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | import app from './app'; 3 | import http from 'http'; 4 | import { Server } from 'socket.io'; 5 | import socketHandler from './socketHandler'; 6 | import dbClient from './dbClient'; 7 | import createAllIndexes from './createIndex'; 8 | import seed from './seed'; 9 | import deliveryAssociateWatchers from './watchers/deliveryAssociates'; 10 | import shipmentWatchers from './watchers/shipment'; 11 | 12 | const server = http.createServer(app); 13 | const io = new Server(server, { 14 | cors: { 15 | origin: '*', 16 | methods: ['GET', 'POST'], 17 | // allowedHeaders: ['my-custom-header'], 18 | // credentials: true 19 | }, 20 | }); 21 | 22 | /** 23 | * Start Express server. 24 | */ 25 | server.listen(app.get('port'), async () => { 26 | try { 27 | await dbClient(); 28 | await createAllIndexes(); 29 | await seed(); 30 | 31 | socketHandler(io); 32 | 33 | // Initialize MongoDB ChangeStream watchers 34 | await deliveryAssociateWatchers(io); 35 | await shipmentWatchers(io); 36 | 37 | // Server start logs 38 | console.log('node version', process.version); 39 | const GREEN_LINE = '\x1b[32m%s\x1b[0m'; 40 | console.log(GREEN_LINE, 'Server started'); 41 | console.log(`Port: ${app.get('port')}`); 42 | console.log(`Environment: ${app.get('env')}`); 43 | } catch (error) { 44 | console.error(error); 45 | } 46 | }); 47 | 48 | export default server; 49 | -------------------------------------------------------------------------------- /src/services/deliveryAssociates/createOne.ts: -------------------------------------------------------------------------------- 1 | import DeliveryAssociateCollection, { 2 | IDeliveryAssociateDocument, 3 | } from '../../models/DeliveryAssociate'; 4 | import IDeliveryAssociate from '../../types/IDeliveryAssociate'; 5 | 6 | export default async function createOne( 7 | deliveryAssociate: IDeliveryAssociate 8 | ): Promise { 9 | try { 10 | const collection = await DeliveryAssociateCollection(); 11 | const newDoc = await collection.insertOne(deliveryAssociate); 12 | const result = await collection.findOne({ _id: newDoc.insertedId }); 13 | return result; 14 | } catch (error) { 15 | throw error; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/services/deliveryAssociates/findByEmail.ts: -------------------------------------------------------------------------------- 1 | import DeliveryAssociateCollection, { 2 | IDeliveryAssociateDocument, 3 | } from '../../models/DeliveryAssociate'; 4 | 5 | const findByEmail = async ( 6 | email: string 7 | ): Promise => { 8 | try { 9 | const collection = await DeliveryAssociateCollection(); 10 | const associate = await collection.findOne({ email }); 11 | return associate; 12 | } catch (error) { 13 | throw error; 14 | } 15 | }; 16 | export default findByEmail; 17 | -------------------------------------------------------------------------------- /src/services/deliveryAssociates/updateLocation.ts: -------------------------------------------------------------------------------- 1 | import { Point } from 'geojson'; 2 | import DeliveryAssociateCollection from '../../models/DeliveryAssociate'; 3 | 4 | const updateLocation = async ( 5 | email: string, 6 | location: Point 7 | ): Promise => { 8 | try { 9 | const collection = await DeliveryAssociateCollection(); 10 | await collection.findOneAndUpdate( 11 | { email }, 12 | { $set: { currentLocation: location } } 13 | ); 14 | } catch (error) { 15 | throw error; 16 | } 17 | }; 18 | export default updateLocation; 19 | -------------------------------------------------------------------------------- /src/services/shipments/createOne.ts: -------------------------------------------------------------------------------- 1 | import ShipmentCollection, { 2 | IShipmentDocument, 3 | } from '../../models/Shipment'; 4 | import IShipment from '../../types/IShipment'; 5 | 6 | export default async function createOne( 7 | shipment: IShipment 8 | ): Promise { 9 | try { 10 | const collection = await ShipmentCollection(); 11 | const newDoc = await collection.insertOne(shipment); 12 | const result = await collection.findOne({ _id: newDoc.insertedId }); 13 | return result; 14 | } catch (error) { 15 | throw error; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/services/shipments/updateDA.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from 'mongodb'; 2 | import ShipmentCollection, { IShipmentDocument } from '../../models/Shipment'; 3 | 4 | export default async function updateDA( 5 | _id: string, 6 | deliveryAssociateId: string 7 | ): Promise { 8 | try { 9 | const collection = await ShipmentCollection(); 10 | await collection.findOneAndUpdate( 11 | { _id: new ObjectId(_id) }, 12 | { $set: { deliveryAssociateId } } 13 | ); 14 | const result = await collection.findOne({ _id: new ObjectId(_id) }); 15 | return result; 16 | } catch (error) { 17 | console.error(error); 18 | throw error; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/services/shipments/updateStatus.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from 'mongodb'; 2 | import ShipmentCollection, { IShipmentDocument } from '../../models/Shipment'; 3 | import { ShipmentStatus } from '../../types/IShipment'; 4 | 5 | export default async function updateStatus( 6 | _id: string, 7 | status: ShipmentStatus 8 | ): Promise { 9 | try { 10 | const collection = await ShipmentCollection(); 11 | await collection.findOneAndUpdate( 12 | { _id: new ObjectId(_id) }, 13 | { $set: { status } } 14 | ); 15 | const result = await collection.findOne({ _id: new ObjectId(_id) }); 16 | return result; 17 | } catch (error) { 18 | throw error; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/services/users/createOne.ts: -------------------------------------------------------------------------------- 1 | import UserCollection, { 2 | IUserDocument, 3 | } from '../../models/User'; 4 | import IUser from '../../types/IUser'; 5 | 6 | export default async function createOne( 7 | user: IUser 8 | ): Promise { 9 | try { 10 | const collection = await UserCollection(); 11 | const newDoc = await collection.insertOne(user); 12 | const result = await collection.findOne({ _id: newDoc.insertedId }); 13 | return result; 14 | } catch (error) { 15 | throw error; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/services/users/findByEmail.ts: -------------------------------------------------------------------------------- 1 | import UserCollection, { 2 | IUserDocument, 3 | } from '../../models/User'; 4 | 5 | const findByEmail = async ( 6 | email: string 7 | ): Promise => { 8 | try { 9 | const collection = await UserCollection(); 10 | const user = await collection.findOne({ email }); 11 | return user; 12 | } catch (error) { 13 | throw error; 14 | } 15 | }; 16 | export default findByEmail; 17 | -------------------------------------------------------------------------------- /src/services/users/findById.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from 'mongodb'; 2 | import UserCollection, { IUserDocument } from '../../models/User'; 3 | 4 | const findById = async (id: string): Promise => { 5 | try { 6 | const collection = await UserCollection(); 7 | const user = await collection.findOne({ _id: new ObjectId(id) }); 8 | return user; 9 | } catch (error) { 10 | throw error; 11 | } 12 | }; 13 | export default findById; 14 | -------------------------------------------------------------------------------- /src/socketHandler.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'socket.io'; 2 | import { Point } from 'geojson'; 3 | import { socketEvents } from './constants'; 4 | import updateLocation from './services/deliveryAssociates/updateLocation'; 5 | 6 | interface IUpdateDALocation { 7 | email: string; 8 | location: Point; 9 | } 10 | interface ISubscribeToShipment { 11 | shipmentId: string; 12 | } 13 | interface ISubscribeToDA { 14 | deliveryAssociateId: string; 15 | } 16 | interface ILeaveRoom { 17 | roomId: string; 18 | } 19 | 20 | const socketHandler = (io: Server) => { 21 | io.on('connection', (socket: any) => { 22 | console.log('A user connected'); 23 | socket.on('disconnect', () => { 24 | console.log('A user disconnected'); 25 | }); 26 | 27 | // UPDATE_DA_LOCATION : Sent by delivery associates when driving 28 | socket.on( 29 | socketEvents.UPDATE_DA_LOCATION, 30 | async (data: IUpdateDALocation) => { 31 | const { email, location } = data; 32 | await updateLocation(email, location); 33 | } 34 | ); 35 | 36 | /** 37 | * Socket rooms are based on shipmentIds or deliveryAssociateIds 38 | * To listen to change streams user needs to subscribe to a shipmentId or deliveryAssociateId 39 | */ 40 | // SUBSCRIBE_TO_SHIPMENT 41 | socket.on( 42 | socketEvents.SUBSCRIBE_TO_SHIPMENT, 43 | (data: ISubscribeToShipment) => { 44 | socket.join(data.shipmentId); 45 | } 46 | ); 47 | // SUBSCRIBE_TO_DA 48 | socket.on(socketEvents.SUBSCRIBE_TO_DA, (data: ISubscribeToDA) => { 49 | socket.join(data.deliveryAssociateId); 50 | }); 51 | 52 | // LEAVE_ROOM 53 | socket.on(socketEvents.LEAVE_ROOM, (data: ILeaveRoom) => { 54 | socket.leave(data.roomId); 55 | }); 56 | }); 57 | }; 58 | export default socketHandler; 59 | -------------------------------------------------------------------------------- /src/types/AppRequest.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import IUserJWTPayload from './IUserJWTPayload'; 3 | 4 | interface AppRequest extends Request { 5 | user: IUserJWTPayload; 6 | } 7 | export default AppRequest; 8 | -------------------------------------------------------------------------------- /src/types/AppResponse.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a recommended response structure which should be used in every res.send() 3 | * This will help maintain a uniform response format from API 4 | */ 5 | export default interface AppResponse { 6 | data: any; 7 | isError: boolean; 8 | errMsg?: string; 9 | statusCode?: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/IDeliveryAssociate.ts: -------------------------------------------------------------------------------- 1 | import { Point } from 'geojson'; 2 | 3 | export enum DeliveryAssociateStatus { 4 | available = 'available', // ready to accept new shipment 5 | delivering = 'delivering', // transporting goods 6 | off = 'off', // on leave 7 | } 8 | 9 | export default interface IDeliveryAssociate { 10 | email: string; 11 | name: string; 12 | status: DeliveryAssociateStatus; 13 | currentLocation: Point; 14 | } 15 | -------------------------------------------------------------------------------- /src/types/IShipment.ts: -------------------------------------------------------------------------------- 1 | import { Point } from 'geojson'; 2 | import IUser from './IUser'; 3 | import IDeliveryAssociate from './IDeliveryAssociate'; 4 | 5 | export enum ShipmentStatus { 6 | requested = 'requested', 7 | deliveryAssociateAssigned = 'deliveryAssociateAssigned', 8 | pickupLocationReached = 'pickupLocationReached', 9 | transporting = 'transporting', 10 | dropLocationReached = 'dropLocationReached', 11 | delivered = 'delivered', 12 | cancelled = 'cancelled', 13 | } 14 | 15 | export default interface IShipment { 16 | pickupLocation: Point; 17 | dropLocation: Point; 18 | userId: string | IUser; 19 | deliveryAssociateId?: string | IDeliveryAssociate; 20 | status: ShipmentStatus; 21 | } 22 | -------------------------------------------------------------------------------- /src/types/IUser.ts: -------------------------------------------------------------------------------- 1 | export default interface IUser { 2 | email: string; 3 | name: string; 4 | password: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/types/IUserJWTPayload.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | interface IUserJWTPayload extends jwt.JwtPayload { 4 | userId: string; 5 | userEmail: string; 6 | organizationId: string; 7 | } 8 | export default IUserJWTPayload; 9 | -------------------------------------------------------------------------------- /src/watchers/deliveryAssociates.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'socket.io'; 2 | import { socketEvents } from '../constants'; 3 | import DeliveryAssociateCollection from '../models/DeliveryAssociate'; 4 | 5 | const watcher = async (io: Server) => { 6 | const collection = await DeliveryAssociateCollection(); 7 | const changeStream = collection.watch([], { fullDocument: 'updateLookup' }); 8 | changeStream.on('change', (event) => { 9 | if (event.operationType === 'update') { 10 | const fullDocument = event.fullDocument; 11 | io.to(String(fullDocument._id)).emit( 12 | socketEvents.DA_LOCATION_CHANGED, 13 | fullDocument 14 | ); 15 | } 16 | }); 17 | }; 18 | 19 | export default watcher; 20 | -------------------------------------------------------------------------------- /src/watchers/shipment.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'socket.io'; 2 | import { socketEvents } from '../constants'; 3 | import ShipmentCollection from '../models/Shipment'; 4 | 5 | const watcher = async (io: Server) => { 6 | const collection = await ShipmentCollection(); 7 | const changeStream = collection.watch([], { fullDocument: 'updateLookup' }); 8 | changeStream.on('change', (event) => { 9 | // @ts-ignore 10 | const fullDocument = event.fullDocument; 11 | if (event.operationType === 'insert') { 12 | // Broadcast Shipment Available Msg to Delivery Associates 13 | io.emit(socketEvents.SHIPMENT_CREATED, fullDocument); 14 | } 15 | if (event.operationType === 'update') { 16 | io.to(String(fullDocument._id)).emit( 17 | socketEvents.SHIPMENT_UPDATED, 18 | fullDocument 19 | ); 20 | } 21 | }); 22 | }; 23 | 24 | export default watcher; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "allowSyntheticDefaultImports": true, 6 | "target": "es6", 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "outDir": "dist", 11 | "baseUrl": ".", 12 | "paths": { 13 | "*": ["node_modules/*", "src/types/*"] 14 | } 15 | }, 16 | "include": ["src/**/*"] 17 | } 18 | --------------------------------------------------------------------------------