loading...
}
@@ -110,14 +115,20 @@ export const DecksPage = () => {
setShowCreateModal(false)}
- onConfirm={createDeck}
+ onConfirm={data => {
+ resetFilters()
+ createDeck(data)
+ }}
onOpenChange={setShowCreateModal}
open={showCreateModal}
/>
- setCurrentTab(value as Tab)} value={currentTab}>
+ setCurrentTab({ authorId: currentUserId, tab: value as Tab })}
+ value={currentTab}
+ >
My decks
All decks
@@ -135,7 +146,7 @@ export const DecksPage = () => {
{
+ 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 (
- {}} />
+
)
}
diff --git a/src/router.tsx b/src/router.tsx
index c46122e..d2c75c8 100644
--- a/src/router.tsx
+++ b/src/router.tsx
@@ -6,9 +6,11 @@ import {
createBrowserRouter,
} from 'react-router-dom'
-import { DecksPage, SignInPage } from './pages'
import { DeckPage } from '@/pages/deck-page/deck-page'
+import { DecksPage, SignInPage } from './pages'
+import { useMeQuery } from './services/auth/auth.service'
+
const publicRoutes: RouteObject[] = [
{
children: [
@@ -41,11 +43,22 @@ const router = createBrowserRouter([
])
export const Router = () => {
+ const { isLoading } = useMeQuery()
+
+ if (isLoading) {
+ return loading...
+ }
+
return
}
function PrivateRoutes() {
- const isAuthenticated = true
+ const { isError, isLoading } = useMeQuery()
+
+ if (isLoading) {
+ return loading...
+ }
+ const isAuthenticated = !isError
return isAuthenticated ? :
}
diff --git a/src/services/auth/auth.service.ts b/src/services/auth/auth.service.ts
new file mode 100644
index 0000000..da53d40
--- /dev/null
+++ b/src/services/auth/auth.service.ts
@@ -0,0 +1,21 @@
+import { baseApi } from '..'
+import { LoginArgs, User } from './auth.types'
+
+export const authService = baseApi.injectEndpoints({
+ endpoints: builder => ({
+ login: builder.mutation({
+ invalidatesTags: ['Me'],
+ query: body => ({
+ body,
+ method: 'POST',
+ url: '/v1/auth/login',
+ }),
+ }),
+ me: builder.query({
+ providesTags: ['Me'],
+ query: () => '/v1/auth/me',
+ }),
+ }),
+})
+
+export const { useLoginMutation, useMeQuery } = authService
diff --git a/src/services/auth/auth.types.ts b/src/services/auth/auth.types.ts
new file mode 100644
index 0000000..fa5be7f
--- /dev/null
+++ b/src/services/auth/auth.types.ts
@@ -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
+}
diff --git a/src/services/base-api.ts b/src/services/base-api.ts
index f2f9776..b78e007 100644
--- a/src/services/base-api.ts
+++ b/src/services/base-api.ts
@@ -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({
- baseQuery: fetchBaseQuery({
- baseUrl: 'https://api.flashcards.andrii.es',
- credentials: 'include',
- prepareHeaders: headers => {
- headers.append('x-auth-skip', 'true')
- },
- }),
+ baseQuery: baseQueryWithReauth,
endpoints: () => ({}),
reducerPath: 'baseApi',
- tagTypes: ['Decks'],
+ tagTypes: ['Decks', 'Me'],
})
diff --git a/src/services/base-query-with-reauth.ts b/src/services/base-query-with-reauth.ts
new file mode 100644
index 0000000..2fa97fb
--- /dev/null
+++ b/src/services/base-query-with-reauth.ts
@@ -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
+}
diff --git a/src/services/decks/decks.service.ts b/src/services/decks/decks.service.ts
index 63d0d0c..7978d20 100644
--- a/src/services/decks/decks.service.ts
+++ b/src/services/decks/decks.service.ts
@@ -7,11 +7,57 @@ import {
UpdateDeckArgs,
baseApi,
} from '@/services'
+import { RootState } from '@/services/store'
const decksService = baseApi.injectEndpoints({
endpoints: builder => ({
createDeck: builder.mutation({
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 => ({
body,
method: 'POST',
@@ -42,6 +88,43 @@ const decksService = baseApi.injectEndpoints({
}),
updateDeck: builder.mutation({
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 }) => ({
body,
method: 'PATCH',
diff --git a/src/services/decks/decks.slice.ts b/src/services/decks/decks.slice.ts
index 0d1f8fb..5df028d 100644
--- a/src/services/decks/decks.slice.ts
+++ b/src/services/decks/decks.slice.ts
@@ -3,6 +3,7 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'
export const decksSlice = createSlice({
initialState: {
+ authorId: undefined as string | undefined,
currentPage: 1,
currentTab: 'all' as Tab,
maxCards: undefined as number | undefined,
@@ -18,14 +19,17 @@ export const decksSlice = createSlice({
resetFilters: state => {
state.search = ''
state.currentTab = 'all'
+ state.authorId = undefined
state.minCards = 0
state.maxCards = undefined
+ state.currentPage = 1
},
setCurrentPage: (state, action: PayloadAction) => {
state.currentPage = action.payload
},
- setCurrentTab: (state, action: PayloadAction) => {
- state.currentTab = action.payload
+ setCurrentTab: (state, action: PayloadAction<{ authorId?: string; tab: Tab }>) => {
+ state.currentTab = action.payload.tab
+ state.authorId = action.payload.authorId
},
setMaxCards: (state, action: PayloadAction) => {
state.maxCards = action.payload
diff --git a/tsconfig.json b/tsconfig.json
index 1c8358f..b151dc6 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -8,7 +8,7 @@
/* Bundler mode */
"moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
+ "allowImportingTsExtensions": false,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,