mirror of
https://github.com/ershisan99/flashcards-example-project.git
synced 2025-12-16 20:59:27 +00:00
lesson 4 live 30/12
This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@reduxjs/toolkit": "^2.0.1",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"@storybook/theming": "^7.6.6",
|
"@storybook/theming": "^7.6.6",
|
||||||
|
"async-mutex": "^0.4.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -38,6 +38,9 @@ dependencies:
|
|||||||
'@storybook/theming':
|
'@storybook/theming':
|
||||||
specifier: ^7.6.6
|
specifier: ^7.6.6
|
||||||
version: 7.6.6(react-dom@18.2.0)(react@18.2.0)
|
version: 7.6.6(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
async-mutex:
|
||||||
|
specifier: ^0.4.0
|
||||||
|
version: 0.4.0
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
@@ -4854,6 +4857,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
|
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/async-mutex@0.4.0:
|
||||||
|
resolution: {integrity: sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==}
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.6.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/async@3.2.5:
|
/async@3.2.5:
|
||||||
resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
|
resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
|
import { ToastContainer } from 'react-toastify'
|
||||||
|
|
||||||
import { Router } from '@/router'
|
import { Router } from '@/router'
|
||||||
import { store } from '@/services/store'
|
import { store } from '@/services/store'
|
||||||
|
|
||||||
|
import 'react-toastify/dist/ReactToastify.css'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
|
<ToastContainer />
|
||||||
<Router />
|
<Router />
|
||||||
</Provider>
|
</Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import s from './deck-dialog.module.scss'
|
|||||||
|
|
||||||
const newDeckSchema = z.object({
|
const newDeckSchema = z.object({
|
||||||
isPrivate: z.boolean(),
|
isPrivate: z.boolean(),
|
||||||
name: z.string().min(3).max(50),
|
name: z.string().min(3).max(5000),
|
||||||
})
|
})
|
||||||
|
|
||||||
type FormValues = z.infer<typeof newDeckSchema>
|
type FormValues = z.infer<typeof newDeckSchema>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { DeckDialog } from '@/components/decks/deck-dialog'
|
|||||||
import { DeleteDeckDialog } from '@/components/decks/delete-deck-dialog'
|
import { DeleteDeckDialog } from '@/components/decks/delete-deck-dialog'
|
||||||
import { Pagination } from '@/components/ui/pagination'
|
import { Pagination } from '@/components/ui/pagination'
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
import { useMeQuery } from '@/services/auth/auth.service'
|
||||||
import {
|
import {
|
||||||
Tab,
|
Tab,
|
||||||
useCreateDeckMutation,
|
useCreateDeckMutation,
|
||||||
@@ -25,6 +26,7 @@ import { useAppDispatch, useAppSelector } from '@/services/store'
|
|||||||
import s from './decks-page.module.scss'
|
import s from './decks-page.module.scss'
|
||||||
|
|
||||||
export const DecksPage = () => {
|
export const DecksPage = () => {
|
||||||
|
const { data: me } = useMeQuery()
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||||
const [deckToDeleteId, setDeckToDeleteId] = useState<null | string>(null)
|
const [deckToDeleteId, setDeckToDeleteId] = useState<null | string>(null)
|
||||||
const [deckToEditId, setDeckToEditId] = useState<null | string>(null)
|
const [deckToEditId, setDeckToEditId] = useState<null | string>(null)
|
||||||
@@ -41,7 +43,8 @@ export const DecksPage = () => {
|
|||||||
const setMinCards = (minCards: number) => dispatch(decksSlice.actions.setMinCards(minCards))
|
const setMinCards = (minCards: number) => dispatch(decksSlice.actions.setMinCards(minCards))
|
||||||
const setMaxCards = (maxCards: number) => dispatch(decksSlice.actions.setMaxCards(maxCards))
|
const setMaxCards = (maxCards: number) => dispatch(decksSlice.actions.setMaxCards(maxCards))
|
||||||
const setSearch = (search: string) => dispatch(decksSlice.actions.setSearch(search))
|
const setSearch = (search: string) => dispatch(decksSlice.actions.setSearch(search))
|
||||||
const setCurrentTab = (tab: Tab) => dispatch(decksSlice.actions.setCurrentTab(tab))
|
const setCurrentTab = (tab: { authorId?: string; tab: Tab }) =>
|
||||||
|
dispatch(decksSlice.actions.setCurrentTab(tab))
|
||||||
|
|
||||||
const resetFilters = () => {
|
const resetFilters = () => {
|
||||||
dispatch(decksSlice.actions.resetFilters())
|
dispatch(decksSlice.actions.resetFilters())
|
||||||
@@ -54,10 +57,9 @@ export const DecksPage = () => {
|
|||||||
setMinCards(value[0])
|
setMinCards(value[0])
|
||||||
setMaxCards(value[1])
|
setMaxCards(value[1])
|
||||||
}
|
}
|
||||||
const currentUserId = 'f2be95b9-4d07-4751-a775-bd612fc9553a'
|
const currentUserId = me?.id
|
||||||
const authorId = currentTab === 'my' ? currentUserId : undefined
|
const authorId = currentTab === 'my' ? currentUserId : undefined
|
||||||
|
const { currentData: decksCurrentData, data: decksData } = useGetDecksQuery({
|
||||||
const { data: decks } = useGetDecksQuery({
|
|
||||||
authorId,
|
authorId,
|
||||||
currentPage,
|
currentPage,
|
||||||
maxCardsCount: maxCards,
|
maxCardsCount: maxCards,
|
||||||
@@ -65,6 +67,8 @@ export const DecksPage = () => {
|
|||||||
name: search,
|
name: search,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const decks = decksCurrentData ?? decksData
|
||||||
|
|
||||||
const showConfirmDeleteModal = !!deckToDeleteId
|
const showConfirmDeleteModal = !!deckToDeleteId
|
||||||
const deckToDeleteName = decks?.items?.find(deck => deck.id === deckToDeleteId)?.name
|
const deckToDeleteName = decks?.items?.find(deck => deck.id === deckToDeleteId)?.name
|
||||||
|
|
||||||
@@ -73,9 +77,10 @@ export const DecksPage = () => {
|
|||||||
const [createDeck] = useCreateDeckMutation()
|
const [createDeck] = useCreateDeckMutation()
|
||||||
const [deleteDeck] = useDeleteDeckMutation()
|
const [deleteDeck] = useDeleteDeckMutation()
|
||||||
const [updateDeck] = useUpdateDeckMutation()
|
const [updateDeck] = useUpdateDeckMutation()
|
||||||
|
|
||||||
const openCreateModal = () => setShowCreateModal(true)
|
const openCreateModal = () => setShowCreateModal(true)
|
||||||
|
|
||||||
if (!decks) {
|
if (!decks || !me) {
|
||||||
return <div>loading...</div>
|
return <div>loading...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,14 +115,20 @@ export const DecksPage = () => {
|
|||||||
<Button onClick={openCreateModal}>Add new deck</Button>
|
<Button onClick={openCreateModal}>Add new deck</Button>
|
||||||
<DeckDialog
|
<DeckDialog
|
||||||
onCancel={() => setShowCreateModal(false)}
|
onCancel={() => setShowCreateModal(false)}
|
||||||
onConfirm={createDeck}
|
onConfirm={data => {
|
||||||
|
resetFilters()
|
||||||
|
createDeck(data)
|
||||||
|
}}
|
||||||
onOpenChange={setShowCreateModal}
|
onOpenChange={setShowCreateModal}
|
||||||
open={showCreateModal}
|
open={showCreateModal}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={s.filters}>
|
<div className={s.filters}>
|
||||||
<TextField onValueChange={setSearch} placeholder={'Search'} search value={search} />
|
<TextField onValueChange={setSearch} placeholder={'Search'} search value={search} />
|
||||||
<Tabs onValueChange={value => setCurrentTab(value as Tab)} value={currentTab}>
|
<Tabs
|
||||||
|
onValueChange={value => setCurrentTab({ authorId: currentUserId, tab: value as Tab })}
|
||||||
|
value={currentTab}
|
||||||
|
>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value={'my'}>My decks</TabsTrigger>
|
<TabsTrigger value={'my'}>My decks</TabsTrigger>
|
||||||
<TabsTrigger value={'all'}>All decks</TabsTrigger>
|
<TabsTrigger value={'all'}>All decks</TabsTrigger>
|
||||||
@@ -135,7 +146,7 @@ export const DecksPage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<DecksTable
|
<DecksTable
|
||||||
currentUserId={currentUserId}
|
currentUserId={currentUserId ?? ''}
|
||||||
decks={decks?.items}
|
decks={decks?.items}
|
||||||
onDeleteClick={setDeckToDeleteId}
|
onDeleteClick={setDeckToDeleteId}
|
||||||
onEditClick={setDeckToEditId}
|
onEditClick={setDeckToEditId}
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
import { Page, SignIn } from '@/components'
|
import { Page, SignIn } from '@/components'
|
||||||
|
import { useLoginMutation } from '@/services/auth/auth.service'
|
||||||
|
import { LoginArgs } from '@/services/auth/auth.types'
|
||||||
|
|
||||||
export const SignInPage = () => {
|
export const SignInPage = () => {
|
||||||
|
const [signIn] = useLoginMutation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const handleSignIn = async (data: LoginArgs) => {
|
||||||
|
try {
|
||||||
|
await signIn(data).unwrap()
|
||||||
|
navigate('/')
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error)
|
||||||
|
toast.error(error?.data?.message ?? 'Could not sign in')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<SignIn onSubmit={() => {}} />
|
<SignIn onSubmit={handleSignIn} />
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import {
|
|||||||
createBrowserRouter,
|
createBrowserRouter,
|
||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
|
|
||||||
import { DecksPage, SignInPage } from './pages'
|
|
||||||
import { DeckPage } from '@/pages/deck-page/deck-page'
|
import { DeckPage } from '@/pages/deck-page/deck-page'
|
||||||
|
|
||||||
|
import { DecksPage, SignInPage } from './pages'
|
||||||
|
import { useMeQuery } from './services/auth/auth.service'
|
||||||
|
|
||||||
const publicRoutes: RouteObject[] = [
|
const publicRoutes: RouteObject[] = [
|
||||||
{
|
{
|
||||||
children: [
|
children: [
|
||||||
@@ -41,11 +43,22 @@ const router = createBrowserRouter([
|
|||||||
])
|
])
|
||||||
|
|
||||||
export const Router = () => {
|
export const Router = () => {
|
||||||
|
const { isLoading } = useMeQuery()
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
return <RouterProvider router={router} />
|
return <RouterProvider router={router} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function PrivateRoutes() {
|
function PrivateRoutes() {
|
||||||
const isAuthenticated = true
|
const { isError, isLoading } = useMeQuery()
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>loading...</div>
|
||||||
|
}
|
||||||
|
const isAuthenticated = !isError
|
||||||
|
|
||||||
return isAuthenticated ? <Outlet /> : <Navigate to={'/login'} />
|
return isAuthenticated ? <Outlet /> : <Navigate to={'/login'} />
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/services/auth/auth.service.ts
Normal file
21
src/services/auth/auth.service.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { baseApi } from '..'
|
||||||
|
import { LoginArgs, User } from './auth.types'
|
||||||
|
|
||||||
|
export const authService = baseApi.injectEndpoints({
|
||||||
|
endpoints: builder => ({
|
||||||
|
login: builder.mutation<void, LoginArgs>({
|
||||||
|
invalidatesTags: ['Me'],
|
||||||
|
query: body => ({
|
||||||
|
body,
|
||||||
|
method: 'POST',
|
||||||
|
url: '/v1/auth/login',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
me: builder.query<User, void>({
|
||||||
|
providesTags: ['Me'],
|
||||||
|
query: () => '/v1/auth/me',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { useLoginMutation, useMeQuery } = authService
|
||||||
14
src/services/auth/auth.types.ts
Normal file
14
src/services/auth/auth.types.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export type LoginArgs = {
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
rememberMe?: boolean
|
||||||
|
}
|
||||||
|
export type User = {
|
||||||
|
avatar: null | string
|
||||||
|
created: string
|
||||||
|
email: string
|
||||||
|
id: string
|
||||||
|
isEmailVerified: boolean
|
||||||
|
name: string
|
||||||
|
updated: string
|
||||||
|
}
|
||||||
@@ -1,14 +1,9 @@
|
|||||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
import { baseQueryWithReauth } from '@/services/base-query-with-reauth'
|
||||||
|
import { createApi } from '@reduxjs/toolkit/query/react'
|
||||||
|
|
||||||
export const baseApi = createApi({
|
export const baseApi = createApi({
|
||||||
baseQuery: fetchBaseQuery({
|
baseQuery: baseQueryWithReauth,
|
||||||
baseUrl: 'https://api.flashcards.andrii.es',
|
|
||||||
credentials: 'include',
|
|
||||||
prepareHeaders: headers => {
|
|
||||||
headers.append('x-auth-skip', 'true')
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
endpoints: () => ({}),
|
endpoints: () => ({}),
|
||||||
reducerPath: 'baseApi',
|
reducerPath: 'baseApi',
|
||||||
tagTypes: ['Decks'],
|
tagTypes: ['Decks', 'Me'],
|
||||||
})
|
})
|
||||||
|
|||||||
47
src/services/base-query-with-reauth.ts
Normal file
47
src/services/base-query-with-reauth.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import {
|
||||||
|
BaseQueryFn,
|
||||||
|
FetchArgs,
|
||||||
|
FetchBaseQueryError,
|
||||||
|
fetchBaseQuery,
|
||||||
|
} from '@reduxjs/toolkit/query/react'
|
||||||
|
import { Mutex } from 'async-mutex'
|
||||||
|
|
||||||
|
const mutex = new Mutex()
|
||||||
|
|
||||||
|
const baseQuery = fetchBaseQuery({
|
||||||
|
baseUrl: 'https://api.flashcards.andrii.es',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const baseQueryWithReauth: BaseQueryFn<
|
||||||
|
FetchArgs | string,
|
||||||
|
unknown,
|
||||||
|
FetchBaseQueryError
|
||||||
|
> = async (args, api, extraOptions) => {
|
||||||
|
await mutex.waitForUnlock()
|
||||||
|
let result = await baseQuery(args, api, extraOptions)
|
||||||
|
|
||||||
|
if (result.error && result.error.status === 401) {
|
||||||
|
if (!mutex.isLocked()) {
|
||||||
|
const release = await mutex.acquire()
|
||||||
|
// try to get a new token
|
||||||
|
const refreshResult = await baseQuery(
|
||||||
|
{ method: 'POST', url: '/v1/auth/refresh-token' },
|
||||||
|
api,
|
||||||
|
extraOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
if (refreshResult.meta?.response?.status === 204) {
|
||||||
|
// retry the initial query
|
||||||
|
result = await baseQuery(args, api, extraOptions)
|
||||||
|
}
|
||||||
|
release()
|
||||||
|
} else {
|
||||||
|
// wait until the mutex is available without locking it
|
||||||
|
await mutex.waitForUnlock()
|
||||||
|
result = await baseQuery(args, api, extraOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -7,11 +7,57 @@ import {
|
|||||||
UpdateDeckArgs,
|
UpdateDeckArgs,
|
||||||
baseApi,
|
baseApi,
|
||||||
} from '@/services'
|
} from '@/services'
|
||||||
|
import { RootState } from '@/services/store'
|
||||||
|
|
||||||
const decksService = baseApi.injectEndpoints({
|
const decksService = baseApi.injectEndpoints({
|
||||||
endpoints: builder => ({
|
endpoints: builder => ({
|
||||||
createDeck: builder.mutation<DeckResponse, CreateDeckArgs>({
|
createDeck: builder.mutation<DeckResponse, CreateDeckArgs>({
|
||||||
invalidatesTags: ['Decks'],
|
invalidatesTags: ['Decks'],
|
||||||
|
async onQueryStarted(_, { dispatch, getState, queryFulfilled }) {
|
||||||
|
const res = await queryFulfilled
|
||||||
|
|
||||||
|
console.log(decksService.util.selectCachedArgsForQuery(getState(), 'getDecks'))
|
||||||
|
for (const { endpointName, originalArgs } of decksService.util.selectInvalidatedBy(
|
||||||
|
getState(),
|
||||||
|
[{ type: 'Decks' }]
|
||||||
|
)) {
|
||||||
|
console.log(endpointName, originalArgs)
|
||||||
|
// we only want to update `getPosts` here
|
||||||
|
if (endpointName !== 'getDecks') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dispatch(
|
||||||
|
decksService.util.updateQueryData(endpointName, originalArgs, draft => {
|
||||||
|
draft.items.unshift(res.data)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(args)
|
||||||
|
// const minCardsCount = state.decks.minCards
|
||||||
|
// const search = state.decks.search
|
||||||
|
// const currentPage = state.decks.currentPage
|
||||||
|
// const maxCardsCount = state.decks.maxCards
|
||||||
|
// const authorId = state.decks.authorId
|
||||||
|
//
|
||||||
|
// console.log(res)
|
||||||
|
//
|
||||||
|
// dispatch(
|
||||||
|
// decksService.util.updateQueryData(
|
||||||
|
// 'getDecks',
|
||||||
|
// {
|
||||||
|
// authorId,
|
||||||
|
// currentPage,
|
||||||
|
// maxCardsCount,
|
||||||
|
// minCardsCount,
|
||||||
|
// name: search,
|
||||||
|
// },
|
||||||
|
// draft => {
|
||||||
|
// draft.items.unshift(res.data)
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
},
|
||||||
query: body => ({
|
query: body => ({
|
||||||
body,
|
body,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -42,6 +88,43 @@ const decksService = baseApi.injectEndpoints({
|
|||||||
}),
|
}),
|
||||||
updateDeck: builder.mutation<DeckResponse, UpdateDeckArgs>({
|
updateDeck: builder.mutation<DeckResponse, UpdateDeckArgs>({
|
||||||
invalidatesTags: ['Decks'],
|
invalidatesTags: ['Decks'],
|
||||||
|
async onQueryStarted({ id, ...patch }, { dispatch, getState, queryFulfilled }) {
|
||||||
|
const state = getState() as RootState
|
||||||
|
|
||||||
|
console.log(state)
|
||||||
|
const minCardsCount = state.decks.minCards
|
||||||
|
const search = state.decks.search
|
||||||
|
const currentPage = state.decks.currentPage
|
||||||
|
const maxCardsCount = state.decks.maxCards
|
||||||
|
const authorId = state.decks.authorId
|
||||||
|
|
||||||
|
const patchResult = dispatch(
|
||||||
|
decksService.util.updateQueryData(
|
||||||
|
'getDecks',
|
||||||
|
{
|
||||||
|
authorId,
|
||||||
|
currentPage,
|
||||||
|
maxCardsCount,
|
||||||
|
minCardsCount,
|
||||||
|
name: search,
|
||||||
|
},
|
||||||
|
draft => {
|
||||||
|
const deck = draft.items.find(deck => deck.id === id)
|
||||||
|
|
||||||
|
if (!deck) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Object.assign(deck, patch)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queryFulfilled
|
||||||
|
} catch {
|
||||||
|
patchResult.undo()
|
||||||
|
}
|
||||||
|
},
|
||||||
query: ({ id, ...body }) => ({
|
query: ({ id, ...body }) => ({
|
||||||
body,
|
body,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'
|
|||||||
|
|
||||||
export const decksSlice = createSlice({
|
export const decksSlice = createSlice({
|
||||||
initialState: {
|
initialState: {
|
||||||
|
authorId: undefined as string | undefined,
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
currentTab: 'all' as Tab,
|
currentTab: 'all' as Tab,
|
||||||
maxCards: undefined as number | undefined,
|
maxCards: undefined as number | undefined,
|
||||||
@@ -18,14 +19,17 @@ export const decksSlice = createSlice({
|
|||||||
resetFilters: state => {
|
resetFilters: state => {
|
||||||
state.search = ''
|
state.search = ''
|
||||||
state.currentTab = 'all'
|
state.currentTab = 'all'
|
||||||
|
state.authorId = undefined
|
||||||
state.minCards = 0
|
state.minCards = 0
|
||||||
state.maxCards = undefined
|
state.maxCards = undefined
|
||||||
|
state.currentPage = 1
|
||||||
},
|
},
|
||||||
setCurrentPage: (state, action: PayloadAction<number>) => {
|
setCurrentPage: (state, action: PayloadAction<number>) => {
|
||||||
state.currentPage = action.payload
|
state.currentPage = action.payload
|
||||||
},
|
},
|
||||||
setCurrentTab: (state, action: PayloadAction<Tab>) => {
|
setCurrentTab: (state, action: PayloadAction<{ authorId?: string; tab: Tab }>) => {
|
||||||
state.currentTab = action.payload
|
state.currentTab = action.payload.tab
|
||||||
|
state.authorId = action.payload.authorId
|
||||||
},
|
},
|
||||||
setMaxCards: (state, action: PayloadAction<number>) => {
|
setMaxCards: (state, action: PayloadAction<number>) => {
|
||||||
state.maxCards = action.payload
|
state.maxCards = action.payload
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": false,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user