mirror of
https://github.com/ershisan99/flashcards-example-project.git
synced 2025-12-16 20:59:27 +00:00
lesson 4 finished
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
"@radix-ui/react-radio-group": "^1.1.3",
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
"@reduxjs/toolkit": "^1.9.5",
|
"@reduxjs/toolkit": "^1.9.5",
|
||||||
"@storybook/theming": "^7.2.1",
|
"@storybook/theming": "^7.2.1",
|
||||||
|
"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
@@ -26,6 +26,9 @@ dependencies:
|
|||||||
'@storybook/theming':
|
'@storybook/theming':
|
||||||
specifier: ^7.2.1
|
specifier: ^7.2.1
|
||||||
version: 7.2.1(react-dom@18.2.0)(react@18.2.0)
|
version: 7.2.1(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
|
||||||
@@ -4646,6 +4649,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.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/async@3.2.4:
|
/async@3.2.4:
|
||||||
resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==}
|
resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import { Button, Card, ControlledTextField, Typography } from '../../ui'
|
|||||||
|
|
||||||
import s from './recover-password.module.scss'
|
import s from './recover-password.module.scss'
|
||||||
|
|
||||||
import { emailSchema } from '@/components'
|
const schema = z.object({
|
||||||
|
email: z.string().email('Invalid email address').nonempty('Enter email'),
|
||||||
const schema = emailSchema
|
})
|
||||||
|
|
||||||
type FormType = z.infer<typeof schema>
|
type FormType = z.infer<typeof schema>
|
||||||
|
|
||||||
|
|||||||
@@ -8,21 +8,17 @@ import { Button, Card, ControlledCheckbox, ControlledTextField, Typography } fro
|
|||||||
|
|
||||||
import s from './sign-in.module.scss'
|
import s from './sign-in.module.scss'
|
||||||
|
|
||||||
export const emailSchema = z.object({
|
const schema = z.object({
|
||||||
|
password: z.string().nonempty('Enter password'),
|
||||||
email: z.string().email('Invalid email address').nonempty('Enter email'),
|
email: z.string().email('Invalid email address').nonempty('Enter email'),
|
||||||
|
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 FormType = z.infer<typeof schema>
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSubmit: (data: FormType) => void
|
onSubmit: (data: FormType) => void
|
||||||
|
isSubmitting?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SignIn = (props: Props) => {
|
export const SignIn = (props: Props) => {
|
||||||
@@ -76,7 +72,7 @@ export const SignIn = (props: Props) => {
|
|||||||
>
|
>
|
||||||
Forgot Password?
|
Forgot Password?
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button className={s.button} fullWidth type={'submit'}>
|
<Button className={s.button} fullWidth type={'submit'} disabled={props.isSubmitting}>
|
||||||
Sign In
|
Sign In
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -9,14 +9,12 @@ import { Button, Card, ControlledTextField, Typography } from '../../ui'
|
|||||||
|
|
||||||
import s from './sign-up.module.scss'
|
import s from './sign-up.module.scss'
|
||||||
|
|
||||||
import { emailSchema } from '@/components'
|
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
password: z.string().nonempty('Enter password'),
|
password: z.string().nonempty('Enter password'),
|
||||||
passwordConfirmation: z.string().nonempty('Confirm your password'),
|
passwordConfirmation: z.string().nonempty('Confirm your password'),
|
||||||
|
email: z.string().email('Invalid email address').nonempty('Enter email'),
|
||||||
})
|
})
|
||||||
.merge(emailSchema)
|
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
if (data.password !== data.passwordConfirmation) {
|
if (data.password !== data.passwordConfirmation) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
|
|||||||
@@ -198,8 +198,6 @@ export const WithSort = {
|
|||||||
return `${sort.key}-${sort.direction}`
|
return `${sort.key}-${sort.direction}`
|
||||||
}, [sort])
|
}, [sort])
|
||||||
|
|
||||||
console.log(sortedString)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table>
|
<table>
|
||||||
<TableHeader columns={columns} sort={sort} onSort={setSort} />
|
<TableHeader columns={columns} sort={sort} onSort={setSort} />
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { Button, TextField } from '@/components'
|
import { useForm } from 'react-hook-form'
|
||||||
|
|
||||||
|
import { Button, ControlledTextField, TextField } from '@/components'
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -9,59 +11,82 @@ import {
|
|||||||
TableHeadCell,
|
TableHeadCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
import { useCreateDeckMutation, useGetDecksQuery } from '@/services/decks'
|
import { useLogoutMutation } from '@/services/auth/auth.ts'
|
||||||
|
import { useCreateDeckMutation, useDeleteDeckMutation, useGetDecksQuery } from '@/services/decks'
|
||||||
import { decksSlice } from '@/services/decks/decks.slice.ts'
|
import { decksSlice } from '@/services/decks/decks.slice.ts'
|
||||||
|
import { CreateDeckArgs } from '@/services/decks/types.ts'
|
||||||
import { useAppDispatch, useAppSelector } from '@/services/store.ts'
|
import { useAppDispatch, useAppSelector } from '@/services/store.ts'
|
||||||
|
|
||||||
export const Decks = () => {
|
export const Decks = () => {
|
||||||
const [cardName, setCardName] = useState('')
|
const [cardName, setCardName] = useState('')
|
||||||
|
|
||||||
|
const { register, control, handleSubmit } = useForm<{
|
||||||
|
name: string
|
||||||
|
cover: File[]
|
||||||
|
}>()
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const itemsPerPage = useAppSelector(state => state.decksSlice.itemsPerPage)
|
const itemsPerPage = useAppSelector(state => state.decksSlice.itemsPerPage)
|
||||||
const currentPage = useAppSelector(state => state.decksSlice.currentPage)
|
const currentPage = useAppSelector(state => state.decksSlice.currentPage)
|
||||||
const searchByName = useAppSelector(state => state.decksSlice.searchByName)
|
const searchByName = useAppSelector(state => state.decksSlice.searchByName)
|
||||||
|
const orderBy = useAppSelector(state => state.decksSlice.orderBy)
|
||||||
|
|
||||||
const setItemsPerPage = (itemsPerPage: number) =>
|
const setItemsPerPage = (itemsPerPage: number) => {
|
||||||
dispatch(decksSlice.actions.setItemsPerPage(itemsPerPage))
|
dispatch(decksSlice.actions.setItemsPerPage(itemsPerPage))
|
||||||
|
}
|
||||||
const setCurrentPage = (currentPage: number) =>
|
const setCurrentPage = (currentPage: number) =>
|
||||||
dispatch(decksSlice.actions.setCurrentPage(currentPage))
|
dispatch(decksSlice.actions.setCurrentPage(currentPage))
|
||||||
const setSearch = (search: string) => dispatch(decksSlice.actions.setSearchByName(search))
|
const setSearch = (search: string) => dispatch(decksSlice.actions.setSearchByName(search))
|
||||||
|
|
||||||
const { isLoading, data, refetch } = useGetDecksQuery({
|
const {
|
||||||
|
isLoading,
|
||||||
|
currentData: data,
|
||||||
|
refetch,
|
||||||
|
} = useGetDecksQuery({
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
currentPage,
|
currentPage,
|
||||||
name: searchByName,
|
name: searchByName,
|
||||||
orderBy: 'created-desc',
|
orderBy,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [createDeck, { isLoading: isCreateDeckLoading }] = useCreateDeckMutation()
|
const [createDeck, { isLoading: isCreateDeckLoading }] = useCreateDeckMutation()
|
||||||
|
const [deleteDeck, { error }] = useDeleteDeckMutation()
|
||||||
|
const [logout] = useLogoutMutation()
|
||||||
|
|
||||||
const handleCreateClicked = () => createDeck({ name: cardName })
|
const handleCreateClicked = handleSubmit(data => {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
console.log(data.cover)
|
||||||
|
formData.append('name', data.name)
|
||||||
|
formData.append('cover', data.cover[0])
|
||||||
|
|
||||||
|
createDeck(formData as unknown as CreateDeckArgs)
|
||||||
|
})
|
||||||
|
|
||||||
if (isLoading) return <div>Loading...</div>
|
if (isLoading) return <div>Loading...</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
{/*<div>*/}
|
||||||
<Button onClick={refetch}>refetch</Button>
|
{/* <Button onClick={refetch}>refetch</Button>*/}
|
||||||
</div>
|
{/* <Button onClick={logout}>Logout</Button>*/}
|
||||||
<div>
|
{/*</div>*/}
|
||||||
<Button onClick={() => setItemsPerPage(10)}>itemsPerPage: 10</Button>
|
{/*<div>*/}
|
||||||
<Button onClick={() => setItemsPerPage(20)}>itemsPerPage: 20</Button>
|
{/* <Button onClick={() => setItemsPerPage(10)}>itemsPerPage: 10</Button>*/}
|
||||||
<Button onClick={() => setItemsPerPage(30)}>itemsPerPage: 30</Button>
|
{/* <Button onClick={() => setItemsPerPage(20)}>itemsPerPage: 20</Button>*/}
|
||||||
</div>
|
{/* <Button onClick={() => setItemsPerPage(30)}>itemsPerPage: 30</Button>*/}
|
||||||
<div>
|
{/*</div>*/}
|
||||||
<Button onClick={() => setCurrentPage(1)}>currentPage: 1</Button>
|
{/*<div>*/}
|
||||||
<Button onClick={() => setCurrentPage(2)}>currentPage: 2</Button>
|
{/* <Button onClick={() => setCurrentPage(1)}>currentPage: 1</Button>*/}
|
||||||
<Button onClick={() => setCurrentPage(3)}>currentPage: 3</Button>
|
{/* <Button onClick={() => setCurrentPage(2)}>currentPage: 2</Button>*/}
|
||||||
</div>
|
{/* <Button onClick={() => setCurrentPage(3)}>currentPage: 3</Button>*/}
|
||||||
<TextField value={searchByName} onChange={e => setSearch(e.currentTarget.value)} />
|
{/*</div>*/}
|
||||||
<TextField
|
{/*<TextField value={searchByName} onChange={e => setSearch(e.currentTarget.value)} />*/}
|
||||||
value={cardName}
|
<form onSubmit={handleCreateClicked}>
|
||||||
onChange={e => setCardName(e.currentTarget.value)}
|
<ControlledTextField name={'name'} control={control} label={'name'} />
|
||||||
label={'card name'}
|
<input type={'file'} {...register('cover')} />
|
||||||
/>
|
<Button>Create deck</Button>
|
||||||
<Button onClick={handleCreateClicked}>Create deck</Button>
|
</form>
|
||||||
isCreateDeckLoading: {isCreateDeckLoading.toString()}
|
isCreateDeckLoading: {isCreateDeckLoading.toString()}
|
||||||
<Table>
|
<Table>
|
||||||
{/* table*/}
|
{/* table*/}
|
||||||
@@ -81,11 +106,13 @@ export const Decks = () => {
|
|||||||
{data?.items.map(deck => {
|
{data?.items.map(deck => {
|
||||||
return (
|
return (
|
||||||
<TableRow key={deck.id}>
|
<TableRow key={deck.id}>
|
||||||
{/* tr*/}
|
|
||||||
<TableCell>{deck.name}</TableCell> {/* td*/}
|
<TableCell>{deck.name}</TableCell> {/* td*/}
|
||||||
<TableCell>{deck.cardsCount}</TableCell>
|
<TableCell>{deck.cardsCount}</TableCell>
|
||||||
<TableCell>{new Date(deck.updated).toLocaleString('en-GB')}</TableCell>
|
<TableCell>{new Date(deck.updated).toLocaleString('en-GB')}</TableCell>
|
||||||
<TableCell>{deck.author.name}</TableCell>
|
<TableCell>{deck.author.name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button onClick={() => deleteDeck({ id: deck.id })}>Delete</Button>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
23
src/pages/sign-in/sign-in.tsx
Normal file
23
src/pages/sign-in/sign-in.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Navigate, useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { SignIn } from '@/components'
|
||||||
|
import { useLoginMutation, useMeQuery } from '@/services/auth/auth.ts'
|
||||||
|
|
||||||
|
export const SignInPage = () => {
|
||||||
|
const { data, isLoading } = useMeQuery()
|
||||||
|
const [signIn, { isLoading: isSigningIn }] = useLoginMutation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>
|
||||||
|
if (data) return <Navigate to="/" />
|
||||||
|
|
||||||
|
const handleSignIn = (data: any) => {
|
||||||
|
signIn(data)
|
||||||
|
.unwrap()
|
||||||
|
.then(() => {
|
||||||
|
navigate('/')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SignIn onSubmit={handleSignIn} isSubmitting={isSigningIn} />
|
||||||
|
}
|
||||||
@@ -7,11 +7,13 @@ import {
|
|||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
|
|
||||||
import { Decks } from '@/pages/decks/decks.tsx'
|
import { Decks } from '@/pages/decks/decks.tsx'
|
||||||
|
import { SignInPage } from '@/pages/sign-in/sign-in.tsx'
|
||||||
|
import { useMeQuery } from '@/services/auth/auth.ts'
|
||||||
|
|
||||||
const publicRoutes: RouteObject[] = [
|
const publicRoutes: RouteObject[] = [
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
element: <div>login</div>,
|
element: <SignInPage />,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -35,7 +37,11 @@ export const Router = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PrivateRoutes() {
|
function PrivateRoutes() {
|
||||||
const isAuthenticated = true
|
const { data, isLoading } = useMeQuery()
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>
|
||||||
|
|
||||||
|
const isAuthenticated = !!data
|
||||||
|
|
||||||
return isAuthenticated ? <Outlet /> : <Navigate to="/login" />
|
return isAuthenticated ? <Outlet /> : <Navigate to="/login" />
|
||||||
}
|
}
|
||||||
|
|||||||
60
src/services/auth/auth.ts
Normal file
60
src/services/auth/auth.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { baseApi } from '@/services/base-api.ts'
|
||||||
|
|
||||||
|
const authApi = baseApi.injectEndpoints({
|
||||||
|
endpoints: builder => {
|
||||||
|
return {
|
||||||
|
me: builder.query<any, void>({
|
||||||
|
query: () => {
|
||||||
|
return {
|
||||||
|
url: `v1/auth/me`,
|
||||||
|
method: 'GET',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extraOptions: {
|
||||||
|
maxRetries: 0,
|
||||||
|
},
|
||||||
|
providesTags: ['Me'],
|
||||||
|
}),
|
||||||
|
login: builder.mutation<any, any>({
|
||||||
|
query: args => {
|
||||||
|
return {
|
||||||
|
url: `v1/auth/login`,
|
||||||
|
method: 'POST',
|
||||||
|
params: args,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
invalidatesTags: ['Me'],
|
||||||
|
}),
|
||||||
|
logout: builder.mutation({
|
||||||
|
query: () => {
|
||||||
|
return {
|
||||||
|
url: `v1/auth/logout`,
|
||||||
|
method: 'POST',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onQueryStarted(_, { dispatch, queryFulfilled }) {
|
||||||
|
const patchResult = dispatch(
|
||||||
|
authApi.util.updateQueryData('me', undefined, () => {
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queryFulfilled
|
||||||
|
} catch {
|
||||||
|
patchResult.undo()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternatively, on failure you can invalidate the corresponding cache tags
|
||||||
|
* to trigger a re-fetch:
|
||||||
|
* dispatch(api.util.invalidateTags(['Post']))
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
invalidatesTags: ['Me'],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { useLoginMutation, useMeQuery, useLogoutMutation } = authApi
|
||||||
51
src/services/base-api-with-refetch.ts
Normal file
51
src/services/base-api-with-refetch.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { BaseQueryFn, FetchArgs, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query'
|
||||||
|
import { Mutex } from 'async-mutex'
|
||||||
|
|
||||||
|
const baseUrl = 'http://localhost:3333'
|
||||||
|
|
||||||
|
// Create a new mutex
|
||||||
|
const mutex = new Mutex()
|
||||||
|
|
||||||
|
const baseQuery = fetchBaseQuery({
|
||||||
|
baseUrl,
|
||||||
|
credentials: 'include',
|
||||||
|
// prepareHeaders: headers => {
|
||||||
|
// headers.append('x-short-access-token', 'true')
|
||||||
|
// },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const customFetchBase: BaseQueryFn<
|
||||||
|
string | FetchArgs,
|
||||||
|
unknown,
|
||||||
|
FetchBaseQueryError
|
||||||
|
> = async (args, api, extraOptions) => {
|
||||||
|
// wait until the mutex is available without locking it
|
||||||
|
await mutex.waitForUnlock()
|
||||||
|
let result = await baseQuery(args, api, extraOptions)
|
||||||
|
|
||||||
|
if (result.error?.status === 401) {
|
||||||
|
if (!mutex.isLocked()) {
|
||||||
|
const release = await mutex.acquire()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const refreshResult = await baseQuery(
|
||||||
|
{ url: 'v1/auth/refresh-token', method: 'POST' },
|
||||||
|
api,
|
||||||
|
extraOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
if (refreshResult?.meta?.response?.status === 204) {
|
||||||
|
// Retry the initial query
|
||||||
|
result = await baseQuery(args, api, extraOptions)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
release()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await mutex.waitForUnlock()
|
||||||
|
result = await baseQuery(args, api, extraOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
import { createApi } from '@reduxjs/toolkit/query/react'
|
||||||
|
|
||||||
|
import { customFetchBase } from '@/services/base-api-with-refetch.ts'
|
||||||
|
|
||||||
export const baseApi = createApi({
|
export const baseApi = createApi({
|
||||||
reducerPath: 'baseApi',
|
reducerPath: 'baseApi',
|
||||||
tagTypes: ['Decks'],
|
tagTypes: ['Decks', 'Me'],
|
||||||
baseQuery: fetchBaseQuery({
|
baseQuery: customFetchBase,
|
||||||
baseUrl: 'https://api.flashcards.andrii.es',
|
|
||||||
credentials: 'include',
|
|
||||||
prepareHeaders: headers => {
|
|
||||||
headers.append('x-auth-skip', 'true')
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
endpoints: () => ({}),
|
endpoints: () => ({}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const initialState = {
|
|||||||
itemsPerPage: 10,
|
itemsPerPage: 10,
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
searchByName: '',
|
searchByName: '',
|
||||||
|
orderBy: 'created-desc',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const decksSlice = createSlice({
|
export const decksSlice = createSlice({
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { baseApi } from '@/services/base-api.ts'
|
import { baseApi } from '@/services/base-api.ts'
|
||||||
import { CreateDeckArgs, Deck, DecksResponse, GetDecksArgs } from '@/services/decks/types.ts'
|
import {
|
||||||
|
CreateDeckArgs,
|
||||||
|
Deck,
|
||||||
|
DecksResponse,
|
||||||
|
DeleteDeckArgs,
|
||||||
|
GetDecksArgs,
|
||||||
|
} from '@/services/decks/types.ts'
|
||||||
|
import { RootState } from '@/services/store.ts'
|
||||||
|
|
||||||
const decksApi = baseApi.injectEndpoints({
|
const decksApi = baseApi.injectEndpoints({
|
||||||
endpoints: builder => {
|
endpoints: builder => {
|
||||||
@@ -15,11 +22,73 @@ const decksApi = baseApi.injectEndpoints({
|
|||||||
providesTags: ['Decks'],
|
providesTags: ['Decks'],
|
||||||
}),
|
}),
|
||||||
createDeck: builder.mutation<Deck, CreateDeckArgs>({
|
createDeck: builder.mutation<Deck, CreateDeckArgs>({
|
||||||
query: ({ name }) => {
|
query: data => {
|
||||||
return {
|
return {
|
||||||
url: 'v1/decks',
|
url: 'v1/decks',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { name },
|
body: data,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onQueryStarted(_, { dispatch, getState, queryFulfilled }) {
|
||||||
|
const state = getState() as RootState
|
||||||
|
|
||||||
|
const { searchByName, orderBy, currentPage, itemsPerPage } = state.decksSlice
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await queryFulfilled
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
decksApi.util.updateQueryData(
|
||||||
|
'getDecks',
|
||||||
|
{ name: searchByName, orderBy, currentPage, itemsPerPage },
|
||||||
|
draft => {
|
||||||
|
draft.items.pop()
|
||||||
|
draft.items.unshift(res.data)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
// patchResult.undo()
|
||||||
|
/**
|
||||||
|
* Alternatively, on failure you can invalidate the corresponding cache tags
|
||||||
|
* to trigger a re-fetch:
|
||||||
|
* dispatch(api.util.invalidateTags(['Post']))
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
invalidatesTags: ['Decks'],
|
||||||
|
}),
|
||||||
|
deleteDeck: builder.mutation<void, DeleteDeckArgs>({
|
||||||
|
query: ({ id }) => {
|
||||||
|
return {
|
||||||
|
url: `v1/decks/${id}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onQueryStarted({ id }, { dispatch, getState, queryFulfilled }) {
|
||||||
|
const state = getState() as RootState
|
||||||
|
|
||||||
|
const { searchByName, orderBy, currentPage, itemsPerPage } = state.decksSlice
|
||||||
|
|
||||||
|
const patchResult = dispatch(
|
||||||
|
decksApi.util.updateQueryData(
|
||||||
|
'getDecks',
|
||||||
|
{ name: searchByName, orderBy, currentPage, itemsPerPage },
|
||||||
|
draft => {
|
||||||
|
draft.items = draft.items.filter(deck => deck.id !== id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queryFulfilled
|
||||||
|
} catch {
|
||||||
|
patchResult.undo()
|
||||||
|
/**
|
||||||
|
* Alternatively, on failure you can invalidate the corresponding cache tags
|
||||||
|
* to trigger a re-fetch:
|
||||||
|
* dispatch(api.util.invalidateTags(['Post']))
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
invalidatesTags: ['Decks'],
|
invalidatesTags: ['Decks'],
|
||||||
@@ -28,4 +97,4 @@ const decksApi = baseApi.injectEndpoints({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const { useGetDecksQuery, useCreateDeckMutation } = decksApi
|
export const { useGetDecksQuery, useCreateDeckMutation, useDeleteDeckMutation } = decksApi
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type GetDecksArgs = PaginatedRequest<{
|
|||||||
export type CreateDeckArgs = {
|
export type CreateDeckArgs = {
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
export type DeleteDeckArgs = Pick<Deck, 'id'>
|
||||||
export interface Author {
|
export interface Author {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
Reference in New Issue
Block a user