Commit 3144fc0a authored by Nurasyl's avatar Nurasyl

update

parent 9eff7f53
import { IProduct } from "@/containers/Products"; import { IProduct } from "@/containers/Products";
import { Box, Button, Grid, TextField } from "@mui/material"; import { Box, Button, Grid, SelectChangeEvent, TextField } from "@mui/material";
import { ChangeEvent, FormEvent, useState } from "react"; import { ChangeEvent, FormEvent, useCallback, useEffect, useState } from "react";
import { FileInput } from "./UI/Form/FileInput"; import { FileInput } from "./UI/Form/FileInput";
import CategorySelect, { ICategory } from "./UI/Form/CategorySelect";
import { axiosApiClient } from "@/helpers/axiosApiClient";
interface Props { interface Props {
...@@ -9,13 +11,24 @@ interface Props { ...@@ -9,13 +11,24 @@ interface Props {
} }
export function ProductForm({onProductFormSubmit}: Props) { export function ProductForm({onProductFormSubmit}: Props) {
const [categories, setCategories] = useState<ICategory[] | null>(null);
const [product, setProduct] = useState<Omit<IProduct, "id">>({ const [product, setProduct] = useState<Omit<IProduct, "id">>({
description:"", description:"",
price:0, price:0,
title:"", title:"",
image:"" image:"",
categoryId: ""
}); });
const getCategories = useCallback(async () => {
const {data} = await axiosApiClient.get('/category')
return data
}, [])
useEffect(() => {
getCategories().then(res => setCategories(res))
}, [getCategories])
const submitFormHandler = (e: FormEvent<HTMLFormElement>) => { const submitFormHandler = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
...@@ -48,6 +61,11 @@ export function ProductForm({onProductFormSubmit}: Props) { ...@@ -48,6 +61,11 @@ export function ProductForm({onProductFormSubmit}: Props) {
} }
} }
const handleChangeSelect = (event: SelectChangeEvent) => {
const val = event.target.value as string
setProduct(prevState => ({...prevState, categoryId: val}))
}
return <Box return <Box
component={"form"} component={"form"}
autoComplete="off" autoComplete="off"
...@@ -95,6 +113,14 @@ export function ProductForm({onProductFormSubmit}: Props) { ...@@ -95,6 +113,14 @@ export function ProductForm({onProductFormSubmit}: Props) {
label="image" label="image"
/> />
</Grid> </Grid>
<Grid item xs>
<CategorySelect
handleChange={handleChangeSelect}
value={product.categoryId}
label="Category"
categories={categories || []}
/>
</Grid>
<Grid item xs> <Grid item xs>
<Button type="submit" color="primary" variant="contained"> <Button type="submit" color="primary" variant="contained">
Create Create
......
import { FormControl, InputLabel, Select, MenuItem, SelectChangeEvent } from "@mui/material"
export interface ICategory {
id: string
title: string
description: string
}
type TProps = {
handleChange: (event: SelectChangeEvent) => void
value: string
label: string
categories: ICategory[]
}
const CategorySelect = ({handleChange, value, label, categories}: TProps) => {
return (
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">{label}</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={value}
label={label}
onChange={handleChange}
>
{
categories.map(item => (
<MenuItem key={item.id} value={item.id}>{item.title}</MenuItem>
))
}
</Select>
</FormControl>
)
}
export default CategorySelect
\ No newline at end of file
...@@ -8,18 +8,18 @@ import { ...@@ -8,18 +8,18 @@ import {
IconButton, IconButton,
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
import { IProduct } from './Products';
import { ArrowForward } from '@mui/icons-material'; import { ArrowForward } from '@mui/icons-material';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { IProductState } from '@/features/productsSlice';
interface Props { interface Props {
product: IProduct; product: IProductState;
} }
const apiUrl = 'http://localhost:8000' const apiUrl = 'http://localhost:8000'
export function ProductItem({ product }: Props) { export function ProductItem({ product }: Props) {
const { title, price, id, description, image } = product; const { title, price, id, description, image, category } = product;
let cardImage = `${apiUrl}/uploads/unnamed.png` let cardImage = `${apiUrl}/uploads/unnamed.png`
if(image && image !== '') { if(image && image !== '') {
...@@ -29,7 +29,7 @@ export function ProductItem({ product }: Props) { ...@@ -29,7 +29,7 @@ export function ProductItem({ product }: Props) {
return ( return (
<Grid item xs={12} sm={12} md={6} lg={4}> <Grid item xs={12} sm={12} md={6} lg={4}>
<Card sx={{ minWidth: 275 }}> <Card sx={{ minWidth: 275 }}>
<CardHeader title={title} /> <CardHeader title={`Категория: ${category.title} - ${title}`} />
<CardMedia <CardMedia
image={cardImage} image={cardImage}
title={title} title={title}
......
...@@ -41,5 +41,6 @@ export interface IProduct { ...@@ -41,5 +41,6 @@ export interface IProduct {
title: string; title: string;
description: string; description: string;
price: number; price: number;
image: string image: string;
categoryId: string
} }
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { IProduct } from "@/containers/Products";
import { axiosApiClient } from "../helpers/axiosApiClient"; import { axiosApiClient } from "../helpers/axiosApiClient";
import { ICategory } from "@/components/UI/Form/CategorySelect";
import { IProduct } from "@/containers/Products";
export interface IProductState {
id: string;
title: string;
description: string;
price: number;
image: string;
category: ICategory
}
interface State { interface State {
products: IProduct[]; products: IProductState[];
error: Error | null; error: Error | null;
loading: boolean; loading: boolean;
} }
...@@ -15,7 +25,7 @@ const initialState: State = { ...@@ -15,7 +25,7 @@ const initialState: State = {
}; };
export const fetchProducts = createAsyncThunk('fetch/products', async () => { export const fetchProducts = createAsyncThunk('fetch/products', async () => {
return await axiosApiClient.get<IProduct[]>('/products').then(res => res.data); return await axiosApiClient.get<IProductState[]>('/products').then(res => res.data);
}); });
export const createProduct = createAsyncThunk('create/product', export const createProduct = createAsyncThunk('create/product',
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@types/multer": "^1.4.11", "@types/multer": "^1.4.11",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
...@@ -499,6 +500,11 @@ ...@@ -499,6 +500,11 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/validator": {
"version": "13.12.0",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.0.tgz",
"integrity": "sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag=="
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
...@@ -1070,6 +1076,16 @@ ...@@ -1070,6 +1076,16 @@
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw=="
}, },
"node_modules/class-validator": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz",
"integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==",
"dependencies": {
"@types/validator": "^13.11.8",
"libphonenumber-js": "^1.10.53",
"validator": "^13.9.0"
}
},
"node_modules/cli-highlight": { "node_modules/cli-highlight": {
"version": "2.1.11", "version": "2.1.11",
"resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz",
...@@ -2360,6 +2376,11 @@ ...@@ -2360,6 +2376,11 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/libphonenumber-js": {
"version": "1.11.5",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.5.tgz",
"integrity": "sha512-TwHR5BZxGRODtAfz03szucAkjT5OArXr+94SMtAM2pYXIlQNVMrxvb6uSCbnaJJV6QXEyICk7+l6QPgn72WHhg=="
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
...@@ -4038,6 +4059,14 @@ ...@@ -4038,6 +4059,14 @@
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="
}, },
"node_modules/validator": {
"version": "13.12.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
"integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
"dependencies": { "dependencies": {
"@types/multer": "^1.4.11", "@types/multer": "^1.4.11",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
......
import { DataSource } from "typeorm"; import { DataSource } from "typeorm";
import { Product } from "./entities/product.entity"; import { Product } from "./entities/product.entity";
import { Category } from "./entities/category.entity";
export const AppDataSource = new DataSource({ export const AppDataSource = new DataSource({
type: "postgres", type: "postgres",
...@@ -7,8 +8,8 @@ export const AppDataSource = new DataSource({ ...@@ -7,8 +8,8 @@ export const AppDataSource = new DataSource({
port: 5432, port: 5432,
username: "postgres", username: "postgres",
password: "root", password: "root",
database: "classwork", database: "postgres",
schema: "classwork", schema: "public",
synchronize: true, synchronize: true,
entities: [Product] entities: [Product, Category]
}) })
\ No newline at end of file
import { CategoryDto } from '@/dto/category.dto';
import { CategoryService } from '@/services/category.service';
import { plainToInstance } from 'class-transformer';
import { RequestHandler } from 'express';
export class CategoryController {
private service: CategoryService;
constructor() {
this.service = new CategoryService();
}
getAllCategories: RequestHandler = async (req, res): Promise<void> => {
const categories = await this.service.getAllCategories();
res.send(categories);
};
getCategory: RequestHandler = async (req, res): Promise<void> => {
const category = await this.service.getCategory(req.params.id);
res.send(category);
};
createCategory: RequestHandler = async (req, res): Promise<void> => {
const categoryDto = plainToInstance(CategoryDto, req.body);
const Category = await this.service.createCategory(categoryDto);
res.send(Category);
};
}
...@@ -2,6 +2,7 @@ import { ProductService } from '@/services/product.service'; ...@@ -2,6 +2,7 @@ import { ProductService } from '@/services/product.service';
import { RequestHandler } from 'express'; import { RequestHandler } from 'express';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { ProductDto } from '@/dto/product.dto'; import { ProductDto } from '@/dto/product.dto';
import { formatErrors } from '@/helpers/formatErrors';
export class ProductController { export class ProductController {
private service: ProductService; private service: ProductService;
...@@ -15,15 +16,23 @@ export class ProductController { ...@@ -15,15 +16,23 @@ export class ProductController {
res.send(products); res.send(products);
}; };
getProduct: RequestHandler = (req, res): void => { getProduct: RequestHandler = async (req, res): Promise<void> => {
const product = this.service.getProduct(req.params.id); const product = await this.service.getProduct(req.params.id);
res.send(product); res.send(product);
}; };
createProduct: RequestHandler = (req, res): void => { createProduct: RequestHandler = async (req, res): Promise<void> => {
try {
const productDto = plainToInstance(ProductDto, req.body); const productDto = plainToInstance(ProductDto, req.body);
if (req.file) productDto.image = req.file.filename; if (req.file) productDto.image = req.file.filename;
const product = this.service.createProduct(productDto); const product = await this.service.createProduct(productDto);
res.send(product); res.send(product);
} catch (e) {
if (Array.isArray(e)) {
res.status(400).send(formatErrors(e));
} else {
res.status(500).send(e);
}
}
}; };
} }
import { Expose } from 'class-transformer';
export class CategoryDto {
@Expose() title!: string;
@Expose() description!: string;
}
import { Expose } from 'class-transformer'; import { Expose } from 'class-transformer';
import { IsNotEmpty, IsNumberString, IsOptional, IsString } from 'class-validator';
export class ProductDto { export class ProductDto {
@Expose() title!: string; @IsNotEmpty({ message: 'Продукт не может быть создан без названия!' })
@IsString({ message: 'Название должно быть строкой' })
@Expose()
title!: string;
@Expose() description!: string; @IsOptional()
@Expose()
description!: string;
@Expose() price!: number; @IsNotEmpty({ message: 'Укажите цену продукта' })
@IsNumberString({}, { message: 'Укажите корректную цену' })
@Expose()
price!: number;
@Expose() image!: string; @IsOptional()
@Expose()
image!: string;
@IsNotEmpty({ message: 'Не указана категория товара' })
@IsNumberString({}, { message: 'Укажите корректную категорию' })
@Expose()
categoryId!: string;
} }
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
import { Product } from "./product.entity";
@Entity({name: 'categories'})
export class Category {
@PrimaryGeneratedColumn()
id!: number;
@Column()
title!: string
@Column()
description!: string
@OneToMany(() => Product, (product) => product.category)
products!: Product[]
}
\ No newline at end of file
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm";
import { Category } from "./category.entity";
@Entity() @Entity()
export class Product { export class Product {
...@@ -16,4 +17,7 @@ export class Product { ...@@ -16,4 +17,7 @@ export class Product {
@Column({nullable: true}) @Column({nullable: true})
image!: string image!: string
@ManyToOne(() => Category, (category) => category.products)
category!: Category
} }
\ No newline at end of file
import { ValidationError } from "class-validator";
export const formatErrors = (errors: ValidationError[]) => {
const updatedErrors: { type: string; messages: string[] }[] = [];
errors.forEach((e) => {
if (e.constraints) {
const error = {
type: e.property,
messages: Object.values(e.constraints),
};
updatedErrors.push(error);
}
});
return updatedErrors;
};
\ No newline at end of file
import { CategoryRoute } from './routes/category.route';
import cors from 'cors'; import cors from 'cors';
import App from './app'; import App from './app';
import logger from './middlewares/logger'; import logger from './middlewares/logger';
...@@ -7,7 +8,7 @@ import { ProductRoute } from './routes/product.route'; ...@@ -7,7 +8,7 @@ import { ProductRoute } from './routes/product.route';
const app = new App({ const app = new App({
port: 8000, port: 8000,
middlewares: [logger(), cors()], middlewares: [logger(), cors()],
controllers: [new ArticleRoute(), new ProductRoute()], controllers: [new ArticleRoute(), new ProductRoute(), new CategoryRoute()],
}); });
app.listen(); app.listen();
import { AppDataSource } from "@/appDataSource";
import { CategoryDto } from "@/dto/category.dto";
import { Category } from "@/entities/category.entity";
import { Repository } from "typeorm";
class CategoryRepo {
private repo: Repository<Category>
constructor() {
this.repo = AppDataSource.getRepository(Category)
}
async create(body: CategoryDto) {
return await this.repo.save(body)
}
async getAll() {
return await this.repo.find()
}
async getOne(id: number) {
return await this.repo.findOne({where: {id: id}})
}
}
export const categoryRepo = new CategoryRepo()
\ No newline at end of file
...@@ -2,6 +2,7 @@ import { AppDataSource } from "@/appDataSource"; ...@@ -2,6 +2,7 @@ import { AppDataSource } from "@/appDataSource";
import { ProductDto } from "@/dto/product.dto"; import { ProductDto } from "@/dto/product.dto";
import { Product } from "@/entities/product.entity"; import { Product } from "@/entities/product.entity";
import { Repository } from "typeorm"; import { Repository } from "typeorm";
import { categoryRepo } from "./category.repository";
class ProductRepo { class ProductRepo {
private repo: Repository<Product> private repo: Repository<Product>
...@@ -11,11 +12,14 @@ class ProductRepo { ...@@ -11,11 +12,14 @@ class ProductRepo {
} }
async create(body: ProductDto) { async create(body: ProductDto) {
return await this.repo.save(body) const category = await categoryRepo.getOne(parseInt(body.categoryId))
if(!category) throw Error('Category Not Found.')
return await this.repo.save({...body, category: category})
} }
async getAll() { async getAll() {
return await this.repo.find() return await this.repo.find({relations: {category: true}})
} }
async getOne(id: number) { async getOne(id: number) {
......
import { CategoryController } from '@/controllers/category.controller';
import { Router } from 'express';
export class CategoryRoute {
public path = '/category';
public router = Router();
private controller: CategoryController;
constructor() {
this.controller = new CategoryController();
this.init();
}
private init() {
this.router.get('/', this.controller.getAllCategories);
this.router.get('/:id', this.controller.getCategory);
this.router.post('/create',this.controller.createCategory);
}
}
import { CategoryDto } from "@/dto/category.dto";
import { Category } from "@/entities/category.entity";
import { categoryRepo } from "@/repositories/category.repository";
export class CategoryService {
getAllCategories = async (): Promise<Category[]> => {
return await categoryRepo.getAll()
};
getCategory = async (id: string): Promise<Category | null> => {
return await categoryRepo.getOne(parseInt(id))
};
createCategory = async (data: CategoryDto): Promise<Category> => {
return await categoryRepo.create(data)
};
}
import { ProductDto } from '@/dto/product.dto'; import { ProductDto } from '@/dto/product.dto';
import { productRepo } from '@/repositories/product.repository'; import { productRepo } from '@/repositories/product.repository';
import { Product } from '@/entities/product.entity'; import { Product } from '@/entities/product.entity';
import { validate } from 'class-validator';
export class ProductService { export class ProductService {
getAllProducts = async (): Promise<Product[]> => { getAllProducts = async (): Promise<Product[]> => {
...@@ -12,6 +13,8 @@ export class ProductService { ...@@ -12,6 +13,8 @@ export class ProductService {
}; };
createProduct = async (data: ProductDto): Promise<Product> => { createProduct = async (data: ProductDto): Promise<Product> => {
return productRepo.create(data) const errors = await validate(data, { whitelist: true });
if (errors.length) throw errors;
return await productRepo.create(data)
}; };
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment