├── .env.example ├── .github └── workflows │ └── docker.yml ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── _img ├── active_packages.png ├── demo.gif └── new_install.png ├── api_server ├── .dockerignore ├── 3rd-party │ ├── parsers.js │ ├── test │ │ ├── test-usps-parser.js │ │ └── test-usps-tracker.js │ └── trackers.js ├── Dockerfile ├── index.js ├── models │ └── package.js ├── package-lock.json ├── package.json ├── routes │ └── api.js └── wait-for.sh ├── client ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── components │ ├── Footer.js │ ├── Input.js │ ├── ListPackages.js │ ├── Package.js │ ├── UpdateAllButton.js │ └── UpdateOneButton.js │ ├── img │ ├── amazon.svg │ ├── fedex.svg │ ├── ontrac.svg │ ├── ups.svg │ └── usps.svg │ ├── index.css │ ├── index.js │ ├── serviceWorker.js │ └── setupTests.js └── docker-compose.yml /.env.example: -------------------------------------------------------------------------------- 1 | # remove the curly braces when you replace the fields below: 2 | UPS_ACCESS_KEY={UPS ACCESS KEY HERE} 3 | USPS_USERNAME={USPS USERID KEY HERE} 4 | 5 | FEDEX_API_URL=https://apis.fedex.com 6 | FEDEX_API_KEY={FEDEX API KEY HERE} 7 | FEDEX_SECRET_KEY={FEDEX SECRET KEY HERE} 8 | 9 | MONGO_USERNAME={USERNAME HERE} 10 | MONGO_PASSWORD={PASSWORD HERE} 11 | MONGO_PORT=27017 12 | MONGO_DB=packages 13 | 14 | TZ='America/Denver' 15 | 16 | # change the following when using the debug-xxx-parser or debug-xxx-tracker 17 | # scripts from package.json 18 | TEST_TRACKING_NUMBER=XYZ123ABC789 -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | env: 6 | REGISTRY: ghcr.io 7 | IMAGE_NAME: ${{ github.repository }} 8 | name: deploy 9 | jobs: 10 | docker: 11 | strategy: 12 | matrix: 13 | image: ["api_server", "client"] 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | steps: 19 | - name: checkout 20 | uses: actions/checkout@v3 21 | 22 | - name: docker meta 23 | id: dockermeta 24 | uses: docker/metadata-action@v4 25 | with: 26 | images: ghcr.io/${{ github.repository }}/${{ matrix.image }} 27 | tags: | 28 | type=sha 29 | type=raw,value=latest 30 | 31 | - name: log in to docker registry 32 | uses: docker/login-action@v2 33 | with: 34 | registry: ${{ env.REGISTRY }} 35 | username: ${{ github.actor }} 36 | password: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: set up qemu 39 | uses: docker/setup-qemu-action@v2 40 | 41 | - name: set up docker buildx 42 | uses: docker/setup-buildx-action@v2 43 | 44 | - name: Build and push 45 | uses: docker/build-push-action@v4 46 | with: 47 | context: ${{ matrix.image }} 48 | push: true 49 | tags: ${{ steps.dockermeta.outputs.tags }} 50 | labels: ${{ steps.dockermeta.outputs.labels }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* -------------------------------------------------------------------------------- /.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 | "name": "Debug Fedex Tracker", 9 | "request": "launch", 10 | "runtimeArgs": [ 11 | "run-script", 12 | "debug-fedex-tracker" 13 | ], 14 | "runtimeExecutable": "npm", 15 | "skipFiles": [ 16 | "/**" 17 | ], 18 | "type": "node" 19 | }, 20 | { 21 | "name": "Debug Fedex Parser", 22 | "request": "launch", 23 | "runtimeArgs": [ 24 | "run-script", 25 | "debug-fedex-parser" 26 | ], 27 | "runtimeExecutable": "npm", 28 | "skipFiles": [ 29 | "/**" 30 | ], 31 | "type": "node" 32 | }, 33 | { 34 | "name": "Debug USPS Tracker", 35 | "request": "launch", 36 | "runtimeArgs": [ 37 | "run-script", 38 | "debug-usps-tracker" 39 | ], 40 | "runtimeExecutable": "npm", 41 | "skipFiles": [ 42 | "/**" 43 | ], 44 | "type": "node" 45 | }, 46 | { 47 | "name": "Debug USPS Tracker (node)", 48 | "request": "launch", 49 | "envFile": "${workspaceFolder}/.env", 50 | "cwd": "${workspaceFolder}/api_server", 51 | "runtimeArgs": [ 52 | "3rd-party/test/test-usps-tracker.js" 53 | ], 54 | "skipFiles": [ 55 | "/**" 56 | ], 57 | "type": "node" 58 | }, 59 | { 60 | "name": "Debug USPS Parser", 61 | "request": "launch", 62 | "runtimeArgs": [ 63 | "run-script", 64 | "debug-usps-parser" 65 | ], 66 | "runtimeExecutable": "npm", 67 | "skipFiles": [ 68 | "/**" 69 | ], 70 | "type": "node" 71 | }, 72 | { 73 | "name": "Debug USPS Parser (node)", 74 | "request": "launch", 75 | "envFile": "${workspaceFolder}/.env", 76 | "cwd": "${workspaceFolder}/api_server", 77 | "runtimeArgs": [ 78 | "3rd-party/test/test-usps-parser.js" 79 | ], 80 | "skipFiles": [ 81 | "/**" 82 | ], 83 | "type": "node" 84 | }, 85 | { 86 | "name": "Debug UPS Tracker", 87 | "request": "launch", 88 | "runtimeArgs": [ 89 | "run-script", 90 | "debug-ups-tracker" 91 | ], 92 | "runtimeExecutable": "npm", 93 | "skipFiles": [ 94 | "/**" 95 | ], 96 | "type": "node" 97 | }, 98 | { 99 | "name": "Debug UPS Parser", 100 | "request": "launch", 101 | "runtimeArgs": [ 102 | "run-script", 103 | "debug-ups-parser" 104 | ], 105 | "runtimeExecutable": "npm", 106 | "skipFiles": [ 107 | "/**" 108 | ], 109 | "type": "node" 110 | }, 111 | { 112 | "name": "Debug Amazon Tracker", 113 | "request": "launch", 114 | "runtimeArgs": [ 115 | "run-script", 116 | "debug-amazon-tracker" 117 | ], 118 | "runtimeExecutable": "npm", 119 | "skipFiles": [ 120 | "/**" 121 | ], 122 | "type": "node" 123 | }, 124 | { 125 | "name": "Debug Amazon Parser", 126 | "request": "launch", 127 | "runtimeArgs": [ 128 | "run-script", 129 | "debug-amazon-parser" 130 | ], 131 | "runtimeExecutable": "npm", 132 | "skipFiles": [ 133 | "/**" 134 | ], 135 | "type": "node" 136 | }, 137 | { 138 | "name": "Debug OnTrac Tracker", 139 | "request": "launch", 140 | "runtimeArgs": [ 141 | "run-script", 142 | "debug-ontrac-tracker" 143 | ], 144 | "runtimeExecutable": "npm", 145 | "skipFiles": [ 146 | "/**" 147 | ], 148 | "type": "node" 149 | }, 150 | { 151 | "name": "Debug OnTrac Parser", 152 | "request": "launch", 153 | "runtimeArgs": [ 154 | "run-script", 155 | "debug-ontrac-parser" 156 | ], 157 | "runtimeExecutable": "npm", 158 | "skipFiles": [ 159 | "/**" 160 | ], 161 | "type": "node" 162 | }, 163 | ] 164 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 - Joshua Taillon 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so, subject to the following 8 | conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies 11 | or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 15 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 16 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 17 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 18 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![animated demo of the client application](_img/demo.gif) 2 |

PackageMate: Self-hosted package tracking!

