From c6dcc35339c4f778669210e9578f2b8cbc40371d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=9B=D0=B0=D0=B9=D0=B5=D1=80?= Date: Mon, 4 Nov 2024 17:46:54 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BF=D1=80=D0=BE=D1=81=D0=BC?= =?UTF-8?q?=D0=BE=D1=82=D1=80=20=D0=B8=D0=B7=D0=BE=D0=B1=D1=80=D0=B0=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=82=D0=BE=D0=B2=D0=B0=D1=80=D0=B0?= =?UTF-8?q?,=20=D1=81=D1=87=D0=B8=D1=82=D1=8B=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=B0=20=D0=B8=D0=B7=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D1=85=D1=80=D0=B0=D0=BD=D0=B8=D0=BB=D0=B8=D1=89=D0=B0,=20?= =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 14 ++ package.json | 1 + src/app/App.tsx | 2 +- src/entities/Register/model/types/types.ts | 2 +- src/entities/ViewProduct/ViewProduct.tsx | 1 + .../ViewProductList.stories.tsx | 2 - .../ViewProductList/ViewProductList.tsx | 11 +- src/entities/ViewProductList/api/request.ts | 8 + .../ViewProductList/model/types/types.ts | 11 +- .../AddProductModal.modal.sass | 10 +- .../ui/AddProductModal/AddProductModal.tsx | 118 +++++++++---- src/homeworks/ts1/1_base.test.js | 21 --- src/homeworks/ts1/1_base.ts | 82 --------- src/homeworks/ts1/2_repair.ts | 55 ------ src/homeworks/ts1/3_write.test.js | 30 ---- src/homeworks/ts1/3_write.ts | 164 ------------------ src/pages/CartPage/CartPage.tsx | 3 +- src/pages/LoginPage/LoginPage.tsx | 3 +- .../RegisterSagaPage/RegisterSagaPage.tsx | 15 +- src/shared/Header/Header.tsx | 5 +- src/store/mainStore.ts | 20 ++- src/store/slices/authAndProfile.ts | 9 +- src/store/slices/productInCartSlice.ts | 7 +- src/store/slices/productSlice.ts | 9 +- src/store/slices/saga/addProductSaga.ts | 24 ++- src/store/slices/saga/appInitiateSaga.ts | 47 +++++ src/store/slices/saga/authAndProfileSaga.ts | 4 +- src/store/slices/saga/constants.ts | 3 + src/store/slices/saga/deleteProductSaga.ts | 9 +- src/store/slices/saga/getProductSaga.ts | 1 - src/store/slices/saga/logOutSaga.ts | 20 +++ .../slices/saga/refreshProductListSaga.ts | 37 ++++ 32 files changed, 317 insertions(+), 431 deletions(-) delete mode 100644 src/homeworks/ts1/1_base.test.js delete mode 100644 src/homeworks/ts1/1_base.ts delete mode 100644 src/homeworks/ts1/2_repair.ts delete mode 100644 src/homeworks/ts1/3_write.test.js delete mode 100644 src/homeworks/ts1/3_write.ts create mode 100644 src/store/slices/saga/appInitiateSaga.ts create mode 100644 src/store/slices/saga/constants.ts create mode 100644 src/store/slices/saga/logOutSaga.ts create mode 100644 src/store/slices/saga/refreshProductListSaga.ts diff --git a/package-lock.json b/package-lock.json index fecba96d5..e58d41894 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "date-fns": "^4.1.0", "i18next": "^23.15.1", "jquery-i18next": "^1.2.1", + "jwt-decode": "^4.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.53.0", @@ -20006,6 +20007,14 @@ "node": ">=8" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", @@ -42887,6 +42896,11 @@ "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", "dev": true }, + "jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==" + }, "keyv": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", diff --git a/package.json b/package.json index c97a2c20f..024fcefa5 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "date-fns": "^4.1.0", "i18next": "^23.15.1", "jquery-i18next": "^1.2.1", + "jwt-decode": "^4.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.53.0", diff --git a/src/app/App.tsx b/src/app/App.tsx index 0174ce97c..26fce3063 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -12,11 +12,11 @@ import '../shared/ThemeProvider/ThemeProvider.css'; import { LoginPage } from '../pages/LoginPage'; import { RoutePrivate } from '../shared/RoutePrivate/RoutePrivate'; import { useDispatch } from 'react-redux'; -import { appInitiated } from '../store/slices/authAndProfile'; import { RoutePrivateAdmin } from '../shared/RoutePrivateAdmin/RoutePrivateAdmin'; import { AdminEditProductPage } from '../pages/AdminEditProductPage/AdminEditProductPage'; import { AccessDeniedPage } from '../pages/AccessDeniedPage/AccessDeniedPage'; import { RegisterSagaPage } from '../pages/RegisterSagaPage/RegisterSagaPage'; +import { appInitiated } from '../store/slices/saga/appInitiateSaga'; function App() { const dispatch = useDispatch(); diff --git a/src/entities/Register/model/types/types.ts b/src/entities/Register/model/types/types.ts index 5910bdd4b..fcc678543 100644 --- a/src/entities/Register/model/types/types.ts +++ b/src/entities/Register/model/types/types.ts @@ -3,7 +3,7 @@ import { z } from "zod"; export const emailPasswordSchema = z.object( { email: z.string().email({ message: 'Не верный адрес электронной почты' }), - password: z.string().min(1, { message: 'Слишком короткий пароль' }) + password: z.string().min(1, { message: 'Слишком короткий пароль' }), } ) diff --git a/src/entities/ViewProduct/ViewProduct.tsx b/src/entities/ViewProduct/ViewProduct.tsx index 67fbceb6e..0083c787c 100644 --- a/src/entities/ViewProduct/ViewProduct.tsx +++ b/src/entities/ViewProduct/ViewProduct.tsx @@ -45,6 +45,7 @@ export const ViewProduct: FC = ({ product, isLast, isEditMode }, [containerRef]); const handleDelete = () => { + console.log('delete component') dispatch(productDelete(product.id)); } diff --git a/src/entities/ViewProductList/ViewProductList.stories.tsx b/src/entities/ViewProductList/ViewProductList.stories.tsx index 69d7263a5..31c7ba526 100644 --- a/src/entities/ViewProductList/ViewProductList.stories.tsx +++ b/src/entities/ViewProductList/ViewProductList.stories.tsx @@ -1,6 +1,4 @@ import { ViewProductList } from "./ViewProductList"; -import { createRandomProduct, Product } from "../../homeworks/ts1/3_write"; -import { getProductsApi } from "./api/request"; export default { title: "UI/ViewProductList", diff --git a/src/entities/ViewProductList/ViewProductList.tsx b/src/entities/ViewProductList/ViewProductList.tsx index 476781dec..961ab6a76 100644 --- a/src/entities/ViewProductList/ViewProductList.tsx +++ b/src/entities/ViewProductList/ViewProductList.tsx @@ -8,6 +8,7 @@ import { AddProductModal } from "./ui/AddProductModal/AddProductModal"; import { CategoryModal } from "./ui/CategoryModal"; import { productGet } from "../../store/slices/saga/getProductSaga"; import { productUpdate } from "../../store/slices/saga/updateProductSaga"; +import { productRefresh } from "../../store/slices/saga/refreshProductListSaga"; interface IViewProductListProps { isEditMode: boolean; @@ -90,15 +91,11 @@ export const ViewProductList = ({ isEditMode }: IViewProductListProps) => { setIsOpenAddCategoryModal(true) }} onClose={() => { - dispatcher(productGet( - { - pageSize: pagination.maxOnPage, - pageNumber: pagination.currentPage, - sorting: pagination.sort, - } - )); setIsOpenAddProductModal(false); }} + onSuccessAddProduct={() => { + setIsOpenAddProductModal(false); + }} /> {/* Модальное окно - выбора\добавления категории */} diff --git a/src/entities/ViewProductList/api/request.ts b/src/entities/ViewProductList/api/request.ts index 529bc4ebd..c63198769 100644 --- a/src/entities/ViewProductList/api/request.ts +++ b/src/entities/ViewProductList/api/request.ts @@ -40,6 +40,14 @@ export const getProductsApi = async (pageSize: number, pageNumber: number, sorti return response.data; } +export const addPhotoApi = async (file: File) => { + const formData = new FormData(); + formData.append('file', file); + + const response = await axiosInstance.post('/upload', formData); + return { url: response.data.url }; +}; + export const addProductApi = async (param: TAddProductParams) => { const response = await axiosInstance.post('/products', param); return response; diff --git a/src/entities/ViewProductList/model/types/types.ts b/src/entities/ViewProductList/model/types/types.ts index cb06c6ca5..113dd47eb 100644 --- a/src/entities/ViewProductList/model/types/types.ts +++ b/src/entities/ViewProductList/model/types/types.ts @@ -38,7 +38,7 @@ export type TProductRaw = { sorting: Sorting, } -// Добавление нового товара +// Добавление нового товара (отправка на сервер) export interface TAddProductParams { name: string; photo?: string; @@ -48,6 +48,15 @@ export interface TAddProductParams { categoryId: string; }; +// Добавление нового товара (Структура данных обрабатываемая формой) +export interface TAddProductFormParams { + name: string; + productImage: File; + desc?: string; + oldPrice?: number; + price: string; + categoryId: string; +}; // Обновление существующего товара diff --git a/src/entities/ViewProductList/ui/AddProductModal/AddProductModal.modal.sass b/src/entities/ViewProductList/ui/AddProductModal/AddProductModal.modal.sass index c2137e293..98d9553cb 100644 --- a/src/entities/ViewProductList/ui/AddProductModal/AddProductModal.modal.sass +++ b/src/entities/ViewProductList/ui/AddProductModal/AddProductModal.modal.sass @@ -3,4 +3,12 @@ margin-left: auto .red - color: red \ No newline at end of file + color: red + +.hidden + display: none + +.imagePreview + display: block + width: 80px + height: 80px \ No newline at end of file diff --git a/src/entities/ViewProductList/ui/AddProductModal/AddProductModal.tsx b/src/entities/ViewProductList/ui/AddProductModal/AddProductModal.tsx index e82e0877e..68c31bf93 100644 --- a/src/entities/ViewProductList/ui/AddProductModal/AddProductModal.tsx +++ b/src/entities/ViewProductList/ui/AddProductModal/AddProductModal.tsx @@ -1,7 +1,7 @@ -import React, { useEffect } from "react" +import React, { LegacyRef, useEffect, useRef, useState } from "react" import { Modal } from "../../../../shared/Modal" import { addProductApi } from "../../api/request"; -import { Category, TAddProductParams } from "../../model/types/types"; +import { Category, TAddProductFormParams, TAddProductParams } from "../../model/types/types"; import { SubmitHandler, useForm } from "react-hook-form"; import { LabelWrapper } from "../../../../shared/LabelWrapper"; import { z } from 'zod'; @@ -13,15 +13,27 @@ import { productAdd } from "../../../../store/slices/saga/addProductSaga"; import { useNavigate } from "react-router-dom"; import { productGet } from "../../../../store/slices/saga/getProductSaga"; +//productImage: z.instanceof(FileList), +// .refine( +// (file) => +// [ +// "image/png", +// "image/jpeg", +// "image/svg", +// ].includes(file.type), +// { message: "Укажите фотографию" } +// ), + +// transform((fileList) => fileList[0]) const schema = z.object({ name: z.string().min(3, { message: 'Минимальная длина имени товара 3 символа' }), - photo: z.string({ message: 'Укажите фотографию' }), desc: z.string().nullable(), oldPrice: z.preprocess((p) => { const res = p === '' ? undefined : Number(p); return res; }, z.number().optional()), - price: z.preprocess((a) => parseInt(z.string().parse(a), 10), z.number().positive({ message: 'Укажите стоимость товара' })), + price: z.preprocess((a) => parseInt(z.string().parse(a), 10), + z.number().positive({ message: 'Укажите стоимость товара' })), categoryId: z.string({ message: 'Обязательно для заполнения' }), }); @@ -29,24 +41,30 @@ export interface IAddProductModalProps { isOpen: boolean; category?: Category, onAddCategory: () => void; + onSuccessAddProduct: () => void; onClose: () => void; } -export const AddProductModal = ({ isOpen, category, onAddCategory, onClose }: IAddProductModalProps) => { +export const AddProductModal = ({ isOpen, category, onAddCategory, onSuccessAddProduct, onClose }: IAddProductModalProps) => { const dispatcher = useAppDispatch(); + const hiddenInputRef = useRef(); + const [preview, setPreview] = useState(""); + const [lastPhotoSelected, setLastPhotoSelected] = useState(); const { register, handleSubmit, - watch, setValue, + setError, + clearErrors, + reset, formState: { errors }, - } = useForm({ + } = useForm({ resolver: zodResolver(schema), defaultValues: { - name: "Имя продукта", - price: 0, + name: "", + price: "0", }, } ); @@ -54,38 +72,57 @@ export const AddProductModal = ({ isOpen, category, onAddCategory, onClose }: IA useEffect(() => { if (category && category.id) { setValue('categoryId', category.id); + clearErrors('categoryId'); } }, [category]); - const onSubmit: SubmitHandler = async (data) => { - const newProduct: TAddProductParams = { + const { ref: registerRef, ...rest } = register("productImage"); + + const handleUploadedFile = (event: React.ChangeEvent) => { + if (event.target.files && event.target.files.length > 0) { + const file = event.target.files[0]; + setLastPhotoSelected(file); + + const urlImage = URL.createObjectURL(file); + setPreview(urlImage); + } + }; + + // Кнопка выбора изображения + const handleUploadImageButton = () => { + if (hiddenInputRef.current && hiddenInputRef.current) { + hiddenInputRef.current.click(); + } + } + + const onSubmit: SubmitHandler = async (data) => { + console.log('onSubmit'); + + if (lastPhotoSelected == undefined) { + setError("productImage", { type: 'custom', message: "Загрузите фотографию" }); + return; + } + + const newProduct: TAddProductFormParams = { name: data.name, price: data.price, - categoryId: category ? category.id : null, - photo: data.photo, + categoryId: category.id, + productImage: lastPhotoSelected, desc: data.desc, oldPrice: data.oldPrice, } await dispatcher(productAdd(newProduct)); - onClose(); + setPreview(""); + setLastPhotoSelected(undefined); + reset(); + onSuccessAddProduct(); } - const handleChangePhoto = async (e: React.ChangeEvent) => { - const [file] = e.target.files; - const formData = new FormData(); - formData.append('file', file); - - const response = await axiosInstance.post('/upload', formData); - return response.data.url; - }; - - return ( <> {/* Модальное окно */} -
@@ -93,14 +130,29 @@ export const AddProductModal = ({ isOpen, category, onAddCategory, onClose }: IA {errors.name &&

{errors.name.message}

} - - ) => { - handleChangePhoto(e).then((url) => { - setValue("photo", url); - }); - }} /> - - {errors.photo &&

{errors.photo.message}

} + + { + registerRef(e); + if (e && e != undefined) { + hiddenInputRef.current = e; + } + }} + /> + {errors.productImage &&

{errors.productImage.message}

} + + {preview && + + } + + + diff --git a/src/homeworks/ts1/1_base.test.js b/src/homeworks/ts1/1_base.test.js deleted file mode 100644 index 7020e2928..000000000 --- a/src/homeworks/ts1/1_base.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { transformCustomers } from './1_base'; - -describe('all', () => { - it('transformCustomers', () => { - const customers = [ - { id: 1, name: 'John', age: 25, isSubscribed: true }, - { id: 2, name: 'Mary', age: 40, isSubscribed: false }, - { id: 3, name: 'Bob', age: 32, isSubscribed: true }, - { id: 4, name: 'Alice', age: 22, isSubscribed: true }, - { id: 5, name: 'David', age: 48, isSubscribed: false }, - ]; - - expect(transformCustomers(customers)).toEqual({ - 1: { name: 'John', age: 25, isSubscribed: true }, - 2: { name: 'Mary', age: 40, isSubscribed: false }, - 3: { name: 'Bob', age: 32, isSubscribed: true }, - 4: { name: 'Alice', age: 22, isSubscribed: true }, - 5: { name: 'David', age: 48, isSubscribed: false }, - }); - }); -}); diff --git a/src/homeworks/ts1/1_base.ts b/src/homeworks/ts1/1_base.ts deleted file mode 100644 index 7066b8b0e..000000000 --- a/src/homeworks/ts1/1_base.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Нужно превратить файл в ts и указать типы аргументов и типы возвращаемого значения - * */ -export const removePlus = (str: string): string => str.replace(/^\+/, ''); - -export const addPlus = (add: string): string => `+${add}`; - -export const removeFirstZeros = (value: string): string => value.replace(/^(-)?[0]+(-?\d+.*)$/, '$1$2'); - -export const getBeautifulNumber = (value?: string, separator = ' '): string | undefined => - value?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator); - -export const round = (value: number, accuracy = 2): number => { - const d = 10 ** accuracy; - return Math.round(value * d) / d; -}; - -const transformRegexp: RegExp = - /(matrix\(-?\d+(\.\d+)?, -?\d+(\.\d+)?, -?\d+(\.\d+)?, -?\d+(\.\d+)?, )(-?\d+(\.\d+)?), (-?\d+(\.\d+)?)\)/; - -export const getTransformFromCss = (transformCssString: string): { x: number, y: number } => { - const data = transformCssString.match(transformRegexp); - if (!data) return { x: 0, y: 0 }; - return { - x: parseInt(data[6], 10), - y: parseInt(data[8], 10), - }; -}; - -export const getColorContrastValue = ([red, green, blue]: [number, number, number]): number => - // http://www.w3.org/TR/AERT#color-contrast - Math.round((red * 299 + green * 587 + blue * 114) / 1000); - -export const getContrastType = (contrastValue: number): string => (contrastValue > 125 ? 'black' : 'white'); - -export const shortColorRegExp: RegExp = /^#[0-9a-f]{3}$/i; -export const longColorRegExp: RegExp = /^#[0-9a-f]{6}$/i; - -export const checkColor = (color: string): void | Error => { - if (!longColorRegExp.test(color) && !shortColorRegExp.test(color)) - throw new Error(`invalid hex color: ${color}`); -}; - -export const hex2rgb = (color: string): [number, number, number] => { - checkColor(color); - if (shortColorRegExp.test(color)) { - const red = parseInt(color.substring(1, 2), 16); - const green = parseInt(color.substring(2, 3), 16); - const blue = parseInt(color.substring(3, 4), 16); - return [red, green, blue]; - } - const red = parseInt(color.substring(1, 3), 16); - const green = parseInt(color.substring(3, 5), 16); - const blue = parseInt(color.substring(5, 8), 16); - return [red, green, blue]; -}; - -type NumberedArray = { - value: string; - index: number; -} - -export const getNumberedArray = (arr: string[]): NumberedArray[] => arr.map((value: string, index: number) => ({ value, index })); -export const toStringArray = (arr: NumberedArray[]): string[] => arr.map(({ value, index }: NumberedArray) => `${value}_${index}`); - -type Customer = { - id: number; - name: string; - age: number; - isSubscribed: boolean; -} - -type DynamicalyObject = { - [id: number]: Omit ; -} - -export const transformCustomers = (customers: Customer[]) => { - return customers.reduce((acc: DynamicalyObject, customer: Customer) => { - acc[customer.id] = { name: customer.name, age: customer.age, isSubscribed: customer.isSubscribed }; - return acc; - }, {}); -}; diff --git a/src/homeworks/ts1/2_repair.ts b/src/homeworks/ts1/2_repair.ts deleted file mode 100644 index ffd2c2439..000000000 --- a/src/homeworks/ts1/2_repair.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Здесь код с ошибками типов. Нужно их устранить - * */ - -// Мы это не проходили, но по тексту ошибки можно понять, как это починить -export const getFakeApi = async (): Promise => { - const result = await fetch('https://jsonplaceholder.typicode.com/todos/1') - .then((response) => response.json()); -}; - -// Мы это не проходили, но по тексту ошибки можно понять, как это починить -export class SomeClass { - set: Set; - channel: BroadcastChannel; - - constructor() { - this.set = new Set([1]); - this.channel = new BroadcastChannel('test-broadcast-channel'); - } -} - -export type Data = { - type: 'Money' | 'Percent'; - value: DataValue; -}; - -export type DataValue = Money | Percent; - -export type Money = { - currency: string; - amount: number; -}; - -export type Percent = { - percent: number; -}; - -const getDataAmountTypeGuard = (type: never): never => { - throw new Error(`Function getDataAmount can be called with a new type of object that it knows nothing about`); -} - -// Здесь, возможно, нужно использовать as, возможно в switch передавать немного по-другому -const getDataAmount = (data: Data): number | Error => { - switch (data.type) { - case 'Money': - return (data.value as Money).amount; - case 'Percent': - throw new Error(`Function getDataAmount impossible use with data type ${data.type}`); - default: { - getDataAmountTypeGuard(data.type); // здесь, возможно, нужно использовать нечто другое. :never должен остаться - } - } -}; - - diff --git a/src/homeworks/ts1/3_write.test.js b/src/homeworks/ts1/3_write.test.js deleted file mode 100644 index a11ac89c3..000000000 --- a/src/homeworks/ts1/3_write.test.js +++ /dev/null @@ -1,30 +0,0 @@ -import { createRandomOperation, createRandomProduct } from './3_write'; - -describe('all', () => { - it('operation', () => { - const createdAt = '2023-06-06T12:06:56.957Z'; - const operation = createRandomOperation(createdAt); - expect(operation).toHaveProperty('createdAt', createdAt); - expect(operation).toHaveProperty('id'); - expect(operation).toHaveProperty('name'); - expect(operation).toHaveProperty('desc'); - expect(operation).toHaveProperty('createdAt'); - expect(operation).toHaveProperty('amount'); - expect(operation).toHaveProperty('category'); - expect(operation).toHaveProperty('type'); - }); - - it('product', () => { - const createdAt = '2023-06-06T12:06:56.957Z'; - const product = createRandomProduct(createdAt); - expect(product).toHaveProperty('createdAt', createdAt); - expect(product).toHaveProperty('id'); - expect(product).toHaveProperty('name'); - expect(product).toHaveProperty('photo'); - expect(product).toHaveProperty('desc'); - expect(product).toHaveProperty('createdAt'); - expect(product).toHaveProperty('oldPrice'); - expect(product).toHaveProperty('price'); - expect(product).toHaveProperty('category'); - }); -}); diff --git a/src/homeworks/ts1/3_write.ts b/src/homeworks/ts1/3_write.ts deleted file mode 100644 index 7ae5ad5fb..000000000 --- a/src/homeworks/ts1/3_write.ts +++ /dev/null @@ -1,164 +0,0 @@ -import drill from '../../asserts/Icons/free-icon-construction-drill.png' -import crane from '../../asserts/Icons/free-icon-crane-truck.png' -import truck from '../../asserts/Icons/free-icon-dump-truck.png' -import excavator from '../../asserts/Icons/free-icon-excavator.png' - - -/** - * Функции написанные здесь пригодятся на последующих уроках - * С помощью этих функций мы будем добавлять элементы в список для проверки динамической загрузки - * Поэтому в идеале чтобы функции возвращали случайные данные, но в то же время не абракадабру. - * В целом сделайте так, как вам будет удобно. - * */ - -/** - * Нужно создать тип Category, он будет использоваться ниже. - * Категория содержит - * - id (строка) - * - name (строка) - * - photo (строка, необязательно) - */ - -/* Продукт (Product) содержит - * - id (строка) - * - name (строка) - * - photo (строка) - * - desc (строка, необязательно) - * - createdAt (строка) - * - oldPrice (число, необязательно) - * - price (число) - * - category (Категория) - */ - -/* Операция (Operation) может быть либо тратой (Cost), либо доходом (Profit) - * - * Трата (Cost) содержит - * - id (строка) - * - name (строка) - * - desc (строка, необязательно) - * - createdAt (строка) - * - amount (число) - * - category (Категория) - * - type ('Cost') - * - * Доход (Profit) содержит - * - id (строка) - * - name (строка) - * - desc (строка, необязательно) - * - createdAt (строка) - * - amount (число) - * - category (Категория) - * - type ('Profit') - * */ - -export type Category = { - id: string; - name: string; - photo?: string; -} - -type BaseId = Omit -type PhotoNotRequired = Pick -type PhotoRequired = Required - -export type Product = BaseId & PhotoRequired & { - desc?: string; - createdAt: string; - oldPrice: string; - price: number; - category: Category; -} - -type Operation = Profit | Cost - -type Profit = BaseId & { - desc?: string; - createdAt: string; - amount: number; - category: Category; - type: 'Profit'; -} - -type OperationWithoutType = Omit - -type Cost = OperationWithoutType & { - type: 'Cost'; -} - -const getRandomNumber = (limit: number) => { - return Math.floor(Math.random() * limit + 1); -} - -const testCategories: Category[] = [ - { - id: "1", - name: "Работа с землей", - }, - { - id: "2", - name: "Подъем грузов", - }, - { - id: "3", - name: "Бурение", - }, -] - - -const photos: string[] = [ - drill, crane, truck, excavator -] - -/** - * Создает случайный продукт (Product). - * Принимает дату создания (строка) - * */ -// export const createRandomProduct = (createdAt: string) => {}; -export const createRandomProduct = (createdAt: string): Product => { - const id = getRandomNumber(10000).toString(); - const name = 'ProductName' + getRandomNumber(1000).toString(); - const photo = photos[getRandomNumber(3)]; - const price = getRandomNumber(10000); - const desc = 'description ' + getRandomNumber(1000).toString(); - const oldPrice = (price - getRandomNumber(10000)).toString(); - const category = testCategories[getRandomNumber(2)]; - - return { - id, - name, - photo, - createdAt, - price, - category, - desc, - oldPrice - } -}; - -/** - * Создает случайную операцию (Operation). - * Принимает дату создания (строка) - * */ -// export const createRandomOperation = (createdAt: string) => {}; - -export const createRandomOperation = (createdAt: string): Operation => { - let randomOperation = Math.floor(Math.random() * 100 % 2); - let type: 'Profit' | 'Cost' = randomOperation == 0 ? "Profit" : "Cost" ; - - let id = getRandomNumber(10000).toString(); - let name = `Операция - ${type}`; - let amount = getRandomNumber(10000); - let desc = type; - const category = testCategories[getRandomNumber(2)]; - - return { - id, - name, - createdAt, - amount, - category, - type, - desc, - } -} - diff --git a/src/pages/CartPage/CartPage.tsx b/src/pages/CartPage/CartPage.tsx index 0783c8237..beaa9d5d4 100644 --- a/src/pages/CartPage/CartPage.tsx +++ b/src/pages/CartPage/CartPage.tsx @@ -6,7 +6,8 @@ import { dellFromCart } from "../../store/slices/productInCartSlice" import { useAppSelector } from "../../store/hooks" export const CartPage = () => { - const productInCart = useAppSelector(state => state.productInCartSlice.productList) + const token = useAppSelector(state => state.authAndProfile.auth.token); + const productInCart = useAppSelector(state => state.productInCartSlice.productList); const dispatch = useDispatch(); const handleOnDeleteFromCart = (id: string) => { diff --git a/src/pages/LoginPage/LoginPage.tsx b/src/pages/LoginPage/LoginPage.tsx index 3457d4152..aef30bd63 100644 --- a/src/pages/LoginPage/LoginPage.tsx +++ b/src/pages/LoginPage/LoginPage.tsx @@ -13,8 +13,9 @@ export const LoginPage = () => { const error = useAppSelector(state => state.authAndProfile.error); const dispatcher = useAppDispatch(); const navigate = useNavigate(); + const { register, handleSubmit, formState: { errors } } = useForm({ - defaultValues: { email: "", password: "" }, + defaultValues: { email: "", password: ""}, resolver: zodResolver(emailPasswordSchema), }); diff --git a/src/pages/RegisterSagaPage/RegisterSagaPage.tsx b/src/pages/RegisterSagaPage/RegisterSagaPage.tsx index 014719054..6ee6e6d50 100644 --- a/src/pages/RegisterSagaPage/RegisterSagaPage.tsx +++ b/src/pages/RegisterSagaPage/RegisterSagaPage.tsx @@ -1,15 +1,16 @@ -import React from "react"; +import React, { useEffect } from "react"; import { useForm } from "react-hook-form"; import s from "./RegisterSagaPage.module.sass"; import { useDispatch } from "react-redux"; import { useAppSelector } from "../../store/hooks"; import { Layout } from "../../shared/Layout"; -import { NavLink } from "react-router-dom"; +import { NavLink, useNavigate } from "react-router-dom"; import { profileRegister } from "../../store/slices/saga/authAndProfileSaga"; import { zodResolver } from "@hookform/resolvers/zod"; import { emailPasswordSchema } from "../../entities/Register/model/types"; export const RegisterSagaPage = () => { + const navigate = useNavigate(); const isLoading = useAppSelector(state => state.authAndProfile.loading.isLoading); const error = useAppSelector(state => state.authAndProfile.error); const profile = useAppSelector(state => state.authAndProfile.profile); @@ -23,6 +24,12 @@ export const RegisterSagaPage = () => { resolver: zodResolver(emailPasswordSchema), }); + useEffect(() => { + if (profile.email && profile.email.length > 0) { + navigate('/profile'); + } + }, [profile]) + const onConfirm = (email: string, password: string) => { dispatcher(profileRegister({ isNewUser: true, email, password })); } @@ -40,7 +47,7 @@ export const RegisterSagaPage = () => { } - {profile.name && + {profile.email &&
{`Регистрация пользователя ${profile.name} прошла успешно.`}
@@ -53,7 +60,7 @@ export const RegisterSagaPage = () => {
{'Пароль:'}
{errors.password &&

{errors.password.message}

} - +
diff --git a/src/shared/Header/Header.tsx b/src/shared/Header/Header.tsx index 902a2781f..843cff7f8 100644 --- a/src/shared/Header/Header.tsx +++ b/src/shared/Header/Header.tsx @@ -8,6 +8,8 @@ import { NavLink, useNavigate } from "react-router-dom"; import { useAppSelector } from "../../store/hooks"; import { useDispatch } from "react-redux"; import { logOut } from "../../store/slices/authAndProfile"; +import { logOutAction } from "../../store/slices/saga/logOutSaga"; +import { cleanCart } from "../../store/slices/productInCartSlice"; export const Header = () => { const dispatch = useDispatch(); @@ -24,7 +26,8 @@ export const Header = () => { {`Добро пожаловать: ${email}`} diff --git a/src/store/mainStore.ts b/src/store/mainStore.ts index b6d429f5e..a664a06af 100644 --- a/src/store/mainStore.ts +++ b/src/store/mainStore.ts @@ -3,23 +3,29 @@ import authAndProfileReducer from "./slices/authAndProfile"; import productInCartSlice from "./slices/productInCartSlice"; import productSlice from "./slices/productSlice"; import createSagaMiddleware from "redux-saga"; -import { takeEvery } from "redux-saga/effects"; +import { takeEvery, takeLatest } from "redux-saga/effects"; import { doProfileRegisterSaga, PROFILE_REGISTER } from "./slices/saga/authAndProfileSaga"; import { doProfileUpdateSaga, PROFILE_UPDATE } from "./slices/saga/profileUpdateSaga"; import { getProductSaga, PRODUCT_GET } from "./slices/saga/getProductSaga"; import { PRODUCT_UPDATE, updateProductSaga } from "./slices/saga/updateProductSaga"; import { PRODUCT_DELETE, deleteProductSaga } from "./slices/saga/deleteProductSaga"; import { PRODUCT_ADD, addProductSaga } from "./slices/saga/addProductSaga"; +import { APP_INITIATED, appInitiateSaga } from "./slices/saga/appInitiateSaga"; +import { LOG_OUT_ACTION, logOutSaga } from "./slices/saga/logOutSaga"; +import { PRODUCT_REFRESH, refreshProductListSaga } from "./slices/saga/refreshProductListSaga"; const sagaMiddleware = createSagaMiddleware(); function* sagas() { - yield takeEvery(PROFILE_REGISTER, doProfileRegisterSaga); - yield takeEvery(PROFILE_UPDATE, doProfileUpdateSaga); - yield takeEvery(PRODUCT_GET, getProductSaga); - yield takeEvery(PRODUCT_UPDATE, updateProductSaga); - yield takeEvery(PRODUCT_DELETE, deleteProductSaga); - yield takeEvery(PRODUCT_ADD, addProductSaga); + yield takeLatest(APP_INITIATED, appInitiateSaga); + yield takeLatest(LOG_OUT_ACTION, logOutSaga); + yield takeLatest(PROFILE_REGISTER, doProfileRegisterSaga); + yield takeLatest(PROFILE_UPDATE, doProfileUpdateSaga); + yield takeLatest(PRODUCT_GET, getProductSaga); + yield takeLatest(PRODUCT_UPDATE, updateProductSaga); + yield takeLatest(PRODUCT_DELETE, deleteProductSaga); + yield takeLatest(PRODUCT_ADD, addProductSaga); + yield takeLatest(PRODUCT_REFRESH, refreshProductListSaga); } const store = configureStore({ diff --git a/src/store/slices/authAndProfile.ts b/src/store/slices/authAndProfile.ts index 0bec1ad5d..4b35a7010 100644 --- a/src/store/slices/authAndProfile.ts +++ b/src/store/slices/authAndProfile.ts @@ -1,5 +1,9 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { TProfile } from "../../shared/profileTypes/profileTypes"; +import { jwtDecode } from "jwt-decode"; +import { date } from "zod"; + + type TAppStarted = { isAppInitiated: boolean; @@ -51,12 +55,11 @@ const authAndProfileSlice = createSlice( name: 'authAndProfile', initialState, reducers: { - appInitiated(state) { + setAppInitiated(state) { state.appStatus.isAppInitiated = true; }, saveToken(state, action: PayloadAction) { state.auth.token = action.payload; - localStorage.setItem('auth_token', action.payload); }, saveProfile(state, action: PayloadAction) { state.profile = { ...action.payload } @@ -83,5 +86,5 @@ const authAndProfileSlice = createSlice( ) export default authAndProfileSlice.reducer; -export const { appInitiated, saveToken, saveProfile, logOut, setIsLoading, setError } = authAndProfileSlice.actions; +export const { setAppInitiated, saveToken, saveProfile, logOut, setIsLoading, setError } = authAndProfileSlice.actions; diff --git a/src/store/slices/productInCartSlice.ts b/src/store/slices/productInCartSlice.ts index 0b137d738..c5ada3ffd 100644 --- a/src/store/slices/productInCartSlice.ts +++ b/src/store/slices/productInCartSlice.ts @@ -33,14 +33,17 @@ const productInCartSlice = createSlice( }, dellFromCart(state, action: PayloadAction) { state.productList = state.productList.filter(p => p.id != action.payload); - } + }, + cleanCart(state){ + state.productList = []; + }, } } ) export default productInCartSlice.reducer; -export const { addToCart, dellFromCart } = productInCartSlice.actions; +export const { addToCart, dellFromCart, cleanCart } = productInCartSlice.actions; export const getCountInCartById = (productListinCart: TProductInCartWithCount[], id: string) => { const found = productListinCart.find(p => p.id === id); diff --git a/src/store/slices/productSlice.ts b/src/store/slices/productSlice.ts index 1ebf4de28..5448fff43 100644 --- a/src/store/slices/productSlice.ts +++ b/src/store/slices/productSlice.ts @@ -1,5 +1,6 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit"; import { Product, Sorting, TUpdateProductParams } from "../../entities/ViewProductList/model/types/types"; +import { MAX_ON_PAGE } from "./saga/constants"; type TError = { isError: boolean; @@ -31,7 +32,7 @@ export type TProductList = { const initialState: TProductList = { productList: [], pagination: { - maxOnPage: 10, + maxOnPage: MAX_ON_PAGE, sort: { type: 'ASC', field: 'id' }, currentPage: 1, }, @@ -80,4 +81,6 @@ const productSlice = createSlice( export default productSlice.reducer; export const { addToTopOfProductList, addProductsToList, cleanProductList, deleteProduct, - updateProductList, setCurrentPage, setIsLoading, setError } = productSlice.actions; \ No newline at end of file + updateProductList, setCurrentPage, setIsLoading, setError } = productSlice.actions; + +export const paginationSelector = (state: TProductList) => state.pagination; \ No newline at end of file diff --git a/src/store/slices/saga/addProductSaga.ts b/src/store/slices/saga/addProductSaga.ts index 1fd30fa05..077ffff65 100644 --- a/src/store/slices/saga/addProductSaga.ts +++ b/src/store/slices/saga/addProductSaga.ts @@ -2,15 +2,29 @@ import { call, put } from "redux-saga/effects"; import { isTErrorResponse, TServerError } from "../../../shared/fetchHelpers/typeGuards"; import { createAction } from "@reduxjs/toolkit"; import { UNKNOWN_ERROR_MESSAGE } from "./constant"; -import { Category, Product, TAddProductParams, TUpdateProductParams } from "../../../entities/ViewProductList/model/types/types"; -import { addProductApi, putProductApi } from "../../../entities/ViewProductList/api/request"; +import { Category, Product, TAddProductFormParams, TAddProductParams, TUpdateProductParams } from "../../../entities/ViewProductList/model/types/types"; +import { addPhotoApi, addProductApi, putProductApi } from "../../../entities/ViewProductList/api/request"; import { setError, cleanProductList } from "../productSlice"; import { getProductSaga, productGet } from "./getProductSaga"; +import { refreshProductListSaga } from "./refreshProductListSaga"; // Saga Effects. Add product -export function* addProductSaga(data: { type: string, payload: TAddProductParams }): any { +export function* addProductSaga(data: { type: string, payload: TAddProductFormParams }): any { try { - yield addProductApi(data.payload); + + // Загрузка фотографии на сервер и получение ссылки на фото на сервере + const uploadPhoto = yield addPhotoApi(data.payload.productImage); + + const { productImage, price, ...others } = data.payload; + const addProduct: TAddProductParams = { + ...others, + price: Number(price), + photo: uploadPhoto.url, + } + + yield addProductApi(addProduct); + + yield call(refreshProductListSaga); } catch (error: unknown) { if (isTErrorResponse(error)) { @@ -25,4 +39,4 @@ export function* addProductSaga(data: { type: string, payload: TAddProductParams } export const PRODUCT_ADD = 'product/addProduct'; -export const productAdd = createAction(PRODUCT_ADD); \ No newline at end of file +export const productAdd = createAction(PRODUCT_ADD); \ No newline at end of file diff --git a/src/store/slices/saga/appInitiateSaga.ts b/src/store/slices/saga/appInitiateSaga.ts new file mode 100644 index 000000000..69aa51eac --- /dev/null +++ b/src/store/slices/saga/appInitiateSaga.ts @@ -0,0 +1,47 @@ +import { call, put } from "redux-saga/effects"; +import { saveProfile, saveToken, setAppInitiated, setError, setIsLoading } from "../authAndProfile"; +import { getProfileApi, postRegisterApi, postSigninApi } from "../../../entities/Register/api/request"; +import { isTRegisterProfile } from "../../../shared/fetchHelpers/registerTypeGuards"; +import { isTProfile, TProfile } from "../../../shared/profileTypes/profileTypes"; +import { COMAND_ID } from "../../../shared/fetchHelpers/fetchSettings"; +import { isTErrorResponse, TServerError } from "../../../shared/fetchHelpers/typeGuards"; +import { createAction } from "@reduxjs/toolkit"; +import { UNKNOWN_ERROR_MESSAGE } from "./constant"; +import { LOCAL_STORAGE_TOKEN } from "./constants"; +import { jwtDecode } from "jwt-decode"; + +// Saga Effects. App started +export function* appInitiateSaga(data: { type: string }): any { + yield put(setAppInitiated()); + + // Проверяем если уже есть авторизация сохраненная в локальном хранилище + const token = localStorage.getItem(LOCAL_STORAGE_TOKEN); + + if (token) { + try { + const decode = jwtDecode(token); + if (decode.exp > Math.round(Date.now() / 1000)) { + yield put(saveToken(token)); + + // Получаем данные профиля + const profile = yield getProfileApi(); + + if (isTProfile(profile)) { + const newProfile: TProfile = { + ...profile, commandId: COMAND_ID + } + yield put(saveProfile(newProfile)); + } else { + throw (new Error(UNKNOWN_ERROR_MESSAGE)); + } + } + + } catch { + localStorage.removeItem(LOCAL_STORAGE_TOKEN); + } + + } +} + +export const APP_INITIATED = 'authAndProfile/appInitiate'; +export const appInitiated = createAction(APP_INITIATED); \ No newline at end of file diff --git a/src/store/slices/saga/authAndProfileSaga.ts b/src/store/slices/saga/authAndProfileSaga.ts index caa0318f2..ed413c6c8 100644 --- a/src/store/slices/saga/authAndProfileSaga.ts +++ b/src/store/slices/saga/authAndProfileSaga.ts @@ -7,6 +7,7 @@ import { COMAND_ID } from "../../../shared/fetchHelpers/fetchSettings"; import { isTErrorResponse, TServerError } from "../../../shared/fetchHelpers/typeGuards"; import { createAction } from "@reduxjs/toolkit"; import { UNKNOWN_ERROR_MESSAGE } from "./constant"; +import { LOCAL_STORAGE_TOKEN } from "./constants"; // Saga Effects export function* doProfileRegisterSaga(data: { type: string, payload: { isNewUser: boolean, email: string, password: string } }): any { @@ -22,6 +23,7 @@ export function* doProfileRegisterSaga(data: { type: string, payload: { isNewUse } if (isTRegisterProfile(response)) { + localStorage.setItem(LOCAL_STORAGE_TOKEN, response.token); yield put(saveToken(response.token)); // Получаем данные профиля const profile = yield getProfileApi(); @@ -33,7 +35,7 @@ export function* doProfileRegisterSaga(data: { type: string, payload: { isNewUse yield put(saveProfile(newProfile)); } } else { - throw (new Error (UNKNOWN_ERROR_MESSAGE)); + throw (new Error(UNKNOWN_ERROR_MESSAGE)); } } catch (error: unknown) { diff --git a/src/store/slices/saga/constants.ts b/src/store/slices/saga/constants.ts new file mode 100644 index 000000000..51fe1bd6b --- /dev/null +++ b/src/store/slices/saga/constants.ts @@ -0,0 +1,3 @@ +export const LOCAL_STORAGE_TOKEN = 'auth_token'; + +export const MAX_ON_PAGE = 50; \ No newline at end of file diff --git a/src/store/slices/saga/deleteProductSaga.ts b/src/store/slices/saga/deleteProductSaga.ts index 8b11c4c5b..57055f41f 100644 --- a/src/store/slices/saga/deleteProductSaga.ts +++ b/src/store/slices/saga/deleteProductSaga.ts @@ -1,4 +1,4 @@ -import { put } from "redux-saga/effects"; +import { call, put } from "redux-saga/effects"; import { isTErrorResponse, TServerError } from "../../../shared/fetchHelpers/typeGuards"; import { createAction } from "@reduxjs/toolkit"; import { UNKNOWN_ERROR_MESSAGE } from "./constant"; @@ -8,8 +8,9 @@ import { deleteProduct, setError } from "../productSlice"; // Saga Effects. Delete product export function* deleteProductSaga(data: { type: string, payload: string }): any { try { - - yield deleteProductApi(data.payload); + console.log('deleteProductSaga ' + data.payload); + + yield call(deleteProductApi, data.payload); yield put(deleteProduct(data.payload)); } catch (error: unknown) { if (isTErrorResponse(error)) { @@ -23,5 +24,5 @@ export function* deleteProductSaga(data: { type: string, payload: string }): any } } -export const PRODUCT_DELETE = 'product/deleteProduct'; +export const PRODUCT_DELETE = 'product/productDeleteSaga'; export const productDelete = createAction(PRODUCT_DELETE); \ No newline at end of file diff --git a/src/store/slices/saga/getProductSaga.ts b/src/store/slices/saga/getProductSaga.ts index c5a239e45..124b4917e 100644 --- a/src/store/slices/saga/getProductSaga.ts +++ b/src/store/slices/saga/getProductSaga.ts @@ -15,7 +15,6 @@ export type TGetProductSagaProps = { // Saga Effects. get product export function* getProductSaga(data: { type: string, payload: TGetProductSagaProps }): any { - console.log('getProductSaga: ') try { yield put(setError({ isError: false, errorMessage: "" })); yield put(setIsLoading(true)); diff --git a/src/store/slices/saga/logOutSaga.ts b/src/store/slices/saga/logOutSaga.ts new file mode 100644 index 000000000..26c739dd6 --- /dev/null +++ b/src/store/slices/saga/logOutSaga.ts @@ -0,0 +1,20 @@ +import { call, put } from "redux-saga/effects"; +import { logOut, saveProfile, saveToken, setAppInitiated, setError, setIsLoading } from "../authAndProfile"; +import { getProfileApi, postRegisterApi, postSigninApi } from "../../../entities/Register/api/request"; +import { isTRegisterProfile } from "../../../shared/fetchHelpers/registerTypeGuards"; +import { isTProfile, TProfile } from "../../../shared/profileTypes/profileTypes"; +import { COMAND_ID } from "../../../shared/fetchHelpers/fetchSettings"; +import { isTErrorResponse, TServerError } from "../../../shared/fetchHelpers/typeGuards"; +import { createAction } from "@reduxjs/toolkit"; +import { UNKNOWN_ERROR_MESSAGE } from "./constant"; +import { LOCAL_STORAGE_TOKEN } from "./constants"; +import { jwtDecode } from "jwt-decode"; + +// Saga Effects. Log out action +export function* logOutSaga(data: { type: string }): any { + localStorage.removeItem(LOCAL_STORAGE_TOKEN); + yield put(logOut()); +} + +export const LOG_OUT_ACTION = 'authAndProfile/logOutAction'; +export const logOutAction = createAction(LOG_OUT_ACTION); \ No newline at end of file diff --git a/src/store/slices/saga/refreshProductListSaga.ts b/src/store/slices/saga/refreshProductListSaga.ts new file mode 100644 index 000000000..b136038eb --- /dev/null +++ b/src/store/slices/saga/refreshProductListSaga.ts @@ -0,0 +1,37 @@ +import { put, select } from "redux-saga/effects"; +import { isTErrorResponse, TServerError } from "../../../shared/fetchHelpers/typeGuards"; +import { createAction } from "@reduxjs/toolkit"; +import { UNKNOWN_ERROR_MESSAGE } from "./constant"; +import { Product, Sorting } from "../../../entities/ViewProductList/model/types/types"; +import { getProductsApi } from "../../../entities/ViewProductList/api/request"; +import productSlice, { addProductsToList, setError, setIsLoading } from "../productSlice"; +import { RawProductDto, TProductGetRaw } from "../../../entities/ViewProductList/model/types/productTypes"; +import * as selectors from '../productSlice'; +import { MAX_ON_PAGE } from "./constants"; + +// Saga Effects. get product +export function* refreshProductListSaga(): any { + try { + yield put(setError({ isError: false, errorMessage: "" })); + yield put(setIsLoading(true)); + + const response = yield getProductsApi(MAX_ON_PAGE, 1, { type: 'ASC', field: 'id' }); + const product: Product[] = response.data.map((d: TProductGetRaw) => new RawProductDto(d)) + yield put(addProductsToList(product)) + + } catch (error: unknown) { + if (isTErrorResponse(error)) { + let allErrors = ""; + error.response.data.errors.forEach((e: TServerError) => allErrors += e.message) + + yield put(setError({ isError: true, errorMessage: allErrors })) + } else { + yield put(setError({ isError: true, errorMessage: UNKNOWN_ERROR_MESSAGE })) + } + } finally { + yield put(setIsLoading(false)); + } +} + +export const PRODUCT_REFRESH = 'product/refreshProduct'; +export const productRefresh = createAction(PRODUCT_REFRESH); \ No newline at end of file