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",
"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

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 { 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')
}
}

View File

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

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'
export const baseApi = createApi({

View File

@@ -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 {

View File

@@ -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 }) => ({

2
todo.md Normal file
View File

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