diff --git a/client/jsconfig.json b/client/jsconfig.json index b8d6842..10781e7 100644 --- a/client/jsconfig.json +++ b/client/jsconfig.json @@ -1,7 +1,12 @@ { "compilerOptions": { "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "@/hooks/*": ["./src/lib/hooks/*"], + "@/public/*": ["./public/*"], + "@/services/*": ["./src/lib/services/*"], + "@/store/*": ["./src/lib/store/*"], + "@/utils/*": ["./src/lib/utils/*"] } } } diff --git a/client/package-lock.json b/client/package-lock.json index 98675d8..56b640f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,11 +8,13 @@ "name": "client", "version": "0.1.0", "dependencies": { + "@reduxjs/toolkit": "^1.9.3", "eslint": "8.36.0", "eslint-config-next": "13.2.4", "next": "13.2.4", "react": "18.2.0", - "react-dom": "18.2.0" + "react-dom": "18.2.0", + "react-redux": "^8.0.5" }, "engines": { "node": "18.14.2", @@ -371,6 +373,29 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz", + "integrity": "sha512-GU2TNBQVofL09VGmuSioNPQIu6Ml0YLf4EJhgj0AvBadRlCGzUWet8372LjvO4fqKZF2vH1xU0htAa7BrK9pZg==", + "dependencies": { + "immer": "^9.0.16", + "redux": "^4.2.0", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.7" + }, + "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/@rushstack/eslint-patch": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", @@ -384,11 +409,45 @@ "tslib": "^2.4.0" } }, + "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/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "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/react": { + "version": "18.0.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz", + "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, + "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/@typescript-eslint/parser": { "version": "5.55.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.55.0.tgz", @@ -784,6 +843,11 @@ "node": ">= 8" } }, + "node_modules/csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -1793,6 +1857,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -1801,6 +1873,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "9.0.19", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.19.tgz", + "integrity": "sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2735,6 +2816,65 @@ "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/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/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", @@ -2756,6 +2896,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reselect": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz", + "integrity": "sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==" + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -3206,6 +3351,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/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/client/package.json b/client/package.json index f3c6a72..81fa310 100644 --- a/client/package.json +++ b/client/package.json @@ -15,10 +15,12 @@ "export": "npm run build && next export" }, "dependencies": { + "@reduxjs/toolkit": "^1.9.3", "eslint": "8.36.0", "eslint-config-next": "13.2.4", "next": "13.2.4", "react": "18.2.0", - "react-dom": "18.2.0" + "react-dom": "18.2.0", + "react-redux": "^8.0.5" } } diff --git a/client/src/common/layout/head/index.js b/client/src/common/layout/head/index.js new file mode 100644 index 0000000..13a2683 --- /dev/null +++ b/client/src/common/layout/head/index.js @@ -0,0 +1,12 @@ +import Head from 'next/head' + +export default function HeadComponent () { + return ( + + React Hooks Playground + + + + + ) +} diff --git a/client/src/common/layout/page/index.js b/client/src/common/layout/page/index.js new file mode 100644 index 0000000..5980e0e --- /dev/null +++ b/client/src/common/layout/page/index.js @@ -0,0 +1,13 @@ +import HeadComponent from '../head' + +export default function Page ({ children }) { + return ( +
+ + { children } +
+ ) +} diff --git a/client/src/common/ui/card/Card.module.css b/client/src/common/ui/card/Card.module.css new file mode 100644 index 0000000..7f38a9c --- /dev/null +++ b/client/src/common/ui/card/Card.module.css @@ -0,0 +1,8 @@ +.card { + border: 1px solid; + min-width: 400px; + max-width: 600px; + min-height: 100px; + border-radius: 16px; + padding: 16px; +} \ No newline at end of file diff --git a/client/src/common/ui/card/index.js b/client/src/common/ui/card/index.js new file mode 100644 index 0000000..d1d9b1e --- /dev/null +++ b/client/src/common/ui/card/index.js @@ -0,0 +1,11 @@ +import styles from './Card.module.css' + +function Card ({ children }) { + return ( +
+ {children} +
+ ) +} + +export default Card diff --git a/client/src/styles/Home.module.css b/client/src/components/home/Home.module.css similarity index 97% rename from client/src/styles/Home.module.css rename to client/src/components/home/Home.module.css index 27dfff5..8073c1c 100644 --- a/client/src/styles/Home.module.css +++ b/client/src/components/home/Home.module.css @@ -1,10 +1,10 @@ .main { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; + + height: 100vh; + display: grid; + place-content: center; padding: 6rem; - min-height: 100vh; + } .description { @@ -16,6 +16,7 @@ width: 100%; z-index: 2; font-family: var(--font-mono); + margin: auto; } .description a { @@ -72,6 +73,11 @@ max-width: 30ch; } +.h2 { + text-align: center; + margin-bottom: 24px; +} + .center { display: flex; justify-content: center; diff --git a/client/src/components/home/index.js b/client/src/components/home/index.js new file mode 100644 index 0000000..b6c20cc --- /dev/null +++ b/client/src/components/home/index.js @@ -0,0 +1,30 @@ +import Link from 'next/link' +import Page from '@/common/layout/page' + +import { Inter } from 'next/font/google' +import styles from './Home.module.css' +import navlinks from './items.json' + +const inter = Inter({ subsets: ['latin'] }) + +export default function HomeComponent() { + return ( + +
+
+
+

+ React Hooks Playground +

+
+ + {navlinks.map((item, index) => ( + + {item.name} + + ))} +
+
+
+ ) +} diff --git a/client/src/components/home/items.json b/client/src/components/home/items.json new file mode 100644 index 0000000..9deab84 --- /dev/null +++ b/client/src/components/home/items.json @@ -0,0 +1,14 @@ +[ + { + "name": "useSyncExternalStore", + "link": "/usesyncexternalstore" + }, + { + "name": "redux toolkit", + "link": "/redux" + }, + { + "name": "useState", + "link": "/usestate" + } +] \ No newline at end of file diff --git a/client/src/components/redux/index.js b/client/src/components/redux/index.js new file mode 100644 index 0000000..0a154f1 --- /dev/null +++ b/client/src/components/redux/index.js @@ -0,0 +1,54 @@ +import PropTypes from 'prop-types' +import Page from '@/common/layout/page' +import Card from '@/common/ui/card' +import TodoListComponent from '@/domain/redux/todolist' + +function ReduxComponent ({ + addTodo, + deleteTodo +}) { + return ( + +

+ Redux Toolkit +

+ +

Testing page re-renders and data rendering from a redux store inside a deeply-nested component.

+ + + +

+ + {/** A deeply-nested component */} + + + + + + + + + + + + + + +
+ + + hello + +
+ ) +} + +ReduxComponent.propTypes = { + addTodo: PropTypes.func, + deleteTodo: PropTypes.func +} + +export default ReduxComponent diff --git a/client/src/components/usestate/index.js b/client/src/components/usestate/index.js new file mode 100644 index 0000000..9b7c353 --- /dev/null +++ b/client/src/components/usestate/index.js @@ -0,0 +1,85 @@ +import { useState } from 'react' +import Card from '@/common/ui/card' +import Page from '@/common/layout/page' + +import TodoListComponentV3 from '@/domain/usestate/todolist' +import TodoListComponentFull from '@/domain/usestate/todolistfull' + +function UseStateComponent () { + const [state, setState] = useState([]) + + const addTodo = () => { + const data = [...state] + data.push({ + id: Math.random().toString(36).substring(2, 8), + text: 'Hello, wooorld!!' + }) + + setState(data) + } + + const deleteTodo = (id) => { + const temp = state.filter(item => item.id !== id) + setState(temp) + } + + return ( + +

+ useState +

+ +

Testing page re-renders and local state set by useState rendering from inside a deeply-nested component.

+
+ + +

+ +

ToDo list state passed from props

+
+ + {/** Renders a list of ToDo items passed from props */} + + + + + + + + + + + + + + +

+ +

ToDo list state isolated on an inner component

+
+ + {/** Renders a list of ToDo items with all local state inside the TodoListComponentFull component */} + + + + + + + + + + + + + +
+ ) +} + +export default UseStateComponent diff --git a/client/src/components/usesyncexternalstore/index.js b/client/src/components/usesyncexternalstore/index.js new file mode 100644 index 0000000..5e7687b --- /dev/null +++ b/client/src/components/usesyncexternalstore/index.js @@ -0,0 +1,57 @@ +import PropTypes from 'prop-types' +import Page from '@/common/layout/page' +import Card from '@/common/ui/card' +import TodoListComponentV2 from '@/domain/usesyncexternalstore/todolist' + +function UseSyncExternalStoreComponent ({ + addTodo, + deleteTodo +}) { + return ( + +

+ useSyncExternalStore +

+ +

+ Testing page re-renders and data rendering from a global variable set by useSyncExternalStore inside a deeply-nested component. +

+

+ + + +

+ + + + + + + + + + + + + + + +
+ + + Hello + +
+ ) +} + +UseSyncExternalStoreComponent.propTypes = { + addTodo: PropTypes.func, + deleteTodo: PropTypes.func +} + +export default UseSyncExternalStoreComponent diff --git a/client/src/domain/redux/todolist/index.js b/client/src/domain/redux/todolist/index.js new file mode 100644 index 0000000..c382d22 --- /dev/null +++ b/client/src/domain/redux/todolist/index.js @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types' +import { useSelector } from 'react-redux' + +function TodoListComponent ({ deleteTodo }) { + const {ids, entities: todos } = useSelector(state => state.todos) + + return ( + + ) +} + +TodoListComponent.propTypes = { + deleteTodo: PropTypes.func +} + +export default TodoListComponent diff --git a/client/src/domain/usestate/todolist/index.js b/client/src/domain/usestate/todolist/index.js new file mode 100644 index 0000000..29949ee --- /dev/null +++ b/client/src/domain/usestate/todolist/index.js @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types' + +function TodoListComponentV3 ({ + todos, + deleteTodo +}) { + return ( + + ) +} + +TodoListComponentV3.propTypes = { + todos: PropTypes.array, + deleteTodo: PropTypes.func +} + +export default TodoListComponentV3 diff --git a/client/src/domain/usestate/todolistfull/index.js b/client/src/domain/usestate/todolistfull/index.js new file mode 100644 index 0000000..aad705a --- /dev/null +++ b/client/src/domain/usestate/todolistfull/index.js @@ -0,0 +1,44 @@ +import { useState } from 'react' + +function TodoListComponentFull () { + const [todos, setState] = useState([]) + + const addTodo = () => { + const data = [...todos] + data.push({ + id: Math.random().toString(36).substring(2, 8), + text: 'Hi, wooorld!!' + }) + + setState(data) + } + + const deleteTodo = (id) => { + const temp = todos.filter(item => item.id !== id) + setState(temp) + } + + return ( + <> + +

+ + + + ) +} + +export default TodoListComponentFull diff --git a/client/src/domain/usesyncexternalstore/todolist/index.js b/client/src/domain/usesyncexternalstore/todolist/index.js new file mode 100644 index 0000000..6b85bfd --- /dev/null +++ b/client/src/domain/usesyncexternalstore/todolist/index.js @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types' +import useTodos from '@/lib/hooks/usetodo' + +function TodoListComponentV2 ({ + deleteTodo +}) { + const { todos } = useTodos() + + return ( + + ) +} + +TodoListComponentV2.propTypes = { + deleteTodo: PropTypes.func +} + +export default TodoListComponentV2 diff --git a/client/src/lib/hooks/usetodo.js b/client/src/lib/hooks/usetodo.js new file mode 100644 index 0000000..756752a --- /dev/null +++ b/client/src/lib/hooks/usetodo.js @@ -0,0 +1,74 @@ +import { useSyncExternalStore } from 'react' + +// https://react.dev/reference/react/useSyncExternalStore + +// Global item id +let nextId = 0 + +// Global store containing an array of objects +let todos = [{ id: nextId++, text: 'Todo #1' }] + +// Internal listeners for each method in todoStore initialized by useSyncExternalStore. +// Needs to call emitChange() to take effect +let listeners = [] + +/** + * Exportable hook that uses useSyncExternalStore and a global variable to store data. + * Usage: const { todos, addTodo, deleteTodo } = useTodos() + * @returns {Object} { todos, addTodo, deleteTodo } + */ +export default function useTodos () { + const todos = useSyncExternalStore( + todoStore.subscribe, + todoStore.getSnapshot, + todoStore.getServerSnapshot + ) + + return { + todos, + addTodo: todoStore.addTodo, + deleteTodo: todoStore.deleteTodo + } +} + +/** + * The external store with available useSyncExternalStore-required methods: + * subscribe() + * - Used and initialized internally by useSyncExternalStore for subscribing to external store events. + * - This should return a clean-up function like in useEffect() with empty dependency arrays. + * getSnapshot() - Returns the current (full) snapshot of the global data variable + * getServerSnapshot() - Used in SSR + */ +export const todoStore = { + addTodo () { + todos = [ ...todos, { id: nextId++, text: 'Todo #' + nextId }] + emitChange() + }, + + deleteTodo (id) { + todos = todos.filter(item => item.id !== id) + emitChange() + }, + + subscribe (listener) { + listeners = [...listeners, listener] + + return () => { + listeners = listeners.filter(item => item !== listener) + } + }, + + getSnapshot () { + return todos + }, + + getServerSnapshot () { + return todos + } +} + +function emitChange () { + for (let listener of listeners) { + listener() + } +} diff --git a/client/src/lib/store/store.js b/client/src/lib/store/store.js new file mode 100644 index 0000000..8e86db4 --- /dev/null +++ b/client/src/lib/store/store.js @@ -0,0 +1,18 @@ +import { combineReducers } from 'redux' +import { configureStore } from '@reduxjs/toolkit' + +import todoSlice from '@/lib/store/todos/todoSlice' + +// Reducers +const combinedReducer = combineReducers({ + todos: todoSlice +}) + +const rootReducer = (state, action) => { + return combinedReducer(state, action) +} + +// Global store +export const store = configureStore({ + reducer: rootReducer +}) diff --git a/client/src/lib/store/todos/todoSlice.js b/client/src/lib/store/todos/todoSlice.js new file mode 100644 index 0000000..dc1f51a --- /dev/null +++ b/client/src/lib/store/todos/todoSlice.js @@ -0,0 +1,54 @@ +// Notes: +// https://redux.js.org/tutorials/essentials/part-6-performance-normalization#normalized-state-structure +// https://redux.js.org/tutorials/essentials/part-6-performance-normalization#optimizing-the-posts-list + +import { + createSlice, + createEntityAdapter +} from '@reduxjs/toolkit' + +const STATES = { + IDLE: 'idle', + PENDING: 'pending' +} + +// Entiti adapter +const todosAdapter = createEntityAdapter({ + selectId: (todo) => todo.id +}) + +// Slice +const todoSlice = createSlice({ + name: 'todos', + initialState: todosAdapter.getInitialState({ + loading: STATES.IDLE, + error: '', + success: '', + todo: null + }), + reducers: { + todoReceived (state, action) { + const id = Math.random().toString(36).substring(2, 8) + + state.loading = STATES.IDLE + state.todo = { ...action.payload, id } + todosAdapter.addOne(state, state.todo) + + }, + todoDelete (state, action) { + todosAdapter.removeOne(state, action.payload) + }, + todosReceived (state, action) { + state.loading = STATES.IDLE + todosAdapter.setAll(state, action.payload) + } + } +}) + +export const { + todoReceived, + todosReceived, + todoDelete +} = todoSlice.actions + +export default todoSlice.reducer diff --git a/client/src/pages/_app.js b/client/src/pages/_app.js index 2300201..bf9ec3f 100644 --- a/client/src/pages/_app.js +++ b/client/src/pages/_app.js @@ -1,5 +1,12 @@ +import { Provider } from 'react-redux' +import { store } from '@/store/store' + import '@/styles/globals.css' export default function App({ Component, pageProps }) { - return + return ( + + + + ) } diff --git a/client/src/pages/index.js b/client/src/pages/index.js index 36e2dd1..5ca4058 100644 --- a/client/src/pages/index.js +++ b/client/src/pages/index.js @@ -1,123 +1,5 @@ -import Head from 'next/head' -import Image from 'next/image' -import { Inter } from 'next/font/google' -import styles from '@/styles/Home.module.css' - -const inter = Inter({ subsets: ['latin'] }) +import HomeComponent from '@/components/home' export default function Home() { - return ( - <> - - Create Next App - - - - -
-
-

