From ab73f0169251c01242006424bf23c72a218dcc69 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 11:59:02 -0400 Subject: [PATCH 01/53] feat: begin refactoring to TypeScript - install types and necessary dependencies; - add TypeScript configuration; - change extention of files to .tsx; - add User and Item types. --- .eslintrc.json | 9 +- package-lock.json | 336 ++++++++++++++++++ package.json | 4 + src/{App.jsx => App.tsx} | 2 + src/api/{config.js => config.ts} | 14 +- src/api/{firebase.js => firebase.ts} | 0 src/api/{index.js => index.ts} | 0 src/components/{AddItems.jsx => AddItems.tsx} | 3 +- src/components/{ListItem.jsx => ListItem.tsx} | 3 +- ...InputElement.jsx => RadioInputElement.tsx} | 2 + .../{ShareList.jsx => ShareList.tsx} | 2 + .../{SingleList.jsx => SingleList.tsx} | 2 + ...tInputElement.jsx => TextInputElement.tsx} | 2 + src/components/{index.js => index.ts} | 0 src/{index.jsx => index.tsx} | 2 +- src/mocks/__fixtures__/{auth.js => auth.ts} | 0 ...hoppingListData.js => shoppingListData.ts} | 0 .../{shoppingLists.js => shoppingLists.ts} | 0 src/types/types.ts | 13 + src/utils/{dates.js => dates.ts} | 0 src/utils/{hooks.js => hooks.ts} | 0 src/utils/{index.js => index.ts} | 0 src/utils/{normalize.js => normalize.ts} | 0 src/views/{Home.jsx => Home.tsx} | 2 + src/views/{Layout.jsx => Layout.tsx} | 2 + src/views/{List.jsx => List.tsx} | 3 +- src/views/{ManageList.jsx => ManageList.tsx} | 2 + src/views/{index.js => index.ts} | 0 tsconfig.json | 13 + vite.config.js => vite.config.ts | 0 30 files changed, 402 insertions(+), 14 deletions(-) rename src/{App.jsx => App.tsx} (98%) rename src/api/{config.js => config.ts} (52%) rename src/api/{firebase.js => firebase.ts} (100%) rename src/api/{index.js => index.ts} (100%) rename src/components/{AddItems.jsx => AddItems.tsx} (98%) rename src/components/{ListItem.jsx => ListItem.tsx} (97%) rename src/components/{RadioInputElement.jsx => RadioInputElement.tsx} (91%) rename src/components/{ShareList.jsx => ShareList.tsx} (97%) rename src/components/{SingleList.jsx => SingleList.tsx} (89%) rename src/components/{TextInputElement.jsx => TextInputElement.tsx} (92%) rename src/components/{index.js => index.ts} (100%) rename src/{index.jsx => index.tsx} (83%) rename src/mocks/__fixtures__/{auth.js => auth.ts} (100%) rename src/mocks/__fixtures__/{shoppingListData.js => shoppingListData.ts} (100%) rename src/mocks/__fixtures__/{shoppingLists.js => shoppingLists.ts} (100%) create mode 100644 src/types/types.ts rename src/utils/{dates.js => dates.ts} (100%) rename src/utils/{hooks.js => hooks.ts} (100%) rename src/utils/{index.js => index.ts} (100%) rename src/utils/{normalize.js => normalize.ts} (100%) rename src/views/{Home.jsx => Home.tsx} (98%) rename src/views/{Layout.jsx => Layout.tsx} (97%) rename src/views/{List.jsx => List.tsx} (96%) rename src/views/{ManageList.jsx => ManageList.tsx} (88%) rename src/views/{index.js => index.ts} (100%) create mode 100644 tsconfig.json rename vite.config.js => vite.config.ts (100%) diff --git a/.eslintrc.json b/.eslintrc.json index 3192ced..c9f7aef 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,7 @@ { "extends": [ "plugin:react/recommended", - "plugin:react/jsx-runtime", + "plugin:@typescript-eslint/recommended", "plugin:jsx-a11y/recommended", "prettier" ], @@ -10,13 +10,16 @@ }, "parserOptions": { "ecmaVersion": "latest", - "sourceType": "module" + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } }, "settings": { "react": { "version": "detect" } }, "rules": { "no-duplicate-imports": "warn", "no-unused-vars": "warn", - "react/jsx-filename-extension": [1, { "allow": "as-needed" }], + "react/jsx-filename-extension": [1, { "extensions": [".tsx"] }], "react/prop-types": "off", "react/jsx-no-target-blank": "off" } diff --git a/package-lock.json b/package-lock.json index 66d6d8c..9eea59a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,9 @@ "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", + "@types/react": "^18.3.6", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.5.0", "@vitejs/plugin-react": "^4.3.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", @@ -27,6 +30,7 @@ "jsdom": "^24.1.1", "lint-staged": "^15.2.8", "prettier": "^3.3.3", + "typescript": "^5.6.2", "vite": "^5.3.5", "vite-plugin-pwa": "^0.20.1", "vite-plugin-svgr": "^4.2.0", @@ -3983,6 +3987,34 @@ "undici-types": "~6.13.0" } }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.6", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.6.tgz", + "integrity": "sha512-CnGaRYNu2iZlkGXGrOYtdg5mLK8neySj0woZ4e2wF/eli2E6Sazmq5X+Nrj6OBrrFVQfJWTUFeqAzoRhWQXYvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -3997,6 +4029,236 @@ "dev": true, "license": "MIT" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz", + "integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/type-utils": "8.5.0", + "@typescript-eslint/utils": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz", + "integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/typescript-estree": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz", + "integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz", + "integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.5.0", + "@typescript-eslint/utils": "8.5.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz", + "integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz", + "integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz", + "integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/typescript-estree": "8.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz", + "integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.5.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -4927,6 +5189,13 @@ "dev": true, "license": "MIT" }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -5807,6 +6076,36 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -7576,6 +7875,16 @@ "dev": true, "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/micromatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", @@ -9490,6 +9799,19 @@ "node": ">=18" } }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", @@ -9599,6 +9921,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 9044227..40470c6 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", + "@types/react": "^18.3.6", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.5.0", "@vitejs/plugin-react": "^4.3.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", @@ -28,6 +31,7 @@ "jsdom": "^24.1.1", "lint-staged": "^15.2.8", "prettier": "^3.3.3", + "typescript": "^5.6.2", "vite": "^5.3.5", "vite-plugin-pwa": "^0.20.1", "vite-plugin-svgr": "^4.2.0", diff --git a/src/App.jsx b/src/App.tsx similarity index 98% rename from src/App.jsx rename to src/App.tsx index 46879b6..1185f09 100644 --- a/src/App.jsx +++ b/src/App.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { Home, Layout, List, ManageList } from './views'; diff --git a/src/api/config.js b/src/api/config.ts similarity index 52% rename from src/api/config.js rename to src/api/config.ts index 102f148..daabe82 100644 --- a/src/api/config.js +++ b/src/api/config.ts @@ -4,13 +4,13 @@ import { getAuth } from 'firebase/auth'; // Your web app's Firebase configuration const firebaseConfig = { - apiKey: "AIzaSyDmGxC-ITYFOyD6U1IrJAszf1KplXbG1Bs", - authDomain: "tcl-75-smart-shopping-list.firebaseapp.com", - projectId: "tcl-75-smart-shopping-list", - storageBucket: "tcl-75-smart-shopping-list.appspot.com", - messagingSenderId: "1081566633611", - appId: "1:1081566633611:web:b07badada6839daf2e3e8e" - }; + apiKey: 'AIzaSyDmGxC-ITYFOyD6U1IrJAszf1KplXbG1Bs', + authDomain: 'tcl-75-smart-shopping-list.firebaseapp.com', + projectId: 'tcl-75-smart-shopping-list', + storageBucket: 'tcl-75-smart-shopping-list.appspot.com', + messagingSenderId: '1081566633611', + appId: '1:1081566633611:web:b07badada6839daf2e3e8e', +}; // Initialize Firebase const app = initializeApp(firebaseConfig); diff --git a/src/api/firebase.js b/src/api/firebase.ts similarity index 100% rename from src/api/firebase.js rename to src/api/firebase.ts diff --git a/src/api/index.js b/src/api/index.ts similarity index 100% rename from src/api/index.js rename to src/api/index.ts diff --git a/src/components/AddItems.jsx b/src/components/AddItems.tsx similarity index 98% rename from src/components/AddItems.jsx rename to src/components/AddItems.tsx index fdf9656..4602a88 100644 --- a/src/components/AddItems.jsx +++ b/src/components/AddItems.tsx @@ -1,4 +1,5 @@ -import { useCallback } from 'react'; +import React, { useCallback } from 'react'; + import { useStateWithStorage, normalizeItemName } from '../utils'; import { addItem } from '../api'; import TextInputElement from './TextInputElement'; diff --git a/src/components/ListItem.jsx b/src/components/ListItem.tsx similarity index 97% rename from src/components/ListItem.jsx rename to src/components/ListItem.tsx index 76b9e6b..b00439b 100644 --- a/src/components/ListItem.jsx +++ b/src/components/ListItem.tsx @@ -1,4 +1,5 @@ -import { useState } from 'react'; +import React, { useState } from 'react'; + import './ListItem.css'; import { updateItem } from '../api'; import { calculateDateNextPurchased, ONE_DAY_IN_MILLISECONDS } from '../utils'; diff --git a/src/components/RadioInputElement.jsx b/src/components/RadioInputElement.tsx similarity index 91% rename from src/components/RadioInputElement.jsx rename to src/components/RadioInputElement.tsx index f63f342..9000e05 100644 --- a/src/components/RadioInputElement.jsx +++ b/src/components/RadioInputElement.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + const RadioInputElement = ({ label, id, value, required }) => { return ( <> diff --git a/src/components/ShareList.jsx b/src/components/ShareList.tsx similarity index 97% rename from src/components/ShareList.jsx rename to src/components/ShareList.tsx index 3501882..884a40b 100644 --- a/src/components/ShareList.jsx +++ b/src/components/ShareList.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + import { shareList, useAuth } from '../api'; import { useStateWithStorage } from '../utils'; import TextInputElement from './TextInputElement'; diff --git a/src/components/SingleList.jsx b/src/components/SingleList.tsx similarity index 89% rename from src/components/SingleList.jsx rename to src/components/SingleList.tsx index 49d1885..8dbf8ea 100644 --- a/src/components/SingleList.jsx +++ b/src/components/SingleList.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + import './SingleList.css'; export function SingleList({ name, path, setListPath }) { diff --git a/src/components/TextInputElement.jsx b/src/components/TextInputElement.tsx similarity index 92% rename from src/components/TextInputElement.jsx rename to src/components/TextInputElement.tsx index 10e7b62..6b59acd 100644 --- a/src/components/TextInputElement.jsx +++ b/src/components/TextInputElement.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + const TextInputElement = ({ label, type, diff --git a/src/components/index.js b/src/components/index.ts similarity index 100% rename from src/components/index.js rename to src/components/index.ts diff --git a/src/index.jsx b/src/index.tsx similarity index 83% rename from src/index.jsx rename to src/index.tsx index 551896b..6cc7470 100644 --- a/src/index.jsx +++ b/src/index.tsx @@ -1,4 +1,4 @@ -import { StrictMode } from 'react'; +import React, { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { App } from './App'; diff --git a/src/mocks/__fixtures__/auth.js b/src/mocks/__fixtures__/auth.ts similarity index 100% rename from src/mocks/__fixtures__/auth.js rename to src/mocks/__fixtures__/auth.ts diff --git a/src/mocks/__fixtures__/shoppingListData.js b/src/mocks/__fixtures__/shoppingListData.ts similarity index 100% rename from src/mocks/__fixtures__/shoppingListData.js rename to src/mocks/__fixtures__/shoppingListData.ts diff --git a/src/mocks/__fixtures__/shoppingLists.js b/src/mocks/__fixtures__/shoppingLists.ts similarity index 100% rename from src/mocks/__fixtures__/shoppingLists.js rename to src/mocks/__fixtures__/shoppingLists.ts diff --git a/src/types/types.ts b/src/types/types.ts new file mode 100644 index 0000000..6a8ca7f --- /dev/null +++ b/src/types/types.ts @@ -0,0 +1,13 @@ +export type Item = { + dateCreated: Date; + dateLastPurchased: Date | null; + dateNextPurchased: Date; + name: string; + totalPurchases: number; +}; + +export type User = { + email: string; + name: string; + uid: number; +}; diff --git a/src/utils/dates.js b/src/utils/dates.ts similarity index 100% rename from src/utils/dates.js rename to src/utils/dates.ts diff --git a/src/utils/hooks.js b/src/utils/hooks.ts similarity index 100% rename from src/utils/hooks.js rename to src/utils/hooks.ts diff --git a/src/utils/index.js b/src/utils/index.ts similarity index 100% rename from src/utils/index.js rename to src/utils/index.ts diff --git a/src/utils/normalize.js b/src/utils/normalize.ts similarity index 100% rename from src/utils/normalize.js rename to src/utils/normalize.ts diff --git a/src/views/Home.jsx b/src/views/Home.tsx similarity index 98% rename from src/views/Home.jsx rename to src/views/Home.tsx index 9d84c9e..6c3fbf7 100644 --- a/src/views/Home.jsx +++ b/src/views/Home.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + import './Home.css'; import { SingleList } from '../components'; import { useNavigate } from 'react-router-dom'; diff --git a/src/views/Layout.jsx b/src/views/Layout.tsx similarity index 97% rename from src/views/Layout.jsx rename to src/views/Layout.tsx index cd9f9dd..778f6e4 100644 --- a/src/views/Layout.jsx +++ b/src/views/Layout.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + /* eslint-disable jsx-a11y/anchor-is-valid */ import { Outlet, NavLink } from 'react-router-dom'; import { useAuth, SignInButton, SignOutButton } from '../api/useAuth'; diff --git a/src/views/List.jsx b/src/views/List.tsx similarity index 96% rename from src/views/List.jsx rename to src/views/List.tsx index 7ff9f94..45ef9d8 100644 --- a/src/views/List.jsx +++ b/src/views/List.tsx @@ -1,5 +1,6 @@ +import React, { useState } from 'react'; + import { ListItem } from '../components'; -import { useState } from 'react'; import { useStateWithStorage } from '../utils'; import { AddItems } from '../components/AddItems'; import TextInputElement from '../components/TextInputElement'; diff --git a/src/views/ManageList.jsx b/src/views/ManageList.tsx similarity index 88% rename from src/views/ManageList.jsx rename to src/views/ManageList.tsx index bf85abb..3df407b 100644 --- a/src/views/ManageList.jsx +++ b/src/views/ManageList.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + import { AddItems } from '../components/AddItems'; import { ShareList } from '../components/ShareList'; diff --git a/src/views/index.js b/src/views/index.ts similarity index 100% rename from src/views/index.js rename to src/views/index.ts diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c071614 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "esnext", + "moduleResolution": "node", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +} diff --git a/vite.config.js b/vite.config.ts similarity index 100% rename from vite.config.js rename to vite.config.ts From f9b3aaeff0cd2c4bf7cc5ad9129cf8175e27af0d Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 14:33:37 -0400 Subject: [PATCH 02/53] feat: add ListPath type --- .eslintrc.json | 3 ++- src/types/types.ts | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index c9f7aef..1fdbba3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -17,9 +17,10 @@ }, "settings": { "react": { "version": "detect" } }, "rules": { + "react/react-in-jsx-scope": "off", "no-duplicate-imports": "warn", "no-unused-vars": "warn", - "react/jsx-filename-extension": [1, { "extensions": [".tsx"] }], + "react/jsx-filename-extension": [1, { "extensions": [".tsx", ".jsx"] }], "react/prop-types": "off", "react/jsx-no-target-blank": "off" } diff --git a/src/types/types.ts b/src/types/types.ts index 6a8ca7f..9604a0c 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,4 +1,6 @@ export type Item = { + id: string; + listPath: string; dateCreated: Date; dateLastPurchased: Date | null; dateNextPurchased: Date; @@ -8,6 +10,10 @@ export type Item = { export type User = { email: string; - name: string; + displayName: string; uid: number; }; + +export type ListPath = { + [key: string]: string; +}; From cf7ad595f0043feb69d265429358bdb87f07d8dd Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 14:34:24 -0400 Subject: [PATCH 03/53] feat: add types to all firebase functions --- src/api/firebase.ts | 94 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 21 deletions(-) diff --git a/src/api/firebase.ts b/src/api/firebase.ts index 9d96534..deae5b0 100644 --- a/src/api/firebase.ts +++ b/src/api/firebase.ts @@ -7,10 +7,17 @@ import { doc, onSnapshot, updateDoc, + DocumentData, } from 'firebase/firestore'; import { useEffect, useState } from 'react'; import { db } from './config'; import { addDaysFromToday } from '../utils'; +import { ListPath, User } from '../types/types'; + +export type UseShoppingListsProps = { + userId: string | null; + userEmail: string | null; +}; /** * A custom hook that subscribes to the user's shopping lists in our Firestore @@ -19,10 +26,12 @@ import { addDaysFromToday } from '../utils'; * @param {string | null} userEmail * @returns */ -export function useShoppingLists(userId, userEmail) { +export const useShoppingLists = ({ + userId, + userEmail, +}: UseShoppingListsProps): ListPath[] => { // Start with an empty array for our data. - const initialState = []; - const [data, setData] = useState(initialState); + const [data, setData] = useState([]); useEffect(() => { // If we don't have a userId or userEmail (the user isn't signed in), @@ -34,7 +43,7 @@ export function useShoppingLists(userId, userEmail) { onSnapshot(userDocRef, (docSnap) => { if (docSnap.exists()) { - const listRefs = docSnap.data().sharedLists; + const listRefs: ListPath[] = docSnap.data().sharedLists; const newData = listRefs.map((listRef) => { // We keep the list's id and path so we can use them later. return { name: listRef.id, path: listRef.path }; @@ -45,7 +54,7 @@ export function useShoppingLists(userId, userEmail) { }, [userId, userEmail]); return data; -} +}; /** * A custom hook that subscribes to a shopping list in our Firestore database @@ -53,10 +62,10 @@ export function useShoppingLists(userId, userEmail) { * @param {string | null} listPath * @see https://firebase.google.com/docs/firestore/query-data/listen */ -export function useShoppingListData(listPath) { +export const useShoppingListData = (listPath: string): DocumentData[] => { // Start with an empty array for our data. /** @type {import('firebase/firestore').DocumentData[]} */ - const initialState = []; + const initialState: DocumentData[] = []; const [data, setData] = useState(initialState); useEffect(() => { @@ -85,13 +94,13 @@ export function useShoppingListData(listPath) { // Return the data so it can be used by our React components. return data; -} +}; /** * Add a new user to the users collection in Firestore. * @param {Object} user The user object from Firebase Auth. */ -export async function addUserToDatabase(user) { +export const addUserToDatabase = async (user: User): Promise => { // Check if the user already exists in the database. const userDoc = await getDoc(doc(db, 'users', user.email)); // If the user already exists, we don't need to do anything. @@ -108,7 +117,13 @@ export async function addUserToDatabase(user) { uid: user.uid, }); } -} +}; + +export type CreateListProps = { + userId: string; + userEmail: string; + listName: string; +}; /** * Create a new list and add it to a user's lists in Firestore. @@ -116,7 +131,11 @@ export async function addUserToDatabase(user) { * @param {string} userEmail The email of the user who owns the list. * @param {string} listName The name of the new list. */ -export async function createList(userId, userEmail, listName) { +export const createList = async ({ + userId, + userEmail, + listName, +}: CreateListProps): Promise => { const listDocRef = doc(db, userId, listName); await setDoc(listDocRef, { @@ -129,14 +148,24 @@ export async function createList(userId, userEmail, listName) { sharedLists: arrayUnion(listDocRef), }); return listDocRef.path; -} +}; + +export type ShareListProps = { + listPath: string; + currentUserId: string; + recipientEmail: string; +}; /** * Shares a list with another user. * @param {string} listPath The path to the list to share. * @param {string} recipientEmail The email of the user to share the list with. */ -export async function shareList(listPath, currentUserId, recipientEmail) { +export const shareList = async ({ + listPath, + currentUserId, + recipientEmail, +}: ShareListProps): Promise => { // Check if current user is owner. if (!listPath.includes(currentUserId)) { return '!owner'; @@ -159,7 +188,15 @@ export async function shareList(listPath, currentUserId, recipientEmail) { } catch { return; } -} +}; + +export type AddItemProps = { + listPath: string; + itemData: { + itemName: string; + daysUntilNextPurchase: number; + }; +}; /** * Add a new item to the user's list in Firestore. @@ -168,7 +205,10 @@ export async function shareList(listPath, currentUserId, recipientEmail) { * @param {string} itemData.itemName The name of the item. * @param {number} itemData.daysUntilNextPurchase The number of days until the user thinks they'll need to buy the item again. */ -export async function addItem(listPath, { itemName, daysUntilNextPurchase }) { +export const addItem = async ({ + listPath, + itemData: { itemName, daysUntilNextPurchase }, +}: AddItemProps): Promise => { const listCollectionRef = collection(db, listPath, 'items'); return addDoc(listCollectionRef, { dateCreated: new Date(), @@ -177,7 +217,17 @@ export async function addItem(listPath, { itemName, daysUntilNextPurchase }) { name: itemName, totalPurchases: 0, }); -} +}; + +export type UpdateItemProps = { + listPath: string; + itemId: string; + updatedData: { + dateLastPurchased: Date; + dateNextPurchased: Date; + totalPurchases: number; + }; +}; /** * Update an item in the user's list in Firestore with new purchase information. @@ -190,11 +240,11 @@ export async function addItem(listPath, { itemName, daysUntilNextPurchase }) { * @returns {Promise} A message confirming the item was successfully updated. * @throws {Error} If the item update fails. */ -export async function updateItem( +export const updateItem = async ({ listPath, itemId, - { dateLastPurchased, dateNextPurchased, totalPurchases }, -) { + updatedData: { dateLastPurchased, dateNextPurchased, totalPurchases }, +}: UpdateItemProps): Promise => { // reference the item path const itemDocRef = doc(db, listPath, 'items', itemId); // update the item with the purchase date and increment the total purchases made @@ -206,9 +256,11 @@ export async function updateItem( }); return 'item purchased'; } catch (error) { - throw new Error(`Failed updating item: ${error.message}`); + throw new Error( + `Failed updating item: ${error instanceof Error ? error.message : error}`, + ); } -} +}; export async function deleteItem() { /** From def133ade0723e1fadff717a4376f61483d456d7 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 14:38:33 -0400 Subject: [PATCH 04/53] fix: change useAuth.jsx component extention to .tsx --- src/api/{useAuth.jsx => useAuth.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/api/{useAuth.jsx => useAuth.tsx} (100%) diff --git a/src/api/useAuth.jsx b/src/api/useAuth.tsx similarity index 100% rename from src/api/useAuth.jsx rename to src/api/useAuth.tsx From 872d8fc3a419a5b1537d2efb7505c4281955b3f9 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 14:55:12 -0400 Subject: [PATCH 05/53] each User type property can be a string and null;add types to useAuth --- src/api/useAuth.tsx | 4 +++- src/types/types.ts | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/api/useAuth.tsx b/src/api/useAuth.tsx index 8ace92e..ca85c72 100644 --- a/src/api/useAuth.tsx +++ b/src/api/useAuth.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { auth } from './config.js'; import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'; import { addUserToDatabase } from './firebase.js'; +import { User } from '../types/types.js'; /** * A button that signs the user in using Google OAuth. When clicked, @@ -32,7 +33,8 @@ export const SignOutButton = () => ( * @see https://firebase.google.com/docs/auth/web/start#set_an_authentication_state_observer_and_get_user_data */ export const useAuth = () => { - const [user, setUser] = useState(null); + const initialState: User | null = null; + const [user, setUser] = useState(initialState); useEffect(() => { auth.onAuthStateChanged((user) => { diff --git a/src/types/types.ts b/src/types/types.ts index 9604a0c..0b3a903 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -9,9 +9,9 @@ export type Item = { }; export type User = { - email: string; - displayName: string; - uid: number; + email: string | null; + displayName: string | null; + uid: string | null; }; export type ListPath = { From 2082dee1626d1c3b4325c1a53563df4a2dccc4de Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 15:08:35 -0400 Subject: [PATCH 06/53] fix: fix the way useShoppingLists takes props --- src/api/firebase.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/api/firebase.ts b/src/api/firebase.ts index deae5b0..0b1af8b 100644 --- a/src/api/firebase.ts +++ b/src/api/firebase.ts @@ -14,11 +14,6 @@ import { db } from './config'; import { addDaysFromToday } from '../utils'; import { ListPath, User } from '../types/types'; -export type UseShoppingListsProps = { - userId: string | null; - userEmail: string | null; -}; - /** * A custom hook that subscribes to the user's shopping lists in our Firestore * database and returns new data whenever the lists change. @@ -26,10 +21,10 @@ export type UseShoppingListsProps = { * @param {string | null} userEmail * @returns */ -export const useShoppingLists = ({ - userId, - userEmail, -}: UseShoppingListsProps): ListPath[] => { +export const useShoppingLists = ( + userId: string | null, + userEmail: string | null, +): ListPath[] => { // Start with an empty array for our data. const [data, setData] = useState([]); From c2f39881de9a1aaba27435e496eef71830f1a5e2 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 15:12:04 -0400 Subject: [PATCH 07/53] fix: remove User type --- src/types/types.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/types/types.ts b/src/types/types.ts index 0b3a903..0f8ff48 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -8,12 +8,6 @@ export type Item = { totalPurchases: number; }; -export type User = { - email: string | null; - displayName: string | null; - uid: string | null; -}; - export type ListPath = { [key: string]: string; }; From 77945a8e8601f8b11d7acd25db147d762cedc529 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 15:12:42 -0400 Subject: [PATCH 08/53] fix: remove redundant User type; use DocumentData type where needed --- src/api/firebase.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/firebase.ts b/src/api/firebase.ts index 0b1af8b..d9875f8 100644 --- a/src/api/firebase.ts +++ b/src/api/firebase.ts @@ -12,7 +12,7 @@ import { import { useEffect, useState } from 'react'; import { db } from './config'; import { addDaysFromToday } from '../utils'; -import { ListPath, User } from '../types/types'; +import { ListPath } from '../types/types'; /** * A custom hook that subscribes to the user's shopping lists in our Firestore @@ -95,7 +95,7 @@ export const useShoppingListData = (listPath: string): DocumentData[] => { * Add a new user to the users collection in Firestore. * @param {Object} user The user object from Firebase Auth. */ -export const addUserToDatabase = async (user: User): Promise => { +export const addUserToDatabase = async (user: DocumentData): Promise => { // Check if the user already exists in the database. const userDoc = await getDoc(doc(db, 'users', user.email)); // If the user already exists, we don't need to do anything. From 2e72f6a17464a975476feeea0351a6a5e85e09b3 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 15:13:09 -0400 Subject: [PATCH 09/53] add type to ManageList props --- src/views/ManageList.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/views/ManageList.tsx b/src/views/ManageList.tsx index 3df407b..d678549 100644 --- a/src/views/ManageList.tsx +++ b/src/views/ManageList.tsx @@ -1,9 +1,12 @@ -import React from 'react'; - import { AddItems } from '../components/AddItems'; import { ShareList } from '../components/ShareList'; +import { Item } from '../types/types'; + +type ManageListProps = { + items: Item[]; +}; -export function ManageList({ items }) { +export function ManageList({ items }: ManageListProps) { return (
From e48619ddbc2ce375134411e321c0ffb7052d0aae Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 15:40:24 -0400 Subject: [PATCH 10/53] fix: fix type of addItem function parameters --- src/api/firebase.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/api/firebase.ts b/src/api/firebase.ts index d9875f8..0051116 100644 --- a/src/api/firebase.ts +++ b/src/api/firebase.ts @@ -185,12 +185,9 @@ export const shareList = async ({ } }; -export type AddItemProps = { - listPath: string; - itemData: { - itemName: string; - daysUntilNextPurchase: number; - }; +type itemData = { + itemName: string; + daysUntilNextPurchase: string; }; /** @@ -200,15 +197,15 @@ export type AddItemProps = { * @param {string} itemData.itemName The name of the item. * @param {number} itemData.daysUntilNextPurchase The number of days until the user thinks they'll need to buy the item again. */ -export const addItem = async ({ - listPath, - itemData: { itemName, daysUntilNextPurchase }, -}: AddItemProps): Promise => { +export const addItem = async ( + listPath: string, + { itemName, daysUntilNextPurchase }: itemData, +): Promise => { const listCollectionRef = collection(db, listPath, 'items'); return addDoc(listCollectionRef, { dateCreated: new Date(), dateLastPurchased: null, - dateNextPurchased: addDaysFromToday(daysUntilNextPurchase), + dateNextPurchased: addDaysFromToday(parseInt(daysUntilNextPurchase)), name: itemName, totalPurchases: 0, }); From 8d648a29343afea7a17de6514ee9beabf6545252 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 15:42:34 -0400 Subject: [PATCH 11/53] feat: add types to AddItems component; pass label prop to TextInputElement --- src/components/AddItems.tsx | 57 +++++++++++++++---------------------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/src/components/AddItems.tsx b/src/components/AddItems.tsx index 4602a88..39033a2 100644 --- a/src/components/AddItems.tsx +++ b/src/components/AddItems.tsx @@ -1,9 +1,14 @@ -import React, { useCallback } from 'react'; +import { useCallback } from 'react'; import { useStateWithStorage, normalizeItemName } from '../utils'; import { addItem } from '../api'; import TextInputElement from './TextInputElement'; import RadioInputElement from './RadioInputElement'; +import { Item } from '../types/types'; + +type Props = { + items: Item[]; +}; const daysUntilPurchaseOptions = { Soon: 7, @@ -11,34 +16,19 @@ const daysUntilPurchaseOptions = { 'Not soon': 30, }; -export function AddItems({ items }) { +export function AddItems({ items }: Props) { const [listPath] = useStateWithStorage('tcl-shopping-list-path', null); const handleSubmit = useCallback( - async (event) => { + async (event: React.FormEvent) => { event.preventDefault(); - - const itemName = event.target.elements['item-name'].value; - const normalizedItemName = itemName - .trim() - .toLowerCase() - .replace(/[&\/\\#, +$!,~%.'":*?<>{}]/g, ''); - if (items) { - const currentItems = items.map((item) => - item.name - .trim() - .toLowerCase() - .replace(/[&\/\\#, +$!,~%.'":*?<>{}]/g, ''), - ); - if (currentItems.includes(normalizedItemName)) { - alert('This item already exists in the list'); - event.target.reset(); - return; - } - } - - const daysUntilNextPurchase = - event.target.elements['purchase-date'].value; + const form = event.target as HTMLFormElement; + const itemName = ( + form.elements.namedItem('item-name') as HTMLInputElement + ).value; + const daysUntilNextPurchase = ( + form.elements.namedItem('purchase-date') as HTMLInputElement + ).value; try { if (itemName.trim() === '') { @@ -65,9 +55,11 @@ export function AddItems({ items }) { `${itemName} was added to the list! The next purchase date is set to ${daysUntilNextPurchase} days from now.`, ); } catch (error) { - alert(`Item was not added to the database, Error: ${error.message}`); + alert( + `Item was not added to the database, Error: ${error instanceof Error ? error.message : error}`, + ); } finally { - event.target.reset(); + form.reset(); } }, [listPath], @@ -80,13 +72,10 @@ export function AddItems({ items }) { type="text" id="item-name" placeholder="Enter item name" - pattern="^[^\s].+[^\s]$" - > - Item Name: - - + label={'Item Name: '} + required={true} + /> + {Object.entries(daysUntilPurchaseOptions).map(([key, value]) => ( Date: Mon, 16 Sep 2024 15:43:34 -0400 Subject: [PATCH 12/53] feat: add types to TextInputElement component props, as well as pattern in the input element --- src/components/TextInputElement.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/TextInputElement.tsx b/src/components/TextInputElement.tsx index 6b59acd..bc6b326 100644 --- a/src/components/TextInputElement.tsx +++ b/src/components/TextInputElement.tsx @@ -1,4 +1,13 @@ -import React from 'react'; +import React, { ChangeEventHandler } from 'react'; + +type Props = { + label: string; + type: string; + id: string; + placeholder: string; + onChange?: ChangeEventHandler; + required: boolean; +}; const TextInputElement = ({ label, @@ -7,7 +16,7 @@ const TextInputElement = ({ placeholder, onChange, required, -}) => { +}: Props) => { return ( <> @@ -18,6 +27,7 @@ const TextInputElement = ({ placeholder={placeholder} onChange={onChange} required={required} + pattern="^[^\s].+[^\s]$" />
From 172a520ae9c7437027a6e959fc3450b303490f47 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 15:44:39 -0400 Subject: [PATCH 13/53] feat: add type to List component props, pass items to AddItems component --- src/views/List.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/views/List.tsx b/src/views/List.tsx index 45ef9d8..5c8193a 100644 --- a/src/views/List.tsx +++ b/src/views/List.tsx @@ -4,16 +4,21 @@ import { ListItem } from '../components'; import { useStateWithStorage } from '../utils'; import { AddItems } from '../components/AddItems'; import TextInputElement from '../components/TextInputElement'; +import { Item } from '../types/types'; -export function List({ data }) { +type ListProps = { + items: Item[]; +}; + +export function List({ items }: ListProps) { const [searchItem, setSearchItem] = useState(''); const [listPath] = useStateWithStorage('tcl-shopping-list-path', null); - const handleTextChange = (event) => { + const handleTextChange = (event: React.ChangeEvent) => { setSearchItem(event.target.value); }; - const filteredItems = data.filter((item) => + const filteredItems = items.filter((item) => item.name.toLowerCase().includes(searchItem.toLowerCase()), ); @@ -21,12 +26,12 @@ export function List({ data }) { return ( <> - {!data.length ? ( + {!items.length ? ( <>

Welcome to {listName}!

Ready to add your first item? Start adding below!

- + ) : ( <> From cb2b2a900aa94db16d27a31fb18a80dfe3ab159a Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 15:48:01 -0400 Subject: [PATCH 14/53] feat: add types to normalizeItemName utility function --- src/utils/normalize.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/normalize.ts b/src/utils/normalize.ts index 621af77..a4e99ec 100644 --- a/src/utils/normalize.ts +++ b/src/utils/normalize.ts @@ -1,4 +1,4 @@ -export function normalizeItemName(itemName) { +export function normalizeItemName(itemName: string): string { return itemName .trim() .toLowerCase() From 568b94349d6c59a37b5c83c0ad2281c8901524d2 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 16:20:52 -0400 Subject: [PATCH 15/53] feat: add types to App component --- src/App.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 1185f09..1e99b03 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -57,10 +57,7 @@ export function App() { /> } /> - } - /> + } /> } /> From 7c2a158c37261003e3559ead8657968b04d5e680 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 16:21:25 -0400 Subject: [PATCH 16/53] fix: add fixes to AddItems component types --- src/components/AddItems.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AddItems.tsx b/src/components/AddItems.tsx index 39033a2..50e5db8 100644 --- a/src/components/AddItems.tsx +++ b/src/components/AddItems.tsx @@ -4,10 +4,10 @@ import { useStateWithStorage, normalizeItemName } from '../utils'; import { addItem } from '../api'; import TextInputElement from './TextInputElement'; import RadioInputElement from './RadioInputElement'; -import { Item } from '../types/types'; +import { DocumentData } from 'firebase/firestore'; type Props = { - items: Item[]; + items: DocumentData[]; }; const daysUntilPurchaseOptions = { From 90bc64d3a4268f8db390e435d918d6fbe2a5daeb Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 16:22:00 -0400 Subject: [PATCH 17/53] feat: add types to Home component --- src/views/Home.tsx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/views/Home.tsx b/src/views/Home.tsx index 6c3fbf7..646e8fd 100644 --- a/src/views/Home.tsx +++ b/src/views/Home.tsx @@ -1,17 +1,26 @@ -import React from 'react'; - import './Home.css'; -import { SingleList } from '../components'; +import { Dispatch } from 'react'; import { useNavigate } from 'react-router-dom'; +import { DocumentData } from 'firebase/firestore'; import { createList } from '../api'; +import { SingleList } from '../components'; import TextInputElement from '../components/TextInputElement'; -export function Home({ data, setListPath, userId, userEmail }) { +type Props = { + data: DocumentData[]; + setListPath: Dispatch; + userId: string; + userEmail: string; +}; + +export function Home({ data, setListPath, userId, userEmail }: Props) { const navigate = useNavigate(); - const handleSubmit = async (event) => { + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); - const listName = event.target['list-name'].value; + const form = event.target as HTMLFormElement; + const listName = (form.elements.namedItem('item-name') as HTMLInputElement) + .value; const currentLists = data.map((list) => { return list.name.toLowerCase(); }); @@ -30,7 +39,7 @@ export function Home({ data, setListPath, userId, userEmail }) { console.error(err); alert('List not created'); } finally { - event.target.reset(); + form.reset(); } }; From 84845a46fc9945be7800d5ba7f4ac2f1ac612cd0 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 16:22:45 -0400 Subject: [PATCH 18/53] fix: add fixes to ManageList component type --- src/views/ManageList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/ManageList.tsx b/src/views/ManageList.tsx index d678549..f6757d1 100644 --- a/src/views/ManageList.tsx +++ b/src/views/ManageList.tsx @@ -1,9 +1,9 @@ +import { DocumentData } from 'firebase/firestore'; import { AddItems } from '../components/AddItems'; import { ShareList } from '../components/ShareList'; -import { Item } from '../types/types'; type ManageListProps = { - items: Item[]; + items: DocumentData[]; }; export function ManageList({ items }: ManageListProps) { From 3ad80a593dcdc181d72cc2396a9667791712ac3d Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 16:23:12 -0400 Subject: [PATCH 19/53] fix: add fixes to List component type --- src/views/List.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/views/List.tsx b/src/views/List.tsx index 5c8193a..3d13b22 100644 --- a/src/views/List.tsx +++ b/src/views/List.tsx @@ -4,10 +4,10 @@ import { ListItem } from '../components'; import { useStateWithStorage } from '../utils'; import { AddItems } from '../components/AddItems'; import TextInputElement from '../components/TextInputElement'; -import { Item } from '../types/types'; +import { DocumentData } from 'firebase/firestore'; type ListProps = { - items: Item[]; + items: DocumentData[]; }; export function List({ items }: ListProps) { @@ -22,7 +22,7 @@ export function List({ items }: ListProps) { item.name.toLowerCase().includes(searchItem.toLowerCase()), ); - const listName = listPath.slice(listPath.indexOf('/') + 1); + const listName = listPath?.slice(listPath.indexOf('/') + 1); return ( <> From 95c060855f93c6e1eebeab9e33b80c5cbebce541 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 16:23:41 -0400 Subject: [PATCH 20/53] feat: add types to Layout component --- src/views/Layout.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/views/Layout.tsx b/src/views/Layout.tsx index 778f6e4..298fcaa 100644 --- a/src/views/Layout.tsx +++ b/src/views/Layout.tsx @@ -3,7 +3,6 @@ import React from 'react'; /* eslint-disable jsx-a11y/anchor-is-valid */ import { Outlet, NavLink } from 'react-router-dom'; import { useAuth, SignInButton, SignOutButton } from '../api/useAuth'; -// import { Home, List, ManageList } from '../views'; import './Layout.css'; From c0d64d4afbce1bcd20935c78f74bf05562a4d1c6 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 18:13:06 -0400 Subject: [PATCH 21/53] feat: add types to useStateWithStorage hook --- src/utils/hooks.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils/hooks.ts b/src/utils/hooks.ts index 92a06ca..671b6c9 100644 --- a/src/utils/hooks.ts +++ b/src/utils/hooks.ts @@ -6,7 +6,10 @@ import { useEffect, useState } from 'react'; * @param {string | null} initialValue The initial value to store in localStorage and React state. * @returns {[string | null, React.Dispatch]} */ -export function useStateWithStorage(storageKey, initialValue) { +export function useStateWithStorage( + storageKey: string, + initialValue: string | null, +): [string | null, React.Dispatch] { const [value, setValue] = useState( () => localStorage.getItem(storageKey) ?? initialValue, ); From 1af02b2886a49fd973d1ec2139abdd2804217a2b Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 18:35:37 -0400 Subject: [PATCH 22/53] fix: change main point of entry from jsx to tsx --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 5016fc3..8b17180 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + @@ -13,7 +13,7 @@ Smart Shopping List - +
From 4312d46d5433ee2653342a749bfa8f525c4b6b3b Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 19:29:55 -0400 Subject: [PATCH 23/53] feat: add types to ListItem component --- src/components/ListItem.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/ListItem.tsx b/src/components/ListItem.tsx index b00439b..eb4d99d 100644 --- a/src/components/ListItem.tsx +++ b/src/components/ListItem.tsx @@ -1,12 +1,18 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import './ListItem.css'; import { updateItem } from '../api'; import { calculateDateNextPurchased, ONE_DAY_IN_MILLISECONDS } from '../utils'; +import { DocumentData, Timestamp } from 'firebase/firestore'; + +type Props = { + item: DocumentData; + listPath: string; +}; const currentDate = new Date(); -const calculateIsPurchased = (dateLastPurchased) => { +const calculateIsPurchased = (dateLastPurchased: Timestamp) => { if (!dateLastPurchased) { return false; } @@ -18,7 +24,7 @@ const calculateIsPurchased = (dateLastPurchased) => { return currentDate < oneDayLater; }; -export function ListItem({ item, listPath }) { +export function ListItem({ item, listPath }: Props) { const [isPurchased, setIsPurchased] = useState(() => calculateIsPurchased(item.dateLastPurchased), ); @@ -40,7 +46,9 @@ export function ListItem({ item, listPath }) { await updateItem(listPath, id, { ...updatedItem }); } catch (error) { - alert(`Item was not marked as purchased`, error.message); + alert( + `Item was not marked as purchased. Error: ${error instanceof Error ? error.message : error}`, + ); } } }; From 4b0078096a1d06bf4f7f1ed885abd148646f4155 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 19:30:39 -0400 Subject: [PATCH 24/53] feat: add types to RadioInputElement component --- src/components/RadioInputElement.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/RadioInputElement.tsx b/src/components/RadioInputElement.tsx index 9000e05..1f9dde7 100644 --- a/src/components/RadioInputElement.tsx +++ b/src/components/RadioInputElement.tsx @@ -1,6 +1,11 @@ -import React from 'react'; +type Props = { + label: string; + id: string; + value: number; + required: boolean; +}; -const RadioInputElement = ({ label, id, value, required }) => { +const RadioInputElement = ({ label, id, value, required }: Props) => { return ( <> Date: Mon, 16 Sep 2024 19:31:15 -0400 Subject: [PATCH 25/53] feat: add types to SingleList component --- src/components/SingleList.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/SingleList.tsx b/src/components/SingleList.tsx index 8dbf8ea..d43ead3 100644 --- a/src/components/SingleList.tsx +++ b/src/components/SingleList.tsx @@ -1,8 +1,13 @@ -import React from 'react'; - +import { Dispatch } from 'react'; import './SingleList.css'; -export function SingleList({ name, path, setListPath }) { +type Props = { + name: string; + path: string; + setListPath: Dispatch; +}; + +export function SingleList({ name, path, setListPath }: Props) { function handleClick() { setListPath(path); } From 9705ebd0352a3505c4a4532a45f781479f017b18 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 19:31:46 -0400 Subject: [PATCH 26/53] feat: add types to ShareList component --- src/components/ShareList.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/ShareList.tsx b/src/components/ShareList.tsx index 884a40b..b73ed46 100644 --- a/src/components/ShareList.tsx +++ b/src/components/ShareList.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import { shareList, useAuth } from '../api'; import { useStateWithStorage } from '../utils'; import TextInputElement from './TextInputElement'; @@ -11,7 +9,7 @@ export function ShareList() { const userId = user?.uid; const userEmail = user?.email; - const shareCurrentList = async (emailData) => { + const shareCurrentList = async (emailData: string) => { const listShared = await shareList(listPath, userId, emailData); if (listShared === '!owner') { @@ -25,10 +23,12 @@ export function ShareList() { } }; - const handleEmailInputSubmit = (event) => { + const handleEmailInputSubmit = (event: React.FormEvent) => { event.preventDefault(); - - const emailData = event.target['email-input'].value; + const form = event.target as HTMLFormElement; + const emailData = ( + form.elements.namedItem('email-input') as HTMLInputElement + ).value; if (userEmail === emailData) { alert('You cannot share the list with yourself.'); @@ -36,7 +36,7 @@ export function ShareList() { shareCurrentList(emailData); } - event.target.reset(); + form.reset(); }; return ( From 69d3615a98e3b9f99b24263f2db0e6511ad3e13f Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 19:32:23 -0400 Subject: [PATCH 27/53] feat: add types to dates.js module --- src/utils/dates.ts | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/utils/dates.ts b/src/utils/dates.ts index b7b8c80..096811f 100644 --- a/src/utils/dates.ts +++ b/src/utils/dates.ts @@ -1,6 +1,7 @@ import { calculateEstimate } from '@the-collab-lab/shopping-list-utils'; +import { DocumentData, Timestamp } from 'firebase/firestore'; -export const ONE_DAY_IN_MILLISECONDS = 86400000; +export const ONE_DAY_IN_MILLISECONDS: number = 86400000; /** * Get a new JavaScript Date that is `offset` days in the future. @@ -9,7 +10,7 @@ export const ONE_DAY_IN_MILLISECONDS = 86400000; * addDaysFromToday(3) * @param {number} daysOffset */ -export function addDaysFromToday(daysOffset) { +export function addDaysFromToday(daysOffset: number): Date { return new Date(Date.now() + daysOffset * ONE_DAY_IN_MILLISECONDS); } @@ -25,7 +26,10 @@ export function addDaysFromToday(daysOffset) { * @returns {Date} - The estimated date of the next purchase. * @throws {Error} - Throws an error if the next purchase date cannot be calculated. */ -export const calculateDateNextPurchased = (currentDate, item) => { +export const calculateDateNextPurchased = ( + currentDate: Date, + item: DocumentData, +): Date => { try { // get purchase intervals and get new estimation for next purchase date const purchaseIntervals = calculatePurchaseIntervals( @@ -51,12 +55,17 @@ export const calculateDateNextPurchased = (currentDate, item) => { * @param {Date} laterDate The ending date. * @returns {number} The number of days between the two dates. */ -function getDaysBetweenDates(earlierDate, laterDate) { +function getDaysBetweenDates(earlierDate: Date, laterDate: Date): number { return Math.floor( (laterDate.getTime() - earlierDate.getTime()) / ONE_DAY_IN_MILLISECONDS, ); } +type PurchaseIntervals = { + lastEstimatedInterval: number; + daysSinceLastPurchase: number; +}; + /** * Calculate the purchase intervals between current, next, and last purchase dates. * @param {Date} currentDate The current date. @@ -65,11 +74,11 @@ function getDaysBetweenDates(earlierDate, laterDate) { * @returns {Object} An object containing the last estimated interval and days since last purchase. */ function calculatePurchaseIntervals( - currentDate, - dateCreated, - dateNextPurchased, - dateLastPurchased, -) { + currentDate: Date, + dateCreated: Timestamp, + dateNextPurchased: Timestamp, + dateLastPurchased: Timestamp | null, +): PurchaseIntervals { const lastPurchaseDate = dateLastPurchased?.toDate(); const lastEstimatedIntervalStartDate = @@ -97,7 +106,10 @@ function calculatePurchaseIntervals( * @returns {Date} The estimated next purchase date. * @throws {Error} If an error occurs during the next purchase estimation process. */ -function getNextPurchaseEstimate(purchaseIntervals, totalPurchases) { +function getNextPurchaseEstimate( + purchaseIntervals: PurchaseIntervals, + totalPurchases: number, +): Date { const { lastEstimatedInterval, daysSinceLastPurchase } = purchaseIntervals; try { From 330b2d634c3424632267c6378d8b4cedf8a4af60 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 19:33:15 -0400 Subject: [PATCH 28/53] fix: adjust how parameters are being passed into functions --- src/api/firebase.ts | 70 ++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/src/api/firebase.ts b/src/api/firebase.ts index 0051116..459a56c 100644 --- a/src/api/firebase.ts +++ b/src/api/firebase.ts @@ -12,7 +12,10 @@ import { import { useEffect, useState } from 'react'; import { db } from './config'; import { addDaysFromToday } from '../utils'; -import { ListPath } from '../types/types'; + +export type ListPath = { + [key: string]: string | null; +}; /** * A custom hook that subscribes to the user's shopping lists in our Firestore @@ -57,7 +60,9 @@ export const useShoppingLists = ( * @param {string | null} listPath * @see https://firebase.google.com/docs/firestore/query-data/listen */ -export const useShoppingListData = (listPath: string): DocumentData[] => { +export const useShoppingListData = ( + listPath: string | null, +): DocumentData[] => { // Start with an empty array for our data. /** @type {import('firebase/firestore').DocumentData[]} */ const initialState: DocumentData[] = []; @@ -114,23 +119,17 @@ export const addUserToDatabase = async (user: DocumentData): Promise => { } }; -export type CreateListProps = { - userId: string; - userEmail: string; - listName: string; -}; - /** * Create a new list and add it to a user's lists in Firestore. * @param {string} userId The id of the user who owns the list. * @param {string} userEmail The email of the user who owns the list. * @param {string} listName The name of the new list. */ -export const createList = async ({ - userId, - userEmail, - listName, -}: CreateListProps): Promise => { +export const createList = async ( + userId: string, + userEmail: string, + listName: string, +): Promise => { const listDocRef = doc(db, userId, listName); await setDoc(listDocRef, { @@ -145,24 +144,18 @@ export const createList = async ({ return listDocRef.path; }; -export type ShareListProps = { - listPath: string; - currentUserId: string; - recipientEmail: string; -}; - /** * Shares a list with another user. * @param {string} listPath The path to the list to share. * @param {string} recipientEmail The email of the user to share the list with. */ -export const shareList = async ({ - listPath, - currentUserId, - recipientEmail, -}: ShareListProps): Promise => { +export const shareList = async ( + listPath: string | null, + currentUserId: string, + recipientEmail: string, +): Promise => { // Check if current user is owner. - if (!listPath.includes(currentUserId)) { + if (!listPath?.includes(currentUserId)) { return '!owner'; } // Get the document for the recipient user. @@ -198,9 +191,12 @@ type itemData = { * @param {number} itemData.daysUntilNextPurchase The number of days until the user thinks they'll need to buy the item again. */ export const addItem = async ( - listPath: string, + listPath: string | null, { itemName, daysUntilNextPurchase }: itemData, ): Promise => { + if (!listPath) { + throw new Error('List path is undefined'); + } const listCollectionRef = collection(db, listPath, 'items'); return addDoc(listCollectionRef, { dateCreated: new Date(), @@ -211,14 +207,10 @@ export const addItem = async ( }); }; -export type UpdateItemProps = { - listPath: string; - itemId: string; - updatedData: { - dateLastPurchased: Date; - dateNextPurchased: Date; - totalPurchases: number; - }; +export type UpdatedData = { + dateLastPurchased: Date; + dateNextPurchased: Date; + totalPurchases: number; }; /** @@ -232,11 +224,11 @@ export type UpdateItemProps = { * @returns {Promise} A message confirming the item was successfully updated. * @throws {Error} If the item update fails. */ -export const updateItem = async ({ - listPath, - itemId, - updatedData: { dateLastPurchased, dateNextPurchased, totalPurchases }, -}: UpdateItemProps): Promise => { +export const updateItem = async ( + listPath: string, + itemId: string, + { dateLastPurchased, dateNextPurchased, totalPurchases }: UpdatedData, +): Promise => { // reference the item path const itemDocRef = doc(db, listPath, 'items', itemId); // update the item with the purchase date and increment the total purchases made From 5fceeb1da69bff538ad01aab810e41c6034511d1 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 19:33:57 -0400 Subject: [PATCH 29/53] fix: remove redundant file --- src/types/types.ts | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/types/types.ts diff --git a/src/types/types.ts b/src/types/types.ts deleted file mode 100644 index 0f8ff48..0000000 --- a/src/types/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type Item = { - id: string; - listPath: string; - dateCreated: Date; - dateLastPurchased: Date | null; - dateNextPurchased: Date; - name: string; - totalPurchases: number; -}; - -export type ListPath = { - [key: string]: string; -}; From 26de5911839b2fafa1ffe4a2ed3f1f2d4f910cac Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 19:54:55 -0400 Subject: [PATCH 30/53] fix: update listPath type to be either string or null --- src/api/firebase.ts | 5 ++++- src/components/ListItem.tsx | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/api/firebase.ts b/src/api/firebase.ts index 459a56c..962a0bd 100644 --- a/src/api/firebase.ts +++ b/src/api/firebase.ts @@ -225,11 +225,14 @@ export type UpdatedData = { * @throws {Error} If the item update fails. */ export const updateItem = async ( - listPath: string, + listPath: string | null, itemId: string, { dateLastPurchased, dateNextPurchased, totalPurchases }: UpdatedData, ): Promise => { // reference the item path + if (!listPath) { + throw new Error(`Invalid list path: ${listPath}`); + } const itemDocRef = doc(db, listPath, 'items', itemId); // update the item with the purchase date and increment the total purchases made try { diff --git a/src/components/ListItem.tsx b/src/components/ListItem.tsx index eb4d99d..a6cd116 100644 --- a/src/components/ListItem.tsx +++ b/src/components/ListItem.tsx @@ -7,7 +7,7 @@ import { DocumentData, Timestamp } from 'firebase/firestore'; type Props = { item: DocumentData; - listPath: string; + listPath: string | null; }; const currentDate = new Date(); From 1a75fdaf656b94c5f7fe953830873ef6753c8560 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 19:56:10 -0400 Subject: [PATCH 31/53] fix: add optional chaining operator to items List component --- src/views/List.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/List.tsx b/src/views/List.tsx index 3d13b22..d044888 100644 --- a/src/views/List.tsx +++ b/src/views/List.tsx @@ -18,7 +18,7 @@ export function List({ items }: ListProps) { setSearchItem(event.target.value); }; - const filteredItems = items.filter((item) => + const filteredItems = items?.filter((item) => item.name.toLowerCase().includes(searchItem.toLowerCase()), ); @@ -26,7 +26,7 @@ export function List({ items }: ListProps) { return ( <> - {!items.length ? ( + {!items?.length ? ( <>

Welcome to {listName}!

Ready to add your first item? Start adding below!

From 764ba2514ea6d48b24040d5f96ef1d637bd6987b Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 19:56:56 -0400 Subject: [PATCH 32/53] fix[test]: update List component to take items prop --- tests/List.test.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/List.test.jsx b/tests/List.test.jsx index b951d71..a623ab3 100644 --- a/tests/List.test.jsx +++ b/tests/List.test.jsx @@ -14,7 +14,7 @@ beforeEach(() => { describe('List Component', () => { test('renders the shopping list name, search field, and all list items from the data prop', () => { - render(); + render(); expect(screen.getByText('groceries')).toBeInTheDocument(); expect(screen.getByLabelText('Search Item:')).toBeInTheDocument(); @@ -25,7 +25,7 @@ describe('List Component', () => { }); test('shows welcome message and AddItems component when no items are present', () => { - render(); + render(); expect(screen.getByText('Welcome to groceries!')).toBeInTheDocument(); expect(screen.getByLabelText('Item Name:')).toBeInTheDocument(); From 67e1b926f8ca1288459200982a947bd4ccc1ebdc Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Mon, 16 Sep 2024 20:37:34 -0400 Subject: [PATCH 33/53] troubleshoot AddItems component --- src/components/AddItems.tsx | 4 ---- src/views/Home.tsx | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/AddItems.tsx b/src/components/AddItems.tsx index 50e5db8..43099ab 100644 --- a/src/components/AddItems.tsx +++ b/src/components/AddItems.tsx @@ -31,10 +31,6 @@ export function AddItems({ items }: Props) { ).value; try { - if (itemName.trim() === '') { - alert('Please add an item name.'); - return; - } // normalize the name by removing all punctuation and spaces to check if the normalized item is already in the list const normalizedItemName = normalizeItemName(itemName); if (items) { diff --git a/src/views/Home.tsx b/src/views/Home.tsx index 646e8fd..c35d0be 100644 --- a/src/views/Home.tsx +++ b/src/views/Home.tsx @@ -19,7 +19,7 @@ export function Home({ data, setListPath, userId, userEmail }: Props) { const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); const form = event.target as HTMLFormElement; - const listName = (form.elements.namedItem('item-name') as HTMLInputElement) + const listName = (form.elements.namedItem('list-name') as HTMLInputElement) .value; const currentLists = data.map((list) => { return list.name.toLowerCase(); From cc87268bac556e54d375447e413f004467439d2c Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Tue, 17 Sep 2024 08:52:26 -0400 Subject: [PATCH 34/53] troubleshoot listPath in Home, List and AddItems components --- src/components/AddItems.tsx | 7 +++++++ src/views/Home.tsx | 10 ++++++++-- src/views/List.tsx | 6 +++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/components/AddItems.tsx b/src/components/AddItems.tsx index 43099ab..7b28b27 100644 --- a/src/components/AddItems.tsx +++ b/src/components/AddItems.tsx @@ -22,6 +22,7 @@ export function AddItems({ items }: Props) { const handleSubmit = useCallback( async (event: React.FormEvent) => { event.preventDefault(); + const form = event.target as HTMLFormElement; const itemName = ( form.elements.namedItem('item-name') as HTMLInputElement @@ -31,13 +32,19 @@ export function AddItems({ items }: Props) { ).value; try { + if (itemName.trim() === '') { + alert('Please add an item name.'); + return; + } // normalize the name by removing all punctuation and spaces to check if the normalized item is already in the list const normalizedItemName = normalizeItemName(itemName); + console.log('Normalized new item:', normalizedItemName); if (items) { // normalize the existing list items to compare them to the new input const currentItems = items.map((item) => normalizeItemName(item.name), ); + console.log('Normalized current items:', currentItems); if (currentItems.includes(normalizedItemName)) { alert('This item already exists in the list'); return; diff --git a/src/views/Home.tsx b/src/views/Home.tsx index c35d0be..12c504c 100644 --- a/src/views/Home.tsx +++ b/src/views/Home.tsx @@ -33,8 +33,14 @@ export function Home({ data, setListPath, userId, userEmail }: Props) { const listPath = await createList(userId, userEmail, listName); setListPath(listPath); - alert('List added'); - navigate('/list'); + console.log(`Setting list path with: ${listPath}`); + setTimeout(() => { + console.log( + `LocalStorage listPath after state update: ${localStorage.getItem('tcl-shopping-list-path')}`, + ); + alert('List added'); + navigate('/list'); + }, 0); } catch (err) { console.error(err); alert('List not created'); diff --git a/src/views/List.tsx b/src/views/List.tsx index d044888..b2e1ee2 100644 --- a/src/views/List.tsx +++ b/src/views/List.tsx @@ -18,15 +18,15 @@ export function List({ items }: ListProps) { setSearchItem(event.target.value); }; - const filteredItems = items?.filter((item) => + const filteredItems = items.filter((item) => item.name.toLowerCase().includes(searchItem.toLowerCase()), ); - + console.log(`Current list path: ${listPath}`); const listName = listPath?.slice(listPath.indexOf('/') + 1); return ( <> - {!items?.length ? ( + {!items.length ? ( <>

Welcome to {listName}!

Ready to add your first item? Start adding below!

From d57923ae936b4e6ca1c6e45491a1a9f68068ddd0 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Tue, 17 Sep 2024 09:47:24 -0400 Subject: [PATCH 35/53] fix: fixed listPath issue with passing listPath as a prop from App to List component; removed temporary console.log from Home component --- src/App.tsx | 5 ++++- src/views/Home.tsx | 12 +++++------- src/views/List.tsx | 5 ++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 1e99b03..2be64cc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -57,7 +57,10 @@ export function App() { /> } /> - } /> + } + /> } /> diff --git a/src/views/Home.tsx b/src/views/Home.tsx index 12c504c..385ec49 100644 --- a/src/views/Home.tsx +++ b/src/views/Home.tsx @@ -34,13 +34,11 @@ export function Home({ data, setListPath, userId, userEmail }: Props) { const listPath = await createList(userId, userEmail, listName); setListPath(listPath); console.log(`Setting list path with: ${listPath}`); - setTimeout(() => { - console.log( - `LocalStorage listPath after state update: ${localStorage.getItem('tcl-shopping-list-path')}`, - ); - alert('List added'); - navigate('/list'); - }, 0); + console.log( + `LocalStorage listPath after state update: ${localStorage.getItem('tcl-shopping-list-path')}`, + ); + alert('List added'); + navigate('/list'); } catch (err) { console.error(err); alert('List not created'); diff --git a/src/views/List.tsx b/src/views/List.tsx index b2e1ee2..a886860 100644 --- a/src/views/List.tsx +++ b/src/views/List.tsx @@ -1,18 +1,17 @@ import React, { useState } from 'react'; import { ListItem } from '../components'; -import { useStateWithStorage } from '../utils'; import { AddItems } from '../components/AddItems'; import TextInputElement from '../components/TextInputElement'; import { DocumentData } from 'firebase/firestore'; type ListProps = { items: DocumentData[]; + listPath: string | null; }; -export function List({ items }: ListProps) { +export function List({ items, listPath }: ListProps) { const [searchItem, setSearchItem] = useState(''); - const [listPath] = useStateWithStorage('tcl-shopping-list-path', null); const handleTextChange = (event: React.ChangeEvent) => { setSearchItem(event.target.value); From 8c395f085db4a9b68a8fa105f2a12c5abd48302f Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Tue, 17 Sep 2024 10:24:47 -0400 Subject: [PATCH 36/53] fix: add listPath prop to List.test --- tests/List.test.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/List.test.jsx b/tests/List.test.jsx index a623ab3..ab03b51 100644 --- a/tests/List.test.jsx +++ b/tests/List.test.jsx @@ -14,7 +14,7 @@ beforeEach(() => { describe('List Component', () => { test('renders the shopping list name, search field, and all list items from the data prop', () => { - render(); + render(); expect(screen.getByText('groceries')).toBeInTheDocument(); expect(screen.getByLabelText('Search Item:')).toBeInTheDocument(); @@ -25,7 +25,7 @@ describe('List Component', () => { }); test('shows welcome message and AddItems component when no items are present', () => { - render(); + render(); expect(screen.getByText('Welcome to groceries!')).toBeInTheDocument(); expect(screen.getByLabelText('Item Name:')).toBeInTheDocument(); From 4117b8e55aefd05fc40f0b72e915299ca6c9e890 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 19 Sep 2024 09:27:47 -0400 Subject: [PATCH 37/53] fix: userId and userEmail can be undefined; update functions to return in case userId or userEmail is undefined --- src/api/firebase.ts | 12 +++++++----- src/api/useAuth.tsx | 9 +++++++-- src/components/ShareList.tsx | 2 ++ src/views/Home.tsx | 4 ++-- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/api/firebase.ts b/src/api/firebase.ts index 962a0bd..03fafcc 100644 --- a/src/api/firebase.ts +++ b/src/api/firebase.ts @@ -25,8 +25,8 @@ export type ListPath = { * @returns */ export const useShoppingLists = ( - userId: string | null, - userEmail: string | null, + userId: string | null | undefined, + userEmail: string | null | undefined, ): ListPath[] => { // Start with an empty array for our data. const [data, setData] = useState([]); @@ -126,10 +126,12 @@ export const addUserToDatabase = async (user: DocumentData): Promise => { * @param {string} listName The name of the new list. */ export const createList = async ( - userId: string, - userEmail: string, + userId: string | null | undefined, + userEmail: string | null | undefined, listName: string, -): Promise => { +): Promise => { + if (!userId || !userEmail) return; + const listDocRef = doc(db, userId, listName); await setDoc(listDocRef, { diff --git a/src/api/useAuth.tsx b/src/api/useAuth.tsx index ca85c72..c4df9b1 100644 --- a/src/api/useAuth.tsx +++ b/src/api/useAuth.tsx @@ -2,7 +2,12 @@ import { useEffect, useState } from 'react'; import { auth } from './config.js'; import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'; import { addUserToDatabase } from './firebase.js'; -import { User } from '../types/types.js'; + +export type User = { + email: string | null | undefined; + displayName: string | null | undefined; + uid: string | null | undefined; +}; /** * A button that signs the user in using Google OAuth. When clicked, @@ -33,7 +38,7 @@ export const SignOutButton = () => ( * @see https://firebase.google.com/docs/auth/web/start#set_an_authentication_state_observer_and_get_user_data */ export const useAuth = () => { - const initialState: User | null = null; + const initialState = null; const [user, setUser] = useState(initialState); useEffect(() => { diff --git a/src/components/ShareList.tsx b/src/components/ShareList.tsx index b73ed46..0e48148 100644 --- a/src/components/ShareList.tsx +++ b/src/components/ShareList.tsx @@ -10,6 +10,8 @@ export function ShareList() { const userEmail = user?.email; const shareCurrentList = async (emailData: string) => { + if (!userId) return; + const listShared = await shareList(listPath, userId, emailData); if (listShared === '!owner') { diff --git a/src/views/Home.tsx b/src/views/Home.tsx index 385ec49..d12c64d 100644 --- a/src/views/Home.tsx +++ b/src/views/Home.tsx @@ -9,8 +9,8 @@ import TextInputElement from '../components/TextInputElement'; type Props = { data: DocumentData[]; setListPath: Dispatch; - userId: string; - userEmail: string; + userId: string | null | undefined; + userEmail: string | null | undefined; }; export function Home({ data, setListPath, userId, userEmail }: Props) { From 640dae8a8c0eaf09fd0ac6e2eacce8bcdb95fef9 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 19 Sep 2024 09:28:52 -0400 Subject: [PATCH 38/53] fix: add a try/catch block in index.ts to handle a case where root element is null --- src/index.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 6cc7470..8f7d396 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,9 +4,16 @@ import { App } from './App'; import './index.css'; -const root = createRoot(document.getElementById('root')); -root.render( - - - , -); +const rootElement = document.getElementById('root'); +try { + if (rootElement) { + const root = createRoot(rootElement); + root.render( + + + , + ); + } +} catch (error) { + throw new Error(`Could not get root element. Error: ${error}`); +} From 1ae6c7a437fc99ece70cfb48da25130ddcd2ac00 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 19 Sep 2024 09:29:12 -0400 Subject: [PATCH 39/53] fix: handle a case where listPath is null --- src/views/Home.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/Home.tsx b/src/views/Home.tsx index d12c64d..9e06077 100644 --- a/src/views/Home.tsx +++ b/src/views/Home.tsx @@ -32,7 +32,7 @@ export function Home({ data, setListPath, userId, userEmail }: Props) { } const listPath = await createList(userId, userEmail, listName); - setListPath(listPath); + setListPath(listPath ?? null); console.log(`Setting list path with: ${listPath}`); console.log( `LocalStorage listPath after state update: ${localStorage.getItem('tcl-shopping-list-path')}`, From ae9a2c8846a3c90be6f791cfd9f6489a6542d8a5 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 26 Sep 2024 13:45:20 -0400 Subject: [PATCH 40/53] feat: update firebase.ts: - ensure that values passed into the functions are not undefined; - update pros typing in order to support intellisense onhover; - add onhover description of deleteItem function. --- src/api/firebase.ts | 77 +++++++++++++++------------------------------ 1 file changed, 26 insertions(+), 51 deletions(-) diff --git a/src/api/firebase.ts b/src/api/firebase.ts index a0b5640..1acb00d 100644 --- a/src/api/firebase.ts +++ b/src/api/firebase.ts @@ -21,13 +21,10 @@ export type ListPath = { /** * A custom hook that subscribes to the user's shopping lists in our Firestore * database and returns new data whenever the lists change. - * @param {string | null} userId - * @param {string | null} userEmail - * @returns */ export const useShoppingLists = ( - userId: string | null | undefined, - userEmail: string | null | undefined, + userId?: string | null, + userEmail?: string | null, ): ListPath[] => { // Start with an empty array for our data. const [data, setData] = useState([]); @@ -58,7 +55,6 @@ export const useShoppingLists = ( /** * A custom hook that subscribes to a shopping list in our Firestore database * and returns new data whenever the list changes. - * @param {string | null} listPath * @see https://firebase.google.com/docs/firestore/query-data/listen */ export const useShoppingListData = ( @@ -99,7 +95,6 @@ export const useShoppingListData = ( /** * Add a new user to the users collection in Firestore. - * @param {Object} user The user object from Firebase Auth. */ export const addUserToDatabase = async (user: DocumentData): Promise => { // Check if the user already exists in the database. @@ -122,17 +117,12 @@ export const addUserToDatabase = async (user: DocumentData): Promise => { /** * Create a new list and add it to a user's lists in Firestore. - * @param {string} userId The id of the user who owns the list. - * @param {string} userEmail The email of the user who owns the list. - * @param {string} listName The name of the new list. */ export const createList = async ( - userId: string | null | undefined, - userEmail: string | null | undefined, listName: string, + userId: string, + userEmail: string, ): Promise => { - if (!userId || !userEmail) return; - const listDocRef = doc(db, userId, listName); await setDoc(listDocRef, { @@ -149,11 +139,9 @@ export const createList = async ( /** * Shares a list with another user. - * @param {string} listPath The path to the list to share. - * @param {string} recipientEmail The email of the user to share the list with. */ export const shareList = async ( - listPath: string | null, + listPath: string, currentUserId: string, recipientEmail: string, ): Promise => { @@ -181,25 +169,19 @@ export const shareList = async ( } }; -type itemData = { - itemName: string; - daysUntilNextPurchase: string; -}; - /** * Add a new item to the user's list in Firestore. - * @param {string} listPath The path of the list we're adding to. - * @param {Object} itemData Information about the new item. - * @param {string} itemData.itemName The name of the item. - * @param {number} itemData.daysUntilNextPurchase The number of days until the user thinks they'll need to buy the item again. */ export const addItem = async ( - listPath: string | null, - { itemName, daysUntilNextPurchase }: itemData, + listPath: string, + { + itemName, + daysUntilNextPurchase, + }: { + itemName: string; + daysUntilNextPurchase: string; + }, ): Promise => { - if (!listPath) { - throw new Error('List path is undefined'); - } const listCollectionRef = collection(db, listPath, 'items'); return addDoc(listCollectionRef, { dateCreated: new Date(), @@ -210,32 +192,22 @@ export const addItem = async ( }); }; -export type UpdatedData = { - dateLastPurchased: Date; - dateNextPurchased: Date; - totalPurchases: number; -}; - /** * Update an item in the user's list in Firestore with new purchase information. - * @param {string} listPath The path of the list the item belongs to. - * @param {string} itemId The ID of the item being updated. - * @param {Object} updatedData Object containing the updated item data. - * @param {Date} updatedData.dateLastPurchased The date the item was last purchased. - * @param {Date} updatedData.dateNextPurchased The estimated date for the next purchase. - * @param {number} updatedData.totalPurchases The total number of times the item has been purchased. - * @returns {Promise} A message confirming the item was successfully updated. - * @throws {Error} If the item update fails. */ export const updateItem = async ( - listPath: string | null, + listPath: string, itemId: string, - { dateLastPurchased, dateNextPurchased, totalPurchases }: UpdatedData, + { + dateLastPurchased, + dateNextPurchased, + totalPurchases, + }: { + dateLastPurchased: Date; + dateNextPurchased: Date; + totalPurchases: number; + }, ): Promise => { - // reference the item path - if (!listPath) { - throw new Error(`Invalid list path: ${listPath}`); - } const itemDocRef = doc(db, listPath, 'items', itemId); // update the item with the purchase date and increment the total purchases made try { @@ -252,6 +224,9 @@ export const updateItem = async ( } }; +/** + * Delete an item from the user's list in Firestore. + */ export async function deleteItem(listPath: string, itemId: string) { // reference the item path const itemDocRef = doc(db, listPath, 'items', itemId); From 705aee5caab18bd61ecf126bd37e202c7d53e1d8 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Thu, 26 Sep 2024 13:47:29 -0400 Subject: [PATCH 41/53] fix: use DocumentData type instead of custom User type --- src/api/useAuth.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/api/useAuth.tsx b/src/api/useAuth.tsx index c4df9b1..d1e7856 100644 --- a/src/api/useAuth.tsx +++ b/src/api/useAuth.tsx @@ -2,12 +2,7 @@ import { useEffect, useState } from 'react'; import { auth } from './config.js'; import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'; import { addUserToDatabase } from './firebase.js'; - -export type User = { - email: string | null | undefined; - displayName: string | null | undefined; - uid: string | null | undefined; -}; +import { DocumentData } from 'firebase/firestore'; /** * A button that signs the user in using Google OAuth. When clicked, @@ -39,7 +34,7 @@ export const SignOutButton = () => ( */ export const useAuth = () => { const initialState = null; - const [user, setUser] = useState(initialState); + const [user, setUser] = useState(initialState); useEffect(() => { auth.onAuthStateChanged((user) => { From cbde3284671cce5052025344a6e408a18f52e949 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Fri, 27 Sep 2024 10:22:56 -0400 Subject: [PATCH 42/53] fix: ensure that initialValue of useStateWithStorage hook cannot be null, replace null with / --- src/App.tsx | 2 +- src/utils/hooks.ts | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 2be64cc..7a5027c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,7 +19,7 @@ export function App() { */ const [listPath, setListPath] = useStateWithStorage( 'tcl-shopping-list-path', - null, + '/', ); /** diff --git a/src/utils/hooks.ts b/src/utils/hooks.ts index 671b6c9..2e3730f 100644 --- a/src/utils/hooks.ts +++ b/src/utils/hooks.ts @@ -2,19 +2,16 @@ import { useEffect, useState } from 'react'; /** * Set some state in React, and also persist that value in localStorage. - * @param {string} storageKey The key of the value in localStorage. - * @param {string | null} initialValue The initial value to store in localStorage and React state. - * @returns {[string | null, React.Dispatch]} */ export function useStateWithStorage( storageKey: string, - initialValue: string | null, -): [string | null, React.Dispatch] { + initialValue: string, +): [string, React.Dispatch] { const [value, setValue] = useState( () => localStorage.getItem(storageKey) ?? initialValue, ); useEffect(() => { - if (value === null || value === undefined) { + if (value === undefined) { return localStorage.removeItem(storageKey); } return localStorage.setItem(storageKey, value); From 14f2c0c297a58fe23a15f8186c1671ca45f55697 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Fri, 27 Sep 2024 10:24:41 -0400 Subject: [PATCH 43/53] fix: ensure type safety --- src/api/firebase.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/api/firebase.ts b/src/api/firebase.ts index 1acb00d..621ef17 100644 --- a/src/api/firebase.ts +++ b/src/api/firebase.ts @@ -40,6 +40,7 @@ export const useShoppingLists = ( onSnapshot(userDocRef, (docSnap) => { if (docSnap.exists()) { const listRefs: ListPath[] = docSnap.data().sharedLists; + const newData = listRefs.map((listRef) => { // We keep the list's id and path so we can use them later. return { name: listRef.id, path: listRef.path }; @@ -57,9 +58,7 @@ export const useShoppingLists = ( * and returns new data whenever the list changes. * @see https://firebase.google.com/docs/firestore/query-data/listen */ -export const useShoppingListData = ( - listPath: string | null, -): DocumentData[] => { +export const useShoppingListData = (listPath: string): DocumentData[] => { // Start with an empty array for our data. /** @type {import('firebase/firestore').DocumentData[]} */ const initialState: DocumentData[] = []; @@ -119,10 +118,10 @@ export const addUserToDatabase = async (user: DocumentData): Promise => { * Create a new list and add it to a user's lists in Firestore. */ export const createList = async ( - listName: string, userId: string, userEmail: string, -): Promise => { + listName: string, +): Promise => { const listDocRef = doc(db, userId, listName); await setDoc(listDocRef, { @@ -144,17 +143,19 @@ export const shareList = async ( listPath: string, currentUserId: string, recipientEmail: string, -): Promise => { +): Promise => { // Check if current user is owner. if (!listPath?.includes(currentUserId)) { - return '!owner'; + throw new Error('You cannot share the list you do not own.'); } // Get the document for the recipient user. const usersCollectionRef = collection(db, 'users'); const recipientDoc = await getDoc(doc(usersCollectionRef, recipientEmail)); // If the recipient user doesn't exist, we can't share the list. if (!recipientDoc.exists()) { - return; + throw new Error( + "The list was not shared because the recipient's email address does not exist in the system.", + ); } // Add the list to the recipient user's sharedLists array. const listDocumentRef = doc(db, listPath); @@ -163,9 +164,11 @@ export const shareList = async ( updateDoc(userDocumentRef, { sharedLists: arrayUnion(listDocumentRef), }); - return 'shared'; - } catch { - return; + return true; + } catch (error) { + throw new Error( + `List was not shared: ${error instanceof Error ? error.message : error}`, + ); } }; From b89e7ce7a466675097ffa7ee3ac4a281d2677319 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Fri, 27 Sep 2024 10:43:37 -0400 Subject: [PATCH 44/53] feat: remove Props type and add types inside the functions declarations to support intellisense hovers --- src/components/RadioInputElement.tsx | 13 +++++++------ src/components/SingleList.tsx | 12 +++++++----- src/components/TextInputElement.tsx | 24 ++++++++++-------------- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/components/RadioInputElement.tsx b/src/components/RadioInputElement.tsx index 1f9dde7..8f84a52 100644 --- a/src/components/RadioInputElement.tsx +++ b/src/components/RadioInputElement.tsx @@ -1,11 +1,14 @@ -type Props = { +export const RadioInputElement = ({ + label, + id, + value, + required, +}: { label: string; id: string; value: number; required: boolean; -}; - -const RadioInputElement = ({ label, id, value, required }: Props) => { +}) => { return ( <> { ); }; - -export default RadioInputElement; diff --git a/src/components/SingleList.tsx b/src/components/SingleList.tsx index d43ead3..bca30bf 100644 --- a/src/components/SingleList.tsx +++ b/src/components/SingleList.tsx @@ -1,13 +1,15 @@ -import { Dispatch } from 'react'; import './SingleList.css'; +import { Dispatch } from 'react'; -type Props = { +export function SingleList({ + name, + path, + setListPath, +}: { name: string; path: string; setListPath: Dispatch; -}; - -export function SingleList({ name, path, setListPath }: Props) { +}) { function handleClick() { setListPath(path); } diff --git a/src/components/TextInputElement.tsx b/src/components/TextInputElement.tsx index bc6b326..d756e40 100644 --- a/src/components/TextInputElement.tsx +++ b/src/components/TextInputElement.tsx @@ -1,22 +1,20 @@ -import React, { ChangeEventHandler } from 'react'; +import { ChangeEventHandler } from 'react'; -type Props = { - label: string; - type: string; - id: string; - placeholder: string; - onChange?: ChangeEventHandler; - required: boolean; -}; - -const TextInputElement = ({ +export const TextInputElement = ({ label, type, id, placeholder, onChange, required, -}: Props) => { +}: { + label: string; + type: string; + id: string; + placeholder: string; + onChange?: ChangeEventHandler; + required: boolean; +}) => { return ( <> @@ -33,5 +31,3 @@ const TextInputElement = ({ ); }; - -export default TextInputElement; From cd979cd5d0ab09fc5389c64fa9f1a1877fdea0dd Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Fri, 27 Sep 2024 10:44:56 -0400 Subject: [PATCH 45/53] fix: instead of typing out every NavLink, map over navLinkOptions to display NavLinks --- src/views/Layout.tsx | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/views/Layout.tsx b/src/views/Layout.tsx index 298fcaa..829f4a3 100644 --- a/src/views/Layout.tsx +++ b/src/views/Layout.tsx @@ -1,18 +1,19 @@ -import React from 'react'; - +import './Layout.css'; /* eslint-disable jsx-a11y/anchor-is-valid */ import { Outlet, NavLink } from 'react-router-dom'; import { useAuth, SignInButton, SignOutButton } from '../api/useAuth'; -import './Layout.css'; +const navLinkOptions: { [key: string]: string } = { + '/': 'Home', + '/list': 'List', + '/manage-list': 'Manage List', +}; -/** - * TODO: The links defined in this file don't work! - * - * Instead of anchor element, they should use a component - * from `react-router-dom` to navigate to the routes - * defined in `App.jsx`. - */ +const navLinks = Object.entries(navLinkOptions).map(([path, linkText]) => ( + + {linkText} + +)); export function Layout() { const { user } = useAuth(); @@ -28,17 +29,7 @@ export function Layout() {
From edf3f70fd1da4e0341f36139f9b3951862dfbc7f Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Fri, 27 Sep 2024 10:45:37 -0400 Subject: [PATCH 46/53] feat: add exports to index.ts to keep imports shorter --- src/components/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/index.ts b/src/components/index.ts index 561a06a..062c3bc 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,2 +1,6 @@ -export * from './ListItem'; export * from './SingleList'; +export * from './ShareList'; +export * from './ListItem'; +export * from './AddItems'; +export * from './RadioInputElement'; +export * from './TextInputElement'; From b3a33359ccdfe948f22984c764be1ad70df358c9 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Fri, 27 Sep 2024 10:46:25 -0400 Subject: [PATCH 47/53] fix: remove redundant JSDocs --- src/utils/dates.ts | 40 ++++++++-------------------------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/src/utils/dates.ts b/src/utils/dates.ts index 096811f..6877aa7 100644 --- a/src/utils/dates.ts +++ b/src/utils/dates.ts @@ -5,10 +5,6 @@ export const ONE_DAY_IN_MILLISECONDS: number = 86400000; /** * Get a new JavaScript Date that is `offset` days in the future. - * @example - * // Returns a Date 3 days in the future - * addDaysFromToday(3) - * @param {number} daysOffset */ export function addDaysFromToday(daysOffset: number): Date { return new Date(Date.now() + daysOffset * ONE_DAY_IN_MILLISECONDS); @@ -17,14 +13,6 @@ export function addDaysFromToday(daysOffset: number): Date { /** * Calculates the estimated date for the next purchase based on current date, purchase history, * and total purchases. - * @param {Date} currentDate - The current date to calculate against. - * @param {Object} item - The item object containing purchase data. - * @param {Date} item.dateCreated - The date the item was created. - * @param {Date} item.dateNextPurchased - The previously estimated next purchase date. - * @param {Date|null} item.dateLastPurchased - The last date the item was actually purchased, or null if not purchased yet. - * @param {number} item.totalPurchases - The total number of purchases made for the item. - * @returns {Date} - The estimated date of the next purchase. - * @throws {Error} - Throws an error if the next purchase date cannot be calculated. */ export const calculateDateNextPurchased = ( currentDate: Date, @@ -51,9 +39,6 @@ export const calculateDateNextPurchased = ( /** * Calculate the number of days between two dates. - * @param {Date} earlierDate The starting date. - * @param {Date} laterDate The ending date. - * @returns {number} The number of days between the two dates. */ function getDaysBetweenDates(earlierDate: Date, laterDate: Date): number { return Math.floor( @@ -61,24 +46,18 @@ function getDaysBetweenDates(earlierDate: Date, laterDate: Date): number { ); } -type PurchaseIntervals = { - lastEstimatedInterval: number; - daysSinceLastPurchase: number; -}; - /** * Calculate the purchase intervals between current, next, and last purchase dates. - * @param {Date} currentDate The current date. - * @param {Date} dateNextPurchased The previously estimated next purchase date. - * @param {Date|null} dateLastPurchased The date the item was last purchased (can be null). - * @returns {Object} An object containing the last estimated interval and days since last purchase. */ function calculatePurchaseIntervals( currentDate: Date, dateCreated: Timestamp, dateNextPurchased: Timestamp, dateLastPurchased: Timestamp | null, -): PurchaseIntervals { +): { + lastEstimatedInterval: number; + daysSinceLastPurchase: number; +} { const lastPurchaseDate = dateLastPurchased?.toDate(); const lastEstimatedIntervalStartDate = @@ -99,15 +78,12 @@ function calculatePurchaseIntervals( /** * Calculate the next purchase estimate based on purchase intervals and total purchases. - * @param {Object} purchaseIntervals The intervals between the previous and current purchases. - * @param {number} purchaseIntervals.lastEstimatedInterval The previously estimated number of days between purchases. - * @param {number} purchaseIntervals.daysSinceLastPurchase The number of days since the last purchase. - * @param {number} totalPurchases The total number of purchases made. - * @returns {Date} The estimated next purchase date. - * @throws {Error} If an error occurs during the next purchase estimation process. */ function getNextPurchaseEstimate( - purchaseIntervals: PurchaseIntervals, + purchaseIntervals: { + lastEstimatedInterval: number; + daysSinceLastPurchase: number; + }, totalPurchases: number, ): Date { const { lastEstimatedInterval, daysSinceLastPurchase } = purchaseIntervals; From 21e73b2699e277cc32ad542238c257e10e14fba8 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Fri, 27 Sep 2024 10:47:43 -0400 Subject: [PATCH 48/53] fix: update imports props typing --- src/views/Home.tsx | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/views/Home.tsx b/src/views/Home.tsx index 9e06077..5e10aad 100644 --- a/src/views/Home.tsx +++ b/src/views/Home.tsx @@ -3,21 +3,26 @@ import { Dispatch } from 'react'; import { useNavigate } from 'react-router-dom'; import { DocumentData } from 'firebase/firestore'; import { createList } from '../api'; -import { SingleList } from '../components'; -import TextInputElement from '../components/TextInputElement'; +import { SingleList, TextInputElement } from '../components'; -type Props = { +export function Home({ + data, + setListPath, + userId, + userEmail, +}: { data: DocumentData[]; - setListPath: Dispatch; - userId: string | null | undefined; - userEmail: string | null | undefined; -}; - -export function Home({ data, setListPath, userId, userEmail }: Props) { + setListPath: Dispatch; + userId?: string | null; + userEmail?: string | null; +}) { const navigate = useNavigate(); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); + + if (!userId || !userEmail) return; + const form = event.target as HTMLFormElement; const listName = (form.elements.namedItem('list-name') as HTMLInputElement) .value; @@ -30,18 +35,14 @@ export function Home({ data, setListPath, userId, userEmail }: Props) { alert('The list already exists. Please enter a different name.'); return; } - const listPath = await createList(userId, userEmail, listName); - setListPath(listPath ?? null); - console.log(`Setting list path with: ${listPath}`); - console.log( - `LocalStorage listPath after state update: ${localStorage.getItem('tcl-shopping-list-path')}`, - ); + + setListPath(listPath); alert('List added'); navigate('/list'); } catch (err) { - console.error(err); alert('List not created'); + throw new Error(`${err instanceof Error ? err.message : err}`); } finally { form.reset(); } From c98404c4f2996949a3fe14b8ce8a506786982517 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Fri, 27 Sep 2024 10:48:46 -0400 Subject: [PATCH 49/53] feat: abstract normalizing logic into normalize.ts and add listPath to submit handler --- src/components/AddItems.tsx | 72 +++++++++++++++++-------------------- src/utils/normalize.ts | 20 +++++++++++ 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/src/components/AddItems.tsx b/src/components/AddItems.tsx index 7b28b27..302782d 100644 --- a/src/components/AddItems.tsx +++ b/src/components/AddItems.tsx @@ -1,26 +1,43 @@ import { useCallback } from 'react'; - -import { useStateWithStorage, normalizeItemName } from '../utils'; +import { useStateWithStorage, normalizeAndVerifyItem } from '../utils'; import { addItem } from '../api'; -import TextInputElement from './TextInputElement'; -import RadioInputElement from './RadioInputElement'; +import { RadioInputElement, TextInputElement } from '../components'; import { DocumentData } from 'firebase/firestore'; -type Props = { - items: DocumentData[]; -}; - const daysUntilPurchaseOptions = { Soon: 7, 'Kind of soon': 14, 'Not soon': 30, }; -export function AddItems({ items }: Props) { - const [listPath] = useStateWithStorage('tcl-shopping-list-path', null); +const addNormalizedItem = async ( + itemName: string, + daysUntilNextPurchase: string, + listPath: string, +) => { + try { + const isItemAdded = await addItem(listPath, { + itemName, + daysUntilNextPurchase, + }); + + if (isItemAdded) { + alert( + `${itemName} was added to the list! The next purchase date is set to ${daysUntilNextPurchase} days from now.`, + ); + } + } catch (error) { + alert( + `Item was not added to the database, Error: ${error instanceof Error ? error.message : error}`, + ); + } +}; + +export function AddItems({ items }: { items: DocumentData[] }) { + const [listPath] = useStateWithStorage('tcl-shopping-list-path', '/'); const handleSubmit = useCallback( - async (event: React.FormEvent) => { + (event: React.FormEvent) => { event.preventDefault(); const form = event.target as HTMLFormElement; @@ -32,40 +49,15 @@ export function AddItems({ items }: Props) { ).value; try { - if (itemName.trim() === '') { - alert('Please add an item name.'); - return; - } - // normalize the name by removing all punctuation and spaces to check if the normalized item is already in the list - const normalizedItemName = normalizeItemName(itemName); - console.log('Normalized new item:', normalizedItemName); - if (items) { - // normalize the existing list items to compare them to the new input - const currentItems = items.map((item) => - normalizeItemName(item.name), - ); - console.log('Normalized current items:', currentItems); - if (currentItems.includes(normalizedItemName)) { - alert('This item already exists in the list'); - return; - } - } - await addItem(listPath, { - itemName, - daysUntilNextPurchase, - }); - alert( - `${itemName} was added to the list! The next purchase date is set to ${daysUntilNextPurchase} days from now.`, - ); + const normalizedItemName = normalizeAndVerifyItem(items, itemName); + addNormalizedItem(normalizedItemName, daysUntilNextPurchase, listPath); } catch (error) { - alert( - `Item was not added to the database, Error: ${error instanceof Error ? error.message : error}`, - ); + alert(error); } finally { form.reset(); } }, - [listPath], + [listPath, items], ); return ( diff --git a/src/utils/normalize.ts b/src/utils/normalize.ts index a4e99ec..aac1935 100644 --- a/src/utils/normalize.ts +++ b/src/utils/normalize.ts @@ -1,6 +1,26 @@ +import { DocumentData } from 'firebase/firestore'; + export function normalizeItemName(itemName: string): string { return itemName .trim() .toLowerCase() .replace(/[&\/\\#, +$!,~%.'":*?<>{}]/g, ''); } + +export function normalizeAndVerifyItem( + items: DocumentData[], + itemName: string, +) { + const normalizedItemName = normalizeItemName(itemName); + + // normalize the existing list items to compare them to the new input + const currentItems = items.map((item) => { + return normalizeItemName(item.name); + }); + + if (currentItems.includes(normalizedItemName)) { + throw new Error('This item already exists in the list'); + } + + return normalizedItemName; +} From 64751501878096a2fad9bcc7aca846be5905b449 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Fri, 27 Sep 2024 10:50:24 -0400 Subject: [PATCH 50/53] fix: update the way props are typed to support intellisense hovers --- src/components/ListItem.tsx | 19 +++++++++---------- src/views/ManageList.tsx | 9 ++------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/components/ListItem.tsx b/src/components/ListItem.tsx index 4736243..97963d1 100644 --- a/src/components/ListItem.tsx +++ b/src/components/ListItem.tsx @@ -1,15 +1,9 @@ -import { useState } from 'react'; - import './ListItem.css'; +import { useState } from 'react'; import { updateItem, deleteItem } from '../api'; import { calculateDateNextPurchased, ONE_DAY_IN_MILLISECONDS } from '../utils'; import { DocumentData, Timestamp } from 'firebase/firestore'; -type Props = { - item: DocumentData; - listPath: string | null; -}; - const currentDate = new Date(); const calculateIsPurchased = (dateLastPurchased: Timestamp) => { @@ -24,7 +18,13 @@ const calculateIsPurchased = (dateLastPurchased: Timestamp) => { return currentDate < oneDayLater; }; -export function ListItem({ item, listPath }: Props) { +export function ListItem({ + item, + listPath, +}: { + item: DocumentData; + listPath: string; +}) { const [isPurchased, setIsPurchased] = useState(() => calculateIsPurchased(item.dateLastPurchased), ); @@ -40,10 +40,10 @@ export function ListItem({ item, listPath }: Props) { const handleChange = async () => { setIsPurchased(!isPurchased); + if (!isPurchased) { try { const updatedItem = updateItemOnPurchase(); - await updateItem(listPath, id, { ...updatedItem }); } catch (error) { alert( @@ -56,7 +56,6 @@ export function ListItem({ item, listPath }: Props) { const handleDeleteItem = async () => { if (confirm(`Are you sure you want to delete this item?`)) { try { - if (!listPath) return; await deleteItem(listPath, id); } catch (error) { alert( diff --git a/src/views/ManageList.tsx b/src/views/ManageList.tsx index f6757d1..2f4134d 100644 --- a/src/views/ManageList.tsx +++ b/src/views/ManageList.tsx @@ -1,12 +1,7 @@ import { DocumentData } from 'firebase/firestore'; -import { AddItems } from '../components/AddItems'; -import { ShareList } from '../components/ShareList'; +import { AddItems, ShareList } from '../components'; -type ManageListProps = { - items: DocumentData[]; -}; - -export function ManageList({ items }: ManageListProps) { +export function ManageList({ items }: { items: DocumentData[] }) { return (
From 84543676de6367325940d9b5b0102d6e7e83b1c9 Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Fri, 27 Sep 2024 10:51:57 -0400 Subject: [PATCH 51/53] fix: update imports and replace initial value from null to a string --- src/components/ShareList.tsx | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/ShareList.tsx b/src/components/ShareList.tsx index 0e48148..28aaa58 100644 --- a/src/components/ShareList.tsx +++ b/src/components/ShareList.tsx @@ -1,26 +1,27 @@ import { shareList, useAuth } from '../api'; import { useStateWithStorage } from '../utils'; -import TextInputElement from './TextInputElement'; +import { TextInputElement } from '../components'; export function ShareList() { - const [listPath] = useStateWithStorage('tcl-shopping-list-path', null); + const [listPath] = useStateWithStorage('tcl-shopping-list-path', '/'); const { user } = useAuth(); const userId = user?.uid; const userEmail = user?.email; const shareCurrentList = async (emailData: string) => { - if (!userId) return; - - const listShared = await shareList(listPath, userId, emailData); - - if (listShared === '!owner') { - alert('You cannot share the list you do not own.'); - } else if (listShared === 'shared') { - alert('List was shared with recipient.'); - } else { + if (!userId || !listPath) return; + try { + const listShared = await shareList(listPath, userId, emailData); + + if (listShared) { + alert('List was shared with recipient.'); + } + } catch (error) { alert( - "The list was not shared because the recipient's email address does not exist in the system.", + error instanceof Error + ? error.message + : 'An error occurred while sharing the list.', ); } }; From 5ec39f7459ebe2ede611bdf17cf5ad23f696188f Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Fri, 27 Sep 2024 10:52:27 -0400 Subject: [PATCH 52/53] feat: add navigate to Home in case listPath is null --- src/views/List.tsx | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/views/List.tsx b/src/views/List.tsx index a886860..b86b726 100644 --- a/src/views/List.tsx +++ b/src/views/List.tsx @@ -1,17 +1,23 @@ import React, { useState } from 'react'; - -import { ListItem } from '../components'; -import { AddItems } from '../components/AddItems'; -import TextInputElement from '../components/TextInputElement'; import { DocumentData } from 'firebase/firestore'; +import { useNavigate } from 'react-router-dom'; +import { ListItem, AddItems, TextInputElement } from '../components'; -type ListProps = { +export function List({ + items, + listPath, +}: { items: DocumentData[]; - listPath: string | null; -}; - -export function List({ items, listPath }: ListProps) { + listPath: string; +}) { const [searchItem, setSearchItem] = useState(''); + const navigate = useNavigate(); + + const navigateHome = () => { + alert('List path is not found'); + navigate('/home'); + return; + }; const handleTextChange = (event: React.ChangeEvent) => { setSearchItem(event.target.value); @@ -20,7 +26,6 @@ export function List({ items, listPath }: ListProps) { const filteredItems = items.filter((item) => item.name.toLowerCase().includes(searchItem.toLowerCase()), ); - console.log(`Current list path: ${listPath}`); const listName = listPath?.slice(listPath.indexOf('/') + 1); return ( @@ -46,11 +51,15 @@ export function List({ items, listPath }: ListProps) { label="Search Item:" /> -
    - {filteredItems.map((item) => { - return ; - })} -
+ {!listPath ? ( + navigateHome() + ) : ( +
    + {filteredItems.map((item) => ( + + ))} +
+ )} )} From 0400b4c8da818551ce59edbdcae5721128f6117b Mon Sep 17 00:00:00 2001 From: Nika Kolesnikova Date: Fri, 27 Sep 2024 10:52:53 -0400 Subject: [PATCH 53/53] fix: add MemoryRouter to support useNavigate --- tests/List.test.jsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/List.test.jsx b/tests/List.test.jsx index ab03b51..bb9f9e5 100644 --- a/tests/List.test.jsx +++ b/tests/List.test.jsx @@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react'; import { List } from '../src/views/List'; import { mockShoppingListData } from '../src/mocks/__fixtures__/shoppingListData'; import { useStateWithStorage } from '../src/utils'; +import { MemoryRouter } from 'react-router-dom'; vi.mock('../src/utils', () => ({ useStateWithStorage: vi.fn(), @@ -14,7 +15,13 @@ beforeEach(() => { describe('List Component', () => { test('renders the shopping list name, search field, and all list items from the data prop', () => { - render(); + render( + + {' '} + {/* Wrap in MemoryRouter */} + + , + ); expect(screen.getByText('groceries')).toBeInTheDocument(); expect(screen.getByLabelText('Search Item:')).toBeInTheDocument(); @@ -25,7 +32,13 @@ describe('List Component', () => { }); test('shows welcome message and AddItems component when no items are present', () => { - render(); + render( + + {' '} + {/* Wrap in MemoryRouter */} + + , + ); expect(screen.getByText('Welcome to groceries!')).toBeInTheDocument(); expect(screen.getByLabelText('Item Name:')).toBeInTheDocument();