diff --git a/README.md b/README.md index e9b474ba2..4e576d727 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,36 @@ # Project Todos -Replace this readme with your own information about your project. +A new and shiny Task List built with React and Redux. Add your task and check them of one-by-one and have fun! +Functions: -Start by briefly describing the assignment in a sentence or two. Keep it short and to the point. +- Add new task +- Remove Task +- Complete Task +- Auto save to local storage + +Tech: + +- HTML5 +- React +- Redux +- CSS ## The problem -Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next? +This was a great exercise to practice working with Redux, a great but sometimes a bit daunting tool. I had a joy building the app. + +Took me a bit of time to figure out the local storage to work with my reducers. Did not see the solution to update the whole store on removing of item in the list. + +### Whats next / Known bugs + +I wanted in my design in figma be able to hide and show the UI of the input field but had to skip it due to lack of time. + +Known bug: The completed counter wont show the correct value when removing items and the number of tasks is less than completed ones + +I will return to this and remove this section 😋 ## View it live -Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about. +[Figma](https://www.figma.com/file/eejBOLV4yFYACOmpYhUlqk/Untitled?node-id=1%3A41&t=6uCZSycN8MXLhhox-1) +[Live-Link](https://redux-tasklist-app.netlify.app/) +[Github](https://github.com/dannebrob/project-todos) diff --git a/code/.eslintrc.json b/code/.eslintrc.json index c9c0675c3..df5250df5 100644 --- a/code/.eslintrc.json +++ b/code/.eslintrc.json @@ -1,7 +1,5 @@ { - "extends": [ - "airbnb" - ], + "extends": ["airbnb"], "globals": { "document": true, "window": true, @@ -22,10 +20,9 @@ "modules": true } }, - "plugins": [ - "react-hooks" - ], + "plugins": ["react-hooks"], "rules": { + "linebreak-style": ["off", "unix"], "react/function-component-definition": [ 2, { @@ -42,10 +39,7 @@ "allowSingleLine": true } ], - "comma-dangle": [ - "error", - "never" - ], + "comma-dangle": ["error", "never"], "consistent-return": "off", "curly": "error", "eol-last": "off", diff --git a/code/.gitignore b/code/.gitignore deleted file mode 100644 index 4d29575de..000000000 --- a/code/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* diff --git a/code/package-lock.json b/code/package-lock.json index bb51e893e..292180b53 100644 --- a/code/package-lock.json +++ b/code/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@babel/eslint-parser": "^7.18.9", + "@reduxjs/toolkit": "^1.9.5", "eslint": "^8.21.0", "eslint-config-airbnb": "^19.0.4", "eslint-plugin-import": "^2.26.0", @@ -16,7 +17,8 @@ "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-redux": "^8.0.5" }, "devDependencies": { "react-scripts": "5.0.1" @@ -3124,6 +3126,29 @@ } } }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3621,6 +3646,15 @@ "@types/node": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -3695,6 +3729,11 @@ "integrity": "sha512-fOwvpvQYStpb/zHMx0Cauwywu9yLDmzWiiQBC7gJyq5tYLUXFZvDG7VK1B7WBxxjBJNKFOZ0zLoOQn8vmATbhw==", "dev": true }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, "node_modules/@types/q": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", @@ -3713,6 +3752,16 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, + "node_modules/@types/react": { + "version": "18.0.37", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.37.tgz", + "integrity": "sha512-4yaZZtkRN3ZIQD3KSEwkfcik8s0SWV+82dlJot1AbGYHCzJkWP3ENBY6wYeDRmKZ6HkrgoGAmR2HqdwYGp6OEw==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -3728,6 +3777,11 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true }, + "node_modules/@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + }, "node_modules/@types/serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", @@ -3768,6 +3822,11 @@ "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==", "dev": true }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "node_modules/@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", @@ -6095,6 +6154,11 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -8515,6 +8579,14 @@ "he": "bin/he" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -8800,10 +8872,9 @@ } }, "node_modules/immer": { - "version": "9.0.15", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.15.tgz", - "integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==", - "dev": true, + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -14375,6 +14446,49 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-redux": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz", + "integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -14531,6 +14645,22 @@ "node": "*" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "peerDependencies": { + "redux": "^4" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -14686,6 +14816,11 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -16340,6 +16475,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -19509,6 +19652,17 @@ "source-map": "^0.7.3" } }, + "@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "requires": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + } + }, "@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -19876,6 +20030,15 @@ "@types/node": "*" } }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -19950,6 +20113,11 @@ "integrity": "sha512-fOwvpvQYStpb/zHMx0Cauwywu9yLDmzWiiQBC7gJyq5tYLUXFZvDG7VK1B7WBxxjBJNKFOZ0zLoOQn8vmATbhw==", "dev": true }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, "@types/q": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", @@ -19968,6 +20136,16 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, + "@types/react": { + "version": "18.0.37", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.37.tgz", + "integrity": "sha512-4yaZZtkRN3ZIQD3KSEwkfcik8s0SWV+82dlJot1AbGYHCzJkWP3ENBY6wYeDRmKZ6HkrgoGAmR2HqdwYGp6OEw==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -19983,6 +20161,11 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true }, + "@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + }, "@types/serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", @@ -20023,6 +20206,11 @@ "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==", "dev": true }, + "@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", @@ -21748,6 +21936,11 @@ } } }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -23521,6 +23714,14 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, "hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -23739,10 +23940,9 @@ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" }, "immer": { - "version": "9.0.15", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.15.tgz", - "integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==", - "dev": true + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" }, "import-fresh": { "version": "3.3.0", @@ -27678,6 +27878,26 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-redux": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz", + "integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==", + "requires": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -27800,6 +28020,20 @@ } } }, + "redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "requires": {} + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -27924,6 +28158,11 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -29163,6 +29402,12 @@ "punycode": "^2.1.0" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/code/package.json b/code/package.json index 7aad26ebc..f971764b9 100644 --- a/code/package.json +++ b/code/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@babel/eslint-parser": "^7.18.9", + "@reduxjs/toolkit": "^1.9.5", "eslint": "^8.21.0", "eslint-config-airbnb": "^19.0.4", "eslint-plugin-import": "^2.26.0", @@ -11,7 +12,8 @@ "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-redux": "^8.0.5" }, "scripts": { "start": "react-scripts start", diff --git a/code/src/App.js b/code/src/App.js index f2007d229..377a7cd41 100644 --- a/code/src/App.js +++ b/code/src/App.js @@ -1,9 +1,24 @@ import React from 'react'; +import { Provider } from 'react-redux'; +import { combineReducers, configureStore } from '@reduxjs/toolkit'; + +import Tasklist from 'components/Tasklist'; +import Hero from 'components/Hero'; +import Input from 'components/Input'; +import { tasks } from './reducers/tasks'; + +const reducer = combineReducers({ + tasks: tasks.reducer +}); + +const store = configureStore({ reducer }); export const App = () => { return ( -
- Find me in src/app.js! -
+ + + + + ); -} +}; diff --git a/code/src/assets/add-square-svgrepo-com.svg b/code/src/assets/add-square-svgrepo-com.svg new file mode 100644 index 000000000..fc5cef8ce --- /dev/null +++ b/code/src/assets/add-square-svgrepo-com.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/code/src/assets/apples.png b/code/src/assets/apples.png new file mode 100644 index 000000000..652753e2b Binary files /dev/null and b/code/src/assets/apples.png differ diff --git a/code/src/assets/icons8-happy-85.png b/code/src/assets/icons8-happy-85.png new file mode 100644 index 000000000..8f530dcf8 Binary files /dev/null and b/code/src/assets/icons8-happy-85.png differ diff --git a/code/src/assets/icons8-trash.svg b/code/src/assets/icons8-trash.svg new file mode 100644 index 000000000..d79c3d107 --- /dev/null +++ b/code/src/assets/icons8-trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/code/src/components/EmptyState.css b/code/src/components/EmptyState.css new file mode 100644 index 000000000..757b8590b --- /dev/null +++ b/code/src/components/EmptyState.css @@ -0,0 +1,6 @@ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} diff --git a/code/src/components/EmptyState.js b/code/src/components/EmptyState.js new file mode 100644 index 000000000..a88685d2d --- /dev/null +++ b/code/src/components/EmptyState.js @@ -0,0 +1,17 @@ +import React from 'react'; + +import HappyFace from '../assets/icons8-happy-85.png'; + +import './EmptyState.css'; + +const EmptyState = () => { + return ( +
+ No tasks left +

No tasks left, lets celebrate!

+

... Or just add one above... I wont tell

+
+ ); +}; + +export default EmptyState; diff --git a/code/src/components/Hero.css b/code/src/components/Hero.css new file mode 100644 index 000000000..78681b38c --- /dev/null +++ b/code/src/components/Hero.css @@ -0,0 +1,12 @@ +.hero { + margin: auto; + height: 20vh; + background-image: url(../assets/apples.png); + background-size: cover; + overflow: hidden; +} +@media only screen and (min-width: 1042px) { + .hero { + margin-top: 10vh; + } +} diff --git a/code/src/components/Hero.js b/code/src/components/Hero.js new file mode 100644 index 000000000..d68d1e176 --- /dev/null +++ b/code/src/components/Hero.js @@ -0,0 +1,8 @@ +import React from 'react'; +import './Hero.css'; + +const Hero = () => { + return
; +}; + +export default Hero; diff --git a/code/src/components/Input.css b/code/src/components/Input.css new file mode 100644 index 000000000..75787e30b --- /dev/null +++ b/code/src/components/Input.css @@ -0,0 +1,103 @@ +/*--- Utility --- */ + +.line-through { + text-decoration: line-through; + color: gray; +} + +/* --- Input --- */ +.form-container { + width: 90%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +form { + display: flex; + flex-direction: column; + margin: 1rem 2rem; + width: 90%; +} +label { + display: flex; + flex-direction: column; + font-size: 1.5rem; + font-weight: 700; +} +input[type='text'] { + display: inline-block; + font-size: 1rem; +} + +form button[type='submit'] { + background-color: #57af60; + min-width: 3rem; + width: 100%; + max-width: 5rem; + font-size: 1rem; + border-radius: 10px; + margin: 0.3rem 0; + transition-timing-function: ease; + transition: 0.25s; +} + +form button[type='submit']:disabled { + background-color: #bdb9b9; + color: #868383; +} + +/* --- Task List --- */ + +.task-list { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.list-item { + display: flex; + width: 90%; + justify-content: space-between; + background-color: #ffffff; + margin: 0.5rem 0; + height: 3rem; + align-items: center; + border-radius: 10px; +} + +input[type='checkbox'] { + margin-left: 20px; + appearance: none; + background-color: #fff; + font: inherit; + color: red; + width: 1.15em; + height: 1.15em; + border: 0.15em solid currentColor; + border-radius: 50%; +} + +input[type='checkbox']:hover { + cursor: pointer; +} + +input[type='checkbox']:checked { + color: #57af60; + background-color: #57af60; +} + +button { + border: none; + background-color: inherit; +} + +button:hover, +button:focus { + cursor: pointer; +} +button > img { + width: 20px; +} diff --git a/code/src/components/Input.js b/code/src/components/Input.js new file mode 100644 index 000000000..270502cf1 --- /dev/null +++ b/code/src/components/Input.js @@ -0,0 +1,48 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable react/jsx-closing-bracket-location */ +/* eslint-disable jsx-a11y/label-has-associated-control */ +import React, { useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { tasks } from 'reducers/tasks'; +import './Input.css'; +import addImg from '../assets/add-square-svgrepo-com.svg'; + +// Add id npm package for new tasks id +const Input = () => { + const [inputValue, setInputValue] = useState(''); + const dispatch = useDispatch(); + const submitHandler = (event) => { + event.preventDefault(); + const newTask = { + id: Date.now().toString(), + text: inputValue, + complete: false + }; + dispatch(tasks.actions.addTask(newTask)); + setInputValue(''); + }; + return ( +
+
+ + +
+
+ ); +}; + +export default Input; diff --git a/code/src/components/Tasklist.css b/code/src/components/Tasklist.css new file mode 100644 index 000000000..48feb61ed --- /dev/null +++ b/code/src/components/Tasklist.css @@ -0,0 +1,3 @@ +.task-list-header-container { + width: 90%; +} diff --git a/code/src/components/Tasklist.js b/code/src/components/Tasklist.js new file mode 100644 index 000000000..b5e3980c4 --- /dev/null +++ b/code/src/components/Tasklist.js @@ -0,0 +1,70 @@ +/* eslint-disable max-len */ +/* eslint-disable no-restricted-globals */ +/* eslint-disable react/jsx-closing-bracket-location */ +/* eslint-disable no-unused-vars */ +import React, { useEffect } from 'react'; + +import { useSelector, useDispatch } from 'react-redux'; +import { tasks } from 'reducers/tasks'; +import EmptyState from './EmptyState'; +import './Tasklist.css'; +import TrashImg from '../assets/icons8-trash.svg'; + +const Tasklist = () => { + // The store + const items = useSelector((store) => store.tasks.tasks); + const dispatch = useDispatch(); + + const completedTasks = useSelector((store) => store.tasks.completedCount); + // Because completedTasks returns NaN if no tasks are compleat, this function returns 0 insted of NaN + // Known bug: is not correct when no items in list... + const unfinishedTasks = () => { + if (isNaN(completedTasks)) { + return '0'; + } else return Math.max(0, completedTasks); + }; + + // Loads all the tasks from Local Storage, if any + useEffect(() => { + const tasksFromLocalStorage = JSON.parse(localStorage.getItem('taskList')); + console.log(tasksFromLocalStorage); + if (tasksFromLocalStorage) { + dispatch(tasks.actions.setupStore(tasksFromLocalStorage)); + } + }, [dispatch]); + return ( +
+ {items.length === 0 ? ( + + ) : ( +
+

Tasks

+

+ Complted: {unfinishedTasks()} / {items.length} +

+
+ )} + {items.map((todo) => ( +
+ dispatch(tasks.actions.toggleChecked(todo))} + /> +

{todo.text}

+ +
+ ))} +
+ ); +}; + +export default Tasklist; diff --git a/code/src/index.css b/code/src/index.css index 4a1df4db7..add363da9 100644 --- a/code/src/index.css +++ b/code/src/index.css @@ -1,13 +1,110 @@ -body { +/* Box sizing rules */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +/* Remove default margin */ +body, +h1, +h2, +h3, +h4, +p, +figure, +blockquote, +dl, +dd { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", +} + +/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ +ul[role='list'], +ol[role='list'] { + list-style: none; + padding: 0; +} + +/* Remove default link styles */ +a { + text-decoration: none; + color: inherit; +} +a:visited { + color: inherit; +} +/* Set core root defaults */ +html:focus-within { + scroll-behavior: smooth; +} + +/* Set core body defaults */ +body { + text-rendering: optimizeSpeed; + line-height: 1.5; +} + +/* A elements that don't have a className get default styles */ +a:not([className]) { + text-decoration-skip-ink: auto; +} + +/* Make images easier to work with */ +img, +picture { + max-width: 100%; + display: block; +} + +/* Inherit fonts for inputs and buttons */ +input, +button, +textarea, +select { + font: inherit; +} + +li { + list-style: none; +} + +/* Remove all animations, transitions and smooth scroll for people that prefer not to see them */ +@media (prefers-reduced-motion: reduce) { + html:focus-within { + scroll-behavior: auto; + } + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} +html { + scroll-behavior: smooth; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + background-color: #f5f5f5; + width: 100vw; } -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", - monospace; +@media only screen and (min-width: 1042px) { + body { + display: flex; + justify-content: center; + } + + #root { + max-width: 70%; + } } diff --git a/code/src/reducers/hideInput.js b/code/src/reducers/hideInput.js new file mode 100644 index 000000000..339244f27 --- /dev/null +++ b/code/src/reducers/hideInput.js @@ -0,0 +1,14 @@ +/* eslint-disable no-unused-vars */ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = true; + +export const hideInputField = createSlice({ + name: 'hideInputField', + initialState, + reducers: { + hide: (store, action) => { + store.hide = !store.hide; + } + } +}); diff --git a/code/src/reducers/tasks.js b/code/src/reducers/tasks.js new file mode 100644 index 000000000..6fc2d1119 --- /dev/null +++ b/code/src/reducers/tasks.js @@ -0,0 +1,35 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + tasks: [], + completedCount: 0 +}; + +export const tasks = createSlice({ + name: 'tasks', + initialState, + reducers: { + setupStore: (store, action) => { + store.tasks = action.payload; + }, + addTask: (store, action) => { + store.tasks = [...store.tasks, action.payload]; + localStorage.setItem('taskList', JSON.stringify(store.tasks)); + }, + deleteTask: (store, action) => { + store.tasks = store.tasks.filter((task) => task.id !== action.payload.id); + localStorage.setItem('taskList', JSON.stringify(store.tasks)); + }, + toggleChecked: (store, action) => { + const { id } = action.payload; + store.tasks = store.tasks.map((task) => { + if (task.id === id) { + return { ...task, complete: !task.complete }; + } + return task; + }); + localStorage.setItem('taskList', JSON.stringify(store.tasks)); + store.completedCount = store.tasks.filter((task) => task.complete).length; + } + } +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..dcae2e183 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "project-todos", + "lockfileVersion": 2, + "requires": true, + "packages": {} +}