- Get started by editing  - src/pages/index.js -

-
- - By{' '} - Vercel Logo - -
-
- -
- Next.js Logo -
- 13 -
-
- -
- -

- Docs -> -

-

- Find in-depth information about Next.js features and API. -

-
- - -

- Learn -> -

-

- Learn about Next.js in an interactive course with quizzes! -

-
- - -

- Templates -> -

-

- Discover and deploy boilerplate example Next.js projects. -

-
- - -

- Deploy -> -

-

- Instantly deploy your Next.js site to a shareable URL - with Vercel. -

-
-
-
- - ) + return () } diff --git a/client/src/pages/redux/index.js b/client/src/pages/redux/index.js new file mode 100644 index 0000000..d25077b --- /dev/null +++ b/client/src/pages/redux/index.js @@ -0,0 +1,27 @@ +import { useDispatch } from 'react-redux' +import { todoReceived, todoDelete } from '@/lib/store/todos/todoSlice' + +import ReduxComponent from '@/components/redux' + +function ReduxContainer () { + const dispatch = useDispatch() + + const addTodo = () => { + dispatch(todoReceived({ + text: 'Hello, world!' + })) + } + + const deleteTodo = (id) => { + dispatch(todoDelete(id)) + } + + return ( + + ) +} + +export default ReduxContainer diff --git a/client/src/pages/usestate/index.js b/client/src/pages/usestate/index.js new file mode 100644 index 0000000..c9fa64c --- /dev/null +++ b/client/src/pages/usestate/index.js @@ -0,0 +1,9 @@ +import UseStateComponent from '@/components/usestate' + +function UseStateContainer () { + return ( + + ) +} + +export default UseStateContainer diff --git a/client/src/pages/usesyncexternalstore/index.js b/client/src/pages/usesyncexternalstore/index.js new file mode 100644 index 0000000..078356c --- /dev/null +++ b/client/src/pages/usesyncexternalstore/index.js @@ -0,0 +1,15 @@ +import UseSyncExternalStoreComponent from '@/components/usesyncexternalstore' +import useTodos from '@/lib/hooks/usetodo' + +function UseSyncExternalStore () { + const { addTodo, deleteTodo } = useTodos() + + return ( + + ) +} + +export default UseSyncExternalStore diff --git a/client/src/styles/globals.css b/client/src/styles/globals.css index d4f491e..7d2317e 100644 --- a/client/src/styles/globals.css +++ b/client/src/styles/globals.css @@ -1,3 +1,7 @@ +html, body { + font-family: Arial, Helvetica, sans-serif; +} + :root { --max-width: 1100px; --border-radius: 12px; @@ -87,12 +91,6 @@ body { body { color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); } a {