From 250775ed3056541cf4fc053a98d3e3d9c0f1b883 Mon Sep 17 00:00:00 2001 From: andres Date: Sun, 8 Oct 2023 13:01:25 +0200 Subject: [PATCH] lesson 4 finished --- package.json | 3 + pnpm-lock.yaml | 112 +++++++++++++++++++- src/App.tsx | 11 +- src/base-query-with-reauth.ts | 45 ++++++++ src/pages/login.tsx | 16 +++ src/router.tsx | 155 ++++++++++++++++++++++++++++ src/services/auth/auth.service.ts | 48 +++++++++ src/services/auth/auth.types.ts | 5 + src/services/base-api.ts | 10 ++ src/services/decks/decks.service.ts | 81 +++++++++++++++ src/services/decks/decks.slice.ts | 17 +++ src/services/decks/decks.types.ts | 42 ++++++++ src/services/decks/index.ts | 2 + src/services/store.ts | 20 ++++ vite.config.ts | 3 + 15 files changed, 566 insertions(+), 4 deletions(-) create mode 100644 src/base-query-with-reauth.ts create mode 100644 src/pages/login.tsx create mode 100644 src/router.tsx create mode 100644 src/services/auth/auth.service.ts create mode 100644 src/services/auth/auth.types.ts create mode 100644 src/services/base-api.ts create mode 100644 src/services/decks/decks.service.ts create mode 100644 src/services/decks/decks.slice.ts create mode 100644 src/services/decks/decks.types.ts create mode 100644 src/services/decks/index.ts create mode 100644 src/services/store.ts diff --git a/package.json b/package.json index 6aa27f2..4a11010 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,14 @@ "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-radio-group": "^1.1.3", + "@reduxjs/toolkit": "^1.9.6", "@storybook/theming": "^7.2.1", + "async-mutex": "^0.4.0", "clsx": "^2.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.45.2", + "react-redux": "^8.1.2", "react-router-dom": "^6.14.2", "react-toastify": "^9.1.3", "remeda": "^1.24.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e03f24..981990d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,15 @@ dependencies: '@radix-ui/react-radio-group': specifier: ^1.1.3 version: 1.1.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@reduxjs/toolkit': + specifier: ^1.9.6 + version: 1.9.6(react-redux@8.1.2)(react@18.2.0) '@storybook/theming': specifier: ^7.2.1 version: 7.2.1(react-dom@18.2.0)(react@18.2.0) + async-mutex: + specifier: ^0.4.0 + version: 0.4.0 clsx: specifier: ^2.0.0 version: 2.0.0 @@ -35,6 +41,9 @@ dependencies: react-hook-form: specifier: ^7.45.2 version: 7.45.2(react@18.2.0) + react-redux: + specifier: ^8.1.2 + version: 8.1.2(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) react-router-dom: specifier: ^6.14.2 version: 6.14.2(react-dom@18.2.0)(react@18.2.0) @@ -2672,6 +2681,25 @@ packages: dependencies: '@babel/runtime': 7.22.6 + /@reduxjs/toolkit@1.9.6(react-redux@8.1.2)(react@18.2.0): + resolution: {integrity: sha512-Gc4ikl90ORF4viIdAkY06JNUnODjKfGxZRwATM30EdHq8hLSVoSrwXne5dd739yenP5bJxAX7tLuOWK5RPGtrw==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.0.2 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + dependencies: + immer: 9.0.21 + react: 18.2.0 + react-redux: 8.1.2(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) + redux: 4.2.1 + redux-thunk: 2.4.2(redux@4.2.1) + reselect: 4.1.8 + dev: false + /@remix-run/router@1.7.2: resolution: {integrity: sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==} engines: {node: '>=14'} @@ -3951,6 +3979,13 @@ packages: '@types/node': 20.4.5 dev: true + /@types/hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-YIQtIg4PKr7ZyqNPZObpxfHsHEmuB8dXCxd6qVcGuQVDK2bpsF7bYNnBJ4Nn7giuACZg+WewExgrtAJ3XnA4Xw==} + dependencies: + '@types/react': 18.2.15 + hoist-non-react-statics: 3.3.2 + dev: false + /@types/http-errors@2.0.1: resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==} @@ -4073,6 +4108,10 @@ packages: resolution: {integrity: sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==} dev: true + /@types/use-sync-external-store@0.0.3: + resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} + dev: false + /@types/yargs-parser@21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} dev: true @@ -4610,6 +4649,12 @@ packages: resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} dev: true + /async-mutex@0.4.0: + resolution: {integrity: sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==} + dependencies: + tslib: 2.6.1 + dev: false + /async@3.2.4: resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} dev: true @@ -6647,7 +6692,6 @@ packages: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} dependencies: react-is: 16.13.1 - dev: true /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -6726,6 +6770,10 @@ packages: engines: {node: '>= 4'} dev: true + /immer@9.0.21: + resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} + dev: false + /immutable@4.3.1: resolution: {integrity: sha512-lj9cnmB/kVS0QHsJnYKD1uo3o39nrbKxszjnqS9Fr6NB7bZzW45U6WSGBPKXDL/CvDKqDNPA4r3DoDQ8GTxo2A==} dev: true @@ -8358,7 +8406,6 @@ packages: /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: true /react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -8366,7 +8413,40 @@ packages: /react-is@18.1.0: resolution: {integrity: sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==} - dev: true + + /react-redux@8.1.2(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1): + resolution: {integrity: sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==} + peerDependencies: + '@types/react': ^16.8 || ^17.0 || ^18.0 + '@types/react-dom': ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + react-native: '>=0.59' + redux: ^4 || ^5.0.0-beta.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + react-dom: + optional: true + react-native: + optional: true + redux: + optional: true + dependencies: + '@babel/runtime': 7.22.6 + '@types/hoist-non-react-statics': 3.3.2 + '@types/react': 18.2.15 + '@types/react-dom': 18.2.7 + '@types/use-sync-external-store': 0.0.3 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.1.0 + redux: 4.2.1 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false /react-refresh@0.14.0: resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} @@ -8565,6 +8645,20 @@ packages: strip-indent: 4.0.0 dev: true + /redux-thunk@2.4.2(redux@4.2.1): + resolution: {integrity: sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==} + peerDependencies: + redux: ^4 + dependencies: + redux: 4.2.1 + dev: false + + /redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + dependencies: + '@babel/runtime': 7.22.6 + dev: false + /regenerate-unicode-properties@10.1.0: resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} engines: {node: '>=4'} @@ -8649,6 +8743,10 @@ packages: engines: {node: '>=0.10.5'} dev: true + /reselect@4.1.8: + resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} + dev: false + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -9814,6 +9912,14 @@ packages: react: 18.2.0 tslib: 2.6.1 + /use-sync-external-store@1.2.0(react@18.2.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} diff --git a/src/App.tsx b/src/App.tsx index 5541660..9dd4037 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,12 @@ +import { Provider } from 'react-redux' + +import { Router } from '@/router.tsx' +import { store } from '@/services/store.ts' + export function App() { - return
Hello
+ return ( + + + + ) } diff --git a/src/base-query-with-reauth.ts b/src/base-query-with-reauth.ts new file mode 100644 index 0000000..00ba133 --- /dev/null +++ b/src/base-query-with-reauth.ts @@ -0,0 +1,45 @@ +import { fetchBaseQuery } from '@reduxjs/toolkit/query' +import type { BaseQueryFn, FetchArgs, FetchBaseQueryError } from '@reduxjs/toolkit/query' +import { Mutex } from 'async-mutex' + +const mutex = new Mutex() +const baseQuery = fetchBaseQuery({ + baseUrl: 'https://api.flashcards.andrii.es', + credentials: 'include', +}) + +export const baseQueryWithReauth: BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError +> = async (args, api, extraOptions) => { + // wait until the mutex is available without locking it + await mutex.waitForUnlock() + let result = await baseQuery(args, api, extraOptions) + + if (result.error && result.error.status === 401) { + // checking whether the mutex is locked + if (!mutex.isLocked()) { + const release = await mutex.acquire() + + try { + const refreshResult = await baseQuery( + { url: '/v1/auth/refresh-token', method: 'POST' }, + api, + extraOptions + ) + + if (!refreshResult.error) { + result = await baseQuery(args, api, extraOptions) + } + } finally { + release() + } + } else { + await mutex.waitForUnlock() + result = await baseQuery(args, api, extraOptions) + } + } + + return result +} diff --git a/src/pages/login.tsx b/src/pages/login.tsx new file mode 100644 index 0000000..20c48f3 --- /dev/null +++ b/src/pages/login.tsx @@ -0,0 +1,16 @@ +import { Navigate } from 'react-router-dom' + +import { SignIn } from '@/components' +import { useLoginMutation, useMeQuery } from '@/services/auth/auth.service.ts' + +export const Login = () => { + const { isError, isLoading } = useMeQuery() + const isAuthenticated = !isError + const [logIn] = useLoginMutation() + + if (isLoading) return
Loading...
+ + if (isAuthenticated) return + + return +} diff --git a/src/router.tsx b/src/router.tsx new file mode 100644 index 0000000..7fa9092 --- /dev/null +++ b/src/router.tsx @@ -0,0 +1,155 @@ +import { useState } from 'react' + +import { + createBrowserRouter, + Navigate, + Outlet, + RouteObject, + RouterProvider, +} from 'react-router-dom' + +import { Button, TextField } from '@/components' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeadCell, + TableRow, +} from '@/components/ui/table' +import { Login } from '@/pages/login.tsx' +import { + useLogoutMutation, + useMeQuery, + useUpdateProfileMutation, +} from '@/services/auth/auth.service.ts' +import { useCreateDeckMutation, useDeleteDeckMutation, useGetDecksQuery } from '@/services/decks' +import { decksSlice } from '@/services/decks/decks.slice.ts' +import { useAppDispatch, useAppSelector } from '@/services/store.ts' + +const publicRoutes: RouteObject[] = [ + { + path: '/login', + element: , + }, +] + +const privateRoutes: RouteObject[] = [ + { + path: '/', + element: , + }, +] + +const router = createBrowserRouter([ + { + element: , + children: privateRoutes, + }, + ...publicRoutes, + { + path: '*', + element:

404

, + }, +]) + +export const Router = () => { + const [logOut] = useLogoutMutation() + + return ( +
+ + +
+ ) +} + +function PrivateRoutes() { + const { isError, isLoading } = useMeQuery() + const isAuthenticated = !isError + + if (isLoading) return
Loading...
+ + return isAuthenticated ? : +} + +function Decks() { + const dispatch = useAppDispatch() + const [value, setValue] = useState() + const searchByName = useAppSelector(state => state.decks.searchByName) + const currentPage = useAppSelector(state => state.decks.currentPage) + const setCurrentPage = (page: number) => dispatch(decksSlice.actions.setCurrentPage(page)) + const setSearchByName = (name: string) => dispatch(decksSlice.actions.setSearchByName(name)) + const { currentData: data } = useGetDecksQuery({ currentPage, name: searchByName }) + const [createDeck] = useCreateDeckMutation() + const [deleteDeck] = useDeleteDeckMutation() + const [updateProfile] = useUpdateProfileMutation() + + return ( +
+ setValue(e.currentTarget.files?.[0])} /> + + + + { + setSearchByName(event.target.value) + }} + /> + + + + Name + Cards + Updated at + Created By + Actions + + + + {data?.items?.map(deck => ( + + {deck.name} + {deck.cardsCount} + {new Date(deck.updated).toLocaleDateString()} + + + + + ))} + +
+
+ {[...Array(data?.pagination?.totalPages)].map((_, index) => ( + + ))} +
+
+ ) +} diff --git a/src/services/auth/auth.service.ts b/src/services/auth/auth.service.ts new file mode 100644 index 0000000..35405bd --- /dev/null +++ b/src/services/auth/auth.service.ts @@ -0,0 +1,48 @@ +import { LoginArgs } from '@/services/auth/auth.types.ts' +import { baseApi } from '@/services/base-api.ts' + +export const authService = baseApi.injectEndpoints({ + endpoints: builder => ({ + me: builder.query({ + query: () => '/v1/auth/me', + providesTags: ['Me'], + }), + updateProfile: builder.mutation({ + query: body => ({ + url: '/v1/auth/me', + method: 'PATCH', + body, + }), + }), + login: builder.mutation({ + query: body => ({ + url: '/v1/auth/login', + method: 'POST', + body, + }), + invalidatesTags: ['Me'], + }), + logout: builder.mutation({ + query: () => ({ + url: '/v1/auth/logout', + method: 'POST', + }), + // onQueryStarted: async (_, { getState, dispatch, queryFulfilled }) => { + // try { + // await queryFulfilled + // dispatch( + // authService.util.updateQueryData('me', undefined, draft => { + // return null + // }) + // ) + // } catch (e) { + // console.error(e) + // } + // }, + invalidatesTags: ['Me'], + }), + }), +}) + +export const { useLoginMutation, useMeQuery, useUpdateProfileMutation, useLogoutMutation } = + authService diff --git a/src/services/auth/auth.types.ts b/src/services/auth/auth.types.ts new file mode 100644 index 0000000..28a5ab2 --- /dev/null +++ b/src/services/auth/auth.types.ts @@ -0,0 +1,5 @@ +export type LoginArgs = { + email: string + password: string + rememberMe?: boolean +} diff --git a/src/services/base-api.ts b/src/services/base-api.ts new file mode 100644 index 0000000..b85daa2 --- /dev/null +++ b/src/services/base-api.ts @@ -0,0 +1,10 @@ +import { createApi } from '@reduxjs/toolkit/query/react' + +import { baseQueryWithReauth } from '@/base-query-with-reauth.ts' + +export const baseApi = createApi({ + reducerPath: 'baseApi', + tagTypes: ['Decks', 'Cards', 'Users', 'Me'], + baseQuery: baseQueryWithReauth, + endpoints: () => ({}), +}) diff --git a/src/services/decks/decks.service.ts b/src/services/decks/decks.service.ts new file mode 100644 index 0000000..a2c2ab7 --- /dev/null +++ b/src/services/decks/decks.service.ts @@ -0,0 +1,81 @@ +import { baseApi } from '../base-api' + +import type { CreateDeckArgs, Deck, DecksResponse, DeleteDeckArgs } from './decks.types.ts' +import { GetDecksParams } from './decks.types.ts' + +import { RootState } from '@/services/store.ts' + +export const DecksService = baseApi.injectEndpoints({ + endpoints: builder => { + return { + getDecks: builder.query({ + query: params => { + return { + url: `v1/decks`, + params: params ?? {}, + } + }, + providesTags: ['Decks'], + }), + createDeck: builder.mutation({ + query: body => ({ + url: `v1/decks`, + method: 'POST', + body, + }), + onQueryStarted: async (_, { getState, queryFulfilled, dispatch }) => { + const state = getState() as RootState + const { searchByName, currentPage } = state.decks + + try { + const result = await queryFulfilled + + dispatch( + DecksService.util.updateQueryData( + 'getDecks', + { currentPage, name: searchByName }, + draft => { + draft?.items?.unshift(result.data) + } + ) + ) + } catch (e) { + console.error(e) + } + }, + invalidatesTags: ['Decks'], + }), + deleteDeck: builder.mutation({ + query: ({ id }) => ({ + url: `v1/decks/${id}`, + method: 'DELETE', + }), + + onQueryStarted: async ({ id }, { getState, queryFulfilled, dispatch }) => { + const state = getState() as RootState + const { searchByName, currentPage } = state.decks + + const patchResult = dispatch( + DecksService.util.updateQueryData( + 'getDecks', + { currentPage, name: searchByName }, + draft => { + draft?.items?.splice(draft?.items?.findIndex(deck => deck.id === id), 1) + } + ) + ) + + try { + await queryFulfilled + } catch (e) { + patchResult.undo() + } + }, + + invalidatesTags: ['Decks'], + }), + } + }, +}) + +export const { useGetDecksQuery, useCreateDeckMutation, useDeleteDeckMutation } = DecksService diff --git a/src/services/decks/decks.slice.ts b/src/services/decks/decks.slice.ts new file mode 100644 index 0000000..1da8c3b --- /dev/null +++ b/src/services/decks/decks.slice.ts @@ -0,0 +1,17 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +export const decksSlice = createSlice({ + name: 'decks', + initialState: { + searchByName: '', + currentPage: 1, + }, + reducers: { + setSearchByName: (state, action: PayloadAction) => { + state.searchByName = action.payload + }, + setCurrentPage: (state, action: PayloadAction) => { + state.currentPage = action.payload + }, + }, +}) diff --git a/src/services/decks/decks.types.ts b/src/services/decks/decks.types.ts new file mode 100644 index 0000000..d78b615 --- /dev/null +++ b/src/services/decks/decks.types.ts @@ -0,0 +1,42 @@ +export type DecksResponse = { + maxCardsCount: number + pagination: Pagination + items: Deck[] +} +export type Pagination = { + totalPages: number + currentPage: number + itemsPerPage: number + totalItems: number +} +export type Author = { + id: string + name: string +} +export type Deck = { + id: string + userId: string + name: string + isPrivate?: boolean + shots: number + cover?: string | null + rating: number + isDeleted?: any + isBlocked?: any + created: string + updated: string + cardsCount: number + author: Author +} + +export type CreateDeckArgs = Pick +export type DeleteDeckArgs = Pick +type Direction = 'asc' | 'desc' +type Field = 'name' | 'updated' + +export type GetDecksParams = { + name?: string + authorId?: string + orderBy?: `${Field}-${Direction}` + currentPage?: number +} diff --git a/src/services/decks/index.ts b/src/services/decks/index.ts new file mode 100644 index 0000000..38749f0 --- /dev/null +++ b/src/services/decks/index.ts @@ -0,0 +1,2 @@ +export * from './decks.service.ts' +export * from './decks.types.ts' diff --git a/src/services/store.ts b/src/services/store.ts new file mode 100644 index 0000000..f5fc17e --- /dev/null +++ b/src/services/store.ts @@ -0,0 +1,20 @@ +import { configureStore } from '@reduxjs/toolkit' +import { setupListeners } from '@reduxjs/toolkit/query' +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' + +import { baseApi } from '@/services/base-api.ts' +import { decksSlice } from '@/services/decks/decks.slice.ts' + +export const store = configureStore({ + reducer: { + [baseApi.reducerPath]: baseApi.reducer, + [decksSlice.name]: decksSlice.reducer, + }, + middleware: getDefaultMiddleware => getDefaultMiddleware().concat(baseApi.middleware), +}) +setupListeners(store.dispatch) +export type AppDispatch = typeof store.dispatch +export type RootState = ReturnType + +export const useAppDispatch: () => AppDispatch = useDispatch +export const useAppSelector: TypedUseSelectorHook = useSelector diff --git a/vite.config.ts b/vite.config.ts index 329cfa9..377b56e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,6 +6,9 @@ import { defineConfig } from 'vite' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + server: { + open: true, + }, resolve: { alias: [{ find: '@', replacement: path.resolve(__dirname, 'src') }], },