mirror of
https://github.com/ershisan99/flashcards-example-project.git
synced 2025-12-16 20:59:27 +00:00
live lesson 28-01-24
This commit is contained in:
16
package.json
16
package.json
@@ -29,6 +29,7 @@
|
||||
"@storybook/theming": "^7.6.6",
|
||||
"async-mutex": "^0.4.0",
|
||||
"clsx": "^2.0.0",
|
||||
"loki": "^0.34.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.49.2",
|
||||
@@ -67,5 +68,20 @@
|
||||
"stylelint": "^16.1.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "5.0.10"
|
||||
},
|
||||
"loki": {
|
||||
"configurations": {
|
||||
"chrome.laptop": {
|
||||
"target": "chrome.app",
|
||||
"width": 1366,
|
||||
"height": 768,
|
||||
"deviceScaleFactor": 1,
|
||||
"mobile": false
|
||||
},
|
||||
"chrome.iphone7": {
|
||||
"target": "chrome.app",
|
||||
"preset": "iPhone 7"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1046
pnpm-lock.yaml
generated
1046
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,34 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Header } from './'
|
||||
|
||||
const meta = {
|
||||
argTypes: {
|
||||
onLogout: { action: 'logout' },
|
||||
},
|
||||
component: Header,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
title: 'Components/Header',
|
||||
} satisfies Meta<typeof Header>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const LoggedIn: Story = {
|
||||
// @ts-expect-error onLogout is required but it is provided through argTypes
|
||||
args: {
|
||||
avatar: 'https://avatars.githubusercontent.com/u/1196870?v=4',
|
||||
email: 'johndoe@gmail.com',
|
||||
isLoggedIn: true,
|
||||
userName: 'John Doe',
|
||||
},
|
||||
}
|
||||
|
||||
export const LoggedOut: Story = {
|
||||
args: {
|
||||
isLoggedIn: false,
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import { Page, SignIn } from '@/components'
|
||||
@@ -7,13 +6,10 @@ import { LoginArgs } from '@/services/auth/auth.types'
|
||||
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,17 +11,7 @@ import { DeckPage } from '@/pages/deck-page/deck-page'
|
||||
|
||||
import { DecksPage, SignInPage } from './pages'
|
||||
|
||||
const publicRoutes: RouteObject[] = [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
element: <SignInPage />,
|
||||
path: '/login',
|
||||
},
|
||||
],
|
||||
element: <Outlet />,
|
||||
},
|
||||
]
|
||||
const publicRoutes: RouteObject[] = []
|
||||
|
||||
const privateRoutes: RouteObject[] = [
|
||||
{
|
||||
@@ -34,13 +24,27 @@ const privateRoutes: RouteObject[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const router = createBrowserRouter([
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: privateRoutes,
|
||||
element: <PrivateRoutes />,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
element: <SignInPage />,
|
||||
path: '/login',
|
||||
},
|
||||
],
|
||||
element: <Outlet />,
|
||||
},
|
||||
],
|
||||
element: <RedirectSignedUserToDecks />,
|
||||
},
|
||||
...publicRoutes,
|
||||
],
|
||||
element: <Layout />,
|
||||
@@ -56,3 +60,9 @@ function PrivateRoutes() {
|
||||
|
||||
return isAuthenticated ? <Outlet /> : <Navigate to={'/login'} />
|
||||
}
|
||||
|
||||
function RedirectSignedUserToDecks() {
|
||||
const { isAuthenticated } = useAuthContext()
|
||||
|
||||
return isAuthenticated ? <Navigate to={'/'} /> : <Outlet />
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { baseQueryWithReauth } from '@/services/base-query-with-reauth'
|
||||
import { baseQueryWithReauth } from '@/services/decks/base-query-with-reauth'
|
||||
import { createApi } from '@reduxjs/toolkit/query/react'
|
||||
|
||||
export const baseApi = createApi({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { router } from '@/router'
|
||||
import {
|
||||
BaseQueryFn,
|
||||
FetchArgs,
|
||||
@@ -5,7 +6,6 @@ import {
|
||||
fetchBaseQuery,
|
||||
} from '@reduxjs/toolkit/query/react'
|
||||
import { Mutex } from 'async-mutex'
|
||||
|
||||
const mutex = new Mutex()
|
||||
|
||||
const baseQuery = fetchBaseQuery({
|
||||
@@ -19,6 +19,7 @@ export const baseQueryWithReauth: BaseQueryFn<
|
||||
FetchBaseQueryError
|
||||
> = async (args, api, extraOptions) => {
|
||||
await mutex.waitForUnlock()
|
||||
|
||||
let result = await baseQuery(args, api, extraOptions)
|
||||
|
||||
if (result.error && result.error.status === 401) {
|
||||
@@ -26,7 +27,10 @@ export const baseQueryWithReauth: BaseQueryFn<
|
||||
const release = await mutex.acquire()
|
||||
// try to get a new token
|
||||
const refreshResult = await baseQuery(
|
||||
{ method: 'POST', url: '/v1/auth/refresh-token' },
|
||||
{
|
||||
method: 'POST',
|
||||
url: '/v1/auth/refresh-token',
|
||||
},
|
||||
api,
|
||||
extraOptions
|
||||
)
|
||||
@@ -34,6 +38,8 @@ export const baseQueryWithReauth: BaseQueryFn<
|
||||
if (refreshResult.meta?.response?.status === 204) {
|
||||
// retry the initial query
|
||||
result = await baseQuery(args, api, extraOptions)
|
||||
} else {
|
||||
await router.navigate('/login')
|
||||
}
|
||||
release()
|
||||
} else {
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getValuable } from '@/utils'
|
||||
|
||||
import {
|
||||
CardsResponse,
|
||||
CreateDeckArgs,
|
||||
@@ -6,9 +8,7 @@ import {
|
||||
GetDecksArgs,
|
||||
UpdateDeckArgs,
|
||||
baseApi,
|
||||
} from '@/services'
|
||||
import { RootState } from '@/services/store'
|
||||
import { getValuable } from '@/utils'
|
||||
} from '../'
|
||||
|
||||
const decksService = baseApi.injectEndpoints({
|
||||
endpoints: builder => ({
|
||||
@@ -21,6 +21,7 @@ const decksService = baseApi.injectEndpoints({
|
||||
getState(),
|
||||
[{ type: 'Decks' }]
|
||||
)) {
|
||||
// we only want to update `getPosts` here
|
||||
if (endpointName !== 'getDecks') {
|
||||
continue
|
||||
}
|
||||
@@ -39,6 +40,35 @@ const decksService = baseApi.injectEndpoints({
|
||||
}),
|
||||
deleteDeck: builder.mutation<void, { id: string }>({
|
||||
invalidatesTags: ['Decks'],
|
||||
async onQueryStarted({ id }, { dispatch, getState, queryFulfilled }) {
|
||||
let patchResult: any
|
||||
|
||||
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
|
||||
}
|
||||
patchResult = dispatch(
|
||||
decksService.util.updateQueryData(endpointName, originalArgs, draft => {
|
||||
const index = draft?.items?.findIndex(deck => deck.id === id)
|
||||
|
||||
if (index !== undefined && index !== -1) {
|
||||
draft?.items?.splice(index, 1)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
await queryFulfilled
|
||||
} catch {
|
||||
patchResult?.undo()
|
||||
}
|
||||
},
|
||||
query: ({ id }) => ({
|
||||
method: 'DELETE',
|
||||
url: `v1/decks/${id}`,
|
||||
@@ -61,40 +91,32 @@ const decksService = baseApi.injectEndpoints({
|
||||
}),
|
||||
updateDeck: builder.mutation<DeckResponse, UpdateDeckArgs>({
|
||||
invalidatesTags: ['Decks'],
|
||||
async onQueryStarted({ id, ...patch }, { dispatch, getState, queryFulfilled }) {
|
||||
const state = getState() as RootState
|
||||
async onQueryStarted({ id, ...data }, { dispatch, getState, queryFulfilled }) {
|
||||
let patchResult: any
|
||||
|
||||
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
|
||||
for (const { endpointName, originalArgs } of decksService.util.selectInvalidatedBy(
|
||||
getState(),
|
||||
[{ type: 'Decks' }]
|
||||
)) {
|
||||
if (endpointName !== 'getDecks') {
|
||||
continue
|
||||
}
|
||||
patchResult = dispatch(
|
||||
decksService.util.updateQueryData(endpointName, originalArgs, draft => {
|
||||
const index = draft?.items?.findIndex(deck => deck.id === id)
|
||||
|
||||
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) {
|
||||
if (!index || index === -1) {
|
||||
return
|
||||
}
|
||||
Object.assign(deck, patch)
|
||||
Object.assign(draft?.items?.[index], data)
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
await queryFulfilled
|
||||
} catch {
|
||||
patchResult.undo()
|
||||
} catch (e) {
|
||||
patchResult?.undo()
|
||||
}
|
||||
},
|
||||
query: ({ id, ...body }) => ({
|
||||
|
||||
Reference in New Issue
Block a user