diff --git a/package-lock.json b/package-lock.json index f75fba9..a7e4aeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", + "axios": "^1.3.4", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^8.0.4", @@ -4998,6 +4999,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz", + "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -13883,6 +13907,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -20570,6 +20599,28 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.5.0.tgz", "integrity": "sha512-4+rr8eQ7+XXS5nZrKcMO/AikHL0hVqy+lHWAnE3xdHl+aguag8SOQ6eEqLexwLNWgXIMfunGuD3ON1/6Kyet0A==" }, + "axios": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz", + "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -26846,6 +26897,11 @@ } } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/package.json b/package.json index 5f675d5..3d98464 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", + "axios": "^1.3.4", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^8.0.4", diff --git a/public/index.html b/public/index.html index 1dbdca6..3428c19 100644 --- a/public/index.html +++ b/public/index.html @@ -15,6 +15,10 @@ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ --> + + + + - React Redux App + React App @@ -39,5 +43,6 @@ To begin the development, run `npm start` or `yarn start`. To create a production bundle, use `npm run build` or `yarn build`. --> + diff --git a/src/App.js b/src/App.js index a40f66d..c6c98ae 100644 --- a/src/App.js +++ b/src/App.js @@ -1,56 +1,18 @@ -import React from 'react'; -import logo from './logo.svg'; -import { Counter } from './features/counter/Counter'; -import './App.css'; +import Create from "./components/Create"; +import List from "./components/List"; + function App() { return ( -
-
- logo - -

- Edit src/App.js and save to reload. -

