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",
|
"@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
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 { 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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -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 }) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user