aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/App.css39
-rw-r--r--src/App.jsx58
-rw-r--r--src/App.test.jsx15
-rw-r--r--src/app/store.js8
-rw-r--r--src/features/counter/Counter.jsx67
-rw-r--r--src/features/counter/Counter.module.css78
-rw-r--r--src/features/counter/counterAPI.js6
-rw-r--r--src/features/counter/counterSlice.js73
-rw-r--r--src/features/counter/counterSlice.spec.js33
-rw-r--r--src/index.css13
-rw-r--r--src/logo.svg1
-rw-r--r--src/main.jsx14
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>
+);