diff --git a/README.md b/README.md index 5146238..eaf6110 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ and technical skills along the way. ### Requirements - Framework: React -- State Management: **Recoil** ( Requirement, otherwise I would not use it ) - Show top 100 albums based on the json feed here: https://itunes.apple.com/us/rss/topalbums/limit=100/json - Allow the top 100 to be searchable diff --git a/package-lock.json b/package-lock.json index 183d69b..c4280f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,15 +11,15 @@ "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@mui/material": "^5.11.14", - "@recoiljs/refine": "^0.1.1", + "@tanstack/react-query": "^5.52.2", "axios": "^1.3.4", "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", - "recoil": "^0.7.7", - "recoil-sync": "^0.2.0", + "react-router-dom": "^6.26.1", "styled-components": "^6.1.11", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "zustand": "^4.5.5" }, "devDependencies": { "@testing-library/jest-dom": "^5.16.5", @@ -45,13 +45,13 @@ "typescript": "^4.9.5" }, "engines": { - "node": "16.0.0" + "node": "18.17.0" } }, "node_modules/@adobe/css-tools": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz", - "integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", "dev": true }, "node_modules/@ampproject/remapping": { @@ -3499,10 +3499,13 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "dev": true }, - "node_modules/@recoiljs/refine": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@recoiljs/refine/-/refine-0.1.1.tgz", - "integrity": "sha512-ry02rHswJePYkH1o8K99qL4O6TBntF9/g7W5wXVwaOUrIJEZUGfl/I3+btPXbUgyyEZvNs5xcwvOw13AufmFQw==" + "node_modules/@remix-run/router": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz", + "integrity": "sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg==", + "engines": { + "node": ">=14.0.0" + } }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", @@ -3846,30 +3849,54 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@tanstack/query-core": { + "version": "5.52.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.52.2.tgz", + "integrity": "sha512-9vvbFecK4A0nDnrc/ks41e3UHONF1DAnGz8Tgbxkl59QcvKWmc0ewhYuIKRh8NC4ja5LTHT9EH16KHbn2AIYWA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.52.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.52.2.tgz", + "integrity": "sha512-d4OwmobpP+6+SvuAxW1RzAY95Pv87Gu+0GjtErzFOUXo+n0FGcwxKvzhswCsXKxsgnAr3bU2eJ2u+GXQAutkCQ==", + "dependencies": { + "@tanstack/query-core": "5.52.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@testing-library/dom": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.2.0.tgz", - "integrity": "sha512-xTEnpUKiV/bMyEsE5bT4oYA0x0Z/colMtxzUY8bKyPXBNLn/e0V4ZjBZkEhms0xE4pv9QsPfSRu9AWS4y5wGvA==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", - "aria-query": "^5.0.0", + "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/@testing-library/jest-dom": { - "version": "5.16.5", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz", - "integrity": "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", + "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==", "dev": true, "dependencies": { "@adobe/css-tools": "^4.0.1", @@ -3920,24 +3947,33 @@ } }, "node_modules/@testing-library/react/node_modules/@testing-library/dom": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz", - "integrity": "sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", + "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", - "aria-query": "^5.0.0", + "aria-query": "5.1.3", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.4.4", + "lz-string": "^1.5.0", "pretty-format": "^27.0.2" }, "engines": { "node": ">=12" } }, + "node_modules/@testing-library/react/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, "node_modules/@testing-library/user-event": { "version": "13.5.0", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", @@ -4406,9 +4442,9 @@ "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" }, "node_modules/@types/testing-library__jest-dom": { - "version": "5.14.5", - "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz", - "integrity": "sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==", + "version": "5.14.9", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", + "integrity": "sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==", "dev": true, "dependencies": { "@types/jest": "*" @@ -5210,12 +5246,12 @@ "dev": true }, "node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "dependencies": { - "deep-equal": "^2.0.5" + "dequal": "^2.0.3" } }, "node_modules/array-buffer-byte-length": { @@ -7530,6 +7566,15 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -9876,11 +9921,6 @@ "node": ">=4" } }, - "node_modules/hamt_plus": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", - "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==" - }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -10413,7 +10453,7 @@ "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "dev": true, + "devOptional": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -15892,6 +15932,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.1.tgz", + "integrity": "sha512-kIwJveZNwp7teQRI5QmwWo39A5bXRyqpH0COKKmPnyD2vBvDwgFXSqDUYtt1h+FEyfnE8eXr7oe0MxRzVwCcvQ==", + "dependencies": { + "@remix-run/router": "1.19.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.1.tgz", + "integrity": "sha512-veut7m41S1fLql4pLhxeSW3jlqs+4MtjRLj0xvuCEXsxusJCbs6I8yn9BxzzDX2XDgafrccY6hwjmd/bL54tFw==", + "dependencies": { + "@remix-run/router": "1.19.1", + "react-router": "6.26.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -16193,37 +16263,6 @@ "node": ">=8.10.0" } }, - "node_modules/recoil": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", - "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==", - "dependencies": { - "hamt_plus": "1.0.2" - }, - "peerDependencies": { - "react": ">=16.13.1" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/recoil-sync": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/recoil-sync/-/recoil-sync-0.2.0.tgz", - "integrity": "sha512-ZYZM1C4LAhGr3EeMMI5MwT4eaEqsr+ddjB4EwdgN8HXXLmE7P5FVCdFHV3HJtMzxR3Y8sOmJDfN1IPrezwKoRg==", - "dependencies": { - "@recoiljs/refine": "^0.1.1", - "transit-js": "^0.8.874" - }, - "peerDependencies": { - "recoil": ">=0.7.3" - } - }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -18503,14 +18542,6 @@ "node": ">= 4.0.0" } }, - "node_modules/transit-js": { - "version": "0.8.874", - "resolved": "https://registry.npmjs.org/transit-js/-/transit-js-0.8.874.tgz", - "integrity": "sha512-IDJJGKRzUbJHmN0P15HBBa05nbKor3r2MmG6aSt0UxXIlJZZKcddTk67/U7WyAeW9Hv/VYI02IqLzolsC4sbPA==", - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/traverse-chain": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", @@ -18831,6 +18862,14 @@ "requires-port": "^1.0.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/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -19992,13 +20031,40 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.5.tgz", + "integrity": "sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q==", + "dependencies": { + "use-sync-external-store": "1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } }, "dependencies": { "@adobe/css-tools": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz", - "integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", "dev": true }, "@ampproject/remapping": { @@ -22355,10 +22421,10 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "dev": true }, - "@recoiljs/refine": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@recoiljs/refine/-/refine-0.1.1.tgz", - "integrity": "sha512-ry02rHswJePYkH1o8K99qL4O6TBntF9/g7W5wXVwaOUrIJEZUGfl/I3+btPXbUgyyEZvNs5xcwvOw13AufmFQw==" + "@remix-run/router": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz", + "integrity": "sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg==" }, "@rollup/plugin-babel": { "version": "5.3.1", @@ -22578,17 +22644,30 @@ "loader-utils": "^2.0.0" } }, + "@tanstack/query-core": { + "version": "5.52.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.52.2.tgz", + "integrity": "sha512-9vvbFecK4A0nDnrc/ks41e3UHONF1DAnGz8Tgbxkl59QcvKWmc0ewhYuIKRh8NC4ja5LTHT9EH16KHbn2AIYWA==" + }, + "@tanstack/react-query": { + "version": "5.52.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.52.2.tgz", + "integrity": "sha512-d4OwmobpP+6+SvuAxW1RzAY95Pv87Gu+0GjtErzFOUXo+n0FGcwxKvzhswCsXKxsgnAr3bU2eJ2u+GXQAutkCQ==", + "requires": { + "@tanstack/query-core": "5.52.2" + } + }, "@testing-library/dom": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.2.0.tgz", - "integrity": "sha512-xTEnpUKiV/bMyEsE5bT4oYA0x0Z/colMtxzUY8bKyPXBNLn/e0V4ZjBZkEhms0xE4pv9QsPfSRu9AWS4y5wGvA==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, "peer": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", - "aria-query": "^5.0.0", + "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", @@ -22596,9 +22675,9 @@ } }, "@testing-library/jest-dom": { - "version": "5.16.5", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz", - "integrity": "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", + "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==", "dev": true, "requires": { "@adobe/css-tools": "^4.0.1", @@ -22636,20 +22715,29 @@ }, "dependencies": { "@testing-library/dom": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz", - "integrity": "sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", + "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", - "aria-query": "^5.0.0", + "aria-query": "5.1.3", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.4.4", + "lz-string": "^1.5.0", "pretty-format": "^27.0.2" } + }, + "aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "requires": { + "deep-equal": "^2.0.5" + } } } }, @@ -23095,9 +23183,9 @@ "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" }, "@types/testing-library__jest-dom": { - "version": "5.14.5", - "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz", - "integrity": "sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==", + "version": "5.14.9", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", + "integrity": "sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==", "dev": true, "requires": { "@types/jest": "*" @@ -23703,12 +23791,12 @@ "dev": true }, "aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "requires": { - "deep-equal": "^2.0.5" + "dequal": "^2.0.3" } }, "array-buffer-byte-length": { @@ -25434,6 +25522,12 @@ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true }, + "dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true + }, "destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -27221,11 +27315,6 @@ "pify": "^3.0.0" } }, - "hamt_plus": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", - "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==" - }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -27609,7 +27698,7 @@ "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "dev": true + "devOptional": true }, "immutable": { "version": "4.3.0", @@ -31502,6 +31591,23 @@ "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "dev": true }, + "react-router": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.1.tgz", + "integrity": "sha512-kIwJveZNwp7teQRI5QmwWo39A5bXRyqpH0COKKmPnyD2vBvDwgFXSqDUYtt1h+FEyfnE8eXr7oe0MxRzVwCcvQ==", + "requires": { + "@remix-run/router": "1.19.1" + } + }, + "react-router-dom": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.1.tgz", + "integrity": "sha512-veut7m41S1fLql4pLhxeSW3jlqs+4MtjRLj0xvuCEXsxusJCbs6I8yn9BxzzDX2XDgafrccY6hwjmd/bL54tFw==", + "requires": { + "@remix-run/router": "1.19.1", + "react-router": "6.26.1" + } + }, "react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -31744,23 +31850,6 @@ "picomatch": "^2.2.1" } }, - "recoil": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", - "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==", - "requires": { - "hamt_plus": "1.0.2" - } - }, - "recoil-sync": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/recoil-sync/-/recoil-sync-0.2.0.tgz", - "integrity": "sha512-ZYZM1C4LAhGr3EeMMI5MwT4eaEqsr+ddjB4EwdgN8HXXLmE7P5FVCdFHV3HJtMzxR3Y8sOmJDfN1IPrezwKoRg==", - "requires": { - "@recoiljs/refine": "^0.1.1", - "transit-js": "^0.8.874" - } - }, "recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -33506,11 +33595,6 @@ } } }, - "transit-js": { - "version": "0.8.874", - "resolved": "https://registry.npmjs.org/transit-js/-/transit-js-0.8.874.tgz", - "integrity": "sha512-IDJJGKRzUbJHmN0P15HBBa05nbKor3r2MmG6aSt0UxXIlJZZKcddTk67/U7WyAeW9Hv/VYI02IqLzolsC4sbPA==" - }, "traverse-chain": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", @@ -33748,6 +33832,12 @@ "requires-port": "^1.0.0" } }, + "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==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -34666,6 +34756,14 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zustand": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.5.tgz", + "integrity": "sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q==", + "requires": { + "use-sync-external-store": "1.2.2" + } } } } diff --git a/package.json b/package.json index 61ef756..7e0ee1d 100644 --- a/package.json +++ b/package.json @@ -18,15 +18,15 @@ "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@mui/material": "^5.11.14", - "@recoiljs/refine": "^0.1.1", + "@tanstack/react-query": "^5.52.2", "axios": "^1.3.4", "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", - "recoil": "^0.7.7", - "recoil-sync": "^0.2.0", + "react-router-dom": "^6.26.1", "styled-components": "^6.1.11", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "zustand": "^4.5.5" }, "devDependencies": { "@testing-library/jest-dom": "^5.16.5", diff --git a/src/components-connected/album-ratings.tsx b/src/components-connected/album-ratings.tsx index 7324c4e..0623975 100644 --- a/src/components-connected/album-ratings.tsx +++ b/src/components-connected/album-ratings.tsx @@ -1,7 +1,7 @@ import { Rating } from '@mui/material' import { useCallback } from 'react' -import { useAlbumRatings } from 'src/stores/music-store' +import { useAlbumRatings } from 'src/hooks/use-album-ratings' type AlbumRatingsProps = { albumId: string @@ -9,19 +9,19 @@ type AlbumRatingsProps = { export default function AlbumRatings(props: AlbumRatingsProps) { const { albumId } = props - const [ratings, setRatings] = useAlbumRatings() + const rating = useAlbumRatings(state => state.ratings[albumId]) + const updateRating = useAlbumRatings(state => state.updateRating) - const updateRatings = useCallback( + const onRatingChange = useCallback( (e: unknown, value: number | null) => { - const newRatings = { ...ratings, [albumId]: value } - setRatings(newRatings) + updateRating(albumId, value) }, - [ratings] + [updateRating] ) return (
- +
) } diff --git a/src/components-connected/albums-search-field.tsx b/src/components-connected/albums-search-field.tsx index 9b4b33c..59dd468 100644 --- a/src/components-connected/albums-search-field.tsx +++ b/src/components-connected/albums-search-field.tsx @@ -2,7 +2,7 @@ import debounce from 'lodash/debounce' import { useCallback } from 'react' import TextField from 'src/components/text-field' -import { useSearchState } from 'src/stores/general-store' +import { useSearchState } from 'src/hooks/use-search-state' export default function AlbumsSearchField() { const [search, setSearch] = useSearchState() diff --git a/src/components-connected/top-albums.tsx b/src/components-connected/top-albums.tsx index 31b0d97..75730e1 100644 --- a/src/components-connected/top-albums.tsx +++ b/src/components-connected/top-albums.tsx @@ -15,8 +15,8 @@ import { Suspense } from 'react' import ErrorBoundary from 'src/components/error-boundary' import Tooltip from 'src/components/tooltip' import { useMinBreakpoint } from 'src/helpers/rwd-helpers' -import { useSearchState } from 'src/stores/general-store' -import { useTopAlbums } from 'src/stores/music-store' +import { useSearchState } from 'src/hooks/use-search-state' +import { useTopAlbums } from 'src/hooks/use-top-albums' import AlbumRatings from './album-ratings' @@ -39,7 +39,7 @@ export default function TopAlbums() { function TopAlbumsContent() { // TODO I did not have time to make it perfectly responsive, so I hide some elements const isBiggerThanSmDevice = useMinBreakpoint('sm') - const albums = useTopAlbums() + const { data: albums = [] } = useTopAlbums() const [search] = useSearchState() const filteredAlbums = search @@ -123,8 +123,13 @@ function TopAlbumsSkeleton() { secondaryAction={ isBiggerThanSmDevice && ( - {new Array(5).fill(null).map(() => ( - + {new Array(5).fill(null).map((_, index) => ( + ))} ) diff --git a/src/helpers/tests-helpers.tsx b/src/helpers/tests-helpers.tsx index 8dc77bb..b911cf0 100644 --- a/src/helpers/tests-helpers.tsx +++ b/src/helpers/tests-helpers.tsx @@ -1,13 +1,11 @@ import { render } from '@testing-library/react' import HttpRequestMock from 'http-request-mock' -import { ReactElement, ReactNode } from 'react' -import { selector, snapshot_UNSTABLE } from 'recoil' -import StoreProvider from 'src/stores/store-provider' - -export function updateBrowserURL(url: string) { - window.history.replaceState(null, '', url) -} +import { Provider as ReactQueryProvider } from 'src/providers/react-query' +import { + CreateRouterForTestingOptions, + createRouterForTesting +} from 'src/providers/react-router' export function getHttpMocker() { return HttpRequestMock.setup() @@ -15,27 +13,7 @@ export function getHttpMocker() { // Keep this as a helper function, because in larger applications we have many other providers, // and we don't want to duplicate this code in each test -export function renderWithProviders(component: ReactElement) { - return render({component}) -} - -function StoreProviderForTesting(props: { children: ReactNode }) { - // clearSelectorCachesState looks very ugly, but it is only way to clean the recoil global state - // https://recoiljs.org/docs/guides/testing#clearing-all-selector-caches - const clearSelectorCachesState = selector({ - key: 'clearSelectorCaches', - get: ({ getCallback }) => - getCallback(({ snapshot, refresh }) => () => { - const nodes = snapshot.getNodes_UNSTABLE() - // @ts-ignore - for (const node of nodes) { - refresh(node) - } - }) - }) - - const snapshot = snapshot_UNSTABLE() - snapshot.getLoadable(clearSelectorCachesState).getValue()() - - return {props.children} +export function renderWithProviders(opts?: CreateRouterForTestingOptions) { + const router = createRouterForTesting(opts) + return render({router}) } diff --git a/src/hooks/use-album-ratings.ts b/src/hooks/use-album-ratings.ts new file mode 100644 index 0000000..c12b823 --- /dev/null +++ b/src/hooks/use-album-ratings.ts @@ -0,0 +1,35 @@ +import isNull from 'lodash/isNull' +import omitBy from 'lodash/omitBy' +import { create } from 'zustand' + +import { + getFromLocalStorage, + setToLocalStorage +} from 'src/helpers/storage-helpers' + +type RatingValue = number | null +type Ratings = Record + +type AlbumRatingsStore = { + ratings: Record + updateRating: (id: string, value: RatingValue) => void +} + +const initialRatings: Ratings = getFromLocalStorage('albumsRatings') || {} + +export const useAlbumRatings = create((set, get) => ({ + ratings: initialRatings, + updateRating: (id, value) => { + const { ratings } = get() + const newRatings = { ...ratings, [id]: value } + + set({ + ratings: newRatings + }) + + // No need to save unselected ratings in localStorage + // localStorage has 5MB limit, so let's keep it small + const ratingsWithoutNull = omitBy(newRatings, isNull) + setToLocalStorage('albumsRatings', ratingsWithoutNull) + } +})) diff --git a/src/hooks/use-search-state.ts b/src/hooks/use-search-state.ts new file mode 100644 index 0000000..efb409f --- /dev/null +++ b/src/hooks/use-search-state.ts @@ -0,0 +1,22 @@ +import { useCallback } from 'react' +import { useSearchParams } from 'react-router-dom' + +export function useSearchState(): [string, (value: string) => void] { + const [searchParams, setSearchParams] = useSearchParams() + const search = searchParams.get('search') || '' + + const setSearch = useCallback( + (value: string) => { + setSearchParams( + prev => { + prev.set('search', value) + return prev + }, + { replace: true } + ) + }, + [setSearchParams] + ) + + return [search, setSearch] +} diff --git a/src/hooks/use-top-albums.ts b/src/hooks/use-top-albums.ts new file mode 100644 index 0000000..4668ee4 --- /dev/null +++ b/src/hooks/use-top-albums.ts @@ -0,0 +1,13 @@ +import { useSuspenseQuery } from '@tanstack/react-query' + +import { findTopAlbums } from 'src/services/music-service' + +export function useTopAlbums() { + return useSuspenseQuery({ + queryKey: ['top-albums'], + queryFn: async () => { + const response = await findTopAlbums(100) + return response.feed.entry + } + }) +} diff --git a/src/index.tsx b/src/index.tsx index 5b2d44d..cc44e8d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,18 +1,18 @@ import { StrictMode } from 'react' import ReactDOM from 'react-dom/client' -import HomePage from 'src/pages/home-page' +import { Provider as ReactQueryProvider } from 'src/providers/react-query' +import { Provider as ReactRouterProvider } from 'src/providers/react-router' import './index.scss' import reportWebVitals from './report-web-vitals' -import StoreProvider from './stores/store-provider' const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) root.render( - - - + + + ) diff --git a/src/pages/home-page.test.tsx b/src/pages/home-page.test.tsx index dee4d29..4816fb8 100644 --- a/src/pages/home-page.test.tsx +++ b/src/pages/home-page.test.tsx @@ -2,20 +2,14 @@ import { screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { getFromLocalStorage } from 'src/helpers/storage-helpers' -import { - getHttpMocker, - renderWithProviders, - updateBrowserURL -} from 'src/helpers/tests-helpers' +import { getHttpMocker, renderWithProviders } from 'src/helpers/tests-helpers' import { TopAlbumsResponse } from 'src/services/music-service' -import HomePage from './home-page' - describe('HomePage', () => { const httpMocker = getHttpMocker() beforeEach(() => { - updateBrowserURL('/') + localStorage.clear() }) it('displays top albums', async () => { @@ -46,7 +40,9 @@ describe('HomePage', () => { status: 200, body: albumsMock }) - renderWithProviders() + renderWithProviders({ + initialEntries: ['/'] + }) expect(await screen.findByText('Thunderstruck')).toBeVisible() expect(await screen.findByText('Back in Black')).toBeVisible() @@ -80,7 +76,9 @@ describe('HomePage', () => { status: 200, body: albumsMock }) - renderWithProviders() + renderWithProviders({ + initialEntries: ['/'] + }) await waitFor(() => { expect(screen.queryAllByTestId('top-albums-item')).toHaveLength(2) @@ -125,9 +123,9 @@ describe('HomePage', () => { body: albumsMock }) - updateBrowserURL('/?search="hotline"') - - renderWithProviders() + renderWithProviders({ + initialEntries: ['/?search=hotline'] + }) await waitFor(() => { expect(screen.queryAllByTestId('top-albums-item')).toHaveLength(1) @@ -143,7 +141,9 @@ describe('HomePage', () => { status: 500 }) - renderWithProviders() + renderWithProviders({ + initialEntries: ['/'] + }) expect( await screen.findByText('Something went wrong with top albums service :(') @@ -158,7 +158,9 @@ describe('HomePage', () => { status: 200 }) - renderWithProviders() + renderWithProviders({ + initialEntries: ['/'] + }) expect( await screen.findByText( @@ -198,7 +200,9 @@ describe('HomePage', () => { body: albumsMock }) - renderWithProviders() + renderWithProviders({ + initialEntries: ['/'] + }) const firstAlbumRating = await screen.findByTestId( 'AlbumRatings-umbrella-id' diff --git a/src/providers/react-query.tsx b/src/providers/react-query.tsx new file mode 100644 index 0000000..d9a3a95 --- /dev/null +++ b/src/providers/react-query.tsx @@ -0,0 +1,19 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { ReactNode, useMemo } from 'react' + +export function Provider({ children }: { children: ReactNode }) { + const queryClient = useMemo( + () => + new QueryClient({ + defaultOptions: { + queries: { + retry: 0 + } + } + }), + [] + ) + return ( + {children} + ) +} diff --git a/src/providers/react-router.tsx b/src/providers/react-router.tsx new file mode 100644 index 0000000..dc62ef6 --- /dev/null +++ b/src/providers/react-router.tsx @@ -0,0 +1,31 @@ +import type { Router as RemixRouter } from '@remix-run/router/dist/router' +import { + RouteObject, + RouterProvider, + createBrowserRouter, + createMemoryRouter +} from 'react-router-dom' + +import HomePage from 'src/pages/home-page' + +const routes: RouteObject[] = [ + { + path: '/', + element: + } +] + +const routerDefault = createBrowserRouter(routes) + +export function Provider({ router = routerDefault }: { router?: RemixRouter }) { + return +} + +export type CreateRouterForTestingOptions = Parameters< + typeof createMemoryRouter +>[1] + +export function createRouterForTesting(opts?: CreateRouterForTestingOptions) { + const routerForTesting = createMemoryRouter(routes, opts) + return +} diff --git a/src/services/http-service.ts b/src/services/http-service.ts index 44f2eae..91427ba 100644 --- a/src/services/http-service.ts +++ b/src/services/http-service.ts @@ -1,28 +1,7 @@ import axios from 'axios' -// axios-retry was throwing weird errors, then I decided to use own handler -function handleRetry(err: any) { - const { config, message } = err +const httpService = axios.create({ + timeout: 5000 +}) - if (!config || !config.retry) { - return Promise.reject(err) - } - - // retry while Network timeout or Network Error - if (!(message.includes('timeout') || message.includes('Network Error'))) { - return Promise.reject(err) - } - config.retry -= 1 - - const delayRetryRequest = new Promise(resolve => { - setTimeout(() => { - resolve() - }, config.retryDelay || 1000) - }) - - return delayRetryRequest.then(() => axios(config)) -} - -axios.interceptors.response.use(undefined, handleRetry) - -export default axios +export default httpService diff --git a/src/services/music-service.ts b/src/services/music-service.ts index 622e672..53c1a24 100644 --- a/src/services/music-service.ts +++ b/src/services/music-service.ts @@ -2,8 +2,7 @@ import httpService from './http-service' export async function findTopAlbums(limit = 10): Promise { const response = await httpService.get( - `https://itunes.apple.com/us/rss/topalbums/limit=${limit}/json`, - { retry: 3, retryDelay: 500, timeout: 5_000 } + `https://itunes.apple.com/us/rss/topalbums/limit=${limit}/json` ) return response.data } diff --git a/src/stores/general-store.ts b/src/stores/general-store.ts deleted file mode 100644 index 76eadf2..0000000 --- a/src/stores/general-store.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { string } from '@recoiljs/refine' -import { atom, useRecoilState } from 'recoil' -import { syncEffect } from 'recoil-sync' - -const search = atom({ - key: 'search', - default: '', - effects: [syncEffect({ refine: string() })] -}) - -export const useSearchState = () => useRecoilState(search) diff --git a/src/stores/music-store.ts b/src/stores/music-store.ts deleted file mode 100644 index 7ae7dd7..0000000 --- a/src/stores/music-store.ts +++ /dev/null @@ -1,34 +0,0 @@ -import isNull from 'lodash/isNull' -import omitBy from 'lodash/omitBy' -import { atom, selector, useRecoilState, useRecoilValue } from 'recoil' - -import { - getFromLocalStorage, - setToLocalStorage -} from 'src/helpers/storage-helpers' -import { TopAlbumsResponse, findTopAlbums } from 'src/services/music-service' - -const topAlbums = selector({ - key: 'topAlbums', - get: async () => { - const response = await findTopAlbums(100) - return response.feed.entry - } -}) - -const albumsRatings = atom>({ - key: 'albumsRatings', - default: getFromLocalStorage('albumsRatings') || {}, - effects: [ - ({ onSet }) => { - onSet(albumsRatings => { - const albumsWithNonNullValues = omitBy(albumsRatings, isNull) - setToLocalStorage('albumsRatings', albumsWithNonNullValues) - }) - } - ] -}) - -export const useTopAlbums = () => useRecoilValue(topAlbums) - -export const useAlbumRatings = () => useRecoilState(albumsRatings) diff --git a/src/stores/store-provider.tsx b/src/stores/store-provider.tsx deleted file mode 100644 index c6f2d39..0000000 --- a/src/stores/store-provider.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { ReactNode } from 'react' -import { RecoilRoot } from 'recoil' -import { RecoilURLSyncJSON } from 'recoil-sync' - -type StoreProviderProps = { - children: ReactNode -} - -export default function StoreProvider(props: StoreProviderProps) { - return ( - - - {props.children} - - - ) -}