live lesson 28-01-24

This commit is contained in:
2024-01-28 12:44:12 +01:00
parent 7af2b43fed
commit fa5f7c19ca
9 changed files with 975 additions and 253 deletions

View File

@@ -29,6 +29,7 @@
"@storybook/theming": "^7.6.6", "@storybook/theming": "^7.6.6",
"async-mutex": "^0.4.0", "async-mutex": "^0.4.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"loki": "^0.34.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.49.2", "react-hook-form": "^7.49.2",
@@ -67,5 +68,20 @@
"stylelint": "^16.1.0", "stylelint": "^16.1.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "5.0.10" "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

File diff suppressed because it is too large Load Diff

View File

@@ -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,
},
}

View File

@@ -1,4 +1,3 @@
import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { Page, SignIn } from '@/components' import { Page, SignIn } from '@/components'
@@ -7,13 +6,10 @@ import { LoginArgs } from '@/services/auth/auth.types'
export const SignInPage = () => { export const SignInPage = () => {
const [signIn] = useLoginMutation() const [signIn] = useLoginMutation()
const navigate = useNavigate()
const handleSignIn = async (data: LoginArgs) => { const handleSignIn = async (data: LoginArgs) => {
try { try {
await signIn(data).unwrap() await signIn(data).unwrap()
navigate('/')
} catch (error: any) { } catch (error: any) {
console.log(error)
toast.error(error?.data?.message ?? 'Could not sign in') toast.error(error?.data?.message ?? 'Could not sign in')
} }
} }

View File

@@ -11,17 +11,7 @@ import { DeckPage } from '@/pages/deck-page/deck-page'
import { DecksPage, SignInPage } from './pages' import { DecksPage, SignInPage } from './pages'
const publicRoutes: RouteObject[] = [ const publicRoutes: RouteObject[] = []
{
children: [
{
element: <SignInPage />,
path: '/login',
},
],
element: <Outlet />,
},
]
const privateRoutes: RouteObject[] = [ const privateRoutes: RouteObject[] = [
{ {
@@ -34,13 +24,27 @@ const privateRoutes: RouteObject[] = [
}, },
] ]
const router = createBrowserRouter([ export const router = createBrowserRouter([
{ {
children: [ children: [
{ {
children: privateRoutes, children: privateRoutes,
element: <PrivateRoutes />, element: <PrivateRoutes />,
}, },
{
children: [
{
children: [
{
element: <SignInPage />,
path: '/login',
},
],
element: <Outlet />,
},
],
element: <RedirectSignedUserToDecks />,
},
...publicRoutes, ...publicRoutes,
], ],
element: <Layout />, element: <Layout />,
@@ -56,3 +60,9 @@ function PrivateRoutes() {
return isAuthenticated ? <Outlet /> : <Navigate to={'/login'} /> return isAuthenticated ? <Outlet /> : <Navigate to={'/login'} />
} }
function RedirectSignedUserToDecks() {
const { isAuthenticated } = useAuthContext()
return isAuthenticated ? <Navigate to={'/'} /> : <Outlet />
}

View File

@@ -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' import { createApi } from '@reduxjs/toolkit/query/react'
export const baseApi = createApi({ export const baseApi = createApi({

View File

@@ -1,3 +1,4 @@
import { router } from '@/router'
import { import {
BaseQueryFn, BaseQueryFn,
FetchArgs, FetchArgs,
@@ -5,7 +6,6 @@ import {
fetchBaseQuery, fetchBaseQuery,
} from '@reduxjs/toolkit/query/react' } from '@reduxjs/toolkit/query/react'
import { Mutex } from 'async-mutex' import { Mutex } from 'async-mutex'
const mutex = new Mutex() const mutex = new Mutex()
const baseQuery = fetchBaseQuery({ const baseQuery = fetchBaseQuery({
@@ -19,6 +19,7 @@ export const baseQueryWithReauth: BaseQueryFn<
FetchBaseQueryError FetchBaseQueryError
> = async (args, api, extraOptions) => { > = async (args, api, extraOptions) => {
await mutex.waitForUnlock() await mutex.waitForUnlock()
let result = await baseQuery(args, api, extraOptions) let result = await baseQuery(args, api, extraOptions)
if (result.error && result.error.status === 401) { if (result.error && result.error.status === 401) {
@@ -26,7 +27,10 @@ export const baseQueryWithReauth: BaseQueryFn<
const release = await mutex.acquire() const release = await mutex.acquire()
// try to get a new token // try to get a new token
const refreshResult = await baseQuery( const refreshResult = await baseQuery(
{ method: 'POST', url: '/v1/auth/refresh-token' }, {
method: 'POST',
url: '/v1/auth/refresh-token',
},
api, api,
extraOptions extraOptions
) )
@@ -34,6 +38,8 @@ export const baseQueryWithReauth: BaseQueryFn<
if (refreshResult.meta?.response?.status === 204) { if (refreshResult.meta?.response?.status === 204) {
// retry the initial query // retry the initial query
result = await baseQuery(args, api, extraOptions) result = await baseQuery(args, api, extraOptions)
} else {
await router.navigate('/login')
} }
release() release()
} else { } else {

View File

@@ -1,3 +1,5 @@
import { getValuable } from '@/utils'
import { import {
CardsResponse, CardsResponse,
CreateDeckArgs, CreateDeckArgs,
@@ -6,9 +8,7 @@ import {
GetDecksArgs, GetDecksArgs,
UpdateDeckArgs, UpdateDeckArgs,
baseApi, baseApi,
} from '@/services' } from '../'
import { RootState } from '@/services/store'
import { getValuable } from '@/utils'
const decksService = baseApi.injectEndpoints({ const decksService = baseApi.injectEndpoints({
endpoints: builder => ({ endpoints: builder => ({
@@ -21,6 +21,7 @@ const decksService = baseApi.injectEndpoints({
getState(), getState(),
[{ type: 'Decks' }] [{ type: 'Decks' }]
)) { )) {
// we only want to update `getPosts` here
if (endpointName !== 'getDecks') { if (endpointName !== 'getDecks') {
continue continue
} }
@@ -39,6 +40,35 @@ const decksService = baseApi.injectEndpoints({
}), }),
deleteDeck: builder.mutation<void, { id: string }>({ deleteDeck: builder.mutation<void, { id: string }>({
invalidatesTags: ['Decks'], 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 }) => ({ query: ({ id }) => ({
method: 'DELETE', method: 'DELETE',
url: `v1/decks/${id}`, url: `v1/decks/${id}`,
@@ -61,40 +91,32 @@ 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 }) { async onQueryStarted({ id, ...data }, { dispatch, getState, queryFulfilled }) {
const state = getState() as RootState let patchResult: any
const minCardsCount = state.decks.minCards for (const { endpointName, originalArgs } of decksService.util.selectInvalidatedBy(
const search = state.decks.search getState(),
const currentPage = state.decks.currentPage [{ type: 'Decks' }]
const maxCardsCount = state.decks.maxCards )) {
const authorId = state.decks.authorId if (endpointName !== 'getDecks') {
continue
}
patchResult = dispatch(
decksService.util.updateQueryData(endpointName, originalArgs, draft => {
const index = draft?.items?.findIndex(deck => deck.id === id)
const patchResult = dispatch( if (!index || index === -1) {
decksService.util.updateQueryData(
'getDecks',
{
authorId,
currentPage,
maxCardsCount,
minCardsCount,
name: search,
},
draft => {
const deck = draft.items.find(deck => deck.id === id)
if (!deck) {
return return
} }
Object.assign(deck, patch) Object.assign(draft?.items?.[index], data)
} })
) )
) }
try { try {
await queryFulfilled await queryFulfilled
} catch { } catch (e) {
patchResult.undo() patchResult?.undo()
} }
}, },
query: ({ id, ...body }) => ({ query: ({ id, ...body }) => ({

2
todo.md Normal file
View File

@@ -0,0 +1,2 @@
- [ ] component 1
- [x] component 2