mirror of
https://github.com/ershisan99/cards-front.git
synced 2025-12-16 20:49:28 +00:00
initial commit
This commit is contained in:
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
# Swap the comments on the following lines if you don't wish to use zero-installs
|
||||||
|
# Documentation here: https://yarnpkg.com/features/zero-installs
|
||||||
|
!.yarn/cache
|
||||||
|
#.pnp.*
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Production
|
||||||
|
build
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
*.local
|
||||||
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
12
.idea/cards-front.iml
generated
Normal file
12
.idea/cards-front.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
58
.idea/codeStyles/Project.xml
generated
Normal file
58
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<HTMLCodeStyleSettings>
|
||||||
|
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||||
|
<option name="HTML_ENFORCE_QUOTES" value="true" />
|
||||||
|
</HTMLCodeStyleSettings>
|
||||||
|
<JSCodeStyleSettings version="0">
|
||||||
|
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||||
|
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||||
|
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||||
|
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||||
|
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||||
|
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||||
|
</JSCodeStyleSettings>
|
||||||
|
<TypeScriptCodeStyleSettings version="0">
|
||||||
|
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||||
|
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||||
|
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||||
|
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||||
|
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||||
|
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||||
|
</TypeScriptCodeStyleSettings>
|
||||||
|
<VueCodeStyleSettings>
|
||||||
|
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||||
|
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||||
|
</VueCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="HTML">
|
||||||
|
<option name="SOFT_MARGINS" value="80" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="JavaScript">
|
||||||
|
<option name="SOFT_MARGINS" value="80" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="TypeScript">
|
||||||
|
<option name="SOFT_MARGINS" value="80" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="Vue">
|
||||||
|
<option name="SOFT_MARGINS" value="80" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
6
.idea/jsLinters/eslint.xml
generated
Normal file
6
.idea/jsLinters/eslint.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="EslintConfiguration">
|
||||||
|
<option name="fix-on-save" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/cards-front.iml" filepath="$PROJECT_DIR$/.idea/cards-front.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/prettier.xml
generated
Normal file
8
.idea/prettier.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PrettierConfiguration">
|
||||||
|
<option name="myConfigurationMode" value="AUTOMATIC" />
|
||||||
|
<option name="myRunOnSave" value="true" />
|
||||||
|
<option name="myFilesPattern" value="{**/*,*}.{js,ts,jsx,tsx,vue,astro,css,scss,json}" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
0
.idea/sonarlint/issuestore/8/e/8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d
generated
Normal file
0
.idea/sonarlint/issuestore/8/e/8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d
generated
Normal file
3
.idea/sonarlint/issuestore/index.pb
generated
Normal file
3
.idea/sonarlint/issuestore/index.pb
generated
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
9
|
||||||
|
README.md,8\e\8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d
|
||||||
0
.idea/sonarlint/securityhotspotstore/8/e/8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d
generated
Normal file
0
.idea/sonarlint/securityhotspotstore/8/e/8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d
generated
Normal file
3
.idea/sonarlint/securityhotspotstore/index.pb
generated
Normal file
3
.idea/sonarlint/securityhotspotstore/index.pb
generated
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
9
|
||||||
|
README.md,8\e\8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d
|
||||||
27
README.md
Normal file
27
README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# vite-template-redux
|
||||||
|
|
||||||
|
Uses [Vite](https://vitejs.dev/), [Vitest](https://vitest.dev/), and [React Testing Library](https://github.com/testing-library/react-testing-library) to create a modern [React](https://react.dev/) app compatible with [Create React App](https://create-react-app.dev/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npx degit reduxjs/redux-templates/packages/vite-template-redux my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Easy migration from Create React App or Vite
|
||||||
|
- As beginner friendly as Create React App
|
||||||
|
- Optimized performance compared to Create React App
|
||||||
|
- Customizable without ejecting
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
- `dev`/`start` - start dev server and open browser
|
||||||
|
- `build` - build for production
|
||||||
|
- `preview` - locally preview production build
|
||||||
|
- `test` - launch test runner
|
||||||
|
|
||||||
|
## Inspiration
|
||||||
|
|
||||||
|
- [Create React App](https://github.com/facebook/create-react-app/tree/main/packages/cra-template)
|
||||||
|
- [Vite](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react)
|
||||||
|
- [Vitest](https://github.com/vitest-dev/vitest/tree/main/examples/react-testing-lib)
|
||||||
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 rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>React Redux App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
61
package.json
Normal file
61
package.json
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-template-redux",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"start": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test": "vitest",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"type-check": "tsc"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.11.0",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@mui/icons-material": "^5.11.16",
|
||||||
|
"@mui/material": "^5.13.2",
|
||||||
|
"@reduxjs/toolkit": "^1.8.1",
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-redux": "^8.0.1",
|
||||||
|
"react-router-dom": "^6.11.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@testing-library/dom": "^9.2.0",
|
||||||
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
|
"@testing-library/react": "^14.0.0",
|
||||||
|
"@testing-library/user-event": "^14.2.5",
|
||||||
|
"@types/react": "^18.0.15",
|
||||||
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"@types/testing-library__jest-dom": "^5.14.5",
|
||||||
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
|
"eslint": "^8.0.0",
|
||||||
|
"eslint-config-react-app": "^7.0.1",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"jsdom": "^21.1.0",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"prettier-config-nick": "^1.0.2",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.0.0",
|
||||||
|
"vitest": "^0.30.1"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"react/jsx-no-target-blank": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prettier": "prettier-config-nick"
|
||||||
|
}
|
||||||
5722
pnpm-lock.yaml
generated
Normal file
5722
pnpm-lock.yaml
generated
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/App.test.tsx
Normal file
14
src/App.test.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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()
|
||||||
|
})
|
||||||
58
src/App.tsx
Normal file
58
src/App.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Counter } from "./features/counter/Counter"
|
||||||
|
import { createBrowserRouter, RouterProvider } from "react-router-dom"
|
||||||
|
import "./App.css"
|
||||||
|
import { store } from "@/app/store"
|
||||||
|
import { Provider } from "react-redux"
|
||||||
|
import { createTheme, ThemeProvider } from "@mui/material"
|
||||||
|
import { useAppDispatch, useAppSelector } from "@/app/hooks"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
import { appActions } from "@/features/app/app.slice"
|
||||||
|
|
||||||
|
export const Test = () => {
|
||||||
|
const isLoading = useAppSelector((state) => state.app.isLoading)
|
||||||
|
const error = useAppSelector((state) => state.app.error)
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
function handleErrorButtonClicked() {
|
||||||
|
dispatch(appActions.setError({ error: "new error" }))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch(appActions.setIsLoading({ isLoading: false }))
|
||||||
|
}, 3000)
|
||||||
|
}, [dispatch])
|
||||||
|
|
||||||
|
if (isLoading) return <div>loading...</div>
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={handleErrorButtonClicked}>create error</button>
|
||||||
|
{!!error && <h2>{error}</h2>}
|
||||||
|
<Counter />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
element: <Test />,
|
||||||
|
path: "/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: <div>hello</div>,
|
||||||
|
path: "/hello",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const theme = createTheme()
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</ThemeProvider>
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
6
src/app/hooks.ts
Normal file
6
src/app/hooks.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
|
||||||
|
import type { RootState, AppDispatch } from "./store"
|
||||||
|
|
||||||
|
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||||
|
export const useAppDispatch: () => AppDispatch = useDispatch
|
||||||
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||||
19
src/app/store.ts
Normal file
19
src/app/store.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit"
|
||||||
|
import counterReducer from "../features/counter/counterSlice"
|
||||||
|
import { appReducer } from "@/features/app/app.slice"
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
counter: counterReducer,
|
||||||
|
app: appReducer,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export type AppDispatch = typeof store.dispatch
|
||||||
|
export type RootState = ReturnType<typeof store.getState>
|
||||||
|
export type AppThunk<ReturnType = void> = ThunkAction<
|
||||||
|
ReturnType,
|
||||||
|
RootState,
|
||||||
|
unknown,
|
||||||
|
Action<string>
|
||||||
|
>
|
||||||
27
src/features/app/app.slice.ts
Normal file
27
src/features/app/app.slice.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
|
||||||
|
|
||||||
|
const slice = createSlice({
|
||||||
|
name: "app",
|
||||||
|
initialState: {
|
||||||
|
error: null as null | string,
|
||||||
|
isLoading: true,
|
||||||
|
isAppInitialized: false,
|
||||||
|
},
|
||||||
|
reducers: {
|
||||||
|
setIsLoading: (state, action: PayloadAction<{ isLoading: boolean }>) => {
|
||||||
|
state.isLoading = action.payload.isLoading
|
||||||
|
},
|
||||||
|
setError: (state, action: PayloadAction<{ error: string | null }>) => {
|
||||||
|
state.error = action.payload.error
|
||||||
|
},
|
||||||
|
setIsAppInitialized: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ isInitialized: boolean }>,
|
||||||
|
) => {
|
||||||
|
state.isAppInitialized = action.payload.isInitialized
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const appReducer = slice.reducer
|
||||||
|
export const appActions = slice.actions
|
||||||
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
68
src/features/counter/Counter.tsx
Normal file
68
src/features/counter/Counter.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
import { useAppSelector, useAppDispatch } from "@/app/hooks"
|
||||||
|
import {
|
||||||
|
decrement,
|
||||||
|
increment,
|
||||||
|
incrementByAmount,
|
||||||
|
incrementAsync,
|
||||||
|
incrementIfOdd,
|
||||||
|
selectCount,
|
||||||
|
} from "./counterSlice"
|
||||||
|
import styles from "./Counter.module.css"
|
||||||
|
|
||||||
|
export function Counter() {
|
||||||
|
const count = useAppSelector(selectCount)
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
6
src/features/counter/counterAPI.ts
Normal file
6
src/features/counter/counterAPI.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// A mock function to mimic making an async request for data
|
||||||
|
export function fetchCount(amount = 1) {
|
||||||
|
return new Promise<{ data: number }>((resolve) =>
|
||||||
|
setTimeout(() => resolve({ data: amount }), 500),
|
||||||
|
)
|
||||||
|
}
|
||||||
34
src/features/counter/counterSlice.spec.ts
Normal file
34
src/features/counter/counterSlice.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import counterReducer, {
|
||||||
|
CounterState,
|
||||||
|
increment,
|
||||||
|
decrement,
|
||||||
|
incrementByAmount,
|
||||||
|
} from "./counterSlice"
|
||||||
|
|
||||||
|
describe("counter reducer", () => {
|
||||||
|
const initialState: CounterState = {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
85
src/features/counter/counterSlice.ts
Normal file
85
src/features/counter/counterSlice.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
|
||||||
|
import { RootState, AppThunk } from "../../app/store"
|
||||||
|
import { fetchCount } from "./counterAPI"
|
||||||
|
|
||||||
|
export interface CounterState {
|
||||||
|
value: number
|
||||||
|
status: "idle" | "loading" | "failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: CounterState = {
|
||||||
|
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: number) => {
|
||||||
|
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: PayloadAction<number>) => {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
.addCase(incrementAsync.rejected, (state) => {
|
||||||
|
state.status = "failed"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
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: RootState) => 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: number): AppThunk =>
|
||||||
|
(dispatch, getState) => {
|
||||||
|
const currentValue = selectCount(getState())
|
||||||
|
if (currentValue % 2 === 1) {
|
||||||
|
dispatch(incrementByAmount(amount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default counterSlice.reducer
|
||||||
13
src/index.css
Normal file
13
src/index.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
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 |
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from "react"
|
||||||
|
import ReactDOM from "react-dom/client"
|
||||||
|
import App from "./App"
|
||||||
|
import "./index.css"
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
2
src/setupTests.ts
Normal file
2
src/setupTests.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/// <reference types="vitest/globals" />
|
||||||
|
import "@testing-library/jest-dom"
|
||||||
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" />
|
||||||
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"types": ["testing-library__jest-dom"]
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
24
vite.config.ts
Normal file
24
vite.config.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { defineConfig } from "vitest/config"
|
||||||
|
import react from "@vitejs/plugin-react"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: [{ find: "@", replacement: path.resolve(__dirname, "src") }],
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: "build",
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: "jsdom",
|
||||||
|
setupFiles: "src/setupTests",
|
||||||
|
mockReset: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user