mirror of
https://github.com/ershisan99/flashcards-example-project.git
synced 2025-12-16 20:59:27 +00:00
lesson 3 finished
This commit is contained in:
@@ -18,11 +18,13 @@
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@storybook/theming": "^7.2.1",
|
||||
"clsx": "^2.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.45.2",
|
||||
"react-redux": "^8.1.2",
|
||||
"react-router-dom": "^6.14.2",
|
||||
"react-toastify": "^9.1.3",
|
||||
"remeda": "^1.24.0",
|
||||
|
||||
103
pnpm-lock.yaml
generated
103
pnpm-lock.yaml
generated
@@ -20,6 +20,9 @@ dependencies:
|
||||
'@radix-ui/react-radio-group':
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@reduxjs/toolkit':
|
||||
specifier: ^1.9.5
|
||||
version: 1.9.5(react-redux@8.1.2)(react@18.2.0)
|
||||
'@storybook/theming':
|
||||
specifier: ^7.2.1
|
||||
version: 7.2.1(react-dom@18.2.0)(react@18.2.0)
|
||||
@@ -35,6 +38,9 @@ dependencies:
|
||||
react-hook-form:
|
||||
specifier: ^7.45.2
|
||||
version: 7.45.2(react@18.2.0)
|
||||
react-redux:
|
||||
specifier: ^8.1.2
|
||||
version: 8.1.2(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1)
|
||||
react-router-dom:
|
||||
specifier: ^6.14.2
|
||||
version: 6.14.2(react-dom@18.2.0)(react@18.2.0)
|
||||
@@ -2672,6 +2678,25 @@ packages:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.22.6
|
||||
|
||||
/@reduxjs/toolkit@1.9.5(react-redux@8.1.2)(react@18.2.0):
|
||||
resolution: {integrity: sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==}
|
||||
peerDependencies:
|
||||
react: ^16.9.0 || ^17.0.0 || ^18
|
||||
react-redux: ^7.2.1 || ^8.0.2
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
react-redux:
|
||||
optional: true
|
||||
dependencies:
|
||||
immer: 9.0.21
|
||||
react: 18.2.0
|
||||
react-redux: 8.1.2(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1)
|
||||
redux: 4.2.1
|
||||
redux-thunk: 2.4.2(redux@4.2.1)
|
||||
reselect: 4.1.8
|
||||
dev: false
|
||||
|
||||
/@remix-run/router@1.7.2:
|
||||
resolution: {integrity: sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -3951,6 +3976,13 @@ packages:
|
||||
'@types/node': 20.4.5
|
||||
dev: true
|
||||
|
||||
/@types/hoist-non-react-statics@3.3.1:
|
||||
resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==}
|
||||
dependencies:
|
||||
'@types/react': 18.2.15
|
||||
hoist-non-react-statics: 3.3.2
|
||||
dev: false
|
||||
|
||||
/@types/http-errors@2.0.1:
|
||||
resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==}
|
||||
|
||||
@@ -4073,6 +4105,10 @@ packages:
|
||||
resolution: {integrity: sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==}
|
||||
dev: true
|
||||
|
||||
/@types/use-sync-external-store@0.0.3:
|
||||
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
|
||||
dev: false
|
||||
|
||||
/@types/yargs-parser@21.0.0:
|
||||
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
|
||||
dev: true
|
||||
@@ -6647,7 +6683,6 @@ packages:
|
||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||
dependencies:
|
||||
react-is: 16.13.1
|
||||
dev: true
|
||||
|
||||
/hosted-git-info@2.8.9:
|
||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||
@@ -6726,6 +6761,10 @@ packages:
|
||||
engines: {node: '>= 4'}
|
||||
dev: true
|
||||
|
||||
/immer@9.0.21:
|
||||
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
|
||||
dev: false
|
||||
|
||||
/immutable@4.3.1:
|
||||
resolution: {integrity: sha512-lj9cnmB/kVS0QHsJnYKD1uo3o39nrbKxszjnqS9Fr6NB7bZzW45U6WSGBPKXDL/CvDKqDNPA4r3DoDQ8GTxo2A==}
|
||||
dev: true
|
||||
@@ -8358,7 +8397,6 @@ packages:
|
||||
|
||||
/react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
dev: true
|
||||
|
||||
/react-is@17.0.2:
|
||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||
@@ -8366,7 +8404,40 @@ packages:
|
||||
|
||||
/react-is@18.1.0:
|
||||
resolution: {integrity: sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==}
|
||||
dev: true
|
||||
|
||||
/react-redux@8.1.2(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1):
|
||||
resolution: {integrity: sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==}
|
||||
peerDependencies:
|
||||
'@types/react': ^16.8 || ^17.0 || ^18.0
|
||||
'@types/react-dom': ^16.8 || ^17.0 || ^18.0
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
react-native: '>=0.59'
|
||||
redux: ^4 || ^5.0.0-beta.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
redux:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.22.6
|
||||
'@types/hoist-non-react-statics': 3.3.1
|
||||
'@types/react': 18.2.15
|
||||
'@types/react-dom': 18.2.7
|
||||
'@types/use-sync-external-store': 0.0.3
|
||||
hoist-non-react-statics: 3.3.2
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-is: 18.1.0
|
||||
redux: 4.2.1
|
||||
use-sync-external-store: 1.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/react-refresh@0.14.0:
|
||||
resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==}
|
||||
@@ -8565,6 +8636,20 @@ packages:
|
||||
strip-indent: 4.0.0
|
||||
dev: true
|
||||
|
||||
/redux-thunk@2.4.2(redux@4.2.1):
|
||||
resolution: {integrity: sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==}
|
||||
peerDependencies:
|
||||
redux: ^4
|
||||
dependencies:
|
||||
redux: 4.2.1
|
||||
dev: false
|
||||
|
||||
/redux@4.2.1:
|
||||
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.22.6
|
||||
dev: false
|
||||
|
||||
/regenerate-unicode-properties@10.1.0:
|
||||
resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -8649,6 +8734,10 @@ packages:
|
||||
engines: {node: '>=0.10.5'}
|
||||
dev: true
|
||||
|
||||
/reselect@4.1.8:
|
||||
resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==}
|
||||
dev: false
|
||||
|
||||
/resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -9814,6 +9903,14 @@ packages:
|
||||
react: 18.2.0
|
||||
tslib: 2.6.1
|
||||
|
||||
/use-sync-external-store@1.2.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
|
||||
11
src/App.tsx
11
src/App.tsx
@@ -1,3 +1,12 @@
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
import { Router } from '@/router'
|
||||
import { store } from '@/services/store'
|
||||
|
||||
export function App() {
|
||||
return <div>Hello</div>
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Router />
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import { Button, Card, ControlledTextField, Typography } from '../../ui'
|
||||
|
||||
import s from './recover-password.module.scss'
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email('Invalid email address').nonempty('Enter email'),
|
||||
})
|
||||
import { emailSchema } from '@/components'
|
||||
|
||||
const schema = emailSchema
|
||||
|
||||
type FormType = z.infer<typeof schema>
|
||||
|
||||
|
||||
@@ -8,12 +8,17 @@ import { Button, Card, ControlledCheckbox, ControlledTextField, Typography } fro
|
||||
|
||||
import s from './sign-in.module.scss'
|
||||
|
||||
const schema = z.object({
|
||||
export const emailSchema = z.object({
|
||||
email: z.string().email('Invalid email address').nonempty('Enter email'),
|
||||
password: z.string().nonempty('Enter password'),
|
||||
rememberMe: z.boolean().optional(),
|
||||
})
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
password: z.string().nonempty('Enter password'),
|
||||
rememberMe: z.boolean().optional(),
|
||||
})
|
||||
.merge(emailSchema)
|
||||
|
||||
type FormType = z.infer<typeof schema>
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -9,12 +9,14 @@ import { Button, Card, ControlledTextField, Typography } from '../../ui'
|
||||
|
||||
import s from './sign-up.module.scss'
|
||||
|
||||
import { emailSchema } from '@/components'
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
email: z.string().email('Invalid email address').nonempty('Enter email'),
|
||||
password: z.string().nonempty('Enter password'),
|
||||
passwordConfirmation: z.string().nonempty('Confirm your password'),
|
||||
})
|
||||
.merge(emailSchema)
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.password !== data.passwordConfirmation) {
|
||||
ctx.addIssue({
|
||||
|
||||
96
src/pages/decks/decks.tsx
Normal file
96
src/pages/decks/decks.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Button, TextField } from '@/components'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeadCell,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { useCreateDeckMutation, useGetDecksQuery } from '@/services/decks'
|
||||
import { decksSlice } from '@/services/decks/decks.slice.ts'
|
||||
import { useAppDispatch, useAppSelector } from '@/services/store.ts'
|
||||
|
||||
export const Decks = () => {
|
||||
const [cardName, setCardName] = useState('')
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const itemsPerPage = useAppSelector(state => state.decksSlice.itemsPerPage)
|
||||
const currentPage = useAppSelector(state => state.decksSlice.currentPage)
|
||||
const searchByName = useAppSelector(state => state.decksSlice.searchByName)
|
||||
|
||||
const setItemsPerPage = (itemsPerPage: number) =>
|
||||
dispatch(decksSlice.actions.setItemsPerPage(itemsPerPage))
|
||||
const setCurrentPage = (currentPage: number) =>
|
||||
dispatch(decksSlice.actions.setCurrentPage(currentPage))
|
||||
const setSearch = (search: string) => dispatch(decksSlice.actions.setSearchByName(search))
|
||||
|
||||
const { isLoading, data, refetch } = useGetDecksQuery({
|
||||
itemsPerPage,
|
||||
currentPage,
|
||||
name: searchByName,
|
||||
orderBy: 'created-desc',
|
||||
})
|
||||
|
||||
const [createDeck, { isLoading: isCreateDeckLoading }] = useCreateDeckMutation()
|
||||
|
||||
const handleCreateClicked = () => createDeck({ name: cardName })
|
||||
|
||||
if (isLoading) return <div>Loading...</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Button onClick={refetch}>refetch</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={() => setItemsPerPage(10)}>itemsPerPage: 10</Button>
|
||||
<Button onClick={() => setItemsPerPage(20)}>itemsPerPage: 20</Button>
|
||||
<Button onClick={() => setItemsPerPage(30)}>itemsPerPage: 30</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={() => setCurrentPage(1)}>currentPage: 1</Button>
|
||||
<Button onClick={() => setCurrentPage(2)}>currentPage: 2</Button>
|
||||
<Button onClick={() => setCurrentPage(3)}>currentPage: 3</Button>
|
||||
</div>
|
||||
<TextField value={searchByName} onChange={e => setSearch(e.currentTarget.value)} />
|
||||
<TextField
|
||||
value={cardName}
|
||||
onChange={e => setCardName(e.currentTarget.value)}
|
||||
label={'card name'}
|
||||
/>
|
||||
<Button onClick={handleCreateClicked}>Create deck</Button>
|
||||
isCreateDeckLoading: {isCreateDeckLoading.toString()}
|
||||
<Table>
|
||||
{/* table*/}
|
||||
<TableHead>
|
||||
{/* thead*/}
|
||||
<TableRow>
|
||||
{/* tr*/}
|
||||
<TableHeadCell>Name</TableHeadCell>
|
||||
{/* th*/}
|
||||
<TableHeadCell>Cards</TableHeadCell>
|
||||
<TableHeadCell>Last Updated</TableHeadCell>
|
||||
<TableHeadCell>Created By</TableHeadCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{/* tbody*/}
|
||||
{data?.items.map(deck => {
|
||||
return (
|
||||
<TableRow key={deck.id}>
|
||||
{/* tr*/}
|
||||
<TableCell>{deck.name}</TableCell> {/* td*/}
|
||||
<TableCell>{deck.cardsCount}</TableCell>
|
||||
<TableCell>{new Date(deck.updated).toLocaleString('en-GB')}</TableCell>
|
||||
<TableCell>{deck.author.name}</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
41
src/router.tsx
Normal file
41
src/router.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
createBrowserRouter,
|
||||
Navigate,
|
||||
Outlet,
|
||||
RouteObject,
|
||||
RouterProvider,
|
||||
} from 'react-router-dom'
|
||||
|
||||
import { Decks } from '@/pages/decks/decks.tsx'
|
||||
|
||||
const publicRoutes: RouteObject[] = [
|
||||
{
|
||||
path: '/login',
|
||||
element: <div>login</div>,
|
||||
},
|
||||
]
|
||||
|
||||
const privateRoutes: RouteObject[] = [
|
||||
{
|
||||
path: '/',
|
||||
element: <Decks />,
|
||||
},
|
||||
]
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
element: <PrivateRoutes />,
|
||||
children: privateRoutes,
|
||||
},
|
||||
...publicRoutes,
|
||||
])
|
||||
|
||||
export const Router = () => {
|
||||
return <RouterProvider router={router} />
|
||||
}
|
||||
|
||||
function PrivateRoutes() {
|
||||
const isAuthenticated = true
|
||||
|
||||
return isAuthenticated ? <Outlet /> : <Navigate to="/login" />
|
||||
}
|
||||
14
src/services/base-api.ts
Normal file
14
src/services/base-api.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
||||
|
||||
export const baseApi = createApi({
|
||||
reducerPath: 'baseApi',
|
||||
tagTypes: ['Decks'],
|
||||
baseQuery: fetchBaseQuery({
|
||||
baseUrl: 'https://api.flashcards.andrii.es',
|
||||
credentials: 'include',
|
||||
prepareHeaders: headers => {
|
||||
headers.append('x-auth-skip', 'true')
|
||||
},
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
})
|
||||
23
src/services/decks/decks.slice.ts
Normal file
23
src/services/decks/decks.slice.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
const initialState = {
|
||||
itemsPerPage: 10,
|
||||
currentPage: 1,
|
||||
searchByName: '',
|
||||
}
|
||||
|
||||
export const decksSlice = createSlice({
|
||||
initialState,
|
||||
name: 'decksSlice',
|
||||
reducers: {
|
||||
setItemsPerPage: (state, action: PayloadAction<number>) => {
|
||||
state.itemsPerPage = action.payload
|
||||
},
|
||||
setCurrentPage: (state, action: PayloadAction<number>) => {
|
||||
state.currentPage = action.payload
|
||||
},
|
||||
setSearchByName: (state, action: PayloadAction<string>) => {
|
||||
state.searchByName = action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
31
src/services/decks/decks.ts
Normal file
31
src/services/decks/decks.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { baseApi } from '@/services/base-api.ts'
|
||||
import { CreateDeckArgs, Deck, DecksResponse, GetDecksArgs } from '@/services/decks/types.ts'
|
||||
|
||||
const decksApi = baseApi.injectEndpoints({
|
||||
endpoints: builder => {
|
||||
return {
|
||||
getDecks: builder.query<DecksResponse, GetDecksArgs>({
|
||||
query: args => {
|
||||
return {
|
||||
url: `v1/decks`,
|
||||
method: 'GET',
|
||||
params: args,
|
||||
}
|
||||
},
|
||||
providesTags: ['Decks'],
|
||||
}),
|
||||
createDeck: builder.mutation<Deck, CreateDeckArgs>({
|
||||
query: ({ name }) => {
|
||||
return {
|
||||
url: 'v1/decks',
|
||||
method: 'POST',
|
||||
body: { name },
|
||||
}
|
||||
},
|
||||
invalidatesTags: ['Decks'],
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const { useGetDecksQuery, useCreateDeckMutation } = decksApi
|
||||
1
src/services/decks/index.ts
Normal file
1
src/services/decks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './decks'
|
||||
37
src/services/decks/types.ts
Normal file
37
src/services/decks/types.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { PaginatedEntity, PaginatedRequest } from '@/services/types'
|
||||
|
||||
export type GetDecksArgs = PaginatedRequest<{
|
||||
minCardsCount?: number
|
||||
maxCardsCount?: number
|
||||
name?: string
|
||||
authorId?: Author['id']
|
||||
orderBy?: string
|
||||
}>
|
||||
|
||||
export type CreateDeckArgs = {
|
||||
name: string
|
||||
}
|
||||
export interface Author {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface Deck {
|
||||
id: string
|
||||
userId: string
|
||||
name: string
|
||||
isPrivate: boolean
|
||||
shots: number
|
||||
cover: string | null
|
||||
rating: number
|
||||
isDeleted: boolean | null
|
||||
isBlocked: boolean | null
|
||||
created: string
|
||||
updated: string
|
||||
cardsCount: number
|
||||
author: Author
|
||||
}
|
||||
|
||||
export type DecksResponse = PaginatedEntity<Deck> & {
|
||||
maxCardsCount: number
|
||||
}
|
||||
18
src/services/store.ts
Normal file
18
src/services/store.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import { baseApi } from '@/services/base-api'
|
||||
import { decksSlice } from '@/services/decks/decks.slice.ts'
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
[baseApi.reducerPath]: baseApi.reducer,
|
||||
[decksSlice.name]: decksSlice.reducer,
|
||||
},
|
||||
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(baseApi.middleware),
|
||||
})
|
||||
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||
12
src/services/types.ts
Normal file
12
src/services/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export type PaginatedEntity<T> = {
|
||||
pagination: Pagination
|
||||
items: T[]
|
||||
}
|
||||
export interface Pagination {
|
||||
totalPages: number
|
||||
currentPage: number
|
||||
itemsPerPage: number
|
||||
totalItems: number
|
||||
}
|
||||
|
||||
export type PaginatedRequest<T> = Partial<Pick<Pagination, 'currentPage' | 'itemsPerPage'>> & T
|
||||
Reference in New Issue
Block a user