diff options
author | Adrián Oliva <adrian.oliva@cimat.mx> | 2023-05-16 19:52:29 -0600 |
---|---|---|
committer | Adrián Oliva <adrian.oliva@cimat.mx> | 2023-05-16 19:52:29 -0600 |
commit | 17c1d3d2070ee0c060c3e71a9e5868f004b2b034 (patch) | |
tree | c0e7cf17901196b089291c63e8f97058068aed35 /src | |
download | ToDo-App-FE-17c1d3d2070ee0c060c3e71a9e5868f004b2b034.tar.gz ToDo-App-FE-17c1d3d2070ee0c060c3e71a9e5868f004b2b034.zip |
Start point.
Visit https://github.com/nvh95/vite-react-template-redux to see original
template.
Diffstat (limited to 'src')
-rw-r--r-- | src/App.css | 39 | ||||
-rw-r--r-- | src/App.jsx | 58 | ||||
-rw-r--r-- | src/App.test.jsx | 15 | ||||
-rw-r--r-- | src/app/store.js | 8 | ||||
-rw-r--r-- | src/features/counter/Counter.jsx | 67 | ||||
-rw-r--r-- | src/features/counter/Counter.module.css | 78 | ||||
-rw-r--r-- | src/features/counter/counterAPI.js | 6 | ||||
-rw-r--r-- | src/features/counter/counterSlice.js | 73 | ||||
-rw-r--r-- | src/features/counter/counterSlice.spec.js | 33 | ||||
-rw-r--r-- | src/index.css | 13 | ||||
-rw-r--r-- | src/logo.svg | 1 | ||||
-rw-r--r-- | src/main.jsx | 14 |
12 files changed, 405 insertions, 0 deletions
diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..01cc586 --- /dev/null +++ b/src/App.css @@ -0,0 +1,39 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-float infinite 3s ease-in-out; + } +} + +.App-header { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); +} + +.App-link { + color: rgb(112, 76, 182); +} + +@keyframes App-logo-float { + 0% { + transform: translateY(0); + } + 50% { + transform: translateY(10px); + } + 100% { + transform: translateY(0px); + } +} diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..e28ac6e --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,58 @@ +import React from "react"; +import logo from "./logo.svg"; +import { Counter } from "./features/counter/Counter"; +import "./App.css"; + +function App() { + return ( + <div className="App"> + <header className="App-header"> + <img src={logo} className="App-logo" alt="logo" /> + <Counter /> + <p> + Edit <code>src/App.js</code> and save to reload. + </p> + <span> + <span>Learn </span> + <a + className="App-link" + href="https://reactjs.org/" + target="_blank" + rel="noopener noreferrer" + > + React + </a> + <span>, </span> + <a + className="App-link" + href="https://redux.js.org/" + target="_blank" + rel="noopener noreferrer" + > + Redux + </a> + <span>, </span> + <a + className="App-link" + href="https://redux-toolkit.js.org/" + target="_blank" + rel="noopener noreferrer" + > + Redux Toolkit + </a> + ,<span> and </span> + <a + className="App-link" + href="https://react-redux.js.org/" + target="_blank" + rel="noopener noreferrer" + > + React Redux + </a> + </span> + </header> + </div> + ); +} + +export default App; diff --git a/src/App.test.jsx b/src/App.test.jsx new file mode 100644 index 0000000..e4874f8 --- /dev/null +++ b/src/App.test.jsx @@ -0,0 +1,15 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Provider } from "react-redux"; +import { store } from "./app/store"; +import App from "./App"; + +test("renders learn react link", () => { + const { getByText } = render( + <Provider store={store}> + <App /> + </Provider> + ); + + expect(getByText(/learn/i)).toBeInTheDocument(); +}); diff --git a/src/app/store.js b/src/app/store.js new file mode 100644 index 0000000..f5b8ae3 --- /dev/null +++ b/src/app/store.js @@ -0,0 +1,8 @@ +import { configureStore } from "@reduxjs/toolkit"; +import counterReducer from "../features/counter/counterSlice"; + +export const store = configureStore({ + reducer: { + counter: counterReducer, + }, +}); diff --git a/src/features/counter/Counter.jsx b/src/features/counter/Counter.jsx new file mode 100644 index 0000000..37d866d --- /dev/null +++ b/src/features/counter/Counter.jsx @@ -0,0 +1,67 @@ +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 ( + <div> + <div className={styles.row}> + <button + className={styles.button} + aria-label="Decrement value" + onClick={() => dispatch(decrement())} + > + - + </button> + <span className={styles.value}>{count}</span> + <button + className={styles.button} + aria-label="Increment value" + onClick={() => dispatch(increment())} + > + + + </button> + </div> + <div className={styles.row}> + <input + className={styles.textbox} + aria-label="Set increment amount" + value={incrementAmount} + onChange={(e) => setIncrementAmount(e.target.value)} + /> + <button + className={styles.button} + onClick={() => dispatch(incrementByAmount(incrementValue))} + > + Add Amount + </button> + <button + className={styles.asyncButton} + onClick={() => dispatch(incrementAsync(incrementValue))} + > + Add Async + </button> + <button + className={styles.button} + onClick={() => dispatch(incrementIfOdd(incrementValue))} + > + Add If Odd + </button> + </div> + </div> + ); +} diff --git a/src/features/counter/Counter.module.css b/src/features/counter/Counter.module.css new file mode 100644 index 0000000..9e70f8e --- /dev/null +++ b/src/features/counter/Counter.module.css @@ -0,0 +1,78 @@ +.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 new file mode 100644 index 0000000..cc9b4a4 --- /dev/null +++ b/src/features/counter/counterAPI.js @@ -0,0 +1,6 @@ +// 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 new file mode 100644 index 0000000..a9441ac --- /dev/null +++ b/src/features/counter/counterSlice.js @@ -0,0 +1,73 @@ +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 new file mode 100644 index 0000000..113526b --- /dev/null +++ b/src/features/counter/counterSlice.spec.js @@ -0,0 +1,33 @@ +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/index.css b/src/index.css new file mode 100644 index 0000000..4a1df4d --- /dev/null +++ b/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + 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; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} diff --git a/src/logo.svg b/src/logo.svg new file mode 100644 index 0000000..8466738 --- /dev/null +++ b/src/logo.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g fill="#764ABC"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></svg> diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..3139d2e --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,14 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { Provider } from "react-redux"; +import { store } from "./app/store"; +import App from "./App"; +import "./index.css"; + +ReactDOM.createRoot(document.getElementById("root")).render( + <React.StrictMode> + <Provider store={store}> + <App /> + </Provider> + </React.StrictMode> +); |