diff --git a/service/.env.TEMPLATE b/service/.env.TEMPLATE new file mode 100644 index 0000000..6592120 --- /dev/null +++ b/service/.env.TEMPLATE @@ -0,0 +1,11 @@ +RUST_LOG=debug,actix=warn,diesel_migrations=warn,reqwest=warn,hyper=warn + +DATABASE_CONTAINER=weather-db +DATABASE_HOST=db +DATABASE_USER=weather +DATABASE_PASSWORD= +DATABASE_NAME=weather +DATABASE_PORT=5432 + +SERVICE_HOST=service +SERVICE_PORT=5000 diff --git a/service/Makefile b/service/Makefile new file mode 100644 index 0000000..fb4b5d5 --- /dev/null +++ b/service/Makefile @@ -0,0 +1,36 @@ +#!make + +include .env + +SHELL := /bin/bash + +.PHONY: help build start stop lint + +help: ## This info + @echo + @cat Makefile | grep -E '^[a-zA-Z\/_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + @echo + +build: ## Build Docker containers + docker compose build + +up: ## Start Docker containers + docker compose up -d + +down: ## Stop Docker containers + docker compose down + +connect: ## Connect to the Weather DB + docker exec -it ${DATABASE_CONTAINER} psql -U postgres + +clean: ## Cleanup Docker containers + docker compose down && \ + docker image rm weather-service || \ + docker network rm weather-frontend || \ + docker network rm weather-backend + +clean-db: ## Remove database + docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "DROP DATABASE IF EXISTS \"${DATABASE_NAME}\";"' + docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "CREATE DATABASE \"${DATABASE_NAME}\";"' || true + + \ No newline at end of file diff --git a/service/docker-compose.yml b/service/docker-compose.yml new file mode 100644 index 0000000..0141a2f --- /dev/null +++ b/service/docker-compose.yml @@ -0,0 +1,44 @@ +version: '3' + +name: weather +services: + db: + image: postgis/postgis:latest + container_name: weather-db + env_file: + - .env + environment: + POSTGRES_USER: ${DATABASE_USER} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + POSTGRES_DB: ${DATABASE_NAME} + volumes: + - db:/var/lib/postgresql/data + - db_logs:/var/log + ports: + - "${DATABASE_PORT:-5432}:5432" + networks: + - weather-backend + restart: unless-stopped + + service: + container_name: weather-service + env_file: + - .env + ports: + - "${SERVICE_PORT:-5000}:5000" + build: + context: service + depends_on: + - db + networks: + - weather-frontend + - weather-backend + restart: unless-stopped + +volumes: + db: + db_logs: + +networks: + weather-frontend: {} + weather-backend: {} diff --git a/ui/package-lock.json b/ui/package-lock.json index 7a19aa2..4f41cd6 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -12,14 +12,13 @@ "@mantine/hooks": "^7.0.0", "@mantine/modals": "^7.0.0", "axios": "^1.4.0", - "chart.js": "^4.4.0", "leaflet": "^1.9.4", "next": "^13.4.19", "react": "^18.2.0", - "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-icons": "^4.11.0", "react-leaflet": "^4.2.1", + "recharts": "^2.8.0", "recoil": "^0.7.7" }, "devDependencies": { @@ -198,11 +197,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "node_modules/@kurkle/color": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", - "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" - }, "node_modules/@mantine/core": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.0.0.tgz", @@ -479,6 +473,60 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-array": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.8.tgz", + "integrity": "sha512-2xAVyAUgaXHX9fubjcCbGAUOqYfRJN1em1EKR2HfzWBpObZhwfnZKvofTN4TplMqJdFQao61I+NVSai/vnBvDQ==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.1.tgz", + "integrity": "sha512-CSAVrHAtM9wfuLJ2tpvvwCU/F22sm7rMHNN+yh9D6O6hyAms3+O0cgMpC1pm6UEUMOntuZC8bMt74PteiDUdCg==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz", + "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.2.tgz", + "integrity": "sha512-zAbCj9lTqW9J9PlF4FwnvEjXZUy75NQqPm7DMHZXuxCFTpuTrdK2NMYGQekf4hlasL78fCYOLu4EE3/tXElwow==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", + "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.5.tgz", + "integrity": "sha512-w/C++3W394MHzcLKO2kdsIn5KKNTOqeQVzyPSGPLzQbkPw/jpeaGtSRlakcKevGgGsjJxGsbqS0fPrVFDbHrDA==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.3.tgz", + "integrity": "sha512-cHMdIq+rhF5IVwAV7t61pcEXfEHsEsrbBUPkFGBwTXuxtTAkBBrnrNA8++6OWm3jwVsXoZYQM8NEekg6CPJ3zw==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.1.tgz", + "integrity": "sha512-5j/AnefKAhCw4HpITmLDTPlf4vhi8o/dES+zbegfPb7LaGfNyqkLxBR6E+4yvTAgnJLmhe80EXFMzUs38fw4oA==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz", + "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==" + }, "node_modules/@types/geojson": { "version": "7946.0.10", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", @@ -1226,16 +1274,10 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chart.js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.0.tgz", - "integrity": "sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==", - "dependencies": { - "@kurkle/color": "^0.3.0" - }, - "engines": { - "pnpm": ">=7" - } + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" }, "node_modules/client-only": { "version": "0.0.1", @@ -1299,6 +1341,11 @@ "node": ">= 8" } }, + "node_modules/css-unit-converter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", + "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1317,6 +1364,116 @@ "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", "devOptional": true }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -1340,6 +1497,11 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1454,6 +1616,14 @@ "node": ">=6.0.0" } }, + "node_modules/dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.512", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.512.tgz", @@ -2175,6 +2345,11 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "node_modules/execa": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", @@ -2210,6 +2385,14 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, + "node_modules/fast-equals": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", + "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -2744,6 +2927,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -3281,6 +3472,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4060,15 +4256,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-chartjs-2": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", - "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", - "peerDependencies": { - "chart.js": "^4.1.1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -4107,6 +4294,11 @@ "react-dom": "^18.0.0" } }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "node_modules/react-number-format": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.3.1.tgz", @@ -4164,6 +4356,32 @@ } } }, + "node_modules/react-resize-detector": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-8.1.0.tgz", + "integrity": "sha512-S7szxlaIuiy5UqLhLL1KY3aoyGHbZzsTpYal9eYMwCyKqoqoVLCmIgAgNyIM1FhnP2KyBygASJxdhejrzjMb+w==", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-smooth": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.4.tgz", + "integrity": "sha512-OkFsrrMBTvQUwEJthE1KXSOj79z57yvEWeFefeXPib+RmQEI9B1Ub1PgzlzzUyBOvl/TjXt5nF2hmD4NsgAh8A==", + "dependencies": { + "fast-equals": "^5.0.0", + "react-transition-group": "2.9.0" + }, + "peerDependencies": { + "prop-types": "^15.6.0", + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -4202,6 +4420,21 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "dependencies": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0", + "react-dom": ">=15.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4211,6 +4444,38 @@ "pify": "^2.3.0" } }, + "node_modules/recharts": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.8.0.tgz", + "integrity": "sha512-nciXqQDh3aW8abhwUlA4EBOBusRHLNiKHfpRZiG/yjups1x+auHb2zWPuEcTn/IMiN47vVMMuF8Sr+vcQJtsmw==", + "dependencies": { + "classnames": "^2.2.5", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.19", + "react-is": "^16.10.2", + "react-resize-detector": "^8.0.4", + "react-smooth": "^2.0.2", + "recharts-scale": "^0.4.4", + "reduce-css-calc": "^2.1.8", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "prop-types": "^15.6.0", + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, "node_modules/recoil": { "version": "0.7.7", "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", @@ -4230,6 +4495,20 @@ } } }, + "node_modules/reduce-css-calc": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", + "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", + "dependencies": { + "css-unit-converter": "^1.1.1", + "postcss-value-parser": "^3.3.0" + } + }, + "node_modules/reduce-css-calc/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -5110,6 +5389,27 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/victory-vendor": { + "version": "36.6.11", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.6.11.tgz", + "integrity": "sha512-nT8kCiJp8dQh8g991J/R5w5eE2KnO8EAIP0xocWlh9l2okngMWglOPoMZzJvek8Q1KUc4XE/mJxTZnvOB1sTYg==", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/ui/package.json b/ui/package.json index 7c82298..4ddaffb 100644 --- a/ui/package.json +++ b/ui/package.json @@ -13,14 +13,13 @@ "@mantine/hooks": "^7.0.0", "@mantine/modals": "^7.0.0", "axios": "^1.4.0", - "chart.js": "^4.4.0", "leaflet": "^1.9.4", "next": "^13.4.19", "react": "^18.2.0", - "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-icons": "^4.11.0", "react-leaflet": "^4.2.1", + "recharts": "^2.8.0", "recoil": "^0.7.7" }, "devDependencies": { diff --git a/ui/src/api/metar.ts b/ui/src/api/metar.ts index d416e6c..63f972d 100644 --- a/ui/src/api/metar.ts +++ b/ui/src/api/metar.ts @@ -1,4 +1,3 @@ -import { Airport } from './airport.types'; import { Metar } from './metar.types'; import { getRequest } from '.'; @@ -6,11 +5,11 @@ interface GetMetarsResponse { data: Metar[]; } -export async function getMetars(airports: Airport[]): Promise { - if (airports.length == 0) { +export async function getMetars(icaos: string[]): Promise { + if (icaos.length == 0) { return { data: [] }; } - const stationICAOs: string = airports.map((airport) => airport.icao).join(','); + const stationICAOs: string = icaos.map((icao) => icao).join(','); const response = await getRequest(`metars/${stationICAOs}`, {}); return response?.data || { data: [] }; } diff --git a/ui/src/app/airport/[icao]/page.tsx b/ui/src/app/airport/[icao]/page.tsx index 9513297..202edfc 100644 --- a/ui/src/app/airport/[icao]/page.tsx +++ b/ui/src/app/airport/[icao]/page.tsx @@ -2,16 +2,23 @@ import { getAirport } from '@/api/airport'; import { Airport } from '@/api/airport.types'; -import Link from 'next/link'; +import { getMetars } from '@/api/metar'; +import { Metar } from '@/api/metar.types'; +import SkyConditions from '@/components/Metars/SkyConditions'; import { useEffect, useState } from 'react'; export default function Page({ params }: { params: { icao: string } }) { const [airport, setAirport] = useState(undefined); + const [metar, setMetar] = useState(undefined); useEffect(() => { async function loadAirport() { - const { data: airport } = await getAirport({ icao: params.icao }); - setAirport(airport); + const { data: airportData } = await getAirport({ icao: params.icao }); + setAirport(airportData); + const { data: metarData } = await getMetars([airportData.icao]); + if (metarData.length > 0) { + setMetar(metarData[0]); + } } loadAirport(); }, []); @@ -21,7 +28,7 @@ export default function Page({ params }: { params: { icao: string } }) { <>