- - Learn - - React - - , - - Redux - - , - - Redux Toolkit - - , and - - React Redux - - -
+
+
+
+ +
+
+ +
+
); } diff --git a/src/app/store.js b/src/app/store.js index 9eca6d2..78397af 100644 --- a/src/app/store.js +++ b/src/app/store.js @@ -1,8 +1,8 @@ -import { configureStore } from '@reduxjs/toolkit'; -import counterReducer from '../features/counter/counterSlice'; +import { configureStore } from "@reduxjs/toolkit"; +import crudReucer from "../features/crud/crudSlice"; export const store = configureStore({ - reducer: { - counter: counterReducer, - }, + reducer: { + crud: crudReucer, + }, }); diff --git a/src/components/Create.js b/src/components/Create.js new file mode 100644 index 0000000..7f1b9e7 --- /dev/null +++ b/src/components/Create.js @@ -0,0 +1,96 @@ +import React, { useEffect, useState } from "react"; +import { + createData, + changeData, + fetchPlaceholderList, +} from "../features/crud/crudSlice"; +import { useDispatch, useSelector } from "react-redux"; + +const Create = () => { + const dispatch = useDispatch(); + const { editing } = useSelector((state) => state.crud) || {}; + + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [editMode, setEditMode] = useState(false); + + const handleCreate = async (e) => { + e.preventDefault(); + dispatch( + createData({ + name, + email, + }) + ); + reset(); + }; + + const handleUpdate = (e) => { + e.preventDefault(); + + dispatch( + changeData({ + id: editing?.id, + data: { + name: name, + email: email, + }, + }) + ); + setEditMode(false); + reset(); + }; + + const reset = () => { + setName(""); + setEmail(""); + }; + + // listen for edit mode active + useEffect(() => { + const { id, name, email } = editing || {}; + if (id) { + setEditMode(true); + setName(name); + setEmail(email); + } else { + setEditMode(false); + reset(); + } + }, [editing]); + + return ( + <> +

{editMode ? "Update" : "Create"}

+ +
+
+ + setName(e.target.value)} + /> +
+
+ + setEmail(e.target.value)} + /> +
+ + +
+ + ); +}; + +export default Create; diff --git a/src/components/List.js b/src/components/List.js new file mode 100644 index 0000000..f654d9f --- /dev/null +++ b/src/components/List.js @@ -0,0 +1,83 @@ +import React, { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { editActive, fetchPlaceholderList, removeData, trushData } from "../features/crud/crudSlice"; +const List = () => { + const dispatch = useDispatch(); + const [post, setpost] = useState([]); + const { isLoading } = useSelector((state) => state.crud); + + useEffect(() => { + dispatch(fetchPlaceholderList()); + }, [dispatch]); + +useEffect(() => { + const postData = localStorage.getItem("placeholderList"); + const jsonData = JSON.parse(postData); + setpost(jsonData); + +},[isLoading]) + + + +const handleEdit = (index) => { + dispatch(editActive(index)); + localStorage.setItem('editIndex', index.id) + +}; + +const handleDelete = (id) => { + // need for server side + dispatch(removeData(id)); + // locally reomove data + dispatch(trushData(id)) + const postData = localStorage.getItem("placeholderList"); + const jsonData = JSON.parse(postData); + setpost(jsonData); + + +}; + + return ( + <> +

List

+ + + + + + + + + + + {post?.map((item, i) => { + return ( + + + + + + + ); + })} + +
#NameEmailAction
{i}{item.name}{item.email} + + +
+ ​ + + ); +}; + +export default List; diff --git a/src/features/counter/Counter.js b/src/features/counter/Counter.js deleted file mode 100644 index 772a6ba..0000000 --- a/src/features/counter/Counter.js +++ /dev/null @@ -1,67 +0,0 @@ -import React, { useState } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import { - decrement, - increment, - incrementByAmount, - incrementAsync, - incrementIfOdd, - selectCount, -} from './counterSlice'; -import styles from './Counter.module.css'; - -export function Counter() { - const count = useSelector(selectCount); - const dispatch = useDispatch(); - const [incrementAmount, setIncrementAmount] = useState('2'); - - const incrementValue = Number(incrementAmount) || 0; - - return ( -
-
- - {count} - -
-
- setIncrementAmount(e.target.value)} - /> - - - -
-
- ); -} diff --git a/src/features/counter/Counter.module.css b/src/features/counter/Counter.module.css deleted file mode 100644 index 004ae33..0000000 --- a/src/features/counter/Counter.module.css +++ /dev/null @@ -1,78 +0,0 @@ -.row { - display: flex; - align-items: center; - justify-content: center; -} - -.row > button { - margin-left: 4px; - margin-right: 8px; -} -.row:not(:last-child) { - margin-bottom: 16px; -} - -.value { - font-size: 78px; - padding-left: 16px; - padding-right: 16px; - margin-top: 2px; - font-family: 'Courier New', Courier, monospace; -} - -.button { - appearance: none; - background: none; - font-size: 32px; - padding-left: 12px; - padding-right: 12px; - outline: none; - border: 2px solid transparent; - color: rgb(112, 76, 182); - padding-bottom: 4px; - cursor: pointer; - background-color: rgba(112, 76, 182, 0.1); - border-radius: 2px; - transition: all 0.15s; -} - -.textbox { - font-size: 32px; - padding: 2px; - width: 64px; - text-align: center; - margin-right: 4px; -} - -.button:hover, -.button:focus { - border: 2px solid rgba(112, 76, 182, 0.4); -} - -.button:active { - background-color: rgba(112, 76, 182, 0.2); -} - -.asyncButton { - composes: button; - position: relative; -} - -.asyncButton:after { - content: ''; - background-color: rgba(112, 76, 182, 0.15); - display: block; - position: absolute; - width: 100%; - height: 100%; - left: 0; - top: 0; - opacity: 0; - transition: width 1s linear, opacity 0.5s ease 1s; -} - -.asyncButton:active:after { - width: 0%; - opacity: 1; - transition: 0s; -} diff --git a/src/features/counter/counterAPI.js b/src/features/counter/counterAPI.js deleted file mode 100644 index cc9b4a4..0000000 --- a/src/features/counter/counterAPI.js +++ /dev/null @@ -1,6 +0,0 @@ -// A mock function to mimic making an async request for data -export function fetchCount(amount = 1) { - return new Promise((resolve) => - setTimeout(() => resolve({ data: amount }), 500) - ); -} diff --git a/src/features/counter/counterSlice.js b/src/features/counter/counterSlice.js deleted file mode 100644 index 8dc4b5c..0000000 --- a/src/features/counter/counterSlice.js +++ /dev/null @@ -1,73 +0,0 @@ -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; -import { fetchCount } from './counterAPI'; - -const initialState = { - value: 0, - status: 'idle', -}; - -// The function below is called a thunk and allows us to perform async logic. It -// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This -// will call the thunk with the `dispatch` function as the first argument. Async -// code can then be executed and other actions can be dispatched. Thunks are -// typically used to make async requests. -export const incrementAsync = createAsyncThunk( - 'counter/fetchCount', - async (amount) => { - const response = await fetchCount(amount); - // The value we return becomes the `fulfilled` action payload - return response.data; - } -); - -export const counterSlice = createSlice({ - name: 'counter', - initialState, - // The `reducers` field lets us define reducers and generate associated actions - reducers: { - increment: (state) => { - // Redux Toolkit allows us to write "mutating" logic in reducers. It - // doesn't actually mutate the state because it uses the Immer library, - // which detects changes to a "draft state" and produces a brand new - // immutable state based off those changes - state.value += 1; - }, - decrement: (state) => { - state.value -= 1; - }, - // Use the PayloadAction type to declare the contents of `action.payload` - incrementByAmount: (state, action) => { - state.value += action.payload; - }, - }, - // The `extraReducers` field lets the slice handle actions defined elsewhere, - // including actions generated by createAsyncThunk or in other slices. - extraReducers: (builder) => { - builder - .addCase(incrementAsync.pending, (state) => { - state.status = 'loading'; - }) - .addCase(incrementAsync.fulfilled, (state, action) => { - state.status = 'idle'; - state.value += action.payload; - }); - }, -}); - -export const { increment, decrement, incrementByAmount } = counterSlice.actions; - -// The function below is called a selector and allows us to select a value from -// the state. Selectors can also be defined inline where they're used instead of -// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)` -export const selectCount = (state) => state.counter.value; - -// We can also write thunks by hand, which may contain both sync and async logic. -// Here's an example of conditionally dispatching actions based on current state. -export const incrementIfOdd = (amount) => (dispatch, getState) => { - const currentValue = selectCount(getState()); - if (currentValue % 2 === 1) { - dispatch(incrementByAmount(amount)); - } -}; - -export default counterSlice.reducer; diff --git a/src/features/counter/counterSlice.spec.js b/src/features/counter/counterSlice.spec.js deleted file mode 100644 index c1fed2c..0000000 --- a/src/features/counter/counterSlice.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import counterReducer, { - increment, - decrement, - incrementByAmount, -} from './counterSlice'; - -describe('counter reducer', () => { - const initialState = { - value: 3, - status: 'idle', - }; - it('should handle initial state', () => { - expect(counterReducer(undefined, { type: 'unknown' })).toEqual({ - value: 0, - status: 'idle', - }); - }); - - it('should handle increment', () => { - const actual = counterReducer(initialState, increment()); - expect(actual.value).toEqual(4); - }); - - it('should handle decrement', () => { - const actual = counterReducer(initialState, decrement()); - expect(actual.value).toEqual(2); - }); - - it('should handle incrementByAmount', () => { - const actual = counterReducer(initialState, incrementByAmount(2)); - expect(actual.value).toEqual(5); - }); -}); diff --git a/src/features/crud/crudApi.js b/src/features/crud/crudApi.js new file mode 100644 index 0000000..211abe7 --- /dev/null +++ b/src/features/crud/crudApi.js @@ -0,0 +1,25 @@ +import axios from "axios"; + +export const getplaceholderData = async () => { + const response = await axios.get("https://jsonplaceholder.typicode.com/users"); + + return response.data; +}; + +export const create = async (data) => { + const response = await axios.post("https://jsonplaceholder.typicode.com/users", data); + + return response.data; +}; + +export const editData = async (id, data) => { + const response = await axios.put(`https://jsonplaceholder.typicode.com/users/${id}`, data); + + return response.data; +}; + +export const deleteData = async (id) => { + const response = axios.delete(`https://jsonplaceholder.typicode.com/users/${id}`); + + return response.data; +}; \ No newline at end of file diff --git a/src/features/crud/crudSlice.js b/src/features/crud/crudSlice.js new file mode 100644 index 0000000..23ce1d9 --- /dev/null +++ b/src/features/crud/crudSlice.js @@ -0,0 +1,158 @@ +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; +import { getplaceholderData, create, editData, deleteData } from "./crudApi"; + +const initialState = { + posts: [], + isLoading: false, + isError: false, + error: "", + editing: {}, +}; + +// async thunks +export const fetchPlaceholderList = createAsyncThunk( + "crud/fetchPlaceholderList", + async () => { + const listData = await getplaceholderData(); + // console.log('list data', listData); + return listData; + } +); + +export const createData = createAsyncThunk("crud/createData", async (data) => { + const creates = await create(data); + return creates; +}); + +export const changeData = createAsyncThunk( + "crud/changeData", + async ({ id, data }) => { + const edit = await editData(id, data); + return edit; + } +); + +export const removeData = createAsyncThunk("crud/removeData", async (id) => { + console.log("id thank", id); + const DataDelete = await deleteData(id); + return DataDelete; +}); + +// create slice +const crudSlice = createSlice({ + name: "crud", + initialState, + reducers: { + editActive: (state, action) => { + state.editing = action.payload; + }, + editInActive: (state) => { + state.editing = {}; + }, + trushData: (state, action) => { + state.isLoading = true; + let blogs = + localStorage.getItem("placeholderList") && + localStorage.getItem("placeholderList").length > 0 + ? JSON.parse(localStorage.getItem("placeholderList")) + : []; + + const _blogs = blogs.filter((blog, blogInIndex) => { + if (blog.id !== action.payload) { + return blog; + } + }); + console.log(_blogs); + localStorage.setItem("placeholderList", JSON.stringify(_blogs)); + state.isLoading = false; + } + }, + extraReducers: (builder) => { + builder + .addCase(fetchPlaceholderList.pending, (state) => { + state.isError = false; + state.isLoading = true; + }) + .addCase(fetchPlaceholderList.fulfilled, (state, action) => { + state.isError = false; + state.isLoading = false; + state.posts = action.payload; + localStorage.setItem("placeholderList", JSON.stringify(action.payload)); + }) + .addCase(fetchPlaceholderList.rejected, (state, action) => { + state.isLoading = false; + state.isError = true; + state.error = action.error?.message; + state.posts = []; + }) + .addCase(createData.pending, (state) => { + state.isError = false; + state.isLoading = true; + }) + .addCase(createData.fulfilled, (state, action) => { + state.isError = false; + state.isLoading = false; + const _blogs = + localStorage.getItem("placeholderList") && + localStorage.getItem("placeholderList").length > 0 + ? JSON.parse(localStorage.getItem("placeholderList")) + : []; + localStorage.setItem( + "placeholderList", + JSON.stringify([..._blogs, action.payload]) + ); + }) + .addCase(createData.rejected, (state, action) => { + state.isLoading = false; + state.isError = true; + state.error = action.error?.message; + }) + .addCase(changeData.pending, (state) => { + state.isError = false; + state.isLoading = true; + }) + .addCase(changeData.fulfilled, (state, action) => { + console.log("action,", action.payload); + const { name, email } = action.payload; + state.isError = false; + state.isLoading = false; + let blogs = + localStorage.getItem("placeholderList") && + localStorage.getItem("placeholderList").length > 0 + ? JSON.parse(localStorage.getItem("placeholderList")) + : []; + + const _blogs = blogs.map((blog, blogInIndex) => { + if (blog.id == localStorage.getItem("editIndex")) { + return { name, email }; + } else { + return blog; + } + }); + localStorage.setItem("placeholderList", JSON.stringify(_blogs)); + }) + .addCase(changeData.rejected, (state, action) => { + state.isLoading = false; + state.isError = true; + state.error = action.error?.message; + }) + .addCase(removeData.pending, (state) => { + state.isError = false; + state.isLoading = true; + }) + .addCase(removeData.fulfilled, (state, action) => { + console.log("action deleted", action); + state.isError = false; + state.isLoading = false; + + }) + .addCase(removeData.rejected, (state, action) => { + state.isLoading = false; + state.isError = true; + state.error = action.error?.message; + }); + }, +}); + +export default crudSlice.reducer; +export const { editActive, editInActive, trushData } = crudSlice.actions; diff --git a/src/index.js b/src/index.js index 732a8cc..29340de 100644 --- a/src/index.js +++ b/src/index.js @@ -1,20 +1,19 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { Provider } from 'react-redux'; -import { store } from './app/store'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; -import './index.css'; +import React from "react"; +import { createRoot } from "react-dom/client"; +import { Provider } from "react-redux"; +import App from "./App"; +import { store } from "./app/store"; +import reportWebVitals from "./reportWebVitals"; -const container = document.getElementById('root'); +const container = document.getElementById("root"); const root = createRoot(container); root.render( - - - - - + + + + + ); // If you want to start measuring performance in your app, pass a function