Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/images upload #77

Merged
merged 4 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
26 changes: 17 additions & 9 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,9 +66,11 @@ 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.price_fixed = 259;
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;
await climbing.save();
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 @@ -49,9 +49,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
68 changes: 68 additions & 0 deletions frontend/src/components/ImageUploader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useState } from "react";

type ImageUploaderProps = {
setFiles: (files: File[]) => void;
};

const ImageUploader: React.FC<ImageUploaderProps> = ({ setFiles }) => {
const [previewImages, setPreviewImages] = useState<string[]>([]);

const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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);
};

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>
</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;
11 changes: 5 additions & 6 deletions frontend/src/components/cards/product-rent/CardProductRent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { isDateRangeOverlap } from "utils/date";

import { useUserDatesResearch } from "contexts/UserDatesResearchContext";

import Image from "next/image";
import CardProductRentAvailabilityViewer from "../../../components/cards/product-rent/CardProductRentAvailabilityViewer";

const CardProductRent = ({
Expand Down Expand Up @@ -55,16 +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}
<img
src={picture[0]}
alt={"Image de " + name}
sizes="(max-width: 640px) 100vw, (max-width: 768px) 50vw, 33vw"
className="h-auto w-auto object-cover object-center"
className="h-full w-full object-cover object-center"
/>
</section>
<div className="flex grow flex-col gap-10 text-hightcontrast">
Expand Down
24 changes: 18 additions & 6 deletions frontend/src/containers/public/product-dts/pres-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,34 @@ const ProductDtsPresSection = () => {
<article className="lg:col-span-2">
<div className="grid min-h-96 grid-cols-3 gap-2 overflow-hidden rounded-xl">
<div className="relative col-span-2">
<Image
{/* <Image
fill
sizes="80vw"
priority={true}
alt="product image"
src={product.picture}
alt={"Image de " + product.name}
src={product.picture[0]}
className="object-cover object-center"
/> */}
<img
sizes="80vw"
alt={"Image de " + product.name}
src={product.picture[0]}
className="object-cover object-center"
/>
</div>
<div className="col-span-1 grid grid-rows-3 gap-2">
<div className="relative">
<Image
{/* <Image
fill
sizes="40vw"
src="https://via.placeholder.com/525"
alt="product image"
src={product.picture[1]}
alt={"Image de " + product.name}
className="object-cover object-center"
/> */}
<img
sizes="40vw"
src={product.picture[1]}
alt={"Image de " + product.name}
className="object-cover object-center"
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/graphql/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export type ProductType = {
name: string;
description_short: string;
description_long: string;
picture: string;
picture: string[];
price_fixed: number;
price_daily: number;
discount?: number;
Expand Down
Loading
Loading