From 617053c0ee201ab8425de4450781e7e578469428 Mon Sep 17 00:00:00 2001 From: kusowl Date: Thu, 26 Feb 2026 19:02:39 +0530 Subject: [PATCH] feature: upload images and show alert after successfull product creation --- src/app/app.config.ts | 4 +- ...eptor.spec.ts => csrf-interceptor.spec.ts} | 4 +- ...uth-interceptor.ts => csrf-interceptor.ts} | 2 +- .../product/add-product/add-product.html | 97 ++++++++++++++++--- .../product/add-product/add-product.ts | 79 ++++++++++++++- .../product/services/category-service.spec.ts | 16 +++ .../product/services/category-service.ts | 21 ++++ src/app/shared/components/error/error.ts | 10 +- .../components/image-input/image-input.html | 2 +- 9 files changed, 208 insertions(+), 27 deletions(-) rename src/app/core/interceptors/{auth-interceptor.spec.ts => csrf-interceptor.spec.ts} (75%) rename src/app/core/interceptors/{auth-interceptor.ts => csrf-interceptor.ts} (90%) create mode 100644 src/app/features/product/services/category-service.spec.ts create mode 100644 src/app/features/product/services/category-service.ts diff --git a/src/app/app.config.ts b/src/app/app.config.ts index baca460..17362d3 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -3,12 +3,12 @@ import { provideRouter } from "@angular/router"; import { routes } from "./app.routes"; import { provideHttpClient, withFetch, withInterceptors } from "@angular/common/http"; -import { authInterceptor } from "./core/interceptors/auth-interceptor"; +import { csrfInterceptor } from "./core/interceptors/csrf-interceptor"; export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), provideRouter(routes), - provideHttpClient(withFetch(), withInterceptors([authInterceptor])), + provideHttpClient(withFetch(), withInterceptors([csrfInterceptor])), ], }; diff --git a/src/app/core/interceptors/auth-interceptor.spec.ts b/src/app/core/interceptors/csrf-interceptor.spec.ts similarity index 75% rename from src/app/core/interceptors/auth-interceptor.spec.ts rename to src/app/core/interceptors/csrf-interceptor.spec.ts index 854ad84..d2a210a 100644 --- a/src/app/core/interceptors/auth-interceptor.spec.ts +++ b/src/app/core/interceptors/csrf-interceptor.spec.ts @@ -1,11 +1,11 @@ import { TestBed } from "@angular/core/testing"; import { HttpInterceptorFn } from "@angular/common/http"; -import { authInterceptor } from "./auth-interceptor"; +import { csrfInterceptor } from "./csrf-interceptor"; describe("authInterceptor", () => { const interceptor: HttpInterceptorFn = (req, next) => - TestBed.runInInjectionContext(() => authInterceptor(req, next)); + TestBed.runInInjectionContext(() => csrfInterceptor(req, next)); beforeEach(() => { TestBed.configureTestingModule({}); diff --git a/src/app/core/interceptors/auth-interceptor.ts b/src/app/core/interceptors/csrf-interceptor.ts similarity index 90% rename from src/app/core/interceptors/auth-interceptor.ts rename to src/app/core/interceptors/csrf-interceptor.ts index 246a565..ba08a92 100644 --- a/src/app/core/interceptors/auth-interceptor.ts +++ b/src/app/core/interceptors/csrf-interceptor.ts @@ -1,6 +1,6 @@ import { HttpInterceptorFn } from "@angular/common/http"; -export const authInterceptor: HttpInterceptorFn = (req, next) => { +export const csrfInterceptor: HttpInterceptorFn = (req, next) => { const getCookie = (name: string): string | null => { const match = document.cookie.match(new RegExp("(^|;\\s*)(" + name + ")=([^;]*)")); return match ? decodeURIComponent(match[3]) : null; diff --git a/src/app/features/product/add-product/add-product.html b/src/app/features/product/add-product/add-product.html index 665ea7b..fc29349 100644 --- a/src/app/features/product/add-product/add-product.html +++ b/src/app/features/product/add-product/add-product.html @@ -1,35 +1,106 @@
+ @if (successMessage()) { + + } +

Add Product

-
+
Title - - + +
Description - - + +
Select category - + + @for (category of categories(); track category.id) { + + } - +
+
+
+ List Price + + + +
+ +
+ Actual Price + + + +
+
+
Image -
+
- +
diff --git a/src/app/features/product/add-product/add-product.ts b/src/app/features/product/add-product/add-product.ts index 2417641..d52f521 100644 --- a/src/app/features/product/add-product/add-product.ts +++ b/src/app/features/product/add-product/add-product.ts @@ -1,6 +1,11 @@ -import { Component, ElementRef, signal, ViewChild } from "@angular/core"; -import { ReactiveFormsModule } from "@angular/forms"; +import { Component, ElementRef, inject, signal, ViewChild } from "@angular/core"; +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; import { ImageInput } from "../../../shared/components/image-input/image-input"; +import { HttpClient } from "@angular/common/http"; +import { API_URL } from "../../../core/tokens/api-url-tokens"; +import { Error } from "../../../shared/components/error/error"; +import { Category, CategoryService } from "../services/category-service"; +import { forkJoin, switchMap } from "rxjs"; export interface ImageSelection { id: string; @@ -10,14 +15,39 @@ export interface ImageSelection { @Component({ selector: "app-add-product", - imports: [ReactiveFormsModule, ImageInput], + imports: [ReactiveFormsModule, ImageInput, Error], templateUrl: "./add-product.html", styleUrl: "./add-product.css", }) export class AddProduct { + http = inject(HttpClient); + apiUrl = inject(API_URL); + categoryService = inject(CategoryService); + @ViewChild("imageDialog") imageDialog!: ElementRef; activeImage = signal(null); selectedImages = signal>({}); + categories = signal([]); + successMessage = signal(null); + + productAddFrom = new FormGroup({ + title: new FormControl("", { + validators: [Validators.required, Validators.minLength(3)], + }), + description: new FormControl("", { + validators: [Validators.required, Validators.minLength(10)], + }), + actual_price: new FormControl(0, { validators: [Validators.required, Validators.min(0)] }), + list_price: new FormControl(0, { validators: [Validators.required, Validators.min(0)] }), + product_category_id: new FormControl("", { validators: [Validators.required] }), + }); + + ngOnInit() { + this.categoryService.getCategories().subscribe({ + next: (categories) => this.categories.set(categories), + error: (error) => console.log(error), + }); + } openPreview(image: ImageSelection) { this.activeImage.set(image); @@ -30,7 +60,6 @@ export class AddProduct { if (current) { this.selectedImages.update((images) => ({ ...images, [current.id]: current })); } - console.log(this.selectedImages()); this.closeDialog(); } @@ -38,4 +67,46 @@ export class AddProduct { this.activeImage.set(null); this.imageDialog.nativeElement.close(); } + + submitProductForm() { + if (this.productAddFrom.invalid) { + this.productAddFrom.markAllAsTouched(); + return; + } + + this.http + .post<{ id: string | number }>(`${this.apiUrl}/products`, this.productAddFrom.value) + .pipe( + switchMap((response) => { + const productId = response.id; + const images = Object.values(this.selectedImages()); + + if (images.length === 0) { + return [response]; + } + + const uploadRequests = images.map((img) => this.uploadImage(img.file, productId)); + + return forkJoin(uploadRequests); + }), + ) + .subscribe({ + next: (results) => { + this.successMessage.set("Product and images uploaded successfully!"); + console.log("Product and all images uploaded successfully", results); + this.productAddFrom.reset(); + this.selectedImages.set({}); + setTimeout(() => this.successMessage.set(null), 3000); + }, + error: (err) => console.error("Upload failed", err), + }); + } + + private uploadImage(file: File, productId: string | number) { + const formData = new FormData(); + formData.append("image", file); + formData.append("product_id", productId.toString()); + + return this.http.post(`${this.apiUrl}/upload/images`, formData); + } } diff --git a/src/app/features/product/services/category-service.spec.ts b/src/app/features/product/services/category-service.spec.ts new file mode 100644 index 0000000..08b7367 --- /dev/null +++ b/src/app/features/product/services/category-service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from "@angular/core/testing"; + +import { CategoryService } from "./category-service"; + +describe("CategoryService", () => { + let service: CategoryService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CategoryService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/features/product/services/category-service.ts b/src/app/features/product/services/category-service.ts new file mode 100644 index 0000000..02319a8 --- /dev/null +++ b/src/app/features/product/services/category-service.ts @@ -0,0 +1,21 @@ +import { inject, Injectable } from "@angular/core"; +import { HttpClient } from "@angular/common/http"; +import { API_URL } from "../../../core/tokens/api-url-tokens"; + +export interface Category { + id: number; + name: string; + slug: string; +} + +@Injectable({ + providedIn: "root", +}) +export class CategoryService { + private http = inject(HttpClient); + private apiUrl = inject(API_URL); + + getCategories() { + return this.http.get(`${this.apiUrl}/categories`); + } +} diff --git a/src/app/shared/components/error/error.ts b/src/app/shared/components/error/error.ts index c447a09..d07f331 100644 --- a/src/app/shared/components/error/error.ts +++ b/src/app/shared/components/error/error.ts @@ -6,11 +6,13 @@ import { UpperCaseFirstPipe } from "../../pipes/upper-case-first-pipe"; selector: "app-error", imports: [UpperCaseFirstPipe], template: ` - @if (this.control && this.control.touched) { - @for (error of getErrorMessages(); track error) { -

{{ error | upperCaseFirst }}

+
+ @if (this.control && this.control.touched) { + @for (error of getErrorMessages(); track error) { +

{{ error | upperCaseFirst }}

+ } } - } +
`, }) export class Error { diff --git a/src/app/shared/components/image-input/image-input.html b/src/app/shared/components/image-input/image-input.html index 5145705..9992e07 100644 --- a/src/app/shared/components/image-input/image-input.html +++ b/src/app/shared/components/image-input/image-input.html @@ -5,7 +5,7 @@ [style.backgroundImage]="'url(' + bgImageUrl + ')'" class="absolute inset-0 bg-cover bg-center bg-no-repeat p-4 w-full h-full flex flex-col space-y-4 justify-center items-center cursor-pointer rounded-xl" > -
+

Change image