3 | 4 | This is a simple app using MongoDB, Express.js, React, and Node to allow a user 5 | to create _package_ records and fetch their status from the carriers' APIs 6 | (or scraping, sometimes). 7 | 8 | **Note:** This app was made as a full-stack app learning experience, so it certainly has some 9 | roughness around the edges, and I'm not responsible if it causes your packages to burst into 10 | flames... 11 | 12 | It requires access to the carriers' API tools, which differs a bit for each carrier. The 13 | following links have more information on how to create accounts and get credentials. 14 | Some of the carriers have unstable (or unavailable) APIs, and so a web scraping approach 15 | is used (currently for FedEx and Ontrac). This means these carriers take slightly longer to 16 | update, depending on how quickly the website responds, but they should eventually update. 17 | 18 | ## Tracking APIs: 19 | 20 | USPS: 21 | - https://www.usps.com/business/web-tools-apis/track-and-confirm-api_files/track-and-confirm-api.htm#_Toc41911503 22 | - Sign up here (free): https://www.usps.com/business/web-tools-apis/documentation-updates.htm 23 | - Once you get it, put your USPS "username" that they provide into `.env` in the proper place 24 | 25 | UPS: 26 | - https://www.ups.com/upsdeveloperkit/ 27 | - Sign up here (free, but need a UPS account with payment method attached): https://www.ups.com/upsdeveloperkit/announcements 28 | - It took a while (about a day) when I signed up for my "account" to show in the list on the 29 | developer kit signup page, but once you sign up, place the UPS "Access key" into `.env` 30 | 31 | Fedex: 32 | - https://developer.fedex.com/api/en-us/home.html 33 | - Sign up here, create an "organization", then a new project (I named mine "PackageMate"), 34 | and then select the "Tracking API" and fill out the information they ask for. You will 35 | be given an "API Key", "Secret Key", and "Shipping Account" under the "TestKey" domain, 36 | but this Test key won't work for any real data. To access real tracking data, you need 37 | to move to "production", which requeires a Fedex Shipping account. Making an account 38 | (and the API access) is free, but does require a credit card. Follow the steps to open 39 | a "shipping account", and then link that account to your developer account. Once done, 40 | you'll get the real "API Key" and "Secret Key" in the production domain. 41 | - Put these values into the `.env` file in the proper place to enable the FedEx tracker. 42 | 43 | OnTrac: 44 | - Becuase of issues getting access to the API, OnTrac package status is obtained by 45 | scraping the public website, so no API credentials are needed. 46 | 47 | ## Initial setup 48 | 49 | The project is built using Node, and so can be run (with a few modifications) by using 50 | the standard `npm start` approach (check the `package.json` files for more details about 51 | the scripts that it uses). 52 | 53 | Since there are a couple moving parts however, it is easier to get up and running using 54 | [Docker](www.docker.com) (specifically, [docker compose](https://docs.docker.com/compose/)). 55 | The `docker-compose.yml` file specifies three containers that will communicate with each 56 | other to run the app: a MongoDB database for data storage, a Node/Express server that 57 | does the API checks/scraping, and a React client application that you interact with in 58 | the browser. 59 | 60 | ### Clone or download the project using git: 61 | 62 | ```sh 63 | $ git clone https://github.com/jat255/PackageMate.git 64 | $ cd PackageMate 65 | ``` 66 | 67 | Once this has downloaded, rename the `.env.example` file to `.env`, and replace the values 68 | indicated with ones that make sense for you. The two API credentials you obtained for 69 | UPS and USPS, and then the username and password for the MongoDB instance you will create 70 | (can be anything, they don't _really_ matter). 71 | 72 | ### Build the Docker images and run the app 73 | 74 | From the cloned folder, you should be able to run the app with one command: 75 | 76 | ```sh 77 | $ docker-compose up --build -d 78 | ``` 79 | 80 | The first time this is run, it will take quite some time, since it will go through 81 | and build the three docker images (and the server requires a number of dependencies 82 | since `playwright` uses a headless version of Chromium for scraping). The `-d` flag 83 | tells Docker to detach after bringing up all the containers. `--build` tells it 84 | to build (or rebuild) the containers. Assuming all went well, 85 | the app should be running in the background and should be accessible in your web 86 | browser at http://localhost (or perhaps a different port if you changed it in 87 | `docker-compose.yml`). 88 | 89 | To view the logs of the application, check the browser console (for the client), or 90 | run the following to see logs from each container in the app stack: 91 | 92 | ```sh 93 | $ docker-compose logs --follow 94 | ``` 95 | 96 | To shut down the containers, run: 97 | 98 | ```sh 99 | $ docker-compose down 100 | ``` 101 | 102 | The application data is stored in a [docker volume](https://docs.docker.com/storage/volumes/), 103 | so to clear the app's data, you'll have to use docker tools. First find a list of the 104 | existing volumes using: 105 | 106 | ```sh 107 | $ docker volume list 108 | ``` 109 | 110 | Then to remove the `dbdata` volume: 111 | 112 | ```sh 113 | $ docker volume rm packagemate_dbdata 114 | ``` 115 | 116 | ## Using the app 117 | 118 | When you visit the app for the first time, there will be no packages in the system, so 119 | the display will be empty: 120 | 121 | ![](_img/new_install.png) 122 | 123 | To add a package to the tracker, select the 124 | correct carrier from the dropdown, paste the 125 | tracking number into the appropriate box, add a 126 | description (if desired), and click the "Add 127 | Package" button (or press enter). 128 | 129 | The package will show up in the table below, and 130 | the app will immediately try to update the status 131 | of the package. 132 | 133 | To update an individual package's status, click 134 | the gray update button to the the right of the 135 | table. Trackers using the scraping method (FedEx 136 | and OnTrac, currently) will be significantly 137 | slower than the direct API methods, unfortunately. 138 | Be patient, and they should finish updating. 139 | 140 | ![app with some packages loaded in](_img/active_packages.png) 141 | 142 | To update all packages at once, click the blue 143 | button above the table. The button will show 144 | the progress of the operation to give an indication 145 | of how many packages are left to track. 146 | 147 | To stop tracking a package, click the red 148 | "archive" button for that package. The 149 | package will be moved to the "Archived" tab, 150 | and will no longer be included in any updates. 151 | 152 | ## How to contribute? 153 | 154 | Pull requests and feature additions are always welcome! In particular, 155 | PackageMate only supports four carriers, currently (the ones that I 156 | receive packages through most often). If there's another carrier 157 | that you would like to support, it should be added to the database 158 | model in `models/package.js` (the `carrier` enum), the update function 159 | in `routes/api.js`, and then in `3rd-party/trackers.js` and 160 | `3rd-party/parsers.js`. 161 | 162 | ### Testing trackers and parsers 163 | 164 | Separate from the Docker and web application stack, there are some npm scripts 165 | included to help debug/test the tracker and parser routines. A few examples 166 | ar provided in the `package.json` such as the scripts `debug-fedex-tracker` and 167 | `debug-fedex-parser`, which can be run from the command line with 168 | `$ npm run debug-fedex-tracker`. This helps accelerate the tracker development 169 | process, as you don't have to wait for docker application to rebuild in order 170 | to test the response (change the tracking number provided in the script to something 171 | that makes sense to get it to work). 172 | -------------------------------------------------------------------------------- /_img/active_packages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jat255/PackageMate/272b97ab895a43640ad89482b28576c1648c9f3c/_img/active_packages.png -------------------------------------------------------------------------------- /_img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jat255/PackageMate/272b97ab895a43640ad89482b28576c1648c9f3c/_img/demo.gif -------------------------------------------------------------------------------- /_img/new_install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jat255/PackageMate/272b97ab895a43640ad89482b28576c1648c9f3c/_img/new_install.png -------------------------------------------------------------------------------- /api_server/.dockerignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .env 3 | node_modules/**/* 4 | node_modules/* 5 | node_modules 6 | client/**/* 7 | client 8 | -------------------------------------------------------------------------------- /api_server/3rd-party/parsers.js: -------------------------------------------------------------------------------- 1 | const util = require('util') 2 | 3 | const uspsParser = (response) => { 4 | const jsdom = require("jsdom"); 5 | const dom = new jsdom.JSDOM(""); 6 | const DOMParser = dom.window.DOMParser; 7 | const parser = new DOMParser; 8 | const document = parser.parseFromString(response[0], "text/xml"); 9 | let stat 10 | 11 | try { 12 | stat = document.querySelector("TrackSummary").textContent; 13 | } catch (error) { 14 | console.error(error); 15 | try { 16 | stat = document.querySelector("Description").textContent; 17 | } catch (error) { 18 | console.error(error); 19 | stat = "Could not parse USPS response"; 20 | } 21 | } 22 | 23 | return stat; 24 | } 25 | 26 | const upsParser = (response) => { 27 | // console.debug("response:") 28 | // console.debug(util.inspect(response, depth=null)); 29 | // console.debug("response[0].trackResponse.shipment[0]") 30 | // console.debug(util.inspect(response[0].trackResponse.shipment[0], depth=null)) 31 | console.debug("response[0].trackResponse.shipment[0].package[0].deliveryDate[0].date") 32 | try { 33 | console.debug(util.inspect(response[0].trackResponse.shipment[0].package[0].deliveryDate[0].date, depth=null)) } 34 | catch(err) { 35 | console.debug("No delivery date found") 36 | } 37 | console.debug(" ") 38 | console.debug("lastActivity") 39 | let lastActivity = response[0].trackResponse.shipment[0].package[0].activity[0]; 40 | console.debug(util.inspect(lastActivity)) 41 | console.debug(" ") 42 | // response format: 43 | // 44 | // { 45 | // location: { 46 | // address: { 47 | // city: 'Denver', 48 | // stateProvince: 'CO', 49 | // postalCode: '80239', 50 | // country: 'US' 51 | // } 52 | // }, 53 | // status: { 54 | // type: 'I', 55 | // description: 'Package departed UPS Mail Innovations facility enroute to USPS for induction', 56 | // code: 'ZR' 57 | // }, 58 | // date: '20200905', 59 | // time: '122200' 60 | // } 61 | let expectedDeliveryDate; 62 | console.debug("getting expectedDeliveryDate") 63 | try { 64 | expectedDeliveryDate = response[0].trackResponse.shipment[0].package[0].deliveryDate[0].date; 65 | } catch(err) { 66 | console.debug('expectedDeliveryDate undefined!'); 67 | expectedDeliveryDate = ''; 68 | } 69 | console.debug(`expectedDeliveryDate: ${expectedDeliveryDate}`) 70 | console.debug(" ") 71 | // date is in '20211117' format 72 | var tc = require("timezonecomplete"); 73 | 74 | let city = `${lastActivity.location.address.city}` 75 | city = titleCase(city); 76 | console.debug(`city: ${city}`); 77 | let state = `${lastActivity.location.address.stateProvince}` 78 | console.debug(`state: ${state}`); 79 | 80 | let location; 81 | if (city == '' && state == '') { 82 | location = 'Unknown location'; 83 | } else if (city == '') { 84 | location = state; 85 | } else if (state == '') { 86 | location = city; 87 | } else { 88 | location = `${city}, ${state}`; 89 | } 90 | console.debug(`location: ${location}`); 91 | 92 | let desc = `${lastActivity.status.description}` 93 | console.debug(`desc: ${desc}`); 94 | let dateRegex = /(\d{4})(\d{2})(\d{2})/; 95 | let dateArray = dateRegex.exec(`${lastActivity.date}`); 96 | console.debug(`dateArray: ${dateArray}`); 97 | let expectedDateArray = dateRegex.exec(expectedDeliveryDate); 98 | console.debug(`expectedDateArray: ${expectedDateArray}`); 99 | let expectedDateStr = 'Unknown'; 100 | if (expectedDateArray !== null) { 101 | let expectedDate = new tc.DateTime( 102 | (+expectedDateArray[1]), 103 | (+expectedDateArray[2]), 104 | (+expectedDateArray[3]) 105 | ); 106 | expectedDateStr = expectedDate.format("yyyy-MM-dd"); 107 | } 108 | console.debug(`expectedDateStr: ${expectedDateStr}`); 109 | let timeRegex = /(\d{2})(\d{2})(\d{2})/; 110 | let timeArray = timeRegex.exec(`${lastActivity.time}`); 111 | 112 | // create tz-aware datetime object: 113 | let updateDate = new tc.DateTime( 114 | (+dateArray[1]), 115 | (+dateArray[2]), 116 | (+dateArray[3]), 117 | (+timeArray[1]), 118 | (+timeArray[2]), 119 | (+timeArray[3]), 120 | ); 121 | 122 | let stat = `${location} (${updateDate.format("yyyy-MM-dd hh:mm a")}) – ${desc} 123 | Expected: ${expectedDateStr}` 124 | 125 | return stat 126 | } 127 | 128 | const fedExParser = (response) => { 129 | // response looks like: 130 | // { 131 | // "transactionId": "4686ea11-12da-4f30-a25c-3498bb76fade", 132 | // "output": { 133 | // "completeTrackResults": [ 134 | // { 135 | // "trackingNumber": "277053421731", 136 | // "trackResults": [ 137 | // { 138 | // "trackingNumberInfo": { 139 | // "trackingNumber": "277053421731", 140 | // "trackingNumberUniqueId": "12024~277053421731~FDEG", 141 | // "carrierCode": "FDXG" 142 | // }, 143 | // "additionalTrackingInfo": { 144 | // "nickname": "", 145 | // "hasAssociatedShipments": false 146 | // }, 147 | // "shipperInformation": { 148 | // "contact": {}, 149 | // "address": { 150 | // "city": "Memphis", 151 | // "stateOrProvinceCode": "TN", 152 | // "countryCode": "US", 153 | // "residential": false, 154 | // "countryName": "United States" 155 | // } 156 | // }, 157 | // "recipientInformation": { 158 | // "contact": {}, 159 | // "address": { 160 | // "city": "HOLDERNESS", 161 | // "stateOrProvinceCode": "NH", 162 | // "countryCode": "US", 163 | // "residential": false, 164 | // "countryName": "United States" 165 | // } 166 | // }, 167 | // "latestStatusDetail": { 168 | // "code": "DP", 169 | // "derivedCode": "IT", 170 | // "statusByLocale": "In transit", 171 | // "description": "Departed FedEx location", 172 | // "scanLocation": { 173 | // "city": "MEMPHIS", 174 | // "stateOrProvinceCode": "TN", 175 | // "countryCode": "US", 176 | // "residential": false, 177 | // "countryName": "United States" 178 | // }, 179 | // "delayDetail": { 180 | // "status": "ON_TIME" 181 | // } 182 | // }, 183 | // "dateAndTimes": [ 184 | // { 185 | // "type": "ESTIMATED_DELIVERY", 186 | // "dateTime": "2022-08-25T00:00:00-06:00" 187 | // }, 188 | // { 189 | // "type": "COMMITMENT", 190 | // "dateTime": "2022-08-25T00:00:00-06:00" 191 | // }, 192 | // { 193 | // "type": "ACTUAL_PICKUP", 194 | // "dateTime": "2022-08-22T00:00:00-06:00" 195 | // }, 196 | // { 197 | // "type": "SHIP", 198 | // "dateTime": "2022-08-22T00:00:00-06:00" 199 | // }, 200 | // { 201 | // "type": "ACTUAL_TENDER", 202 | // "dateTime": "2022-08-22T00:00:00-06:00" 203 | // }, 204 | // { 205 | // "type": "ANTICIPATED_TENDER", 206 | // "dateTime": "2022-08-22T00:00:00-06:00" 207 | // } 208 | // ], 209 | // "availableImages": [], 210 | // "packageDetails": { 211 | // "packagingDescription": { 212 | // "type": "YOUR_PACKAGING", 213 | // "description": "Package" 214 | // }, 215 | // "physicalPackagingType": "PACKAGE", 216 | // "sequenceNumber": "1", 217 | // "count": "1", 218 | // "weightAndDimensions": { 219 | // "weight": [ 220 | // { 221 | // "value": "5.3", 222 | // "unit": "LB" 223 | // }, 224 | // { 225 | // "value": "2.4", 226 | // "unit": "KG" 227 | // } 228 | // ] 229 | // }, 230 | // "packageContent": [] 231 | // }, 232 | // "shipmentDetails": { 233 | // "possessionStatus": true 234 | // }, 235 | // "scanEvents": [ 236 | // { 237 | // "date": "2022-08-23T09:59:57-05:00", 238 | // "eventType": "DP", 239 | // "eventDescription": "Departed FedEx location", 240 | // "exceptionCode": "", 241 | // "exceptionDescription": "", 242 | // "scanLocation": { 243 | // "streetLines": [ 244 | // "" 245 | // ], 246 | // "city": "MEMPHIS", 247 | // "stateOrProvinceCode": "TN", 248 | // "postalCode": "38106", 249 | // "countryCode": "US", 250 | // "residential": false, 251 | // "countryName": "United States" 252 | // }, 253 | // "locationId": "0381", 254 | // "locationType": "FEDEX_FACILITY", 255 | // "derivedStatusCode": "IT", 256 | // "derivedStatus": "In transit" 257 | // }, 258 | // { 259 | // "date": "2022-08-22T17:07:00-05:00", 260 | // "eventType": "AR", 261 | // "eventDescription": "Arrived at FedEx location", 262 | // "exceptionCode": "", 263 | // "exceptionDescription": "", 264 | // "scanLocation": { 265 | // "streetLines": [ 266 | // "" 267 | // ], 268 | // "city": "MEMPHIS", 269 | // "stateOrProvinceCode": "TN", 270 | // "postalCode": "38106", 271 | // "countryCode": "US", 272 | // "residential": false, 273 | // "countryName": "United States" 274 | // }, 275 | // "locationId": "0381", 276 | // "locationType": "FEDEX_FACILITY", 277 | // "derivedStatusCode": "IT", 278 | // "derivedStatus": "In transit" 279 | // }, 280 | // { 281 | // "date": "2022-08-22T10:37:00-05:00", 282 | // "eventType": "OC", 283 | // "eventDescription": "Shipment information sent to FedEx", 284 | // "exceptionCode": "", 285 | // "exceptionDescription": "", 286 | // "scanLocation": { 287 | // "streetLines": [ 288 | // "" 289 | // ], 290 | // "postalCode": "38118", 291 | // "countryCode": "US", 292 | // "residential": false, 293 | // "countryName": "United States" 294 | // }, 295 | // "locationType": "CUSTOMER", 296 | // "derivedStatusCode": "IN", 297 | // "derivedStatus": "Initiated" 298 | // }, 299 | // { 300 | // "date": "2022-08-22T00:00:00", 301 | // "eventType": "PU", 302 | // "eventDescription": "Picked up", 303 | // "exceptionCode": "", 304 | // "exceptionDescription": "", 305 | // "scanLocation": { 306 | // "streetLines": [ 307 | // "" 308 | // ], 309 | // "city": "OLIVE BRANCH", 310 | // "stateOrProvinceCode": "MS", 311 | // "postalCode": "38654", 312 | // "countryCode": "US", 313 | // "residential": false, 314 | // "countryName": "United States" 315 | // }, 316 | // "locationId": "0386", 317 | // "locationType": "PICKUP_LOCATION", 318 | // "derivedStatusCode": "PU", 319 | // "derivedStatus": "Picked up" 320 | // } 321 | // ], 322 | // "availableNotifications": [ 323 | // "ON_DELIVERY", 324 | // "ON_EXCEPTION" 325 | // ], 326 | // "deliveryDetails": { 327 | // "deliveryAttempts": "0", 328 | // "deliveryOptionEligibilityDetails": [ 329 | // { 330 | // "option": "INDIRECT_SIGNATURE_RELEASE", 331 | // "eligibility": "POSSIBLY_ELIGIBLE" 332 | // }, 333 | // { 334 | // "option": "REDIRECT_TO_HOLD_AT_LOCATION", 335 | // "eligibility": "POSSIBLY_ELIGIBLE" 336 | // }, 337 | // { 338 | // "option": "REROUTE", 339 | // "eligibility": "POSSIBLY_ELIGIBLE" 340 | // }, 341 | // { 342 | // "option": "RESCHEDULE", 343 | // "eligibility": "POSSIBLY_ELIGIBLE" 344 | // }, 345 | // { 346 | // "option": "RETURN_TO_SHIPPER", 347 | // "eligibility": "POSSIBLY_ELIGIBLE" 348 | // }, 349 | // { 350 | // "option": "DISPUTE_DELIVERY", 351 | // "eligibility": "POSSIBLY_ELIGIBLE" 352 | // }, 353 | // { 354 | // "option": "SUPPLEMENT_ADDRESS", 355 | // "eligibility": "INELIGIBLE" 356 | // } 357 | // ] 358 | // }, 359 | // "originLocation": { 360 | // "locationContactAndAddress": { 361 | // "address": { 362 | // "city": "OLIVE BRANCH", 363 | // "stateOrProvinceCode": "MS", 364 | // "countryCode": "US", 365 | // "residential": false, 366 | // "countryName": "United States" 367 | // } 368 | // } 369 | // }, 370 | // "lastUpdatedDestinationAddress": { 371 | // "city": "HOLDERNESS", 372 | // "stateOrProvinceCode": "NH", 373 | // "countryCode": "US", 374 | // "residential": false, 375 | // "countryName": "United States" 376 | // }, 377 | // "serviceDetail": { 378 | // "type": "GROUND_HOME_DELIVERY", 379 | // "description": "FedEx Home Delivery", 380 | // "shortDescription": "HD" 381 | // }, 382 | // "standardTransitTimeWindow": { 383 | // "window": { 384 | // "ends": "2022-08-25T00:00:00-06:00" 385 | // } 386 | // }, 387 | // "estimatedDeliveryTimeWindow": { 388 | // "window": {} 389 | // }, 390 | // "goodsClassificationCode": "", 391 | // "returnDetail": {} 392 | // } 393 | // ] 394 | // } 395 | // ] 396 | // } 397 | // } 398 | console.debug('Parsing Fedex response[0]'); 399 | console.debug(JSON.stringify(response[0], undefined, 2)); 400 | response = response[0]; 401 | 402 | if (response && typeof response === 'object' && 'error' in response) { 403 | console.debug('Found error, so returning error message') 404 | return response.error; 405 | } 406 | 407 | console.debug("Loading timezonecomplete") 408 | var tc = require("timezonecomplete"); 409 | 410 | console.debug("Extracting trackResults") 411 | let trackResults = response.output.completeTrackResults[0].trackResults[0]; 412 | console.debug(`trackResults: ${JSON.stringify(trackResults, undefined, 2)}`); 413 | 414 | // catch tracking error early 415 | if (trackResults && typeof trackResults === 'object' && 'error' in trackResults) { 416 | console.debug('Found error in track results, returning error message:'); 417 | console.debug(trackResults.error.message) 418 | return trackResults.error.message; 419 | } 420 | 421 | let scanEvents = trackResults.scanEvents; 422 | console.debug(`scanEvents: ${JSON.stringify(scanEvents, undefined, 2)}`); 423 | 424 | let latestScan = scanEvents[0]; 425 | console.debug(`latestScan: ${JSON.stringify(latestScan, undefined, 2)}`); 426 | 427 | let dateAndTimes = trackResults.dateAndTimes; 428 | console.debug(`dateAndTimes: ${JSON.stringify(dateAndTimes, undefined, 2)}`); 429 | 430 | // get expected delivery date 431 | const estimatedDate = dateAndTimes.find(item => item.type === 'ESTIMATED_DELIVERY'); 432 | console.debug(`estimatedDate: ${JSON.stringify(estimatedDate, undefined, 2)}`); 433 | 434 | let estimatedDateText 435 | if (estimatedDate === undefined) { 436 | estimatedDateText = 'Expected date unknown' 437 | } else { 438 | estimatedDateText = `Expected: ${new tc.DateTime(estimatedDate.dateTime).format('YYYY-MM-dd')}` 439 | } 440 | console.debug(`estimatedDateText: ${estimatedDateText}`) 441 | 442 | // get current location and status 443 | let locationString; 444 | let city = latestScan.scanLocation.city; 445 | console.debug(`city: ${city}`) 446 | let state = latestScan.scanLocation.stateOrProvinceCode; 447 | console.debug(`state: ${state}`) 448 | 449 | if (city === undefined && state === undefined) { 450 | locationString = 'Unknown location' 451 | } else { 452 | locationString = `${titleCase(city).trim()}, ${state}` 453 | } 454 | console.debug(`locationString: ${locationString}`) 455 | 456 | 457 | let desc = latestScan.eventDescription; 458 | console.debug(`desc: ${desc}`) 459 | 460 | let scanDate = new tc.DateTime(latestScan.date).format("(yyyy-MM-dd hh:mm a)") 461 | console.debug(`scanDate: ${scanDate}`) 462 | 463 | let status = `${locationString} ${scanDate} - ${desc} 464 | ${estimatedDateText}` 465 | console.debug(`status: ${status}`) 466 | 467 | 468 | return status 469 | } 470 | 471 | const onTracParser = (response) => { 472 | var tc = require("timezonecomplete"); 473 | let res = response[0]; 474 | // response format: 475 | // { 476 | // expected_date: '05/23/23', 477 | // event_summary: 'Package en route.', 478 | // event_detail: 'The package arrived at an originating OnTrac location and is on its way to your local OnTrac facility for final delivery.', 479 | // event_location: 'PHOENIX, AZ', 480 | // event_date: '05/20/23 at 6:55 PM' 481 | // } 482 | let [city, state] = res.event_location.split(', '); 483 | city = city.charAt(0).toUpperCase() + city.slice(1).toLowerCase(); 484 | console.debug(`city, state: ${city}, ${state}`); 485 | let [event_date, event_time] = res.event_date.split(' at '); 486 | let dateTime = new tc.DateTime( 487 | `${event_date} - ${event_time}`, 488 | "MM/dd/yy - hh:mm aa" 489 | ) 490 | 491 | let stat = `${city}, ${state} (${dateTime.format("yyyy-MM-dd hh:mm a")}) - ${res.event_summary} 492 | ${res.event_detail} 493 | Expected: ${res.expected_date}`; 494 | console.debug(`stat: ${stat}`) 495 | // Expected: ${res[res.length - 1]}`; 496 | return stat; 497 | } 498 | 499 | const amazonParser = (response) => { 500 | console.debug("Parsing Amazon response") 501 | var tc = require("timezonecomplete"); 502 | // console.debug("loaded timezonecomplete") 503 | // response format: 504 | 505 | // { progressTracker: 506 | // '{"progressMeter":{"milestoneList":[{"position":0,"mileStoneType":"NORMAL","eventSummary":{"statusElement":{"translatorString":{"localisedStringId":"swa_rex_shipping_label_created","fieldValueMap":null},"statusCode":null},"reasonElement":null,"metaDataElement":{"translatorString":{"localisedStringId":null,"fieldValueMap":null}},"timeElement":null},"isActive":true},{"position":1,"mileStoneType":"NORMAL","eventSummary":{"statusElement":{"translatorString":{"localisedStringId":"swa_rex_intransit","fieldValueMap":null},"statusCode":null},"reasonElement":null,"metaDataElement":{"translatorString":{"localisedStringId":null,"fieldValueMap":null}},"timeElement":"2021-06-08T03:00:00.000Z"},"isActive":true},{"position":2,"mileStoneType":"NORMAL","eventSummary":{"statusElement":{"translatorString":{"localisedStringId":"swa_rex_ofd","fieldValueMap":null},"statusCode":null},"reasonElement":null,"metaDataElement":{"translatorString":{"localisedStringId":null,"fieldValueMap":null}},"timeElement":"2021-06-08T21:41:45.000Z"},"isActive":true},{"position":3,"mileStoneType":"NORMAL","eventSummary":{"statusElement":{"translatorString":{"localisedStringId":"swa_rex_delivered","fieldValueMap":null},"statusCode":null},"reasonElement":null,"metaDataElement":{"translatorString":{"localisedStringId":null,"fieldValueMap":null}},"timeElement":"2021-06-08T21:41:45.000Z"},"isActive":true}]},"customerRescheduleRequestInfo":null,"errors":null,"summary":{"status":"Delivered","metadata":{"deliveryAddressId":{"stringValue":"XQI6PNXLLXFBJN4NYWJHYHJWYN4NJBFXLLXNP6IQXMHFRAQPXTQ2EIA4G28ARFHM","type":"STRING"},"promisedDeliveryDate":{"date":"2021-06-10T06:59:59.000Z","type":"DATE"},"expectedDeliveryDate":{"date":"2021-06-09T03:00:00.000Z","type":"DATE"},"trackingStatus":{"stringValue":"DELIVERED","type":null},"creationDate":{"date":"2021-06-07T19:12:05.156Z","type":null},"deliveryDate":{"date":"2021-06-08T21:41:45.000Z","type":"DATE"},"lastLegCarrier":{"stringValue":"Amazon","type":"STRING"}},"proofOfDelivery":null},"expectedDeliveryDate":"2021-06-09T03:00:00.000Z","legType":"FORWARD","hasDeliveryDelayed":false,"trackerSource":"MCF"}', 507 | // eventHistory: 508 | // '{"eventHistory":[{"eventCode":"CreationConfirmed","statusSummary":{"localisedStringId":"swa_rex_detail_creation_confirmed","fieldValueMap":null},"eventTime":"2021-06-07T19:12:20.000Z","location":null,"subReasonCode":null,"eventMetadata":{"eventImage":null}},{"eventCode":"Received","statusSummary":{"localisedStringId":"swa_rex_arrived_at_sort_center","fieldValueMap":null},"eventTime":"2021-06-08T04:11:25.000Z","location":{"addressId":null,"city":"Aurora","stateProvince":"CO","countryCode":"US","postalCode":"80011"},"subReasonCode":null,"eventMetadata":{"eventImage":null}},{"eventCode":"Departed","statusSummary":{"localisedStringId":"swa_rex_detail_departed","fieldValueMap":null},"eventTime":"2021-06-08T06:26:13.000Z","location":{"addressId":null,"city":"Aurora","stateProvince":"CO","countryCode":"US","postalCode":"80011"},"subReasonCode":null,"eventMetadata":{"eventImage":null}},{"eventCode":"Received","statusSummary":{"localisedStringId":"swa_rex_arrived_at_sort_center","fieldValueMap":null},"eventTime":"2021-06-08T09:30:16.000Z","location":{"addressId":null,"city":"THORNTON","stateProvince":"CO","countryCode":"US","postalCode":"80241"},"subReasonCode":null,"eventMetadata":{"eventImage":null}},{"eventCode":"OutForDelivery","statusSummary":{"localisedStringId":"swa_rex_detail_arrived_at_delivery_Center","fieldValueMap":null},"eventTime":"2021-06-08T16:13:52.000Z","location":{"addressId":null,"city":"THORNTON","stateProvince":"CO","countryCode":"US","postalCode":"80241"},"subReasonCode":"NONE","eventMetadata":{"eventImage":null}},{"eventCode":"Delivered","statusSummary":{"localisedStringId":"swa_rex_detail_delivered","fieldValueMap":null},"eventTime":"2021-06-08T21:41:45.000Z","location":{"addressId":"XQI6PNXLLXFBJN4NYWJHYHJWYN4NJBFXLLXNP6IQXMHFRAQPXTQ2EIA4G28ARFHM","city":null,"stateProvince":null,"countryCode":null,"postalCode":null},"subReasonCode":"DELIVERED_TO_HOUSEHOLD_MEMBER","eventMetadata":{"eventImage":null}}],"errors":null,"summary":{"status":null,"metadata":null,"proofOfDelivery":null},"trackerSource":"MCF"}', 509 | // addresses: 510 | // '{"XQI6PNXLLXFBJN4NYWJHYHJWYN4NJBFXLLXNP6IQXMHFRAQPXTQ2EIA4G28ARFHM":{"state":"CO","city":"BOULDER","country":"US","fullAddress":null}}', 511 | // eligibleActions: null, 512 | // proofOfDeliveryImage: null, 513 | // notificationWeblabTreatment: null, 514 | // claimYourPackageWeblabTreatment: null, 515 | // packageMarketplaceDetail: null, 516 | // returnDetails: null, 517 | // shipperBrandingDetails: null, 518 | // geocodeDetails: null } 519 | 520 | console.debug("parsing progressTracker") 521 | console.debug(response[0]) 522 | const res = JSON.parse(response[0]['progressTracker']) 523 | console.debug('got res') 524 | 525 | if ('errors' in res) { 526 | return `Error from parser: ${titleCase(res.errors[0].errorMessage)}` 527 | } 528 | 529 | // console.debug('parsing eventHistory') 530 | const hist = JSON.parse(response[0]['eventHistory'])['eventHistory'] 531 | // console.debug('getting last_event') 532 | const last_event = hist[hist.length - 1] 533 | 534 | let eventCode = last_event['eventCode']; 535 | if (eventCode == 'OutForDelivery') { 536 | eventCode = 'Out for delivery'; 537 | } 538 | let eventTime = last_event['eventTime']; 539 | let dateTime; 540 | let dateStr = ''; 541 | if (eventTime) { 542 | dateTime = new tc.DateTime(eventTime, tc.utc()) 543 | dateTime = dateTime.convert(tc.local()) 544 | dateStr = dateTime.format('yyyy-MM-dd hh:mm a') 545 | } 546 | let city = last_event['location']['city']; 547 | if (city) { 548 | city = city.charAt(0).toUpperCase() + city.slice(1).toLowerCase(); 549 | city = `${city}, ` 550 | } else { 551 | city = '' 552 | } 553 | let state = last_event['location']['state']; 554 | if (!state) { 555 | state = '' 556 | } 557 | 558 | const summary = res['summary'] 559 | const edd = res['expectedDeliveryDate'] 560 | let eddStr = ''; 561 | if (edd) { 562 | edd_dt = new tc.DateTime(edd, tc.utc()) 563 | edd_dt = edd_dt.convert(tc.local()) 564 | eddStr = ` - Expected: ${edd_dt.format('yyyy-MM-dd')}` 565 | } 566 | 567 | let stat = `${city}${state} (${dateStr}) - ${eventCode}${eddStr}` 568 | console.debug(`Status is "${stat}"`) 569 | return stat; 570 | } 571 | 572 | function titleCase(str) { 573 | if (str == '') return ''; 574 | return str.toLowerCase().split(' ').map(function(word) { 575 | return word.replace(word[0], word[0].toUpperCase()); 576 | }).join(' '); 577 | } 578 | 579 | parsers = { 580 | usps: uspsParser, 581 | ups: upsParser, 582 | fedex: fedExParser, 583 | ontrac: onTracParser, 584 | amazon: amazonParser 585 | } 586 | 587 | module.exports = parsers; 588 | -------------------------------------------------------------------------------- /api_server/3rd-party/test/test-usps-parser.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | require("../trackers").usps( 3 | process.env.TEST_TRACKING_NUMBER 4 | ).then( 5 | res => console.log( 6 | require("../parsers").usps([res])) 7 | 8 | ) 9 | -------------------------------------------------------------------------------- /api_server/3rd-party/test/test-usps-tracker.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | require("../trackers").usps( 3 | process.env.TEST_TRACKING_NUMBER 4 | ).then( 5 | res => console.log(res) 6 | ) 7 | -------------------------------------------------------------------------------- /api_server/3rd-party/trackers.js: -------------------------------------------------------------------------------- 1 | const util = require('util') 2 | const axios = require('axios'); 3 | const playwright = require('playwright'); 4 | 5 | const uspsTracker = (trackingNumber) => { 6 | let url = "https://secure.shippingapis.com/ShippingAPI.dll?API=TrackV2&XML="; 7 | let xml = `` 8 | console.debug(`Using url ${url} with xml ${xml}`); 9 | return axios.get(`${url}${xml}`) 10 | .then(res => { 11 | console.log(`USPS tracker res: ${res.data}`); 12 | return res.data 13 | }) 14 | .catch(err => { 15 | return err 16 | }) 17 | } 18 | 19 | const upsTracker = (trackingNumber) => { 20 | let url = "https://onlinetools.ups.com/track/v1/details/" 21 | 22 | return axios.get(`${url}${trackingNumber}`, { 23 | headers: { 24 | 'AccessLicenseNumber': `${process.env.UPS_ACCESS_KEY}` 25 | } 26 | }) 27 | .then(res => { 28 | console.log(`UPS tracker res: ${util.inspect(res.data, depth = null)}`); 29 | return res.data 30 | }) 31 | .catch(err => { return err }) 32 | } 33 | 34 | const fedExTracker = (trackingNumber) => { 35 | // get bearer token 36 | 37 | let url = `${process.env.FEDEX_API_URL}/oauth/token` 38 | 39 | let params = new URLSearchParams(); 40 | params.append('grant_type', 'client_credentials'); 41 | params.append('client_id', process.env.FEDEX_API_KEY); 42 | params.append('client_secret', process.env.FEDEX_SECRET_KEY); 43 | 44 | let options = { 45 | method: 'POST', 46 | url: url, 47 | headers: { 48 | 'Content-Type': 'application/x-www-form-urlencoded' 49 | }, 50 | data: params 51 | }; 52 | 53 | console.debug(`Getting Fedex tracking information for ${trackingNumber}`) 54 | 55 | // first get access_token at auth endpoint 56 | return axios.request(options) 57 | .then((response) => { 58 | // if we succeeded, save access token and use to call Track API 59 | console.debug("FedEx: Got API access tokent") 60 | let access_token = response.data.access_token; 61 | let track_url = `${process.env.FEDEX_API_URL}/track/v1/trackingnumbers` 62 | let track_options = { 63 | method: 'POST', url: track_url, 64 | headers: { 65 | 'Authorization': `Bearer ${access_token}`, 66 | 'Content-Type': 'application/json' 67 | }, 68 | data: { 69 | trackingInfo: [ 70 | { 71 | 'trackingNumberInfo': { 72 | 'trackingNumber': trackingNumber 73 | } 74 | } 75 | ], 76 | 'includeDetailedScans': true 77 | } 78 | }; 79 | return axios.request(track_options) 80 | .then((response) => { 81 | console.log(`Fedex tracker res: ${util.inspect(response.data, depth = 4)}`); 82 | return response.data; 83 | }).catch((error) => { 84 | console.error('Fedex tracker error:') 85 | console.error(error); 86 | return { 'error': `Error getting tracking info: ${error.toJSON().message}` } 87 | }) 88 | }).catch((error) => { 89 | console.error(error); 90 | return { 'error': `Error authenticating to API: ${error.toJSON().message}` } 91 | }); 92 | } 93 | 94 | const onTracTracker = (trackingNumber) => { 95 | const url = `https://www.ontrac.com/tracking/?number=${trackingNumber}`; 96 | 97 | // async function to scrape status from ontrac website (since API is 98 | // unavailable...) 99 | return (async () => { 100 | const browser = await playwright.chromium.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] }); 101 | const page = await browser.newPage(); 102 | await page.goto(url); 103 | 104 | // Get expected delivery date 105 | const EXP_DELIVERY_SELECTOR = 'p[name="ExpectedDeliveryDateFormatted"]'; 106 | await page.waitForSelector(EXP_DELIVERY_SELECTOR); 107 | const exp_delivery_res = await page.$(EXP_DELIVERY_SELECTOR); 108 | let exp_delivery_date = await exp_delivery_res.evaluate(element => element.innerText); 109 | 110 | const LATEST_EVENT_SELECTOR = 'p[name="EventShortDescriptionFormatted"]'; 111 | await page.waitForSelector(LATEST_EVENT_SELECTOR); 112 | const latest_event_res = await page.$(LATEST_EVENT_SELECTOR); 113 | let latest_event_text = await latest_event_res.evaluate(el => el.innerText); 114 | 115 | const LATEST_EVENT_DETAIL_SELECTOR = 'p[name="EventLongDescriptionFormatted"]'; 116 | await page.waitForSelector(LATEST_EVENT_DETAIL_SELECTOR); 117 | const latest_event_detail_res = await page.$(LATEST_EVENT_DETAIL_SELECTOR); 118 | let latest_event_detail_text = await latest_event_detail_res.evaluate(el => el.innerText); 119 | 120 | const LOCATION_SELECTOR = 'p[name="EventCityFormatted"]'; 121 | await page.waitForSelector(LOCATION_SELECTOR); 122 | const location_res = await page.$(LOCATION_SELECTOR); 123 | let location_text = await location_res.evaluate(el => el.innerText); 124 | 125 | const EVENT_DATE_SELECTOR = 'p[name="EventLastDateFormatted"]'; 126 | await page.waitForSelector(EVENT_DATE_SELECTOR); 127 | const event_date_res = await page.$(EVENT_DATE_SELECTOR); 128 | let event_date_text = await event_date_res.evaluate(el => el.innerText); 129 | 130 | let res = { 131 | expected_date: exp_delivery_date, 132 | event_summary: latest_event_text, 133 | event_detail: latest_event_detail_text, 134 | event_location: location_text, 135 | event_date: event_date_text 136 | } 137 | 138 | // console.log(`Ontrac tracker res: ${res}`); 139 | console.debug(`Ontrac tracker res: ${util.inspect(res, depth = null)}`); 140 | 141 | // Returns object with keys of 142 | // ["expected_date", "event_summary", "event_detail", "event_location", "event_date"] 143 | return res; 144 | })(); 145 | } 146 | 147 | amazonTracker = (trackingNumber) => { 148 | let url = `https://track.amazon.com/api/tracker/${trackingNumber}` 149 | console.debug(`Fetching Amazon status with url: ${url}`); 150 | return axios.get(url) 151 | .then(res => { 152 | // console.debug(`returning Amazon ${res.data}`) 153 | return res.data 154 | }) 155 | .catch(err => { return err }) 156 | } 157 | 158 | trackers = { 159 | usps: uspsTracker, 160 | ups: upsTracker, 161 | fedex: fedExTracker, 162 | ontrac: onTracTracker, 163 | amazon: amazonTracker 164 | } 165 | 166 | module.exports = trackers; 167 | -------------------------------------------------------------------------------- /api_server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-bullseye-slim 2 | RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app 3 | 4 | RUN apt-get update 5 | RUN apt-get install -y netcat ffmpeg libnss3 libnspr4 libatk-bridge2.0-0 libx11-xcb1 libxcb-dri3-0 libdrm2 libgbm1 libasound2 libatspi2.0-0 libgtk-3-0 6 | 7 | WORKDIR /home/node/app 8 | COPY --chown=node:node package*.json ./ 9 | USER node 10 | RUN npm install 11 | COPY --chown=node:node . . 12 | ENV DEBUG=pw:api 13 | 14 | CMD [ "node", "index.js" ] 15 | -------------------------------------------------------------------------------- /api_server/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const mongoose = require('mongoose'); 4 | const routes = require('./routes/api'); 5 | const path = require('path'); 6 | require('dotenv').config(); 7 | 8 | const app = express() 9 | 10 | const port = process.env.PORT || 5000; 11 | 12 | const { 13 | MONGO_USERNAME, 14 | MONGO_PASSWORD, 15 | MONGO_HOSTNAME, 16 | MONGO_PORT, 17 | MONGO_DB 18 | } = process.env; 19 | 20 | const options = { 21 | useNewUrlParser: true, 22 | connectTimeoutMS: 10000, 23 | }; 24 | 25 | const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`; 26 | 27 | // connect to the mongoDB database 28 | mongoose.connect(url, options) 29 | .then(() => console.log('Database connected successfully')) 30 | .catch(err => console.error(err)); 31 | 32 | // since mongoose promis is deprecated, overwrite it with node Promise 33 | mongoose.Promise = global.Promise; 34 | 35 | app.use((req, res, next) => { 36 | res.header("Access-Control-Allow-Origin", "*"); 37 | res.header("Access-Control-Allow-Headers", 38 | "Origin, X-Requested-With, Content-Type, Accept"); 39 | next(); 40 | }); 41 | 42 | app.use(bodyParser.json()); 43 | 44 | app.use('/api', routes); 45 | 46 | app.use((err, req, res, next) => { 47 | console.log(err); 48 | next(); 49 | }) 50 | 51 | app.listen(port, () => { 52 | console.log(`Server running on port ${port}`) 53 | }) -------------------------------------------------------------------------------- /api_server/models/package.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | //create schema for packages 5 | const PackageSchema = new Schema({ 6 | carrier: { 7 | type: String, 8 | enum: ['UPS', 'USPS', 'FedEx', 'Amazon', 'OnTrac'], 9 | required: [true, 'The carrier text field is required'] 10 | }, 11 | trackingNumber: { 12 | type: String, 13 | required: [true, 'The carrier tracking number is required'] 14 | }, 15 | lastStatus: { 16 | type: String, 17 | default: 'Package has been added to tracker' 18 | }, 19 | lastUpdate: { 20 | type: Date, 21 | default: Date.now 22 | }, 23 | dateAdded: { 24 | type: Date, 25 | default: Date.now 26 | }, 27 | description: String, 28 | dateDelivered: { 29 | type: Date, 30 | default: null 31 | }, 32 | isArchived: { 33 | type: Boolean, 34 | default: false 35 | } 36 | }); 37 | 38 | //create model for todo 39 | const Package = mongoose.model('package', PackageSchema); 40 | 41 | module.exports = Package; 42 | -------------------------------------------------------------------------------- /api_server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "packagemate-server", 3 | "version": "1.2.4", 4 | "description": "A self-hosted package tracking app", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "start-watch": "nodemon index.js", 9 | "dev": "concurrently \"yarn run start-watch\" \"cd client && yarn start\"", 10 | "debug-usps-tracker": "node -e 'require(\"dotenv\").config(); require(\"./3rd-party/trackers\").usps(process.env.TEST_TRACKING_NUMBER).then(res => console.log(res))'", 11 | "debug-usps-parser": "node -e 'require(\"dotenv\").config(); require(\"./3rd-party/trackers\").usps(process.env.TEST_TRACKING_NUMBER).then(res => console.log(require(\"./3rd-party/parsers\").usps([res])))'", 12 | "debug-fedex-tracker": "node -e 'require(\"dotenv\").config(); require(\"./3rd-party/trackers\").fedex(process.env.TEST_TRACKING_NUMBER).then(res => console.log(res))'", 13 | "debug-fedex-parser": "node -e 'require(\"dotenv\").config(); require(\"./3rd-party/trackers\").fedex(process.env.TEST_TRACKING_NUMBER).then(res => console.log(require(\"./3rd-party/parsers\").fedex([res])))'", 14 | "debug-amazon-tracker": "node -e 'require(\"dotenv\").config(); require(\"./3rd-party/trackers\").amazon(process.env.TEST_TRACKING_NUMBER).then(res => console.log(res))'", 15 | "debug-amazon-parser": "node -e 'require(\"dotenv\").config(); require(\"./3rd-party/trackers\").amazon(process.env.TEST_TRACKING_NUMBER).then(res => console.log(require(\"./3rd-party/parsers\").amazon([res])))'", 16 | "debug-ups-tracker": "node -e 'require(\"dotenv\").config(); require(\"./3rd-party/trackers\").ups(process.env.TEST_TRACKING_NUMBER).then(res => console.log(res))'", 17 | "debug-ups-parser": "node -e 'require(\"dotenv\").config(); require(\"./3rd-party/trackers\").ups(process.env.TEST_TRACKING_NUMBER).then(res => console.log(require(\"./3rd-party/parsers\").ups([res])))'", 18 | "debug-ontrac-tracker": "node -e 'require(\"dotenv\").config(); require(\"./3rd-party/trackers\").ontrac(process.env.TEST_TRACKING_NUMBER).then(res => console.log(res))'", 19 | "debug-ontrac-parser": "node -e 'require(\"dotenv\").config(); require(\"./3rd-party/trackers\").ontrac(process.env.TEST_TRACKING_NUMBER).then(res => console.log(require(\"./3rd-party/parsers\").ontrac([res])))'" 20 | }, 21 | "author": "Joshua Taillon", 22 | "license": "MIT", 23 | "dependencies": { 24 | "axios": ">=0.21.1", 25 | "body-parser": "^1.18.3", 26 | "dotenv": "^8.2.0", 27 | "express": "^4.17.1", 28 | "ini": ">=1.3.6", 29 | "jsdom": "^16.4.0", 30 | "mongoose": "^6.5.2", 31 | "mquery": ">=3.2.3", 32 | "playwright": "^1.8.0", 33 | "timezonecomplete": "^5.11.2", 34 | "yarn": "^1.22.5" 35 | }, 36 | "devDependencies": { 37 | "concurrently": "^5.3.0", 38 | "nodemon": "^2.0.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /api_server/routes/api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const Package = require('../models/package'); 4 | const trackers = require('../3rd-party/trackers') 5 | const parsers = require('../3rd-party/parsers') 6 | const tc = require("timezonecomplete"); 7 | 8 | router.get('/carriers', (req, res, next) => { 9 | // endpoint to return allowable carriers that 10 | // we know how to handle 11 | res.json(Package.schema.path('carrier').enumValues) 12 | }) 13 | 14 | router.get('/packages', (req, res, next) => { 15 | // this will return all the data 16 | // to the client 17 | Package.find({}) 18 | .then(data => res.json(data)) 19 | .catch(next) 20 | }); 21 | 22 | router.get('/packages/active', (req, res, next) => { 23 | // this will return packages with isArchived === false 24 | Package.find({isArchived: false}) 25 | .then(data => res.json(data)) 26 | .catch(next) 27 | }); 28 | 29 | router.get('/packages/update/:id', (req, res, next) => { 30 | // update a package's status via third-party API 31 | Package.findById(req.params.id) 32 | .then(data => { 33 | // console.log(`Getting tracker for ${data.carrier}`) 34 | if ( data.carrier === 'USPS' ){ 35 | r = trackers.usps(data.trackingNumber); 36 | } else if ( data.carrier === 'FedEx') { 37 | r = trackers.fedex(data.trackingNumber); 38 | } else if ( data.carrier === 'UPS' ) { 39 | r = trackers.ups(data.trackingNumber); 40 | } else if ( data.carrier === 'OnTrac' ) { 41 | r = trackers.ontrac(data.trackingNumber); 42 | } else if ( data.carrier === 'Amazon' ) { 43 | r = trackers.amazon(data.trackingNumber); 44 | } else { 45 | r = Promise.reject(`Did not know how to process carrier: ${data.carrier}`); 46 | } 47 | // once the promise has returned, parse the result (method depends on carrier) 48 | Promise.all([r]) 49 | .catch(err => console.error(err)) 50 | .then(results => { 51 | // console.log(`results: ${results}`) 52 | if ( data.carrier === 'USPS' ){ 53 | stat = parsers.usps(results); 54 | } else if ( data.carrier === 'FedEx') { 55 | stat = parsers.fedex(results); 56 | } else if ( data.carrier === 'UPS' ) { 57 | stat = parsers.ups(results); 58 | } else if ( data.carrier === 'OnTrac' ) { 59 | stat = parsers.ontrac(results); 60 | } else if ( data.carrier === 'Amazon' ) { 61 | stat = parsers.amazon(results); 62 | } else { 63 | stat = 'Could not parse tracker response' 64 | } 65 | data.status = stat 66 | console.log(`data.status is: ${data.status}`) 67 | }) 68 | .then(() => { 69 | Package.updateOne( 70 | {"_id": req.params.id}, 71 | { 72 | lastStatus: data.status, 73 | lastUpdate: tc.DateTime.nowLocal() 74 | }) 75 | .then(data => res.json(data)) 76 | .catch(next) 77 | }) 78 | .catch(results => { 79 | res.json({ 80 | carrier: data.carrier, 81 | trackingNumber: data.trackingNumber, 82 | status: results[0] 83 | }) 84 | }) 85 | }) 86 | .catch(next) 87 | }); 88 | 89 | router.get('/packages/archived', (req, res, next) => { 90 | // this will return packages with isArchived === true 91 | Package.find({isArchived: true}) 92 | .then(data => res.json(data)) 93 | .catch(next) 94 | }); 95 | 96 | router.post('/packages', (req, res, next) => { 97 | // adds a package with the given carrier and Tracking number; 98 | // dateAdded, lastStatus, and isArchived have default values 99 | if(req.body.carrier && req.body.trackingNumber){ 100 | Package.create(req.body) 101 | .then(data => res.json(data)) 102 | .catch(next) 103 | } else { 104 | res.json({ 105 | error: "Tracking number and carrier must be provided" 106 | }) 107 | } 108 | }); 109 | 110 | router.delete('/packages/:id', (req, res, next) => { 111 | Package.updateOne( 112 | {"_id": req.params.id}, 113 | {isArchived: true}) 114 | .then(data => res.json(data)) 115 | .catch(next) 116 | }); 117 | 118 | module.exports = router; 119 | -------------------------------------------------------------------------------- /api_server/wait-for.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # original script: https://github.com/eficode/wait-for/blob/master/wait-for 4 | 5 | TIMEOUT=15 6 | QUIET=0 7 | 8 | echoerr() { 9 | if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi 10 | } 11 | 12 | usage() { 13 | exitcode="$1" 14 | cat << USAGE >&2 15 | Usage: 16 | $cmdname host:port [-t timeout] [-- command args] 17 | -q | --quiet Do not output any status messages 18 | -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout 19 | -- COMMAND ARGS Execute command with args after the test finishes 20 | USAGE 21 | exit "$exitcode" 22 | } 23 | 24 | wait_for() { 25 | for i in `seq $TIMEOUT` ; do 26 | nc -z "$HOST" "$PORT" > /dev/null 2>&1 27 | 28 | result=$? 29 | if [ $result -eq 0 ] ; then 30 | if [ $# -gt 0 ] ; then 31 | exec "$@" 32 | fi 33 | exit 0 34 | fi 35 | sleep 1 36 | done 37 | echo "Operation timed out" >&2 38 | exit 1 39 | } 40 | 41 | while [ $# -gt 0 ] 42 | do 43 | case "$1" in 44 | *:* ) 45 | HOST=$(printf "%s\n" "$1"| cut -d : -f 1) 46 | PORT=$(printf "%s\n" "$1"| cut -d : -f 2) 47 | shift 1 48 | ;; 49 | -q | --quiet) 50 | QUIET=1 51 | shift 1 52 | ;; 53 | -t) 54 | TIMEOUT="$2" 55 | if [ "$TIMEOUT" = "" ]; then break; fi 56 | shift 2 57 | ;; 58 | --timeout=*) 59 | TIMEOUT="${1#*=}" 60 | shift 1 61 | ;; 62 | --) 63 | shift 64 | break 65 | ;; 66 | --help) 67 | usage 0 68 | ;; 69 | *) 70 | echoerr "Unknown argument: $1" 71 | usage 1 72 | ;; 73 | esac 74 | done 75 | 76 | if [ "$HOST" = "" -o "$PORT" = "" ]; then 77 | echoerr "Error: you need to provide a host and port to test." 78 | usage 2 79 | fi 80 | 81 | wait_for "$@" 82 | -------------------------------------------------------------------------------- /client/.dockerignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .env 3 | node_modules/**/* 4 | node_modules 5 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | RUN mkdir -p /home/node/app/client/node_modules && chown -R node:node /home/node/app 3 | 4 | WORKDIR /home/node/app/client 5 | COPY --chown=node:node package*.json ./ 6 | USER node 7 | RUN npm install 8 | COPY --chown=node:node . . 9 | EXPOSE 3000 10 | 11 | CMD [ "node", "index.js" ] 12 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "packagemate-client", 3 | "version": "1.2.4", 4 | "private": true, 5 | "proxy": "http://packagemate-server:5000", 6 | "dependencies": { 7 | "@fortawesome/fontawesome-svg-core": "^1.2.30", 8 | "@fortawesome/free-brands-svg-icons": "^5.14.0", 9 | "@fortawesome/free-solid-svg-icons": "^5.14.0", 10 | "@fortawesome/react-fontawesome": "^0.1.11", 11 | "@testing-library/jest-dom": "^4.2.4", 12 | "@testing-library/react": "^9.5.0", 13 | "@testing-library/user-event": "^7.2.1", 14 | "axios": ">=0.21.1", 15 | "bootstrap": "^4.5.2", 16 | "react": "^16.13.1", 17 | "react-bootstrap": "^1.3.0", 18 | "react-dom": "^16.13.1", 19 | "react-ladda": "^6.0.0", 20 | "react-scripts": ">=5.0.1" 21 | }, 22 | "overrides": { 23 | "@svgr/webpack": "^6.3.1" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jat255/PackageMate/272b97ab895a43640ad89482b28576c1648c9f3c/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | PackageMate 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jat255/PackageMate/272b97ab895a43640ad89482b28576c1648c9f3c/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "PackageMate", 3 | "name": "PackageMate Client", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | font-size: calc(10px + 2vmin); 4 | width: 60%; 5 | margin-left: auto; 6 | margin-right: auto; 7 | } 8 | 9 | .main-container { 10 | margin-bottom: 30px; 11 | } 12 | 13 | div.tab-pane.active { 14 | border: 1px solid transparent; 15 | border-bottom-left-radius: .25rem; 16 | border-color: #dee2e6 #dee2e6; 17 | border-bottom-right-radius: .25rem; 18 | 19 | padding: 0.5em; 20 | } 21 | 22 | a#package-tabs-tab-active { 23 | color: #155724; 24 | } 25 | 26 | a#package-tabs-tab-active:hover { 27 | background-color: #ebf7ee; 28 | border-bottom-color:#0b2e13; 29 | } 30 | 31 | a#package-tabs-tab-active.active { 32 | color: #155724; 33 | background-color: #d4edda; 34 | border-color: #c3e6cb; 35 | border-bottom-color:#0b2e13; 36 | } 37 | 38 | a#package-tabs-tab-archived { 39 | color: #856404; 40 | } 41 | 42 | a#package-tabs-tab-archived:hover { 43 | background-color: #fdf6e0; 44 | border-bottom-color:#533f03; 45 | } 46 | 47 | a#package-tabs-tab-archived.active { 48 | color: #856404; 49 | background-color: #fff3cd; 50 | border-color: #ffeeba; 51 | border-bottom-color:#533f03; 52 | } 53 | 54 | #updateButton, #github-btn { 55 | margin-bottom:1em; 56 | } 57 | 58 | #alert-row { 59 | font-size: medium; 60 | } 61 | 62 | @media only screen and (min-width: 300px) { 63 | .App { 64 | width: 80%; 65 | } 66 | 67 | /* input { 68 | width: 100% 69 | } */ 70 | 71 | } 72 | 73 | @media only screen and (min-width: 640px) { 74 | .App { 75 | width: 80%; 76 | } 77 | 78 | /* input { 79 | width: 50%; 80 | } */ 81 | 82 | } 83 | 84 | .detail-cell { 85 | line-height: 1rem; 86 | white-space: pre-wrap; 87 | max-width: 16vw; 88 | } 89 | 90 | /* responsive styling for top input area and package list table */ 91 | @media only screen and (max-width: 830px) { 92 | .App { 93 | width: 80%; 94 | } 95 | 96 | /* Carrier button and container */ 97 | #input-group-div > .input-group-prepend { 98 | width: 100%; 99 | } 100 | 101 | button#input-group-dropdown-1 { 102 | width: 100%; 103 | border-radius: unset; 104 | } 105 | 106 | /* "Tracking number" input */ 107 | input#trackingNumberInput { 108 | width: 100%; 109 | text-align: center; 110 | } 111 | 112 | /* "Description" input and container */ 113 | div#description-input-group-append { 114 | width: 100%; 115 | } 116 | 117 | input#description-input { 118 | text-align: center; 119 | width: 100%; 120 | } 121 | 122 | /* "Add package" button and container */ 123 | div#add-package-input-group { 124 | width: 100% 125 | } 126 | 127 | button#add-package-btn { 128 | width: 100%; 129 | border-radius: unset; 130 | } 131 | 132 | td, tr, th { 133 | display: block; 134 | width: 100%; 135 | } 136 | 137 | .detail-cell { 138 | max-width: unset; 139 | } 140 | 141 | .update-one-btn, .archive-btn { 142 | width: 50%; 143 | } 144 | } 145 | 146 | .footer { 147 | border-top: 1px solid #eee; 148 | position: fixed; 149 | background-color: white; 150 | width: 100%; 151 | bottom: 0; 152 | left: 0; 153 | color: #999; 154 | margin-left: auto; 155 | margin-right: auto; 156 | font-size: small; 157 | } 158 | .footer > p { 159 | margin-top: 0.5rem; 160 | margin-bottom: 0.5rem; 161 | } -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Package from './components/Package'; 4 | import Footer from "./components/Footer"; 5 | import './App.css'; 6 | 7 | const App = () => { 8 | return ( 9 |
10 | 11 |
12 |
13 | ); 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /client/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import packageJson from '../../package.json'; 3 | 4 | const Footer = () => ( 5 |
6 |

PackageMate v{packageJson.version}

7 |
8 | ); 9 | 10 | export default Footer; 11 | -------------------------------------------------------------------------------- /client/src/components/Input.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import axios from 'axios'; 3 | import Button from 'react-bootstrap/Button' 4 | import InputGroup from 'react-bootstrap/InputGroup' 5 | import FormControl from 'react-bootstrap/FormControl' 6 | import Dropdown from 'react-bootstrap/Dropdown' 7 | import DropdownButton from 'react-bootstrap/DropdownButton' 8 | import Row from 'react-bootstrap/Row' 9 | import Col from 'react-bootstrap/Col' 10 | 11 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 12 | import { faUsps } from '@fortawesome/free-brands-svg-icons' 13 | import { faFedex } from '@fortawesome/free-brands-svg-icons' 14 | import { faUps } from '@fortawesome/free-brands-svg-icons' 15 | import { faAmazon } from '@fortawesome/free-brands-svg-icons' 16 | 17 | 18 | // code to wait for an element to be present 19 | // from https://stackoverflow.com/a/47776379 20 | function rafAsync() { 21 | return new Promise(resolve => { 22 | requestAnimationFrame(resolve); //faster than set time out 23 | }); 24 | } 25 | async function waitForElement(selector) { 26 | let querySelector = document.querySelector(selector); 27 | while (querySelector === null) { 28 | querySelector = document.querySelector(selector); 29 | await rafAsync() 30 | } 31 | return querySelector; 32 | } 33 | 34 | class Input extends Component { 35 | state = { 36 | dropDownValue: "Carrier", 37 | carrier: "", 38 | trackingNumber: "", 39 | description: "" 40 | } 41 | 42 | carrierIcons = { 43 | USPS: , 44 | FedEx: , 45 | UPS: , 46 | Amazon: , 47 | } 48 | 49 | addPackage = () => { 50 | const pkg = { 51 | carrier: this.state.carrier, 52 | trackingNumber: this.state.trackingNumber, 53 | description: this.state.description 54 | } 55 | if (pkg.carrier && pkg.trackingNumber && pkg.trackingNumber.length > 0) { 56 | axios.post('/api/packages', pkg) 57 | .then(res => { 58 | if (res.data) { 59 | this.props.getPackages(); 60 | this.setState({ 61 | trackingNumber: "", 62 | carrier: "", 63 | description: "", 64 | dropDownValue: "Carrier" 65 | }) 66 | // this is hacky and dumb, but we wait for the new button 67 | // to be added to the package list and then click the update button 68 | // so we get some progress indication (rather than just making) 69 | // a call to the API. There's surely a way to do this in react I don't 70 | // know about 71 | waitForElement(`#updateButton-${res.data._id}`) //use whichever selector you want 72 | .then(() => { 73 | document.getElementById(`updateButton-${res.data._id}`).click() 74 | }); 75 | } 76 | }) 77 | .catch(err => console.log(err)) 78 | } else { 79 | console.log('Tracking number and Carrier required') 80 | } 81 | } 82 | 83 | handleTrackingNumberChange = (e) => { 84 | this.setState({ 85 | trackingNumber: e.target.value 86 | }) 87 | } 88 | handleCarrierChange = (e) => { 89 | this.setState({ 90 | carrier: e.target.value 91 | }) 92 | } 93 | handleDescriptionChange = (e) => { 94 | this.setState({ 95 | description: e.target.value 96 | }) 97 | } 98 | changeDropDownValue(text) { 99 | this.setState({ 100 | dropDownValue: text, 101 | carrier: text 102 | }) 103 | } 104 | createDropdownItems() { 105 | let items = []; 106 | for (let i = 0; i <= this.props.possibleCarriers.length; i++) { 107 | let c = this.props.possibleCarriers[i] 108 | if (c in this.carrierIcons) { 109 | items.push( 110 | this.changeDropDownValue(c)} > 111 | {this.carrierIcons[c]} {c} 112 | 113 | ); 114 | } else { 115 | items.push( 116 | this.changeDropDownValue(c)} > 117 | {c} 118 | 119 | ) 120 | } 121 | } 122 | return items; 123 | } 124 | 125 | render() { 126 | return ( 127 | 128 | 129 | 132 | 139 | { 140 | this.props.possibleCarriers && 141 | this.props.possibleCarriers.length > 0 142 | ? 143 | this.createDropdownItems() 144 | : 145 | No supported carriers were found! 146 | } 147 | 148 | 149 | { 156 | if (event.key === "Enter") { 157 | this.addPackage(); 158 | } 159 | }} 160 | /> 161 | 162 | 164 | { 171 | if (event.key === "Enter") { 172 | this.addPackage(); 173 | } 174 | }} 175 | /> 176 | 177 | 179 | 183 | 184 | 185 | 186 | 187 | ) 188 | } 189 | } 190 | 191 | export default Input 192 | -------------------------------------------------------------------------------- /client/src/components/ListPackages.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tabs from 'react-bootstrap/Tabs' 3 | import Tab from 'react-bootstrap/Tab' 4 | import TabContainer from 'react-bootstrap/TabContainer' 5 | import Table from 'react-bootstrap/Table' 6 | 7 | import LaddaButton, { XS } from 'react-ladda'; 8 | 9 | import uspsLogo from '../img/usps.svg' 10 | import fedexLogo from '../img/fedex.svg' 11 | import upsLogo from '../img/ups.svg' 12 | import ontracLogo from '../img/ontrac.svg' 13 | import amazonLogo from '../img/amazon.svg' 14 | 15 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 16 | import { faBoxOpen } from '@fortawesome/free-solid-svg-icons' 17 | 18 | import UpdateOnePackage from './UpdateOneButton' 19 | 20 | const getUrl = (trackingNumber, carrier) => { 21 | if ( carrier === 'USPS' ){ 22 | return `https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=${trackingNumber}` 23 | } else if ( carrier === 'UPS' ) { 24 | return `https://www.ups.com/track?loc=en_US&tracknum=${trackingNumber}` 25 | } else if ( carrier === 'FedEx' ) { 26 | return `https://www.fedex.com/apps/fedextrack/?tracknumbers=${trackingNumber}&locale=en_US` 27 | } else if ( carrier === 'Amazon' ) { 28 | return `https://track.amazon.com/tracking/${trackingNumber}` 29 | } else { 30 | return `https://www.ontrac.com/trackingresults.asp?tracking_number=${trackingNumber}` 31 | } 32 | } 33 | 34 | const getLogo = (carrier) => { 35 | let logo 36 | if ( carrier === 'USPS' ){ 37 | logo = uspsLogo 38 | } else if ( carrier === 'UPS' ) { 39 | logo = upsLogo 40 | } else if ( carrier === 'FedEx' ) { 41 | logo = fedexLogo 42 | } else if ( carrier === 'OnTrac' ) { 43 | logo = ontracLogo 44 | } else if ( carrier === 'Amazon' ) { 45 | logo = amazonLogo 46 | } else { 47 | logo = null 48 | } 49 | const svgPath = `${logo}#svgView(preserveAspectRatio(none))`; 50 | const altText = `${carrier} logo` 51 | return ( 52 | {altText} 53 | ) 54 | } 55 | 56 | const getLocaleDateString = (utcDate) => { 57 | let d = new Date(utcDate) 58 | return d.toLocaleString() 59 | } 60 | 61 | const ListPackages = ({ activePackages, archivedPackages, archivePackage, updateOnePackage, getPackages }) => { 62 | return ( 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | { 80 | activePackages && activePackages.length > 0 ? 81 | (activePackages.map(pkg => { 82 | return ( 83 | 84 | 85 | 93 | 94 | 95 | 96 | 103 | 111 | 112 | )})) 113 | : 114 | 115 | 116 | 117 | } 118 | 119 |
CarrierTracking #DescriptionStatusLast updateUpdateArchive?
{getLogo(pkg.carrier)} 86 | 90 | {pkg.trackingNumber} 91 | 92 | {pkg.description}{pkg.lastStatus}{getLocaleDateString(pkg.lastUpdate)} 97 | 102 | 104 | archivePackage(pkg._id)} 106 | data-size={XS} 107 | className='btn btn-outline-danger archive-btn'> 108 | 109 | 110 |
No active packages found in database!
120 |
121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | { 134 | archivedPackages && archivedPackages.length > 0 ? 135 | (archivedPackages.sort((a, b) => { 136 | // sort in descending order (newest first) 137 | return new Date(b.lastUpdate) - new Date(a.lastUpdate); 138 | }).map(pkg => { 139 | return ( 140 | 141 | 142 | 150 | 151 | 152 | 153 | 154 | )})) 155 | : 156 | 157 | 158 | 159 | } 160 | 161 |
CarrierTracking #DescriptionDateStatus
{getLogo(pkg.carrier)} 143 | 147 | {pkg.trackingNumber} 148 | 149 | {pkg.description}{getLocaleDateString(pkg.lastUpdate)}{pkg.lastStatus}
No packages found in database!
162 |
163 |
164 |
165 | ) 166 | } 167 | 168 | export default ListPackages 169 | -------------------------------------------------------------------------------- /client/src/components/Package.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import axios from 'axios'; 3 | 4 | import 'ladda/dist/ladda-themeless.min.css'; 5 | 6 | import Input from './Input'; 7 | import ListPackages from './ListPackages'; 8 | import UpdateAllButton from './UpdateAllButton'; 9 | import Container from 'react-bootstrap/Container'; 10 | import Row from 'react-bootstrap/Row' 11 | import Col from 'react-bootstrap/Col' 12 | import Button from 'react-bootstrap/Button' 13 | import Alert from 'react-bootstrap/Alert' 14 | 15 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 16 | import { faShippingFast } from '@fortawesome/free-solid-svg-icons' 17 | import { faGithub } from '@fortawesome/free-brands-svg-icons' 18 | 19 | // This is the main app component 20 | 21 | class Package extends Component { 22 | 23 | state = { 24 | activePackages: [], 25 | archivedPackages: [], 26 | possibleCarriers: [], 27 | updateProgress: 0.0 28 | } 29 | 30 | componentDidMount() { 31 | this.getPackages(); 32 | this.getCarriers(); 33 | } 34 | 35 | getCarriers = () => { 36 | axios.get('/api/carriers') 37 | .then(res => { 38 | if (res.data) { 39 | this.setState({ 40 | possibleCarriers: res.data 41 | }) 42 | } 43 | }) 44 | .catch(err => console.log(err)); 45 | } 46 | 47 | getPackages = () => { 48 | axios.get('/api/packages/active') 49 | .then(res => { 50 | if (res.data) { 51 | this.setState({ 52 | activePackages: res.data 53 | }) 54 | } 55 | }) 56 | .catch(err => console.log(err)); 57 | axios.get('/api/packages/archived') 58 | .then(res => { 59 | if (res.data) { 60 | this.setState({ 61 | archivedPackages: res.data 62 | }) 63 | } 64 | }) 65 | .catch(err => console.log(err)) 66 | } 67 | 68 | updateOnePackage = (pkg) => { 69 | return axios.get(`/api/packages/update/${pkg._id}`) 70 | } 71 | 72 | updateAllPackages = () => { 73 | var arrayLength = this.state.activePackages.length; 74 | var numFinished = 0.0 75 | var promList = [] 76 | this.setState({ 77 | loading: true 78 | }) 79 | 80 | var updateNum = () => { 81 | this.getPackages(); 82 | numFinished += 1; 83 | this.setState({ 84 | updateProgress: numFinished / arrayLength 85 | }) 86 | } 87 | 88 | for (let i = 0; i < arrayLength; i++) { 89 | let pkg = this.state.activePackages[i]; 90 | let p = axios.get(`/api/packages/update/${pkg._id}`) 91 | promList.push(p) 92 | p.then(() => updateNum()) 93 | .catch(err => console.log(err)) 94 | } 95 | Promise.all(promList) 96 | .then(() => { 97 | this.setState({ 98 | loading: false 99 | }) 100 | }) 101 | } 102 | 103 | archivePackage = (id) => { 104 | axios.delete(`/api/packages/${id}`) 105 | .then(res => { 106 | if (res.data) { 107 | this.getPackages() 108 | } 109 | }) 110 | .catch(err => console.log(err)) 111 | } 112 | 113 | render() { 114 | return ( 115 | 116 | 117 | 118 |

PackageMate

119 | 120 |
121 | 122 | 123 | 133 | 134 | 135 | 136 | 137 | 138 | The Amazon tracker is not currently working, 139 | so any packages added for Amazon will return 140 | "Invalid Tracking_id". See this issue for details/updates. 143 | 144 | 145 | 146 | {' '} 149 | 153 | 159 |
160 | ) 161 | } 162 | } 163 | 164 | export default Package; 165 | -------------------------------------------------------------------------------- /client/src/components/UpdateAllButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Row from 'react-bootstrap/Row' 3 | import Col from 'react-bootstrap/Col' 4 | 5 | import LaddaButton, { S, EXPAND_RIGHT } from 'react-ladda'; 6 | 7 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 8 | import { faSyncAlt } from '@fortawesome/free-solid-svg-icons' 9 | 10 | class UpdateAllButton extends Component { 11 | 12 | toggle = () => { 13 | this.props.updateAllPackages(); 14 | } 15 | 16 | render() { 17 | return ( 18 | 19 | 20 | 33 | 34 | Update all package statuses 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | } 42 | 43 | export default UpdateAllButton 44 | -------------------------------------------------------------------------------- /client/src/components/UpdateOneButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import LaddaButton, { XS, ZOOM_OUT } from 'react-ladda'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 4 | import { faRedoAlt } from '@fortawesome/free-solid-svg-icons' 5 | 6 | class UpdateOneButton extends Component { 7 | state = { loading: false }; 8 | 9 | render() { 10 | return ( 11 | { 15 | this.setState({loading: true}) 16 | this.props.updateOnePackage(this.props.pkg) 17 | .then(() => { 18 | this.setState({loading: false}) 19 | this.props.getPackages(); 20 | })}} 21 | data-color="red" 22 | data-size={XS} 23 | data-style={ZOOM_OUT} 24 | data-spinner-color="#6c757d" 25 | data-spinner-lines={12} 26 | className="btn btn-outline-secondary update-one-btn" 27 | > 28 | 29 | 30 | ) 31 | } 32 | } 33 | 34 | export default UpdateOneButton 35 | -------------------------------------------------------------------------------- /client/src/img/amazon.svg: -------------------------------------------------------------------------------- 1 | 8 | 15 | 22 | 28 | 34 | 40 | 46 | 52 | 58 | -------------------------------------------------------------------------------- /client/src/img/fedex.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /client/src/img/ontrac.svg: -------------------------------------------------------------------------------- 1 | 7 | 11 | 15 | 19 | 23 | 27 | 31 | 35 | -------------------------------------------------------------------------------- /client/src/img/ups.svg: -------------------------------------------------------------------------------- 1 | 7 | 13 | 18 | -------------------------------------------------------------------------------- /client/src/img/usps.svg: -------------------------------------------------------------------------------- 1 | 8 | 13 | 14 | 18 | 22 | 26 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | margin-bottom: 0.5em !important; 3 | } -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | import 'bootstrap/dist/css/bootstrap.min.css'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | 15 | // If you want your app to work offline and load faster, you can change 16 | // unregister() to register() below. Note this comes with some pitfalls. 17 | // Learn more about service workers: https://bit.ly/CRA-PWA 18 | serviceWorker.unregister(); 19 | -------------------------------------------------------------------------------- /client/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /client/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | packagemate-server: 5 | build: 6 | context: api_server 7 | dockerfile: Dockerfile 8 | image: packagemate-server 9 | container_name: packagemate-server 10 | restart: unless-stopped 11 | env_file: .env 12 | environment: 13 | - MONGO_USERNAME=$MONGO_USERNAME 14 | - MONGO_PASSWORD=$MONGO_PASSWORD 15 | - MONGO_HOSTNAME=db 16 | - MONGO_PORT=$MONGO_PORT 17 | - MONGO_DB=$MONGO_DB 18 | - UPS_ACCESS_KEY=$UPS_ACCESS_KEY 19 | - USPS_USERNAME=$USPS_USERNAME 20 | networks: 21 | - app-network 22 | # this will work as long as you haven't changed the mongodb port from the default 23 | # if you have, change it here as well 24 | command: ./wait-for.sh db:27017 -- npm start 25 | packagemate-client: 26 | build: 27 | context: client 28 | dockerfile: Dockerfile 29 | image: packagemate-client 30 | container_name: packagemate-client 31 | restart: unless-stopped 32 | env_file: .env 33 | depends_on: 34 | - packagemate-server 35 | - db 36 | environment: 37 | - MONGO_USERNAME=$MONGO_USERNAME 38 | - MONGO_PASSWORD=$MONGO_PASSWORD 39 | - MONGO_HOSTNAME=db 40 | - MONGO_PORT=$MONGO_PORT 41 | - MONGO_DB=$MONGO_DB 42 | - UPS_ACCESS_KEY=$UPS_ACCESS_KEY 43 | - USPS_USERNAME=$USPS_USERNAME 44 | ports: 45 | # this is the port of the main app, by default, port 80 is 46 | # exposed on the host machine (localhost or 127.0.0.1) 47 | - "80:3000" 48 | networks: 49 | - app-network 50 | command: npm start 51 | stdin_open: true 52 | db: 53 | image: mongo:4.1.8-xenial 54 | container_name: db 55 | restart: unless-stopped 56 | env_file: .env 57 | environment: 58 | - MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME 59 | - MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD 60 | volumes: 61 | - dbdata:/data/db 62 | networks: 63 | - app-network 64 | 65 | networks: 66 | app-network: 67 | driver: bridge 68 | 69 | volumes: 70 | dbdata: 71 | --------------------------------------------------------------------------------