From defb7c3dfd824102792f428902a5817493bf7757 Mon Sep 17 00:00:00 2001 From: Matt Warman Date: Tue, 2 Jul 2024 12:00:42 -0400 Subject: [PATCH] 5 List users (#6) * initial users page and components * #5 home page grid * nested routing * router navigation * route params * routing * tab navigation * AppMenu component * jsdoc * jsdoc * jsdoc * docs * jsdoc * unused scss * jsdom --- index.html | 12 +- package-lock.json | 287 +++++++++++++++++- package.json | 13 +- public/manifest.json | 4 +- src/App.tsx | 36 +-- src/common/components/Block/Block.scss | 14 + src/common/components/Block/Block.tsx | 41 +++ src/common/components/Header/Header.tsx | 46 +++ src/common/components/Menu/AppMenu.scss | 5 + src/common/components/Menu/AppMenu.tsx | 62 ++++ src/common/components/Router/AppRouter.tsx | 29 ++ .../components/Router/TabNavigation.scss} | 0 .../components/Router/TabNavigation.tsx | 62 ++++ src/common/components/types.ts | 23 ++ .../hooks/__tests__/useConfig.test.ts | 2 +- src/{ => common}/hooks/useConfig.ts | 0 src/common/models/user.ts | 36 +++ src/{ => common}/providers/ConfigProvider.tsx | 2 +- .../__tests__/ConfigProvider.test.tsx | 2 +- src/common/utils/constants.ts | 6 + src/common/utils/query-client.ts | 14 + src/components/ExploreContainer.css | 24 -- src/components/ExploreContainer.tsx | 14 - src/pages/Home/HomePage.scss | 3 + src/pages/Home/HomePage.tsx | 70 ++--- .../components/WelcomeBlock/WelcomeBlock.tsx | 40 +++ src/pages/Items/NewItemPage.tsx | 35 --- .../Items/__tests__/NewItemPage.test.tsx | 15 - src/pages/Users/api/useGetUser.ts | 37 +++ src/pages/Users/api/useGetUsers.ts | 27 ++ .../components/UserDetail/UserDetailPage.tsx | 36 +++ .../Users/components/UserList/UserList.tsx | 46 +++ .../components/UserList/UserListItem.scss | 5 + .../components/UserList/UserListItem.tsx | 41 +++ .../components/UserList/UserListPage.scss | 3 + .../components/UserList/UserListPage.tsx | 23 ++ .../UserSummaryCard/UserSummaryCard.scss | 5 + .../UserSummaryCard/UserSummaryCard.tsx | 49 +++ src/test/query-client.ts | 13 + src/test/wrappers/WithAllProviders.tsx | 8 +- tsconfig.json | 8 +- vite.config.ts | 6 +- 42 files changed, 1016 insertions(+), 188 deletions(-) create mode 100644 src/common/components/Block/Block.scss create mode 100644 src/common/components/Block/Block.tsx create mode 100644 src/common/components/Header/Header.tsx create mode 100644 src/common/components/Menu/AppMenu.scss create mode 100644 src/common/components/Menu/AppMenu.tsx create mode 100644 src/common/components/Router/AppRouter.tsx rename src/{pages/Home/HomePage.css => common/components/Router/TabNavigation.scss} (100%) create mode 100644 src/common/components/Router/TabNavigation.tsx create mode 100644 src/common/components/types.ts rename src/{ => common}/hooks/__tests__/useConfig.test.ts (94%) rename src/{ => common}/hooks/useConfig.ts (100%) create mode 100644 src/common/models/user.ts rename src/{ => common}/providers/ConfigProvider.tsx (96%) rename src/{ => common}/providers/__tests__/ConfigProvider.test.tsx (94%) create mode 100644 src/common/utils/constants.ts create mode 100644 src/common/utils/query-client.ts delete mode 100644 src/components/ExploreContainer.css delete mode 100644 src/components/ExploreContainer.tsx create mode 100644 src/pages/Home/HomePage.scss create mode 100644 src/pages/Home/components/WelcomeBlock/WelcomeBlock.tsx delete mode 100644 src/pages/Items/NewItemPage.tsx delete mode 100644 src/pages/Items/__tests__/NewItemPage.test.tsx create mode 100644 src/pages/Users/api/useGetUser.ts create mode 100644 src/pages/Users/api/useGetUsers.ts create mode 100644 src/pages/Users/components/UserDetail/UserDetailPage.tsx create mode 100644 src/pages/Users/components/UserList/UserList.tsx create mode 100644 src/pages/Users/components/UserList/UserListItem.scss create mode 100644 src/pages/Users/components/UserList/UserListItem.tsx create mode 100644 src/pages/Users/components/UserList/UserListPage.scss create mode 100644 src/pages/Users/components/UserList/UserListPage.tsx create mode 100644 src/pages/Users/components/UserSummaryCard/UserSummaryCard.scss create mode 100644 src/pages/Users/components/UserSummaryCard/UserSummaryCard.tsx create mode 100644 src/test/query-client.ts diff --git a/index.html b/index.html index 6c233a8..b592a6c 100644 --- a/index.html +++ b/index.html @@ -2,10 +2,10 @@ - Ionic App - + Ionic Playground + - + - + - + - + diff --git a/package-lock.json b/package-lock.json index 88bd0fd..4688e23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,12 @@ "@capacitor/status-bar": "6.0.0", "@ionic/react": "8.2.3", "@ionic/react-router": "8.2.3", + "@tanstack/react-query": "5.49.0", + "@tanstack/react-query-devtools": "5.49.0", "@types/react-router": "5.1.20", "@types/react-router-dom": "5.3.3", + "axios": "1.7.2", + "classnames": "2.5.1", "ionicons": "7.4.0", "react": "18.3.1", "react-dom": "18.3.1", @@ -40,6 +44,7 @@ "eslint": "8.57.0", "eslint-plugin-react": "7.34.3", "jsdom": "24.1.0", + "sass": "1.77.6", "terser": "5.31.1", "typescript": "5.5.2", "vite": "5.3.2", @@ -3159,6 +3164,55 @@ "npm": ">=7.10.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.49.0.tgz", + "integrity": "sha512-xUTjCPHC8G+ZvIUzjoMOLnMpNXYPQI4HjhlizTVVBwtSp24iWo4/kaBzHAzsrCVyfbiaPIFFkUvicsY4r8kF8A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.49.0.tgz", + "integrity": "sha512-Mzv87fXWSdqTo4TDACnrZpYzSGYZYJLWcHV6t/XKId31wyFbWwCT/lJwEfp333Nq2xt2ffvBTKFUjcFIp0dw7Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.49.0.tgz", + "integrity": "sha512-3A0BDwGVk6UzWFdz+WTC9HHts9kI42XYLK78/DGmoj9fd6W/NsjEjI5S4lPPebgq9cWWPo9QNciaSWfH71RgNg==", + "dependencies": { + "@tanstack/query-core": "5.49.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.49.0.tgz", + "integrity": "sha512-8zrzL9xW3I68c4A4FKXEXkfN5lUzW9ypWFikmr0qkzI/KRc+lalGveGpxiAjwWNsvkWwOyQLrlGVlNtCmGOKGA==", + "dependencies": { + "@tanstack/query-devtools": "5.49.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.49.0", + "react": "^18 || ^19" + } + }, "node_modules/@testing-library/dom": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.2.0.tgz", @@ -3797,6 +3851,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -4019,8 +4086,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -4061,6 +4127,34 @@ "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==", "dev": true }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", @@ -4144,6 +4238,18 @@ "node": ">=0.6" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/blob-util": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", @@ -4178,6 +4284,18 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.23.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", @@ -4394,6 +4512,42 @@ "node": ">= 0.8.0" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -4418,6 +4572,11 @@ "node": ">=8" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -4498,7 +4657,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -4869,7 +5027,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -5591,6 +5748,18 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5627,6 +5796,25 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -6159,6 +6347,12 @@ "node": ">= 4" } }, + "node_modules/immutable": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -6284,6 +6478,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -6481,6 +6687,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-number-object": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", @@ -7228,7 +7443,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -7237,7 +7451,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -7418,6 +7631,15 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -7781,6 +8003,18 @@ "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -8115,6 +8349,18 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -8468,6 +8714,23 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/sass": { + "version": "1.77.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", + "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/sax": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", @@ -9025,6 +9288,18 @@ "node": ">=4" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", diff --git a/package.json b/package.json index 1fc5a41..63d80d3 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,10 @@ { "name": "ionic8-playground", + "description": "A playground application for Ionic 8 with React.", + "repository": { + "type": "git", + "url": "https://github.com/mwarman/ionic8-playground" + }, "license": "MIT", "version": "0.1.0", "type": "module", @@ -21,8 +26,12 @@ "@capacitor/status-bar": "6.0.0", "@ionic/react": "8.2.3", "@ionic/react-router": "8.2.3", + "@tanstack/react-query": "5.49.0", + "@tanstack/react-query-devtools": "5.49.0", "@types/react-router": "5.1.20", "@types/react-router-dom": "5.3.3", + "axios": "1.7.2", + "classnames": "2.5.1", "ionicons": "7.4.0", "react": "18.3.1", "react-dom": "18.3.1", @@ -45,10 +54,10 @@ "eslint": "8.57.0", "eslint-plugin-react": "7.34.3", "jsdom": "24.1.0", + "sass": "1.77.6", "terser": "5.31.1", "typescript": "5.5.2", "vite": "5.3.2", "vitest": "1.6.0" - }, - "description": "An Ionic project" + } } diff --git a/public/manifest.json b/public/manifest.json index 5808705..7851987 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "Ionic App", - "name": "My Ionic App", + "short_name": "Ionic Playground", + "name": "Ionic Playground", "icons": [ { "src": "assets/icon/favicon.png", diff --git a/src/App.tsx b/src/App.tsx index 5299777..95bfd94 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,10 @@ -import { Redirect, Route } from 'react-router-dom'; -import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react'; -import { IonReactRouter } from '@ionic/react-router'; +import { IonApp, setupIonicReact } from '@ionic/react'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import ConfigContextProvider from './providers/ConfigProvider'; -import HomePage from './pages/Home/HomePage'; -import NewItemPage from './pages/Items/NewItemPage'; +import ConfigContextProvider from './common/providers/ConfigProvider'; +import { queryClient } from 'common/utils/query-client'; +import AppRouter from 'common/components/Router/AppRouter'; /* Core CSS required for Ionic components to work properly */ import '@ionic/react/css/core.css'; @@ -38,22 +38,18 @@ import './theme/variables.css'; setupIonicReact(); -const App: React.FC = () => ( +/** + * The application root module. The outermost component of the Ionic React + * application hierarchy. Declares application-wide providers. + * @returns JSX + */ +const App = (): JSX.Element => ( - - - - - - - - - - - - - + + + + ); diff --git a/src/common/components/Block/Block.scss b/src/common/components/Block/Block.scss new file mode 100644 index 0000000..fff47a7 --- /dev/null +++ b/src/common/components/Block/Block.scss @@ -0,0 +1,14 @@ +.block { + margin: 0.75rem; + .block-title { + font-size: 1.25rem; + font-weight: 500; + line-height: 1.75rem; + margin-bottom: 0.5rem; + } + .block-content { + color: var(--ion-color-medium); + line-height: 1.5rem; + margin-bottom: 0.5rem; + } +} diff --git a/src/common/components/Block/Block.tsx b/src/common/components/Block/Block.tsx new file mode 100644 index 0000000..efaf833 --- /dev/null +++ b/src/common/components/Block/Block.tsx @@ -0,0 +1,41 @@ +import { ReactNode } from 'react'; +import classNames from 'classnames'; + +import './Block.scss'; +import { BaseComponentProps } from '../types'; + +/** + * Properties for the `Block`component. + * @param {ReactNode} [content] - The content of the block. + * @param {string} [title] - The title of the block. + * @see {@link BaseComponentProps} + */ +interface BlockProps extends BaseComponentProps { + content?: ReactNode; + title?: string; +} + +/** + * The `Block` component renders a section of content. Similar to an `IonCard` + * but does not render the border and shadow. + * @param {BlockProps} props - Component properties. + * @returns JSX + */ +const Block = ({ className, content, testid = 'block', title }: BlockProps): JSX.Element => { + return ( +
+ {title && ( +
+ {title} +
+ )} + {content && ( +
+ {content} +
+ )} +
+ ); +}; + +export default Block; diff --git a/src/common/components/Header/Header.tsx b/src/common/components/Header/Header.tsx new file mode 100644 index 0000000..8a77db9 --- /dev/null +++ b/src/common/components/Header/Header.tsx @@ -0,0 +1,46 @@ +import { + IonBackButton, + IonButtons, + IonHeader, + IonMenuButton, + IonTitle, + IonToolbar, +} from '@ionic/react'; + +/** + * Properties for the `Header` component. + * @param {boolean} [backButton] - Optional. Indicates the back button + * should be rendered. + * @param {string} [defaultHref] - Optional. The default back navigation + * href if there is no history in the route stack. + * @param {string} [title] - Optional. The header title. + */ +interface HeaderProps extends Pick { + backButton?: boolean; + title?: string; +} + +const Header = ({ backButton = false, defaultHref, title }: HeaderProps): JSX.Element => { + return ( + + + {backButton && ( + + + + )} + {title} + + + + + + ); +}; + +export default Header; diff --git a/src/common/components/Menu/AppMenu.scss b/src/common/components/Menu/AppMenu.scss new file mode 100644 index 0000000..02b74c6 --- /dev/null +++ b/src/common/components/Menu/AppMenu.scss @@ -0,0 +1,5 @@ +.menu-app { + .icon { + margin-right: 1rem; + } +} diff --git a/src/common/components/Menu/AppMenu.tsx b/src/common/components/Menu/AppMenu.tsx new file mode 100644 index 0000000..77dff2a --- /dev/null +++ b/src/common/components/Menu/AppMenu.tsx @@ -0,0 +1,62 @@ +import { + IonContent, + IonHeader, + IonIcon, + IonItem, + IonLabel, + IonMenu, + IonMenuToggle, + IonTitle, + IonToolbar, +} from '@ionic/react'; +import { home, people } from 'ionicons/icons'; +import classNames from 'classnames'; + +import './AppMenu.scss'; +import { BaseComponentProps } from '../types'; + +/** + * Properties for the `AppMenu` component. + * @see {@link BaseComponentProps} + */ +interface AppMenuProps extends BaseComponentProps {} + +/** + * The `AppMenu` component renders the main application menu. Facilitates + * navigation throughout the major sections of the application. + * @param {AppMenuProps} props - Component properties. + * @returns JSX + */ +const AppMenu = ({ className, testid = 'menu-app' }: AppMenuProps): JSX.Element => { + return ( + + + + Menu + + + + + + + Home + + + + + + Users + + + + + ); +}; + +export default AppMenu; diff --git a/src/common/components/Router/AppRouter.tsx b/src/common/components/Router/AppRouter.tsx new file mode 100644 index 0000000..07f38c1 --- /dev/null +++ b/src/common/components/Router/AppRouter.tsx @@ -0,0 +1,29 @@ +import { IonRouterOutlet } from '@ionic/react'; +import { IonReactRouter } from '@ionic/react-router'; +import { Redirect, Route } from 'react-router'; + +import TabNavigation from './TabNavigation'; + +/** + * The application router. This is the main router for the Ionic React + * application. + * + * This application uses Ionic tab navigation, therefore, the main + * router redirect users to the `TabNavigation` component. + * @returns JSX + * @see {@link TabNavigation} + */ +const AppRouter = (): JSX.Element => { + return ( + + + } /> + + + + + + ); +}; + +export default AppRouter; diff --git a/src/pages/Home/HomePage.css b/src/common/components/Router/TabNavigation.scss similarity index 100% rename from src/pages/Home/HomePage.css rename to src/common/components/Router/TabNavigation.scss diff --git a/src/common/components/Router/TabNavigation.tsx b/src/common/components/Router/TabNavigation.tsx new file mode 100644 index 0000000..58fea33 --- /dev/null +++ b/src/common/components/Router/TabNavigation.tsx @@ -0,0 +1,62 @@ +import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react'; +import { home, people } from 'ionicons/icons'; +import { Redirect, Route } from 'react-router'; + +import AppMenu from '../Menu/AppMenu'; +import HomePage from 'pages/Home/HomePage'; +import UserDetailPage from 'pages/Users/components/UserDetail/UserDetailPage'; +import UserListPage from 'pages/Users/components/UserList/UserListPage'; + +/** + * The `TabNavigation` component provides a router outlet for all of the + * application routes. The component renders two main application + * navigation controls. + * + * On smaller viewport sizes, Ionic mobile tab navigation is rendered at + * the bottom of the page. + * + * On larger viewport sizes, the Ionic [side] menu is rendered. The menu + * may be toggled using the hamburger (three lines) icon in the top + * toolbar. + * + * @returns JSX + * @see {@link AppMenu} + */ +const TabNavigation = (): JSX.Element => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + Home + + + + Users + + + + + ); +}; + +export default TabNavigation; diff --git a/src/common/components/types.ts b/src/common/components/types.ts new file mode 100644 index 0000000..c491cdf --- /dev/null +++ b/src/common/components/types.ts @@ -0,0 +1,23 @@ +/** + * Component properties with CSS class name(s). + * @param {string} [className] - Optional. CSS class names. + */ +export interface PropsWithClassName { + className?: string; +} + +/** + * Component properties with a test identifier. + * @param {string} [testid] - Optional. A testing library identifier. + */ +export interface PropsWithTestId { + testid?: string; +} + +/** + * Utility interface combining the most commonly used React component + * properties interfaces. + * @see {@link PropsWithClassName} + * @see {@link PropsWithTestId} + */ +export interface BaseComponentProps extends PropsWithClassName, PropsWithTestId {} diff --git a/src/hooks/__tests__/useConfig.test.ts b/src/common/hooks/__tests__/useConfig.test.ts similarity index 94% rename from src/hooks/__tests__/useConfig.test.ts rename to src/common/hooks/__tests__/useConfig.test.ts index 652f051..6014ff3 100644 --- a/src/hooks/__tests__/useConfig.test.ts +++ b/src/common/hooks/__tests__/useConfig.test.ts @@ -3,7 +3,7 @@ import { renderHook as renderHookWithoutWrapper } from '@testing-library/react'; import { renderHook, waitFor } from 'test/test-utils'; -import { useConfig } from 'hooks/useConfig'; +import { useConfig } from '../useConfig'; describe('useConfig', () => { it('should return the context', async () => { diff --git a/src/hooks/useConfig.ts b/src/common/hooks/useConfig.ts similarity index 100% rename from src/hooks/useConfig.ts rename to src/common/hooks/useConfig.ts diff --git a/src/common/models/user.ts b/src/common/models/user.ts new file mode 100644 index 0000000..7bfaea8 --- /dev/null +++ b/src/common/models/user.ts @@ -0,0 +1,36 @@ +/** + * The `Address` type. + */ +export type Address = { + street: string; + suite: string; + city: string; + zipcode: string; + geo: { + lat: string; + lng: string; + }; +}; + +/** + * The `Company` type. + */ +export type Company = { + name: string; + catchPhrase: string; + bs: string; +}; + +/** + * The `User` type. + */ +export type User = { + id: number; + name: string; + username: string; + email: string; + phone: string; + website: string; + address: Address; + company: Company; +}; diff --git a/src/providers/ConfigProvider.tsx b/src/common/providers/ConfigProvider.tsx similarity index 96% rename from src/providers/ConfigProvider.tsx rename to src/common/providers/ConfigProvider.tsx index b9fce43..7e5e99e 100644 --- a/src/providers/ConfigProvider.tsx +++ b/src/common/providers/ConfigProvider.tsx @@ -48,7 +48,7 @@ export const ConfigContext = React.createContext(undefined); * @param {PropsWithChildren} props - Component properties, `PropsWithChildren`. * @returns {JSX.Element} JSX */ -const ConfigContextProvider = ({ children }: PropsWithChildren) => { +const ConfigContextProvider = ({ children }: PropsWithChildren): JSX.Element => { const [isReady, setIsReady] = useState(false); const [config, setConfig] = useState(); diff --git a/src/providers/__tests__/ConfigProvider.test.tsx b/src/common/providers/__tests__/ConfigProvider.test.tsx similarity index 94% rename from src/providers/__tests__/ConfigProvider.test.tsx rename to src/common/providers/__tests__/ConfigProvider.test.tsx index 0c9bdfe..c993022 100644 --- a/src/providers/__tests__/ConfigProvider.test.tsx +++ b/src/common/providers/__tests__/ConfigProvider.test.tsx @@ -2,7 +2,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { render, screen } from 'test/test-utils'; -import ConfigContextProvider from 'providers/ConfigProvider'; +import ConfigContextProvider from '../ConfigProvider'; describe('ConfigProvider', () => { it('should render successfully', async () => { diff --git a/src/common/utils/constants.ts b/src/common/utils/constants.ts new file mode 100644 index 0000000..10a4ad1 --- /dev/null +++ b/src/common/utils/constants.ts @@ -0,0 +1,6 @@ +/** + * React Query cache keys. + */ +export enum QueryKey { + Users = 'Users', +} diff --git a/src/common/utils/query-client.ts b/src/common/utils/query-client.ts new file mode 100644 index 0000000..75f5a68 --- /dev/null +++ b/src/common/utils/query-client.ts @@ -0,0 +1,14 @@ +import { QueryClient } from '@tanstack/react-query'; + +/** + * React Query `QueryClient` and configuration. + * @see {@link https://tanstack.com/query/latest/docs/react/guides/important-defaults | Important Defaults} + * @see {@link https://tanstack.com/query/latest/docs/react/reference/QueryClient | QueryClient} + */ +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, // 5 minutes + }, + }, +}); diff --git a/src/components/ExploreContainer.css b/src/components/ExploreContainer.css deleted file mode 100644 index c2c47cf..0000000 --- a/src/components/ExploreContainer.css +++ /dev/null @@ -1,24 +0,0 @@ -#container { - text-align: center; - position: absolute; - left: 0; - right: 0; - top: 50%; - transform: translateY(-50%); -} - -#container strong { - font-size: 20px; - line-height: 26px; -} - -#container p { - font-size: 16px; - line-height: 22px; - color: #8c8c8c; - margin: 0; -} - -#container a { - text-decoration: none; -} \ No newline at end of file diff --git a/src/components/ExploreContainer.tsx b/src/components/ExploreContainer.tsx deleted file mode 100644 index 1b68a3c..0000000 --- a/src/components/ExploreContainer.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import './ExploreContainer.css'; - -interface ContainerProps { } - -const ExploreContainer: React.FC = () => { - return ( -
- Ready to create an app? -

