Skip to content

Commit

Permalink
feat(many): ✨ Modify type of picture and add upload images to create …
Browse files Browse the repository at this point in the history
…product form

Modify picture type for an array of urls pictures and upload images from create product form
  • Loading branch information
kjarret committed Jun 14, 2024
1 parent 7208b06 commit 31c6315
Show file tree
Hide file tree
Showing 18 changed files with 1,155 additions and 777 deletions.
28 changes: 16 additions & 12 deletions backend/src/entities/product.entity.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { IsDate, IsInt, Length, MinLength } from "class-validator";
import { Field, ID, ObjectType } from "type-graphql";
import {
BaseEntity,
Column,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
BaseEntity,
Column,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from "typeorm";
import { Category } from "./category.entity";

Expand All @@ -25,17 +25,21 @@ export class Product extends BaseEntity {

@Field()
@Column()
@Length(5, 150, { message: "Short description have to be between 5 and 150 characters" })
@Length(5, 150, {
message: "Short description have to be between 5 and 150 characters",
})
description_short: string;

@Field()
@Column()
@MinLength(100, { message: "Long description have to be above 150 characters" })
@MinLength(100, {
message: "Long description have to be above 150 characters",
})
description_long: string;

@Field()
@Column()
picture: string;
@Field(() => [String])
@Column("text", { array: true })
picture: string[];

@Field()
@Column()
Expand Down
24 changes: 16 additions & 8 deletions backend/src/fillDatabaseIfEmpty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ export async function fillDatabaseIfEmpty() {
snowboard.description_short = "Super snowboard à louer !";
snowboard.description_long =
"Ceci est une description d'un snowboard à louer ! Il est super cool et vous permettra de glisser en toute sécurité !";
snowboard.picture =
"https://images.unsplash.com/photo-1498146831523-fbe41acdc5ad";
snowboard.picture = [
"https://images.unsplash.com/photo-1614358536373-1ce27819009e",
"https://images.unsplash.com/photo-1498146831523-fbe41acdc5ad",
];
snowboard.price_fixed = 199;
snowboard.price_daily = 20;
snowboard.quantity = 5;
Expand All @@ -49,8 +51,10 @@ export async function fillDatabaseIfEmpty() {
tent.description_short = "Superbe tente à louer !";
tent.description_long =
"Ceci est une description d'une tente à louer ! Elle est super cool et vous permettra de camper en toute sécurité !";
tent.picture =
"https://images.unsplash.com/photo-1624923686627-514dd5e57bae";
tent.picture = [
"https://images.unsplash.com/photo-1504280390367-361c6d9f38f4",
"https://images.unsplash.com/photo-1624923686627-514dd5e57bae",
];
tent.price_fixed = 149;
tent.price_daily = 15;
tent.quantity = 8;
Expand All @@ -62,8 +66,10 @@ export async function fillDatabaseIfEmpty() {
climbing.description_short = "Super matériel d'escalade à louer !";
climbing.description_long =
"Ceci est une description du matériel d'escalade à louer ! Il est super cool et vous permettra de grimper en toute sécurité !";
climbing.picture =
"https://images.unsplash.com/photo-1630432328419-bee5f50d6390";
climbing.picture = [
"https://images.unsplash.com/photo-1586685256769-4e869a64f1eb",
"https://images.unsplash.com/photo-1630432328419-bee5f50d6390",
];
climbing.price_fixed = 199;
climbing.price_daily = 20;
climbing.quantity = 3;
Expand All @@ -75,8 +81,10 @@ export async function fillDatabaseIfEmpty() {
backpack.description_short = "Super sac de voyage à louer !";
backpack.description_long =
"Ceci est une description d'un sac de voyage à louer ! Il est super cool et vous permettra de voyager en toute sécurité !";
backpack.picture =
"https://images.unsplash.com/photo-1509762774605-f07235a08f1f";
backpack.picture = [
"https://images.unsplash.com/photo-1622260614153-03223fb72052",
"https://images.unsplash.com/photo-1509762774605-f07235a08f1f",
];
backpack.price_fixed = 99;
backpack.price_daily = 10;
backpack.quantity = 11;
Expand Down
16 changes: 10 additions & 6 deletions backend/src/inputs/InputCreateProduct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ export class InputCreateProduct {
@Field()
description_long: string;

@Field({
nullable: true,
defaultValue:
"https://img.freepik.com/vecteurs-premium/vecteur-icone-image-par-defaut-page-image-manquante-pour-conception-site-web-application-mobile-aucune-photo-disponible_87543-11093.jpg",
})
picture?: string;
// @Field(() => [String], {
// nullable: true,
// defaultValue: [
// "https://img.freepik.com/vecteurs-premium/vecteur-icone-image-par-defaut-page-image-manquante-pour-conception-site-web-application-mobile-aucune-photo-disponible_87543-11093.jpg",
// ],
// })
// picture?: string[];

@Field(() => [String], { nullable: true })
picture?: string[];

@Field()
price_fixed: number;
Expand Down
4 changes: 2 additions & 2 deletions backend/src/inputs/InputUpdateProduct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export class InputUpdateProduct {
@Field(() => String, { nullable: true })
description_long: string;

@Field(() => String, { nullable: true })
picture: string;
@Field(() => [String], { nullable: true })
picture: string[];

@Field(() => Number, { nullable: true })
price_fixed: number;
Expand Down
5 changes: 2 additions & 3 deletions backend/src/resolvers/product.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ export default class ProductResolver {
@Arg("id") id: number,
@Arg("infos") data: InputUpdateProduct
) {
const productToUpdate = await this.productService.update(id, { ...data });
console.log(productToUpdate);
return productToUpdate;
const updatedProduct = await this.productService.update(id, data);
return updatedProduct;
}

@Mutation(() => Boolean)
Expand Down
23 changes: 13 additions & 10 deletions backend/src/services/product.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,25 +64,28 @@ export default class ProductService {
return await this.db.save(newProduct);
}

async update(id: number, data: InputUpdateProduct) {
const categoryToLink = await new CategoryService().find(data.category);
async update(id: number, data: InputUpdateProduct): Promise<Product> {
const categoryToLink = await new CategoryService().find(+data.category);
if (!categoryToLink) {
throw new Error("category doesnt exist");
throw new Error(`Category with ID ${data.category} not found`);
}

const productToUpdate = await this.findById(id);
if (!productToUpdate) {
throw new Error("Product doesnt exist");
throw new Error(`Product with ID ${id} not found`);
}
const producToSave = this.db.merge(productToUpdate, {

const updatedProduct = this.db.merge(productToUpdate, {
...data,
category: categoryToLink,
});
const errors = await validate(producToSave);
if (errors.length !== 0) {
console.log(errors);
throw new Error("Error when validate");
const errors = await validate(updatedProduct);
if (errors.length > 0) {
console.error("Validation errors:", errors);
throw new Error("Product data validation failed");
}
return await this.db.save(producToSave);

return await this.db.save(updatedProduct);
}

async deleteProduct(id: number) {
Expand Down
14 changes: 12 additions & 2 deletions frontend/next.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ["avatars.githubusercontent.com"],
domains: [
"avatars.githubusercontent.com",
"via.placeholder.com",
"*.unsplash.com",
"localhost",
],
remotePatterns: [
{
protocol: "https",
Expand All @@ -17,7 +22,12 @@ const nextConfig = {
protocol: "https",
hostname: "**.unsplash.com",
pathname: "**",
}
},
{
protocol: "http",
hostname: "**/localhost/**",
pathname: "**",
},
],
},
};
Expand Down
121 changes: 121 additions & 0 deletions frontend/src/components/ImageUploader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import axios, { AxiosResponse } from "axios";
import React, { useState } from "react";
import { IoIosCheckboxOutline } from "react-icons/io";

type ImageUploaderProps = {
setImageURL: (urls: string[]) => void;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => Promise<void>;
};

const ImageUploader: React.FC<ImageUploaderProps> = ({
setImageURL,
onChange,
}) => {
const [files, setFiles] = useState<File[]>([]);
const [previewImages, setPreviewImages] = useState<string[]>([]);
const [localImageURL, setLocalImageURL] = useState<string[]>([]);

const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e);
const selectedFiles = e.target.files;
if (!selectedFiles) return;

if (selectedFiles.length > 9) {
alert("Vous ne pouvez pas sélectionner plus de 9 fichiers.");
return;
}

const selectedFilesArray = Array.from(selectedFiles).slice(0, 9);
setFiles(selectedFilesArray);

const selectedFilesPreview = selectedFilesArray.map((file) =>
URL.createObjectURL(file),
);
setPreviewImages(selectedFilesPreview);
};

const handleValidateButtonClick = async (
e: React.MouseEvent<HTMLButtonElement>,
) => {
e.preventDefault();

if (files.length === 0) {
alert("Sélectionnez au moins un fichier à télécharger.");
return;
}

if (files.length > 9) {
alert("Vous ne pouvez pas télécharger plus de 9 fichiers à la fois.");
return;
}

const urlPost = "http://localhost:8000/upload";
try {
const uploadPromises = files.map((singleFile) => {
const formData = new FormData();
formData.append("file", singleFile, singleFile.name);
return axios.post(urlPost, formData);
});

const responses = await Promise.all(uploadPromises);
const filenames = responses.map(
(res: AxiosResponse) => res.data.filename,
);

setLocalImageURL(filenames);
setImageURL(filenames);
} catch (err) {
console.log("error", err);
}
};

return (
<div className="my-4">
<form className="flex items-center justify-between gap-2">
<input
multiple={true}
accept=".png, .jpeg, .jpg"
type="file"
onChange={handleFileInputChange}
className="hidden"
id="file-upload"
/>
<label
htmlFor="file-upload"
className="grow cursor-pointer items-center gap-2 rounded-sm bg-blue-600 px-3 py-2 text-center text-white"
>
<span>Choisir des images</span>
</label>

<button
type="button"
onClick={handleValidateButtonClick}
className="flex grow items-center gap-1 rounded-sm bg-green-600 px-3 py-2 text-center text-white"
>
<IoIosCheckboxOutline size={20} />
<span>Valider la sélection</span>
</button>
</form>
<div className="mt-4 grid grid-cols-3 gap-4">
{previewImages.map((previewUrl, index) => (
<div
key={index}
className={`relative ${
index === 0 ? "col-span-2 row-span-2" : "col-span-1 row-span-1"
}`}
>
<img
src={previewUrl}
alt={`Preview-${index}`}
className={`h-${index === 0 ? "300" : "50"} w-${
index === 0 ? "300" : "50"
} rounded object-cover`}
/>
</div>
))}
</div>
</div>
);
};

export default ImageUploader;
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,16 @@ const CardProductRent = ({
setIsHovered(false);
};

console.log(picture);

return (
<article className="relative flex flex-col gap-4 rounded-md bg-lowcontrast p-4">
<Link className="flex gap-4" href={`/products/${id}`}>
<section className="relative aspect-square h-80 w-80 overflow-hidden rounded-lg bg-zinc-300">
<Image
fill
src={picture}
alt="Product picture"
src={picture[0]}
alt={"Image de " + name}
className="h-full w-full object-cover object-center"
/>
</section>
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/containers/public/product-dts/pres-section.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useQuery } from "@apollo/client";
import Image from "next/image";
import { useRouter } from "next/router";
import { useQuery } from "@apollo/client";

import { GET_PRODUCT_BY_ID, ProductType } from "lib/graphql/queries";

Expand Down Expand Up @@ -34,7 +34,7 @@ const ProductDtsPresSection = () => {
sizes="80vw"
priority={true}
alt="product image"
src={product.picture}
src={product.picture[0]}
className="object-cover object-center"
/>
</div>
Expand All @@ -43,7 +43,7 @@ const ProductDtsPresSection = () => {
<Image
fill
sizes="40vw"
src="https://via.placeholder.com/525"
src={product.picture[1]}
alt="product image"
className="object-cover object-center"
/>
Expand Down Expand Up @@ -90,6 +90,6 @@ const ProductDtsPresSection = () => {
<ProductDtsPriceSidebar product={product} />
</section>
);
}
};

export default ProductDtsPresSection;
export default ProductDtsPresSection;
Loading

0 comments on commit 31c6315

Please sign in to comment.