diff --git a/.nvmrc b/.nvmrc index 3c03207..209e3ef 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18 +20 diff --git a/package-lock.json b/package-lock.json index c9c1220..8a1a5f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,22 +10,23 @@ "hasInstallScript": true, "license": "ISC", "dependencies": { + "@ariakit/react": "^0.4.7", + "@tanstack/react-query": "^5.40.1", "bezier-js": "^2.5.1", "classnames": "^2.5.1", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-favicon": "1.0.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-favicon": "2.0.5", "react-loading-indicators": "^0.2.3", "react-router-dom": "^6.23.1", "react-spring": "^8.0.27", - "reakit": "^1.3.11", - "seamless-scroll-polyfill": "^1.0.10", + "seamless-scroll-polyfill": "^2.3.4", "timeago.js": "^4.0.2" }, "devDependencies": { - "@types/node": "^20.14.1", - "@types/react": "^17.0.80", - "@types/react-dom": "^17.0.17", + "@types/node": "^20.14.2", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^7.12.0", "@typescript-eslint/parser": "^7.12.0", "@vitejs/plugin-react": "^4.3.0", @@ -37,7 +38,7 @@ "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-react": "^7.34.2", "eslint-plugin-react-hooks": "^4.6.2", - "prettier": "^3.3.0", + "prettier": "^3.3.1", "typescript": "^5.4.5", "vite": "^5.2.12" }, @@ -68,6 +69,41 @@ "node": ">=6.0.0" } }, + "node_modules/@ariakit/core": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.7.tgz", + "integrity": "sha512-GUy/3ZY4kW1KdYHtMZRrRlj5FYbZTAyHlimxHI7Zs0xsC+kAzIf8lopnf67Y9IYLgEMr37KosIV7kwpkJpNs5Q==" + }, + "node_modules/@ariakit/react": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.7.tgz", + "integrity": "sha512-uUruuCo1M0Nj2oq1nTwDfUlVTLuoI9xeHP75EkuXX46lg5hzE5vVWbSMO1D6MCy7UwrUx2Ts4IqxdKr97suTwQ==", + "dependencies": { + "@ariakit/react-core": "0.4.7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ariakit" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@ariakit/react-core": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.7.tgz", + "integrity": "sha512-OogUyQ20cxkRNRuqLI05JbmpR4Lr5HwhUIqnb/sipzt6bkg/3wCXEnUAjfxg3nPjLTMjJ8+ODWmPC9JMJTW/yg==", + "dependencies": { + "@ariakit/core": "0.4.7", + "@floating-ui/dom": "^1.0.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", @@ -625,6 +661,28 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", + "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", + "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", + "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -712,18 +770,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", - "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", @@ -787,15 +833,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/@remix-run/router": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", @@ -817,6 +854,30 @@ "darwin" ] }, + "node_modules/@tanstack/query-core": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.40.0.tgz", + "integrity": "sha512-eD8K8jsOIq0Z5u/QbvOmfvKKE/XC39jA7yv4hgpl/1SRiU+J8QCIwgM/mEHuunQsL87dcvnHqSVLmf9pD4CiaA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.40.1.tgz", + "integrity": "sha512-gOcmu+gpFd2taHrrgMM9RemLYYEDYfsCqszxCC0xtx+csDa4R8t7Hr7SfWXQP13S2sF+mOxySo/+FNXJFYBqcA==", + "dependencies": { + "@tanstack/query-core": "5.40.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -858,32 +919,12 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/eslint": { - "version": "8.56.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", - "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -891,9 +932,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.1.tgz", - "integrity": "sha512-T2MzSGEu+ysB/FkWfqmhV3PLyQlowdptmmgD20C6QxsS8Fmv5SjpZ1ayXaEC0S21/h5UJ9iA6W/5vSNU5l00OA==", + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -906,31 +947,24 @@ "dev": true }, "node_modules/@types/react": { - "version": "17.0.80", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.80.tgz", - "integrity": "sha512-LrgHIu2lEtIo8M7d1FcI3BdwXWoRQwMoXOZ7+dPTW0lYREjmlHl3P0U1VD0i/9tppOuv8/sam7sOjx34TxSFbA==", + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", "dev": true, "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "^0.16", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "17.0.25", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.25.tgz", - "integrity": "sha512-urx7A7UxkZQmThYA4So0NelOVjx3V4rNFVJwp0WZlbIK5eM4rNJDiN3R/E9ix0MBh6kAEojk/9YL+Te6D9zHNA==", + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "dev": true, "dependencies": { - "@types/react": "^17" + "@types/react": "*" } }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.12.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.12.0.tgz", @@ -1442,11 +1476,6 @@ "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-2.6.1.tgz", "integrity": "sha512-jelZM33eNzcZ9snJ/5HqJLw3IzXvA8RFcBjkdOB8SDYyOvW8Y2tTosojAiBTnD1MhbHoWUYNbxUXxBl61TxbRg==" }, - "node_modules/body-scroll-lock": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz", - "integrity": "sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg==" - }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -1500,14 +1529,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -3913,9 +3934,9 @@ } }, "node_modules/prettier": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.0.tgz", - "integrity": "sha512-J9odKxERhCQ10OC2yb93583f6UnYutOeiV5i0zEDS7UGTdUt0u+y8erxl3lBKvwo/JHyyoEdXjwp4dke9oyZ/g==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz", + "integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -3979,34 +4000,32 @@ ] }, "node_modules/react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "17.0.2" + "react": "^18.3.1" } }, "node_modules/react-favicon": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/react-favicon/-/react-favicon-1.0.1.tgz", - "integrity": "sha512-tbMjIfIE5cLj4BpnkJznQ1U7Dmvdp3iN02AyzXTSzfhxAGFjuf7tMBmapMRSdK7v1zX6+CktuOG1CT1jlrjPXw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-favicon/-/react-favicon-2.0.5.tgz", + "integrity": "sha512-WEw1t+bU7O2VKWb5hedyQ0qT0qx4L/OlA7tfCdevLsNsZQUI1i+cGAtgN8boA7aMGv387WiRN/tjZVEoSoJsOQ==", "dependencies": { "prop-types": "^15.8.1" }, @@ -4086,58 +4105,6 @@ "react-dom": ">= 16.8.0" } }, - "node_modules/reakit": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/reakit/-/reakit-1.3.11.tgz", - "integrity": "sha512-mYxw2z0fsJNOQKAEn5FJCPTU3rcrY33YZ/HzoWqZX0G7FwySp1wkCYW79WhuYMNIUFQ8s3Baob1RtsEywmZSig==", - "dependencies": { - "@popperjs/core": "^2.5.4", - "body-scroll-lock": "^3.1.5", - "reakit-system": "^0.15.2", - "reakit-utils": "^0.15.2", - "reakit-warning": "^0.6.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ariakit" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" - } - }, - "node_modules/reakit-system": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/reakit-system/-/reakit-system-0.15.2.tgz", - "integrity": "sha512-TvRthEz0DmD0rcJkGamMYx+bATwnGNWJpe/lc8UV2Js8nnPvkaxrHk5fX9cVASFrWbaIyegZHCWUBfxr30bmmA==", - "dependencies": { - "reakit-utils": "^0.15.2" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" - } - }, - "node_modules/reakit-utils": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/reakit-utils/-/reakit-utils-0.15.2.tgz", - "integrity": "sha512-i/RYkq+W6hvfFmXw5QW7zvfJJT/K8a4qZ0hjA79T61JAFPGt23DsfxwyBbyK91GZrJ9HMrXFVXWMovsKBc1qEQ==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" - } - }, - "node_modules/reakit-warning": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/reakit-warning/-/reakit-warning-0.6.2.tgz", - "integrity": "sha512-z/3fvuc46DJyD3nJAUOto6inz2EbSQTjvI/KBQDqxwB0y02HDyeP8IWOJxvkuAUGkWpeSx+H3QWQFSNiPcHtmw==", - "dependencies": { - "reakit-utils": "^0.15.2" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -4345,18 +4312,17 @@ } }, "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "node_modules/seamless-scroll-polyfill": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/seamless-scroll-polyfill/-/seamless-scroll-polyfill-1.2.4.tgz", - "integrity": "sha512-1CwHvQRS88m/HwN76hnjFu9dzU2HUJmGbvUkKMoRq70o6Pd1DcD4aGvsku0OqRhUuFKQOsjm0Imo1cC/PWWOfA==" + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/seamless-scroll-polyfill/-/seamless-scroll-polyfill-2.3.4.tgz", + "integrity": "sha512-Biobd4LDoeFODX1bfiru84GveSajzFELFB3ejMZsRR2TpD73W46uEZYAEqSx5RtKpN2umhGWaoSRivs0ZcHp9g==" }, "node_modules/semver": { "version": "7.6.2", @@ -4477,18 +4443,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/spawn-command": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", @@ -4660,34 +4614,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/terser": { - "version": "5.27.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.1.tgz", - "integrity": "sha512-29wAr6UU/oQpnTw5HoadwjUZnFQXGdOfj0LjZ4sVxzqwHh/QVkvr7m8y9WoR4iN3FRitVduTc6KdjcW38Npsug==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4952,6 +4878,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/vite": { "version": "5.2.12", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz", diff --git a/package.json b/package.json index 8df8a46..04c9e07 100644 --- a/package.json +++ b/package.json @@ -29,22 +29,23 @@ }, "homepage": "https://github.com/transitmatters/new-train-tracker#readme", "dependencies": { + "@ariakit/react": "^0.4.7", + "@tanstack/react-query": "^5.40.1", "bezier-js": "^2.5.1", "classnames": "^2.5.1", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-favicon": "1.0.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-favicon": "2.0.5", "react-loading-indicators": "^0.2.3", "react-router-dom": "^6.23.1", "react-spring": "^8.0.27", - "reakit": "^1.3.11", - "seamless-scroll-polyfill": "^1.0.10", + "seamless-scroll-polyfill": "^2.3.4", "timeago.js": "^4.0.2" }, "devDependencies": { - "@types/node": "^20.14.1", - "@types/react": "^17.0.80", - "@types/react-dom": "^17.0.17", + "@types/node": "^20.14.2", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^7.12.0", "@typescript-eslint/parser": "^7.12.0", "@vitejs/plugin-react": "^4.3.0", @@ -56,7 +57,7 @@ "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-react": "^7.34.2", "eslint-plugin-react-hooks": "^4.6.2", - "prettier": "^3.3.0", + "prettier": "^3.3.1", "typescript": "^5.4.5", "vite": "^5.2.12" } diff --git a/server/app.py b/server/app.py index 2ad07f8..d79eda9 100644 --- a/server/app.py +++ b/server/app.py @@ -7,10 +7,10 @@ import json import asyncio -from chalicelib import (background, last_seen, mbta_api) +from chalicelib import (last_seen, mbta_api) import chalicelib.healthcheck from datadog_lambda.wrapper import datadog_lambda_wrapper -from chalice import Chalice, CORSConfig, ConvertToMiddleware, Response +from chalice import Chalice, CORSConfig, ConvertToMiddleware, Response, Cron app = Chalice(app_name="new-train-tracker") @@ -24,12 +24,6 @@ cors_config = CORSConfig(allow_origin="*", max_age=3600) -# Start a background thread to run `schedule` (i.e. the package) jobs, -# which in our case is just the "last seen" update -background_thread = background.run_continuously() -last_seen.initialize() - - # takes a comma-delimited string of route ids @app.route("/trains/{route_ids_string}", cors=cors_config) def trains(route_ids_string): @@ -63,6 +57,11 @@ def vehicles(trip_id, stop_id): return Response(json.dumps(departure), headers={"Content-Type": "application/json"}) +@app.schedule(Cron("0/10", "0-6,9-23", "*", "*", "?", "*")) +def update_last_seen(event): + asyncio.run(last_seen.update_recent_sightings()) + + @app.route("/healthcheck", cors=cors_config) def healthcheck(): return chalicelib.healthcheck.run() diff --git a/server/chalicelib/background.py b/server/chalicelib/background.py deleted file mode 100644 index f3cbc16..0000000 --- a/server/chalicelib/background.py +++ /dev/null @@ -1,35 +0,0 @@ -import threading -import time -import schedule - - -# Copied from https://schedule.readthedocs.io/en/stable/background-execution.html - - -def run_continuously(interval_s=1) -> threading.Event: - """Continuously run, while executing pending jobs at each - elapsed time interval. - @return cease_continuous_run: threading. Event which can - be set to cease continuous run. Please note that it is - *intended behavior that run_continuously() does not run - missed jobs*. For example, if you've registered a job that - should run every minute and you set a continuous run - interval of one hour then your job won't be run 60 times - at each interval but only once. - """ - cease_continuous_run = threading.Event() - - class ScheduleThread(threading.Thread): - @classmethod - def run(cls): - while not cease_continuous_run.is_set(): - schedule.run_pending() - time.sleep(interval_s) - - # preston added daemon=True. By making it a daemon thread, the main thread getting a signal - # will cause only the background thread to remain, and Python will then exit fully. Without that, - # the background thread will continue to run. - # See https://docs.python.org/3/library/threading.html#threading.Thread.daemon - continuous_thread = ScheduleThread(daemon=True) - continuous_thread.start() - return cease_continuous_run diff --git a/server/chalicelib/last_seen.py b/server/chalicelib/last_seen.py index c7019ae..f769c3e 100644 --- a/server/chalicelib/last_seen.py +++ b/server/chalicelib/last_seen.py @@ -1,69 +1,43 @@ import asyncio import datetime import json -import schedule +import chalicelib.s3 as s3 import chalicelib.mbta_api as mbta_api -import chalicelib.secrets as secrets -from chalicelib.routes import get_line_for_route -from chalicelib.routes import DEFAULT_ROUTE_IDS +from chalicelib.routes import DEFAULT_ROUTE_IDS, get_line_for_route from chalicelib.util import filter_new JSON_PATH = "last_seen.json" -LAST_SEEN_TIMES = {} ROUTES = DEFAULT_ROUTE_IDS -def update_recent_sightings_sync(): - asyncio.run(update_recent_sightings()) - - -def initialize(): - global LAST_SEEN_TIMES - - # Read from disk if there's a previous file there - try: - with open(JSON_PATH, "r", encoding="utf-8") as file: - LAST_SEEN_TIMES = json.load(file) - except FileNotFoundError: - print("Last seen file doesn't exist; starting fresh") - - if not secrets.LAST_SEEN_UPDATE: - print("LAST_SEEN_UPDATE is false, so I'm not continuously updating last seen times for you.") - return - schedule.every().minute.do(update_recent_sightings_sync) - - async def update_recent_sightings(): + try: + last_seen_times = json.loads(s3.download(JSON_PATH, "utf8", compressed=False)) + except Exception as e: + print("Couldn't read last seen times from s3: ", e) + last_seen_times = {} try: print("Updating recent sightings...") now = datetime.datetime.utcnow() - all_vehicles = await mbta_api.vehicle_data_for_routes(ROUTES) + all_vehicles = asyncio.run(mbta_api.vehicle_data_for_routes(ROUTES)) new_vehicles = filter_new(all_vehicles) for vehicle in new_vehicles: line = get_line_for_route(vehicle["route"]) - LAST_SEEN_TIMES[line] = { + last_seen_times[line] = { "car": vehicle["label"], # Python isoformat() doesn't include TZ, but we know this is UTC because we used utcnow() above "time": now.isoformat()[:-3] + "Z", } - - with open(JSON_PATH, "w", encoding="utf-8") as file: - json.dump(LAST_SEEN_TIMES, file, indent=4, sort_keys=True, default=str) + s3.upload(JSON_PATH, json.dumps(last_seen_times), compress=False) except Exception as e: - print("Couldn't write last seen times to disk: ", e) + print("Couldn't write last seen times to s3: ", e) # Get the last time that a new train was seen on each line # This is the function that other modules use def get_recent_sightings_for_lines(): - return LAST_SEEN_TIMES - - -# For development/testing only! -if __name__ == "__main__": - initialize() - asyncio.run(update_recent_sightings()) + return json.loads(s3.download(JSON_PATH, "utf8")) diff --git a/server/chalicelib/s3.py b/server/chalicelib/s3.py index c5d588b..57f13d6 100644 --- a/server/chalicelib/s3.py +++ b/server/chalicelib/s3.py @@ -1,14 +1,24 @@ +import os import boto3 import zlib + s3 = boto3.client("s3") +BUCKET = os.environ.get("TM_CORS_HOST", "ntt-beta.labs.transitmatters.org") -def download(bucket, key, encoding="utf8", compressed=True): - obj = s3.get_object(Bucket=bucket, Key=key) +def download(key, encoding="utf8", compressed=True): + print(BUCKET) + obj = s3.get_object(Bucket=BUCKET, Key=key) s3_data = obj["Body"].read() if not compressed: return s3_data.decode(encoding) # 32 should detect zlib vs gzip decompressed = zlib.decompress(s3_data, zlib.MAX_WBITS | 32).decode(encoding) return decompressed + + +def upload(key, bytes, compress=True): + if compress: + bytes = zlib.compress(bytes) + s3.put_object(Bucket=BUCKET, Key=key, Body=bytes) diff --git a/src/components/AgeTabPicker.tsx b/src/components/AgeTabPicker.tsx index fc2956b..c629a22 100644 --- a/src/components/AgeTabPicker.tsx +++ b/src/components/AgeTabPicker.tsx @@ -1,5 +1,5 @@ import { useRef, useLayoutEffect } from 'react'; -import { TabList, Tab } from 'reakit'; +import { TabList, Tab, TabProvider } from '@ariakit/react'; import { VehiclesAge } from '../types'; import { useAgeSearchParam } from '../hooks/searchParams'; @@ -36,32 +36,33 @@ export const AgeTabPicker: React.FC = ({ tabColor }) => { }, [tabColor, ageSearchParam]); return ( - -
+ + +
- {trainTypes.map((trainType) => { - return ( - { - setAgeSearchParam(trainType.key); - }} - > -
{ + return ( + { + setAgeSearchParam(trainType.key); + }} > - {trainType.label.toUpperCase()} -
-
trains
-
- ); - })} - +
+ {trainType.label.toUpperCase()} +
+
trains
+ + ); + })} + + ); }; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 4be1e33..87ee845 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,4 +1,4 @@ -export const Footer: React.FC<{ version: string }> = ({ version }) => { +export const Footer: React.FC<{ version?: string }> = ({ version }) => { return (

diff --git a/src/components/Line.tsx b/src/components/Line.tsx index 15b9b1f..7eabf1d 100644 --- a/src/components/Line.tsx +++ b/src/components/Line.tsx @@ -7,9 +7,9 @@ import { renderTextTrainlabel } from '../labels'; import { Train } from './Train'; import { PopoverContainerContext, getTrainRoutePairsForLine, setCssVariable } from './util'; -import { getInitialDataByKey } from '../initialData'; import { Line as TLine, Pair, StationPositions, VehiclesAge } from '../types'; import { MBTAApi } from '../hooks/useMbtaApi'; +import { useLastSightingByLine } from '../hooks/useLastSighting'; const AGE_WORD_MAP = new Map([ ['new_vehicles', ' new '], @@ -46,22 +46,22 @@ const sortTrainRoutePairsByDistance = (pairs: Pair[], stationPositions: StationP return pairs.sort((a, b) => distanceMap.get(a) - distanceMap.get(b)); }; -const renderEmptyNoticeForLine = (line, age) => { +const EmptyNoticeForLine: React.FC<{ line: string; age: VehiclesAge }> = ({ line, age }) => { + const sightingForLine = useLastSightingByLine(line); + const ageWord = AGE_WORD_MAP.get(age); // What to show when old or all is selected if (age !== 'new_vehicles') { - return `No${ageWord}trains on the ${line} Line right now.`; + return <>{`No${ageWord}trains on the ${line} Line right now.`}; } // What to show when new is selected - const sightings = getInitialDataByKey('sightings'); - const sightingForLine = sightings && sightings[line]; if (sightingForLine) { const { car, time } = sightingForLine; const ago = timeago.format(time); - return `A new ${line} Line train (#${car}) was last seen ${ago}.`; + return <>{`A new ${line} Line train (#${car}) was last seen ${ago}.`}; } - return `No new trains on the ${line} Line right now.`; + return <>{`No new trains on the ${line} Line right now.`}; }; const getRouteColor = (colors, routeId, focusedRouteId) => { @@ -224,7 +224,9 @@ export const Line: React.FC = ({ api, line, age }) => { if (trainRoutePairs.length === 0) { return (

-
{renderEmptyNoticeForLine(line.name, age)}
+
+ +
); } diff --git a/src/components/LineTabPicker.tsx b/src/components/LineTabPicker.tsx index ec4e453..49cbd39 100644 --- a/src/components/LineTabPicker.tsx +++ b/src/components/LineTabPicker.tsx @@ -1,5 +1,5 @@ import { useRef, useLayoutEffect } from 'react'; -import { TabList, Tab } from 'reakit'; +import { TabList, Tab, TabProvider } from '@ariakit/react'; import { Line, Train } from '../types'; import { getTrainRoutePairsForLine } from './util'; @@ -35,38 +35,39 @@ export const LineTabPicker: React.FC = ({ lines, trainsByRou }, [lineSearchParam, totalTrainCount]); return ( - -
- {lines.map((line) => { - const trains = getTrainRoutePairsForLine(trainsByRoute, line.routes); - return ( - { - setLineSearchParam(line); - }} - > -
+ +
+ {lines.map((line) => { + const trains = getTrainRoutePairsForLine(trainsByRoute, line.routes); + return ( + { + setLineSearchParam(line); + }} > - {line.abbreviation} -
-
- {trains.length}{' '} - - {' '} - {trains.length === 1 ? 'train' : 'trains'}{' '} - -
- - ); - })} -
+
+ {line.abbreviation} +
+
+ {trains.length}{' '} + + {' '} + {trains.length === 1 ? 'train' : 'trains'}{' '} + +
+ + ); + })} + + ); }; diff --git a/src/components/Train.tsx b/src/components/Train.tsx index 18465a9..c712dd4 100644 --- a/src/components/Train.tsx +++ b/src/components/Train.tsx @@ -67,7 +67,6 @@ export const Train = ({ train, route, colors, focusOnMount, labelPosition, onFoc elementScrollIntoView(element, { behavior: prefersReducedMotion() ? 'auto' : 'smooth', block: 'center', - duration: 250, }); } }, [element, isTracked]); diff --git a/src/hooks/useLastSighting.ts b/src/hooks/useLastSighting.ts new file mode 100644 index 0000000..324f224 --- /dev/null +++ b/src/hooks/useLastSighting.ts @@ -0,0 +1,21 @@ +import { useQuery } from '@tanstack/react-query'; + +const FIVE_MINUTES = 5 * 60 * 1000; + +export const fetchLastSighting = (): Promise<{ + [line: string]: { car: string; time: string }; +}> => { + const url = new URL(`/last_seen.json`, window.location.origin); + return fetch(url.toString()).then((resp) => resp.json()); +}; + +export const useLastSightingByLine = (line: string) => { + const { data: sightings } = useQuery({ + queryKey: ['lastSeen'], + queryFn: fetchLastSighting, + staleTime: FIVE_MINUTES, + }); + if (sightings && sightings[line]) { + return sightings[line]; + } +}; diff --git a/src/hooks/useMbtaApi.ts b/src/hooks/useMbtaApi.ts index 509da88..81ed05c 100644 --- a/src/hooks/useMbtaApi.ts +++ b/src/hooks/useMbtaApi.ts @@ -5,7 +5,6 @@ Provides the __NTT_INITIAL_DATA__ JSON blob that is embedded in the initial serv import { useEffect, useState, useCallback } from 'react'; -import { getInitialDataByKey } from '../initialData'; import { Line, Route, Station, Train, VehiclesAge } from '../types'; import { APP_DATA_BASE_PATH } from '../constants'; @@ -18,13 +17,7 @@ export interface MBTAApi { // if isFirstRequest is true, get train positions from intial request data JSON // if isFirstRequest is false, makes request for new train positions through backend server via chalice route defined in app.py -const getTrainPositions = (routes: string[], isFirstRequest: boolean | null) => { - if (isFirstRequest) { - const initialTrainsData = getInitialDataByKey('vehicles'); - if (initialTrainsData) { - return Promise.resolve(initialTrainsData); - } - } +const getTrainPositions = (routes: string[]) => { return fetch(`${APP_DATA_BASE_PATH}/trains/${routes.join(',')}`).then((res) => res.json()); }; @@ -49,18 +42,10 @@ const filterTrains = (trains: Train[] | undefined, vehiclesAge: VehiclesAge) => }; const getStationsForRoute = (route: string) => { - const initialStopsData = getInitialDataByKey('stops'); - if (initialStopsData && initialStopsData[route]) { - return Promise.resolve(initialStopsData[route]); - } return fetch(`${APP_DATA_BASE_PATH}/stops/${route}`).then((res) => res.json()); }; const getRoutesInfo = (routes: string[]) => { - const initialRoutesData = getInitialDataByKey('routes'); - if (initialRoutesData) { - return Promise.resolve(initialRoutesData); - } return fetch(`${APP_DATA_BASE_PATH}/routes/${routes.join(',')}`).then((res) => res.json()); }; @@ -74,7 +59,6 @@ export const useMbtaApi = (lines: Line[], vehiclesAge: VehiclesAge = 'new_vehicl const [routesInfoByRoute, setRoutesInfoByRoute] = useState | null>(null); const [stationsByRoute, setStationsByRoute] = useState | null>(null); const [trainsByRoute, setTrainsByRoute] = useState | null>(null); - const [isInitialFetch, setIsInitialFetch] = useState(true); const isReady = !!stationsByRoute && !!trainsByRoute && !!routesInfoByRoute; const getTrains = useCallback(() => { @@ -82,14 +66,13 @@ export const useMbtaApi = (lines: Line[], vehiclesAge: VehiclesAge = 'new_vehicl routeNames.forEach((routeName) => { nextTrainsByRoute[routeName] = []; }); - getTrainPositions(routeNames, isInitialFetch).then((trains: Train[]) => { + getTrainPositions(routeNames).then((trains: Train[]) => { filterTrains(trains, vehiclesAge)?.forEach((train) => nextTrainsByRoute[train.route].push(train) ); setTrainsByRoute(nextTrainsByRoute); }); - setIsInitialFetch(false); - }, [routeNames, isInitialFetch, vehiclesAge]); + }, [routeNames, vehiclesAge]); useEffect(() => { const nextStopsByRoute: Record = {}; diff --git a/src/index.tsx b/src/index.tsx index fed9749..1b36c66 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,9 +1,20 @@ import * as React from 'react'; -import * as ReactDOM from 'react-dom'; +import ReactDOM from 'react-dom/client'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import { App } from './components/App'; import './main.css'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: 0, + staleTime: 10000, // 10 seconds + }, + }, +}); const router = createBrowserRouter([ { @@ -13,9 +24,10 @@ const router = createBrowserRouter([ ]); const container = document.getElementById('root'); -ReactDOM.render( +ReactDOM.createRoot(container!).render( - - , - container + + + + ); diff --git a/src/main.css b/src/main.css index ecf622c..7dddec6 100644 --- a/src/main.css +++ b/src/main.css @@ -182,6 +182,10 @@ a { color: white; border-radius: 100px; z-index: 1; + background-color: unset; + border: unset; + font-family: 'Nunito', sans-serif; + padding: 0; } .tab-picker > .tab:not(:last-child) { diff --git a/tsconfig.json b/tsconfig.json index ff8ae55..64eda8f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, - "types": ["vite/client"], + "types": ["vite/client", "node"], /* Bundler mode */ "moduleResolution": "bundler",