Start with Ionic UI Components

-
- ); -}; - -export default ExploreContainer; diff --git a/src/pages/Home/HomePage.scss b/src/pages/Home/HomePage.scss new file mode 100644 index 0000000..122954b --- /dev/null +++ b/src/pages/Home/HomePage.scss @@ -0,0 +1,3 @@ +ion-grid { + --ion-grid-columns: 2; +} diff --git a/src/pages/Home/HomePage.tsx b/src/pages/Home/HomePage.tsx index 81ca13e..a8c5b8a 100644 --- a/src/pages/Home/HomePage.tsx +++ b/src/pages/Home/HomePage.tsx @@ -1,57 +1,31 @@ -import { - IonBadge, - IonCheckbox, - IonContent, - IonFab, - IonFabButton, - IonHeader, - IonIcon, - IonItem, - IonList, - IonNote, - IonPage, - IonTitle, - IonToolbar, -} from '@ionic/react'; -import { add } from 'ionicons/icons'; -import { useHistory } from 'react-router'; +import { IonCol, IonContent, IonGrid, IonPage, IonRow } from '@ionic/react'; -import './HomePage.css'; +import './HomePage.scss'; +import Header from 'common/components/Header/Header'; +import UserSummaryCard from 'pages/Users/components/UserSummaryCard/UserSummaryCard'; +import WelcomeBlock from './components/WelcomeBlock/WelcomeBlock'; +/** + * The `HomePage` component renders the layout for the home page. It displays + * blocks and cards containing information in a responsive grid. + * @returns JSX + */ const HomePage = (): JSX.Element => { - const history = useHistory(); - return ( - - - Home - - - - - - Home - - +
- - - -

Create Idea

- Run Idea by Joe -
- - 5 Days - -
-
- - - history.push('/new')}> - - - + + + + + + + + + + + ); diff --git a/src/pages/Home/components/WelcomeBlock/WelcomeBlock.tsx b/src/pages/Home/components/WelcomeBlock/WelcomeBlock.tsx new file mode 100644 index 0000000..51ce588 --- /dev/null +++ b/src/pages/Home/components/WelcomeBlock/WelcomeBlock.tsx @@ -0,0 +1,40 @@ +import Block from 'common/components/Block/Block'; +import { BaseComponentProps } from 'common/components/types'; + +/** + * Properties for the `WelcomeBlock` component. + * @see {@link BaseComponentProps} + */ +interface WelcomeBlockProps extends BaseComponentProps {} + +/** + * The `WelcomeBlock` component renders a `Block` of information about the + * application. + * @param {WelcomeBlockProps} props - Component properties. + * @returns JSX + */ +const WelcomeBlock = ({ className, testid = 'block-welcome' }: WelcomeBlockProps): JSX.Element => { + return ( + + Welcome to the Ionic playground project. This project demonstrates how to create a + cross-platform application using the{' '} + + Ionic + {' '} + framework and{' '} + + React + {' '} + components. + + } + /> + ); +}; + +export default WelcomeBlock; diff --git a/src/pages/Items/NewItemPage.tsx b/src/pages/Items/NewItemPage.tsx deleted file mode 100644 index 5861329..0000000 --- a/src/pages/Items/NewItemPage.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { - IonBackButton, - IonBadge, - IonButtons, - IonContent, - IonFooter, - IonHeader, - IonPage, - IonTitle, - IonToolbar, -} from '@ionic/react'; -import { useConfig } from '../../hooks/useConfig'; - -const NewItemPage = (): JSX.Element => { - const config = useConfig(); - - return ( - - - - - - - New Item - - - - - {config.VITE_BUILD_ENV_CODE} - - - ); -}; - -export default NewItemPage; diff --git a/src/pages/Items/__tests__/NewItemPage.test.tsx b/src/pages/Items/__tests__/NewItemPage.test.tsx deleted file mode 100644 index d7a656c..0000000 --- a/src/pages/Items/__tests__/NewItemPage.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { render, screen } from 'test/test-utils'; -import NewItemPage from '../NewItemPage'; - -describe('NewItemPage', () => { - it('should render successfully', async () => { - // ARRANGE - render(); - await screen.findByTestId('page-item-new'); - - // ASSERT - expect(screen.getByTestId('page-item-new')).toBeDefined(); - }); -}); diff --git a/src/pages/Users/api/useGetUser.ts b/src/pages/Users/api/useGetUser.ts new file mode 100644 index 0000000..07c4c38 --- /dev/null +++ b/src/pages/Users/api/useGetUser.ts @@ -0,0 +1,37 @@ +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; + +import { useConfig } from 'common/hooks/useConfig'; +import { User } from 'common/models/user'; +import { QueryKey } from 'common/utils/constants'; + +/** + * Properties for the `useGetUser` API hook. + * @param {string} userId - A `User` identifier. + */ +interface UseGetUserProps { + userId: string; +} + +/** + * An API hook which fetches a single `User` object. + * @param {UseGetUserProps} props - Hook properties. + * @returns Returns a `UseQueryResult` with `User` data. + */ +export const useGetUser = ({ userId }: UseGetUserProps) => { + const config = useConfig(); + + const getUser = async (): Promise => { + const response = await axios.request({ + url: `${config.VITE_BASE_URL_API}/users/${userId}`, + }); + + return response.data; + }; + + return useQuery({ + queryKey: [QueryKey.Users, userId], + queryFn: () => getUser(), + enabled: !!userId, + }); +}; diff --git a/src/pages/Users/api/useGetUsers.ts b/src/pages/Users/api/useGetUsers.ts new file mode 100644 index 0000000..8248cdc --- /dev/null +++ b/src/pages/Users/api/useGetUsers.ts @@ -0,0 +1,27 @@ +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; + +import { useConfig } from 'common/hooks/useConfig'; +import { User } from 'common/models/user'; +import { QueryKey } from 'common/utils/constants'; + +/** + * An API hook which fetches a collection of `User` objects. + * @returns Returns a `UseQueryResult` with `User` collection data. + */ +export const useGetUsers = () => { + const config = useConfig(); + + const getUsers = async (): Promise => { + const response = await axios.request({ + url: `${config.VITE_BASE_URL_API}/users`, + }); + + return response.data; + }; + + return useQuery({ + queryKey: [QueryKey.Users], + queryFn: () => getUsers(), + }); +}; diff --git a/src/pages/Users/components/UserDetail/UserDetailPage.tsx b/src/pages/Users/components/UserDetail/UserDetailPage.tsx new file mode 100644 index 0000000..9b93715 --- /dev/null +++ b/src/pages/Users/components/UserDetail/UserDetailPage.tsx @@ -0,0 +1,36 @@ +import { IonContent, IonPage } from '@ionic/react'; +import { useParams } from 'react-router'; + +import Header from 'common/components/Header/Header'; +import { useGetUser } from 'pages/Users/api/useGetUser'; + +/** + * Router path parameters for the `UserDetailPage`. + * @param {string} userId - A user identifier. + */ +interface UserDetailPageRouteParams { + userId: string; +} + +/** + * The `UserDetailPage` component renders information about a single `User`. + * @returns JSX + */ +export const UserDetailPage = (): JSX.Element => { + const { userId } = useParams(); + const { data: user } = useGetUser({ userId }); + + const headerTitle: string = user ? user.name : ''; + + return ( + +
+ + + {user &&

{user.name}

} +
+ + ); +}; + +export default UserDetailPage; diff --git a/src/pages/Users/components/UserList/UserList.tsx b/src/pages/Users/components/UserList/UserList.tsx new file mode 100644 index 0000000..465a83e --- /dev/null +++ b/src/pages/Users/components/UserList/UserList.tsx @@ -0,0 +1,46 @@ +import { IonList, IonListHeader } from '@ionic/react'; + +import { useGetUsers } from 'pages/Users/api/useGetUsers'; +import UserListItem from './UserListItem'; +import { BaseComponentProps } from 'common/components/types'; + +/** + * Properties for the `UserList` component. + * @param {string} [header] - Optional. The list header title. Default: `Users`. + * @param {boolean} [showHeader] - Optional. Indicates if the header is shown. Default: `false`. + */ +interface UserListProps extends BaseComponentProps { + header?: string; + showHeader?: boolean; +} + +/** + * The `UserList` component renders a list of `User` objects. Uses the `IonList` + * component to provide base functionality. + * @param {UserListProps} props - Component properties. + * @returns JSX + */ +const UserList = ({ + className, + header = 'Users', + showHeader = false, + testid = 'list-user', +}: UserListProps): JSX.Element => { + const { data: users } = useGetUsers(); + + return ( + + {showHeader && {header}} + {users && + users.map((user, index) => ( + + ))} + + ); +}; + +export default UserList; diff --git a/src/pages/Users/components/UserList/UserListItem.scss b/src/pages/Users/components/UserList/UserListItem.scss new file mode 100644 index 0000000..ea1cbdf --- /dev/null +++ b/src/pages/Users/components/UserList/UserListItem.scss @@ -0,0 +1,5 @@ +.list-item-user { + .name { + margin-bottom: 0.25rem; + } +} diff --git a/src/pages/Users/components/UserList/UserListItem.tsx b/src/pages/Users/components/UserList/UserListItem.tsx new file mode 100644 index 0000000..e2ec3d6 --- /dev/null +++ b/src/pages/Users/components/UserList/UserListItem.tsx @@ -0,0 +1,41 @@ +import { IonItem, IonLabel, IonNote } from '@ionic/react'; + +import './UserListItem.scss'; +import { User } from 'common/models/user'; + +/** + * Properties for the `UserListItem` component. + * @param {string} [lines] - See `lines` from `IonItem`. + * @param {User} user - A `User` object. + * @see {@link HTMLIonItemElement} + */ +interface UserListItemProps extends Pick { + user: User; +} + +/** + * The `UserListItem` component renders a single item in the list of + * `User` objects. Uses `IonItem` to provide base functionality. + * + * When clicked, navigates to the user detail page. + * @param {UserListItemProps} props - Component properties. + * @returns JSX + */ +const UserListItem = ({ lines, user }: UserListItemProps): JSX.Element => { + return ( + + +
{user.name}
+ {user.email} +
+
+ ); +}; + +export default UserListItem; diff --git a/src/pages/Users/components/UserList/UserListPage.scss b/src/pages/Users/components/UserList/UserListPage.scss new file mode 100644 index 0000000..04d2f0e --- /dev/null +++ b/src/pages/Users/components/UserList/UserListPage.scss @@ -0,0 +1,3 @@ +.user-list { + margin: 1rem 0; +} diff --git a/src/pages/Users/components/UserList/UserListPage.tsx b/src/pages/Users/components/UserList/UserListPage.tsx new file mode 100644 index 0000000..16b8aca --- /dev/null +++ b/src/pages/Users/components/UserList/UserListPage.tsx @@ -0,0 +1,23 @@ +import { IonContent, IonPage } from '@ionic/react'; + +import './UserListPage.scss'; +import Header from 'common/components/Header/Header'; +import UserList from './UserList'; + +/** + * The `UserListPage` component renders a list of all `User` objects. + * @returns JSX + */ +export const UserListPage = (): JSX.Element => { + return ( + +
+ + + + + + ); +}; + +export default UserListPage; diff --git a/src/pages/Users/components/UserSummaryCard/UserSummaryCard.scss b/src/pages/Users/components/UserSummaryCard/UserSummaryCard.scss new file mode 100644 index 0000000..4ebc984 --- /dev/null +++ b/src/pages/Users/components/UserSummaryCard/UserSummaryCard.scss @@ -0,0 +1,5 @@ +.card-user-summary { + .badge { + margin-left: 0.5rem; + } +} diff --git a/src/pages/Users/components/UserSummaryCard/UserSummaryCard.tsx b/src/pages/Users/components/UserSummaryCard/UserSummaryCard.tsx new file mode 100644 index 0000000..8f8a87e --- /dev/null +++ b/src/pages/Users/components/UserSummaryCard/UserSummaryCard.tsx @@ -0,0 +1,49 @@ +import { + IonBadge, + IonCard, + IonCardContent, + IonCardHeader, + IonCardSubtitle, + IonCardTitle, +} from '@ionic/react'; + +import './UserSummaryCard.scss'; +import { useGetUsers } from 'pages/Users/api/useGetUsers'; + +/** + * Properties for the `UserSummaryCard` component. + */ +interface UserSummaryCardProps {} + +/** + * The `UserSummaryCard` component renders an `IonCard` containing summary + * information about the `User` data. Facilitates navigation to the user + * list page. + * @param {UserSummaryCardProps} props - Component properties. + * @returns JSX + */ +const UserSummaryCard = ({}: UserSummaryCardProps): JSX.Element => { + const { data: users } = useGetUsers(); + + return ( + + + + Users + {users && {users.length}} + + Tap to view all users. + + + Browse and search all the users. View user profiles and read their posts. + + + ); +}; + +export default UserSummaryCard; diff --git a/src/test/query-client.ts b/src/test/query-client.ts new file mode 100644 index 0000000..80df0a1 --- /dev/null +++ b/src/test/query-client.ts @@ -0,0 +1,13 @@ +import { QueryClient } from '@tanstack/react-query'; + +/** + * React Query `QueryClient` with configuration optimized for test suite + * execution. + */ +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); diff --git a/src/test/wrappers/WithAllProviders.tsx b/src/test/wrappers/WithAllProviders.tsx index 891a7a0..055efd1 100644 --- a/src/test/wrappers/WithAllProviders.tsx +++ b/src/test/wrappers/WithAllProviders.tsx @@ -1,12 +1,16 @@ import { PropsWithChildren } from 'react'; import { MemoryRouter } from 'react-router'; -import ConfigContextProvider from 'providers/ConfigProvider'; +import { queryClient } from 'test/query-client'; +import ConfigContextProvider from 'common/providers/ConfigProvider'; +import { QueryClientProvider } from '@tanstack/react-query'; const WithAllProviders = ({ children }: PropsWithChildren): JSX.Element => { return ( - {children} + + {children} + ); }; diff --git a/tsconfig.json b/tsconfig.json index 60a1865..8dbcb5f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,14 +19,10 @@ /* Absolute imports */ "paths": { "__fixtures__/*": ["./src/__fixtures__/*"], - "api/*": ["./src/api/*"], "assets/*": ["./src/assets/*"], - "components/*": ["./src/components/*"], - "hooks/*": ["./src/hooks/*"], + "common/*": ["./src/common/*"], "pages/*": ["./src/pages/*"], - "providers/*": ["./src/providers/*"], - "test/*": ["./src/test/*"], - "utils/*": ["./src/utils/*"] + "test/*": ["./src/test/*"] } }, "include": ["src"], diff --git a/vite.config.ts b/vite.config.ts index 174df98..3e1606e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,14 +11,10 @@ export default defineConfig({ resolve: { alias: { __fixtures__: '/src/__fixtures__', - api: '/src/api', assets: '/src/assets', - components: '/src/components', - hooks: '/src/hooks', + common: '/src/common', pages: '/src/pages', - providers: '/src/providers', test: '/src/test', - utils: '/src/utils', }, }, test: {