{airport.full_name}

- Back + {metar && }
); diff --git a/ui/src/components/Metars/MapTiles.tsx b/ui/src/components/Metars/MapTiles.tsx index 49ad0ed..cfb497f 100644 --- a/ui/src/components/Metars/MapTiles.tsx +++ b/ui/src/components/Metars/MapTiles.tsx @@ -41,7 +41,7 @@ export default function MapTiles() { async function updateAirports(bounds: LatLngBounds) { const ne = bounds.getNorthEast(); const sw = bounds.getSouthWest(); - const { data: _airports } = await getAirports({ + const { data: airportData } = await getAirports({ bounds: { northEast: { lat: ne.lat, lon: ne.lng }, southWest: { lat: sw.lat, lon: sw.lng } @@ -49,15 +49,15 @@ export default function MapTiles() { limit: 100, page: 1 }); - const { data: metars } = await getMetars(_airports); + const { data: metars } = await getMetars(airportData.map((a) => a.icao)); metars.forEach((metar) => { - _airports.forEach((airport) => { + airportData.forEach((airport) => { if (metar.station_id == airport.icao) { airport.metar = metar; } }); }); - setAirports(_airports); + setAirports(airportData); } function metarIcon(airport: Airport) { diff --git a/ui/src/components/Metars/MetarModal.tsx b/ui/src/components/Metars/MetarModal.tsx index 0f4ce39..948e54c 100644 --- a/ui/src/components/Metars/MetarModal.tsx +++ b/ui/src/components/Metars/MetarModal.tsx @@ -31,7 +31,7 @@ export default function MetarModal({ airport, isOpen, onClose }: MetarModalProps } return ( - + {airport.icao} {airport.full_name} @@ -45,39 +45,6 @@ export default function MetarModal({ airport, isOpen, onClose }: MetarModalProps

{airport.metar && } - {/*

{airport.metar?.raw_text}

-
- - {airport.metar?.flight_category ? airport.metar?.flight_category : 'UNKN'} - -
- - {airport.metar && airport.metar.wind_dir_degrees && Number(airport.metar.wind_dir_degrees) > 0 ? ( - - ) : ( - <> - )} - {airport.metar && airport.metar.wind_dir_degrees && airport.metar.wind_dir_degrees == 'VRB' ? ( - - ) : ( - <> - )} - - {airport.metar?.wind_speed_kt != undefined && airport.metar?.wind_speed_kt > 0 - ? `${airport.metar?.wind_speed_kt} KT` - : `CALM`} - - - {airport.metar?.wx_string?.split(' ').map((wx) => )} -
-
*/}
); diff --git a/ui/src/components/Metars/SkyConditions.tsx b/ui/src/components/Metars/SkyConditions.tsx index 7ea9435..db1dc32 100644 --- a/ui/src/components/Metars/SkyConditions.tsx +++ b/ui/src/components/Metars/SkyConditions.tsx @@ -1,20 +1,18 @@ 'use client'; import { Metar, SkyCondition } from '@/api/metar.types'; +import { Box } from '@mantine/core'; import { - Chart as ChartJS, - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Filler, - Legend -} from 'chart.js'; -import { Line } from 'react-chartjs-2'; - -ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Filler, Legend); + Customized, + Label, + LabelList, + Line, + LineChart, + ReferenceLine, + ResponsiveContainer, + XAxis, + YAxis +} from 'recharts'; export default function SkyConditions({ metar }: { metar: Metar }) { function skyConditionColor(skyCondition: SkyCondition) { @@ -32,35 +30,54 @@ export default function SkyConditions({ metar }: { metar: Metar }) { return '#e6194b'; } } - function createDataset(skyCondition: SkyCondition) { - return { - fill: true, - label: skyCondition.sky_cover, - data: [skyCondition.cloud_base_ft_agl, skyCondition.cloud_base_ft_agl], - backgroundColor: skyConditionColor(skyCondition) - }; - } if (metar.sky_condition && metar.sky_condition.length > 0) { - console.log(metar); - const options = { - responsive: true, - plugins: { - legend: { - display: false - }, - title: { - display: true, - text: 'Sky Conditions' - } + const data: any = [ + { + name: 'start' + }, + { + name: 'end' } - }; - const data = { - labels: ['', ''], - datasets: metar.sky_condition.map((skyCondition) => createDataset(skyCondition)) - }; + ]; + metar.sky_condition.forEach((skyCondition, index) => { + data[0][index] = skyCondition.cloud_base_ft_agl; + data[1][index] = skyCondition.cloud_base_ft_agl; + }); - return ; + return ( + + (dataMax < 1000 ? 1000 : Math.ceil(dataMax / 1000) * 1000)]} /> + {metar.sky_condition.map((skyCondition, index) => ( + + renderCustomizedLabel(props, skyCondition.sky_cover)} + /> + + ))} + + ); } else { return <>; } } + +const renderCustomizedLabel = (props: any, skyCover: string) => { + const { x, y, value, index } = props; + if (index == 1) { + return ( + + {skyCover} {value} + + ); + } else { + return <>; + } +};