init: create redux toolkit app
This commit is contained in:
commit
e1b74e41b1
100
.eslintrc.cjs
Normal file
100
.eslintrc.cjs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
ignorePatterns: ["dist"],
|
||||||
|
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
commonjs: true,
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
project: true,
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
|
||||||
|
extends: ["eslint:recommended", "plugin:prettier/recommended"],
|
||||||
|
|
||||||
|
// Rules
|
||||||
|
rules: {
|
||||||
|
"linebreak-style": ["error", "unix"],
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_",
|
||||||
|
caughtErrorsIgnorePattern: "^_",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@typescript-eslint/consistent-type-imports": [2, { fixStyle: "separate-type-imports" }],
|
||||||
|
"@typescript-eslint/no-restricted-imports": [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
name: "react-redux",
|
||||||
|
importNames: ["useSelector", "useStore", "useDispatch"],
|
||||||
|
message: "Please use pre-typed versions from `src/app/hooks.ts` instead.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"prettier/prettier": ["error"],
|
||||||
|
},
|
||||||
|
|
||||||
|
overrides: [
|
||||||
|
// React
|
||||||
|
{
|
||||||
|
files: ["**/*.{js,jsx,ts,tsx}"],
|
||||||
|
plugins: ["react", "jsx-a11y"],
|
||||||
|
extends: [
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react/jsx-runtime",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"plugin:jsx-a11y/recommended",
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
react: { version: "detect" },
|
||||||
|
formComponents: ["Form"],
|
||||||
|
linkComponents: [
|
||||||
|
{ name: "Link", linkAttribute: "to" },
|
||||||
|
{ name: "NavLink", linkAttribute: "to" },
|
||||||
|
],
|
||||||
|
"import/resolver": { typescript: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Typescript
|
||||||
|
{
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
plugins: ["@typescript-eslint", "import"],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
settings: {
|
||||||
|
"import/internal-regex": "^~/",
|
||||||
|
"import/resolver": {
|
||||||
|
node: { extensions: [".ts", ".tsx"] },
|
||||||
|
typescript: {
|
||||||
|
alwaysTryTypes: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||||
|
"plugin:import/recommended",
|
||||||
|
"plugin:import/typescript",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Node
|
||||||
|
{
|
||||||
|
files: [".eslintrc.{js,cjs}"],
|
||||||
|
env: { node: true },
|
||||||
|
parserOptions: { sourceType: "script" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
pnpm exec lint-staged
|
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
/dist/
|
||||||
|
pnpm-lock.yaml
|
9
.prettierrc.json
Normal file
9
.prettierrc.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"printWidth": 120,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
14
index.html
Normal file
14
index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link href="/vite.svg" rel="icon" type="image/svg+xml" />
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||||
|
<title>WOJ - Online Judge</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="/src/main.tsx" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
50
package.json
Normal file
50
package.json
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-template-redux",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"start": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
|
||||||
|
"lint:fix": "eslint --cache --cache-location ./node_modules/.cache/eslint --fix .",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"prepare": "husky"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^2.2.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-redux": "^9.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.57",
|
||||||
|
"@types/react-dom": "^18.2.19",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
|
"eslint-plugin-import": "^2.29.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"husky": "^9.0.11",
|
||||||
|
"jsdom": "^23.2.0",
|
||||||
|
"lint-staged": "^15.2.2",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.1.3",
|
||||||
|
"vite-tsconfig-paths": "^4.3.1"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,jsx,ts,tsx}": [
|
||||||
|
"prettier --write",
|
||||||
|
"eslint --cache --cache-location ./node_modules/.cache/eslint --fix"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
3789
pnpm-lock.yaml
Normal file
3789
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
39
src/App.css
Normal file
39
src/App.css
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
48
src/App.tsx
Normal file
48
src/App.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import "./App.css";
|
||||||
|
import { Counter } from "./features/counter/Counter";
|
||||||
|
import { Quotes } from "./features/quotes/Quotes";
|
||||||
|
import logo from "./logo.svg";
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<header className="App-header">
|
||||||
|
<img src={logo} className="App-logo" alt="logo" />
|
||||||
|
<Counter />
|
||||||
|
<p>
|
||||||
|
Edit <code>src/App.tsx</code> and save to reload.
|
||||||
|
</p>
|
||||||
|
<Quotes />
|
||||||
|
<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>, </span>
|
||||||
|
<a className="App-link" href="https://react-redux.js.org" target="_blank" rel="noopener noreferrer">
|
||||||
|
React Redux
|
||||||
|
</a>
|
||||||
|
,<span> and </span>
|
||||||
|
<a className="App-link" href="https://reselect.js.org" target="_blank" rel="noopener noreferrer">
|
||||||
|
Reselect
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
5
src/app/createAppSlice.ts
Normal file
5
src/app/createAppSlice.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { asyncThunkCreator, buildCreateSlice } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
export const createAppSlice = buildCreateSlice({
|
||||||
|
creators: { asyncThunk: asyncThunkCreator },
|
||||||
|
});
|
6
src/app/hooks.ts
Normal file
6
src/app/hooks.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-restricted-imports */
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import type { AppDispatch, RootState } from "./store";
|
||||||
|
|
||||||
|
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
||||||
|
export const useAppSelector = useSelector.withTypes<RootState>();
|
27
src/app/store.ts
Normal file
27
src/app/store.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import type { Action, ThunkAction } from "@reduxjs/toolkit";
|
||||||
|
import { combineSlices, configureStore } from "@reduxjs/toolkit";
|
||||||
|
import { setupListeners } from "@reduxjs/toolkit/query";
|
||||||
|
|
||||||
|
import { counterSlice } from "../features/counter/counterSlice";
|
||||||
|
import { quotesApiSlice } from "../features/quotes/quotesApiSlice";
|
||||||
|
|
||||||
|
const rootReducer = combineSlices(counterSlice, quotesApiSlice);
|
||||||
|
export type RootState = ReturnType<typeof rootReducer>;
|
||||||
|
|
||||||
|
export const makeStore = (preloadedState?: Partial<RootState>) => {
|
||||||
|
const store = configureStore({
|
||||||
|
reducer: rootReducer,
|
||||||
|
middleware: (getDefaultMiddleware) => {
|
||||||
|
return getDefaultMiddleware().concat(quotesApiSlice.middleware);
|
||||||
|
},
|
||||||
|
preloadedState,
|
||||||
|
});
|
||||||
|
setupListeners(store.dispatch);
|
||||||
|
return store;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const store = makeStore();
|
||||||
|
|
||||||
|
export type AppStore = typeof store;
|
||||||
|
export type AppDispatch = AppStore["dispatch"];
|
||||||
|
export type AppThunk<ThunkReturnType = void> = ThunkAction<ThunkReturnType, RootState, unknown, Action>;
|
79
src/features/counter/Counter.module.css
Normal file
79
src/features/counter/Counter.module.css
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
.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;
|
||||||
|
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: rgba(112, 76, 182, 0.1) none;
|
||||||
|
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;
|
||||||
|
}
|
67
src/features/counter/Counter.tsx
Normal file
67
src/features/counter/Counter.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
|
import styles from "./Counter.module.css";
|
||||||
|
import {
|
||||||
|
decrement,
|
||||||
|
increment,
|
||||||
|
incrementAsync,
|
||||||
|
incrementByAmount,
|
||||||
|
incrementIfOdd,
|
||||||
|
selectCount,
|
||||||
|
selectStatus,
|
||||||
|
} from "./counterSlice";
|
||||||
|
|
||||||
|
export const Counter = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const count = useAppSelector(selectCount);
|
||||||
|
const status = useAppSelector(selectStatus);
|
||||||
|
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 aria-label="Count" 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}
|
||||||
|
type="number"
|
||||||
|
onChange={(e) => {
|
||||||
|
setIncrementAmount(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button className={styles.button} onClick={() => dispatch(incrementByAmount(incrementValue))}>
|
||||||
|
Add Amount
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.asyncButton}
|
||||||
|
disabled={status !== "idle"}
|
||||||
|
onClick={() => void dispatch(incrementAsync(incrementValue))}
|
||||||
|
>
|
||||||
|
Add Async
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.button}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(incrementIfOdd(incrementValue));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add If Odd
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
4
src/features/counter/counterAPI.ts
Normal file
4
src/features/counter/counterAPI.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// A mock function to mimic making an async request for data
|
||||||
|
export const fetchCount = (amount = 1) => {
|
||||||
|
return new Promise<{ data: number }>((resolve) => setTimeout(() => resolve({ data: amount }), 500));
|
||||||
|
};
|
86
src/features/counter/counterSlice.ts
Normal file
86
src/features/counter/counterSlice.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import { createAppSlice } from "../../app/createAppSlice";
|
||||||
|
import type { AppThunk } from "../../app/store";
|
||||||
|
import { fetchCount } from "./counterAPI";
|
||||||
|
|
||||||
|
export interface CounterSliceState {
|
||||||
|
value: number;
|
||||||
|
status: "idle" | "loading" | "failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: CounterSliceState = {
|
||||||
|
value: 0,
|
||||||
|
status: "idle",
|
||||||
|
};
|
||||||
|
|
||||||
|
// If you are not using async thunks you can use the standalone `createSlice`.
|
||||||
|
export const counterSlice = createAppSlice({
|
||||||
|
name: "counter",
|
||||||
|
// `createSlice` will infer the state type from the `initialState` argument
|
||||||
|
initialState,
|
||||||
|
// The `reducers` field lets us define reducers and generate associated actions
|
||||||
|
reducers: (create) => ({
|
||||||
|
increment: create.reducer((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: create.reducer((state) => {
|
||||||
|
state.value -= 1;
|
||||||
|
}),
|
||||||
|
// Use the `PayloadAction` type to declare the contents of `action.payload`
|
||||||
|
incrementByAmount: create.reducer((state, action: PayloadAction<number>) => {
|
||||||
|
state.value += action.payload;
|
||||||
|
}),
|
||||||
|
// 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.
|
||||||
|
incrementAsync: create.asyncThunk(
|
||||||
|
async (amount: number) => {
|
||||||
|
const response = await fetchCount(amount);
|
||||||
|
// The value we return becomes the `fulfilled` action payload
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pending: (state) => {
|
||||||
|
state.status = "loading";
|
||||||
|
},
|
||||||
|
fulfilled: (state, action) => {
|
||||||
|
state.status = "idle";
|
||||||
|
state.value += action.payload;
|
||||||
|
},
|
||||||
|
rejected: (state) => {
|
||||||
|
state.status = "failed";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
// You can define your selectors here. These selectors receive the slice
|
||||||
|
// state as their first argument.
|
||||||
|
selectors: {
|
||||||
|
selectCount: (counter) => counter.value,
|
||||||
|
selectStatus: (counter) => counter.status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Action creators are generated for each case reducer function.
|
||||||
|
export const { decrement, increment, incrementByAmount, incrementAsync } = counterSlice.actions;
|
||||||
|
|
||||||
|
// Selectors returned by `slice.selectors` take the root state as their first argument.
|
||||||
|
export const { selectCount, selectStatus } = counterSlice.selectors;
|
||||||
|
|
||||||
|
// 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: number): AppThunk =>
|
||||||
|
(dispatch, getState) => {
|
||||||
|
const currentValue = selectCount(getState());
|
||||||
|
|
||||||
|
if (currentValue % 2 === 1 || currentValue % 2 === -1) {
|
||||||
|
dispatch(incrementByAmount(amount));
|
||||||
|
}
|
||||||
|
};
|
18
src/features/quotes/Quotes.module.css
Normal file
18
src/features/quotes/Quotes.module.css
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.select {
|
||||||
|
font-size: 25px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
size: 50px;
|
||||||
|
outline: none;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
color: rgb(112, 76, 182);
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: rgba(112, 76, 182, 0.1);
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
58
src/features/quotes/Quotes.tsx
Normal file
58
src/features/quotes/Quotes.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import styles from "./Quotes.module.css";
|
||||||
|
import { useGetQuotesQuery } from "./quotesApiSlice";
|
||||||
|
|
||||||
|
const options = [5, 10, 20, 30];
|
||||||
|
|
||||||
|
export const Quotes = () => {
|
||||||
|
const [numberOfQuotes, setNumberOfQuotes] = useState(10);
|
||||||
|
// Using a query hook automatically fetches data and returns query values
|
||||||
|
const { data, isError, isLoading, isSuccess } = useGetQuotesQuery(numberOfQuotes);
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>There was an error!!!</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Loading...</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSuccess) {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<h3>Select the Quantity of Quotes to Fetch:</h3>
|
||||||
|
<select
|
||||||
|
className={styles.select}
|
||||||
|
value={numberOfQuotes}
|
||||||
|
onChange={(e) => {
|
||||||
|
setNumberOfQuotes(Number(e.target.value));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{data.quotes.map(({ author, quote, id }) => (
|
||||||
|
<blockquote key={id}>
|
||||||
|
“{quote}”
|
||||||
|
<footer>
|
||||||
|
<cite>{author}</cite>
|
||||||
|
</footer>
|
||||||
|
</blockquote>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
38
src/features/quotes/quotesApiSlice.ts
Normal file
38
src/features/quotes/quotesApiSlice.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// Need to use the React-specific entry point to import `createApi`
|
||||||
|
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||||
|
|
||||||
|
interface Quote {
|
||||||
|
id: number;
|
||||||
|
quote: string;
|
||||||
|
author: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QuotesApiResponse {
|
||||||
|
quotes: Quote[];
|
||||||
|
total: number;
|
||||||
|
skip: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define a service using a base URL and expected endpoints
|
||||||
|
export const quotesApiSlice = createApi({
|
||||||
|
baseQuery: fetchBaseQuery({ baseUrl: "https://dummyjson.com/quotes" }),
|
||||||
|
reducerPath: "quotesApi",
|
||||||
|
// Tag types are used for caching and invalidation.
|
||||||
|
tagTypes: ["Quotes"],
|
||||||
|
endpoints: (build) => ({
|
||||||
|
// Supply generics for the return type (in this case `QuotesApiResponse`)
|
||||||
|
// and the expected query argument. If there is no argument, use `void`
|
||||||
|
// for the argument type instead.
|
||||||
|
getQuotes: build.query<QuotesApiResponse, number>({
|
||||||
|
query: (limit = 10) => `?limit=${limit}`,
|
||||||
|
// `providesTags` determines which 'tag' is attached to the
|
||||||
|
// cached data returned by the query.
|
||||||
|
providesTags: (result, error, id) => [{ type: "Quotes", id }],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hooks are auto-generated by RTK-Query
|
||||||
|
// Same as `quotesApiSlice.endpoints.getQuotes.useQuery`
|
||||||
|
export const { useGetQuotesQuery } = quotesApiSlice;
|
11
src/index.css
Normal file
11
src/index.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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;
|
||||||
|
}
|
1
src/logo.svg
Normal file
1
src/logo.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 1.1 KiB |
25
src/main.tsx
Normal file
25
src/main.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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 "./index.css";
|
||||||
|
|
||||||
|
const container = document.getElementById("root");
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
const root = createRoot(container);
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Root element with ID 'root' was not found in the document. Ensure there is a corresponding HTML element with the ID 'root' in your HTML file.",
|
||||||
|
);
|
||||||
|
}
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
9
tsconfig.node.json
Normal file
9
tsconfig.node.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
13
vite.config.ts
Normal file
13
vite.config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import packageJson from "./package.json";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tsconfigPaths()],
|
||||||
|
build: { sourcemap: true },
|
||||||
|
define: {
|
||||||
|
"import.meta.env.PACKAGE_VERSION": JSON.stringify(packageJson.version),
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user