Skip to content

Commit

Permalink
Добавлен предпросмотр изображения товара, считывание токена из локаль…
Browse files Browse the repository at this point in the history
…ного хранилища, исправление ошибок.
  • Loading branch information
Иван Лайер committed Nov 4, 2024
1 parent aa2c369 commit c6dcc35
Show file tree
Hide file tree
Showing 32 changed files with 317 additions and 431 deletions.
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/entities/Register/model/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'Слишком короткий пароль' }),
}
)

Expand Down
1 change: 1 addition & 0 deletions src/entities/ViewProduct/ViewProduct.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const ViewProduct: FC<IViewProductProps> = ({ product, isLast, isEditMode
}, [containerRef]);

const handleDelete = () => {
console.log('delete component')
dispatch(productDelete(product.id));
}

Expand Down
2 changes: 0 additions & 2 deletions src/entities/ViewProductList/ViewProductList.stories.tsx
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
11 changes: 4 additions & 7 deletions src/entities/ViewProductList/ViewProductList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}}
/>

{/* Модальное окно - выбора\добавления категории */}
Expand Down
8 changes: 8 additions & 0 deletions src/entities/ViewProductList/api/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 10 additions & 1 deletion src/entities/ViewProductList/model/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export type TProductRaw = {
sorting: Sorting,
}

// Добавление нового товара
// Добавление нового товара (отправка на сервер)
export interface TAddProductParams {
name: string;
photo?: string;
Expand All @@ -48,6 +48,15 @@ export interface TAddProductParams {
categoryId: string;
};

// Добавление нового товара (Структура данных обрабатываемая формой)
export interface TAddProductFormParams {
name: string;
productImage: File;
desc?: string;
oldPrice?: number;
price: string;
categoryId: string;
};


// Обновление существующего товара
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,12 @@
margin-left: auto

.red
color: red
color: red

.hidden
display: none

.imagePreview
display: block
width: 80px
height: 80px
118 changes: 85 additions & 33 deletions src/entities/ViewProductList/ui/AddProductModal/AddProductModal.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,94 +13,146 @@ 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: 'Обязательно для заполнения' }),
});

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<HTMLInputElement>();
const [preview, setPreview] = useState("");
const [lastPhotoSelected, setLastPhotoSelected] = useState<File>();

const {
register,
handleSubmit,
watch,
setValue,
setError,
clearErrors,
reset,
formState: { errors },

} = useForm<TAddProductParams>({
} = useForm<TAddProductFormParams>({
resolver: zodResolver(schema),
defaultValues: {
name: "Имя продукта",
price: 0,
name: "",
price: "0",
},
}
);

useEffect(() => {
if (category && category.id) {
setValue('categoryId', category.id);
clearErrors('categoryId');
}
}, [category]);

const onSubmit: SubmitHandler<TAddProductParams> = async (data) => {
const newProduct: TAddProductParams = {
const { ref: registerRef, ...rest } = register("productImage");

const handleUploadedFile = (event: React.ChangeEvent<HTMLInputElement>) => {
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<TAddProductFormParams> = 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<HTMLInputElement>) => {
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 (
<>
{/* Модальное окно */}
<Modal isOpen={isOpen} onClose={onClose}>

<form onSubmit={handleSubmit(onSubmit)}>

<LabelWrapper title={"Имя"} required={true}>
<input {...register("name")} />
</LabelWrapper>
{errors.name && <p className={s.red}>{errors.name.message}</p>}

<LabelWrapper title={"Фото"} required={true}>
<input type="file" onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
handleChangePhoto(e).then((url) => {
setValue("photo", url);
});
}} />
</LabelWrapper>
{errors.photo && <p className={s.red}>{errors.photo.message}</p>}

<input className={s.hidden} type="file"
name="productImage"
{...rest}
onChange={handleUploadedFile}
accept="image/png, image/jpeg"
ref={(e: any) => {
registerRef(e);
if (e && e != undefined) {
hiddenInputRef.current = e;
}
}}
/>
{errors.productImage && <p className={s.red}>{errors.productImage.message}</p>}

{preview &&
<img className={s.imagePreview} src={preview} />
}

<button type="button" onClick={handleUploadImageButton} >
{'Загрузить фотографию'}
</button>


<LabelWrapper title={"Стоимость"} required={true}>
<input {...register("price")} />
Expand Down
21 changes: 0 additions & 21 deletions src/homeworks/ts1/1_base.test.js

This file was deleted.

Loading

0 comments on commit c6dcc35

Please sign in to comment.