From f5393f51100666d825beea579830527ccd45f31e Mon Sep 17 00:00:00 2001 From: kusowl Date: Mon, 2 Mar 2026 18:47:43 +0530 Subject: [PATCH] feature: show products on the home page and add individual product page --- src/app/app.config.ts | 4 +- src/app/app.routes.ts | 2 +- src/app/app.ts | 2 +- src/app/core/layouts/footer/footer.html | 2 +- src/app/core/models/paginated.model.ts | 25 ++++ src/app/core/models/product.model.ts | 16 +++ src/app/features/home/home.ts | 4 +- .../componets/product-card/product-card.html | 17 --- .../componets/product-card/product-card.ts | 12 -- src/app/features/home/products/products.html | 14 -- src/app/features/home/products/products.ts | 9 -- .../product/add-product/add-product.html | 4 +- .../components/product-card/product-card.html | 21 +++ .../product-card/product-card.spec.ts | 0 .../components/product-card/product-card.ts | 22 +++ src/app/features/product/product.html | 12 +- src/app/features/product/product.routes.ts | 5 + src/app/features/product/product.ts | 23 ++- .../product/services/category-service.ts | 2 +- .../product/services/product-service.spec.ts | 16 +++ .../product/services/product-service.ts | 21 +++ .../show-product.css} | 0 .../product/show-product/show-product.html | 132 ++++++++++++++++++ .../show-product/show-product.spec.ts} | 12 +- .../product/show-product/show-product.ts | 40 ++++++ src/styles.css | 4 + 26 files changed, 347 insertions(+), 74 deletions(-) create mode 100644 src/app/core/models/paginated.model.ts create mode 100644 src/app/core/models/product.model.ts delete mode 100644 src/app/features/home/products/componets/product-card/product-card.html delete mode 100644 src/app/features/home/products/componets/product-card/product-card.ts delete mode 100644 src/app/features/home/products/products.html delete mode 100644 src/app/features/home/products/products.ts create mode 100644 src/app/features/product/components/product-card/product-card.html rename src/app/features/{home/products/componets => product/components}/product-card/product-card.spec.ts (100%) create mode 100644 src/app/features/product/components/product-card/product-card.ts create mode 100644 src/app/features/product/services/product-service.spec.ts create mode 100644 src/app/features/product/services/product-service.ts rename src/app/features/product/{product.css => show-product/show-product.css} (100%) create mode 100644 src/app/features/product/show-product/show-product.html rename src/app/features/{home/products/products.spec.ts => product/show-product/show-product.spec.ts} (58%) create mode 100644 src/app/features/product/show-product/show-product.ts diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 17362d3..0c81531 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,5 +1,5 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from "@angular/core"; -import { provideRouter } from "@angular/router"; +import { provideRouter, withComponentInputBinding } from "@angular/router"; import { routes } from "./app.routes"; import { provideHttpClient, withFetch, withInterceptors } from "@angular/common/http"; @@ -8,7 +8,7 @@ import { csrfInterceptor } from "./core/interceptors/csrf-interceptor"; export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), - provideRouter(routes), + provideRouter(routes, withComponentInputBinding()), provideHttpClient(withFetch(), withInterceptors([csrfInterceptor])), ], }; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index fe77d90..c25961e 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -18,6 +18,6 @@ export const routes: Routes = [ loadChildren: () => import("./features/product/product.routes").then((routes) => routes.productRoutes), canActivate: [authGuard, roleGuard], - data: { roles: ["broker"] }, + data: { roles: ["admin", "broker"] }, }, ]; diff --git a/src/app/app.ts b/src/app/app.ts index 780b269..9eb83b9 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,6 +1,6 @@ import { Component, signal } from "@angular/core"; import { RouterOutlet } from "@angular/router"; -import { Products } from "./features/home/products/products"; +import { Product } from "./features/product/product"; import { Footer } from "./core/layouts/footer/footer"; import { Header } from "./core/layouts/header/header"; diff --git a/src/app/core/layouts/footer/footer.html b/src/app/core/layouts/footer/footer.html index 068c3d5..aa791bb 100644 --- a/src/app/core/layouts/footer/footer.html +++ b/src/app/core/layouts/footer/footer.html @@ -18,7 +18,7 @@ diff --git a/src/app/core/models/paginated.model.ts b/src/app/core/models/paginated.model.ts new file mode 100644 index 0000000..9dfb658 --- /dev/null +++ b/src/app/core/models/paginated.model.ts @@ -0,0 +1,25 @@ +export interface PaginatedResponse { + data: T[]; + links: { + next: string | null; + prev: string | null; + last: string | null; + first: string | null; + }; + meta: { + total: number; + per_page: number; + current_page: number; + last_page: number; + from: number; + to: number; + links: links[]; + }; +} + +interface links { + url: string | null; + label: string; + active: boolean; + page: number | null; +} diff --git a/src/app/core/models/product.model.ts b/src/app/core/models/product.model.ts new file mode 100644 index 0000000..b509785 --- /dev/null +++ b/src/app/core/models/product.model.ts @@ -0,0 +1,16 @@ +import { PaginatedResponse } from "./paginated.model"; +import { Category } from "../../features/product/services/category-service"; + +export interface ProductModel { + id: number; + title: string; + slug: string; + description: string; + actualPrice: number; + listPrice: number; + category: Category; + productImages: string[]; + updatedAt: string; +} + +export interface ProductCollection extends PaginatedResponse {} diff --git a/src/app/features/home/home.ts b/src/app/features/home/home.ts index cbc2461..f5e5889 100644 --- a/src/app/features/home/home.ts +++ b/src/app/features/home/home.ts @@ -1,9 +1,9 @@ import { Component } from "@angular/core"; -import { Products } from "./products/products"; +import { Product } from "../product/product"; @Component({ selector: "app-home", - imports: [Products], + imports: [Product], templateUrl: "./home.html", styleUrl: "./home.css", }) diff --git a/src/app/features/home/products/componets/product-card/product-card.html b/src/app/features/home/products/componets/product-card/product-card.html deleted file mode 100644 index 7e8b73d..0000000 --- a/src/app/features/home/products/componets/product-card/product-card.html +++ /dev/null @@ -1,17 +0,0 @@ -
- - - - -
- -
- -

Product Name

-

⭐4.5

-

Price: 4999/-

-
diff --git a/src/app/features/home/products/componets/product-card/product-card.ts b/src/app/features/home/products/componets/product-card/product-card.ts deleted file mode 100644 index b609c53..0000000 --- a/src/app/features/home/products/componets/product-card/product-card.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component } from "@angular/core"; -import { LucideAngularModule, Heart } from "lucide-angular"; - -@Component({ - selector: "app-product-card", - standalone: true, - imports: [LucideAngularModule], - templateUrl: "./product-card.html", -}) -export class ProductCard { - readonly HeartIcon = Heart; -} diff --git a/src/app/features/home/products/products.html b/src/app/features/home/products/products.html deleted file mode 100644 index c9a866f..0000000 --- a/src/app/features/home/products/products.html +++ /dev/null @@ -1,14 +0,0 @@ -
-

Our Products

-
- -
- - - - - - -
diff --git a/src/app/features/home/products/products.ts b/src/app/features/home/products/products.ts deleted file mode 100644 index ef2c403..0000000 --- a/src/app/features/home/products/products.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from "@angular/core"; -import { ProductCard } from "./componets/product-card/product-card"; - -@Component({ - selector: "app-products", - imports: [ProductCard], - templateUrl: "./products.html", -}) -export class Products {} diff --git a/src/app/features/product/add-product/add-product.html b/src/app/features/product/add-product/add-product.html index fc29349..a7afa68 100644 --- a/src/app/features/product/add-product/add-product.html +++ b/src/app/features/product/add-product/add-product.html @@ -62,8 +62,8 @@ Select category diff --git a/src/app/features/product/components/product-card/product-card.html b/src/app/features/product/components/product-card/product-card.html new file mode 100644 index 0000000..43e1fff --- /dev/null +++ b/src/app/features/product/components/product-card/product-card.html @@ -0,0 +1,21 @@ +
+ + + + +
+ +
+ +

{{product.title}}

+

⭐4.5

+

+ Price: + {{product.actualPrice}} + {{product.listPrice}}/- +

+
diff --git a/src/app/features/home/products/componets/product-card/product-card.spec.ts b/src/app/features/product/components/product-card/product-card.spec.ts similarity index 100% rename from src/app/features/home/products/componets/product-card/product-card.spec.ts rename to src/app/features/product/components/product-card/product-card.spec.ts diff --git a/src/app/features/product/components/product-card/product-card.ts b/src/app/features/product/components/product-card/product-card.ts new file mode 100644 index 0000000..edcd9e0 --- /dev/null +++ b/src/app/features/product/components/product-card/product-card.ts @@ -0,0 +1,22 @@ +import { Component, inject, Input } from "@angular/core"; +import { LucideAngularModule, Heart } from "lucide-angular"; +import { ProductModel } from "../../../../core/models/product.model"; +import { BACKEND_URL } from "../../../../core/tokens/api-url-tokens"; +import { Router } from "@angular/router"; + +@Component({ + selector: "app-product-card", + standalone: true, + imports: [LucideAngularModule], + templateUrl: "./product-card.html", +}) +export class ProductCard { + readonly HeartIcon = Heart; + readonly router = inject(Router); + + @Input() product!: ProductModel; + + goToProductDetails() { + this.router.navigate(["/products", this.product.slug]); + } +} diff --git a/src/app/features/product/product.html b/src/app/features/product/product.html index 772b623..67121c9 100644 --- a/src/app/features/product/product.html +++ b/src/app/features/product/product.html @@ -1 +1,11 @@ -

product works!

+
+

Our Product

+
+ +
+ @for (product of products(); track product) { + + } +
diff --git a/src/app/features/product/product.routes.ts b/src/app/features/product/product.routes.ts index c11716c..bd85c26 100644 --- a/src/app/features/product/product.routes.ts +++ b/src/app/features/product/product.routes.ts @@ -1,9 +1,14 @@ import { Routes } from "@angular/router"; import { AddProduct } from "./add-product/add-product"; +import { ShowProduct } from "./show-product/show-product"; export const productRoutes: Routes = [ { path: "create", component: AddProduct, }, + { + path: ":slug", + component: ShowProduct, + }, ]; diff --git a/src/app/features/product/product.ts b/src/app/features/product/product.ts index 9c169a0..aade0bf 100644 --- a/src/app/features/product/product.ts +++ b/src/app/features/product/product.ts @@ -1,9 +1,22 @@ -import { Component } from "@angular/core"; +import { Component, inject, signal } from "@angular/core"; +import { ProductCard } from "./components/product-card/product-card"; +import { HttpClient } from "@angular/common/http"; +import { API_URL } from "../../core/tokens/api-url-tokens"; +import { ProductModel } from "../../core/models/product.model"; +import { ProductService } from "./services/product-service"; @Component({ - selector: "app-product", - imports: [], + selector: "app-products", + imports: [ProductCard], templateUrl: "./product.html", - styleUrl: "./product.css", }) -export class Product {} +export class Product { + productService = inject(ProductService); + products = signal([]); + + ngOnInit() { + this.productService.getProducts().subscribe((data) => { + this.products.set(data.data); + }); + } +} diff --git a/src/app/features/product/services/category-service.ts b/src/app/features/product/services/category-service.ts index 02319a8..7ffc3de 100644 --- a/src/app/features/product/services/category-service.ts +++ b/src/app/features/product/services/category-service.ts @@ -3,7 +3,7 @@ import { HttpClient } from "@angular/common/http"; import { API_URL } from "../../../core/tokens/api-url-tokens"; export interface Category { - id: number; + id?: number; name: string; slug: string; } diff --git a/src/app/features/product/services/product-service.spec.ts b/src/app/features/product/services/product-service.spec.ts new file mode 100644 index 0000000..dae1d10 --- /dev/null +++ b/src/app/features/product/services/product-service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from "@angular/core/testing"; + +import { ProductService } from "./product-service"; + +describe("ProductService", () => { + let service: ProductService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ProductService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/features/product/services/product-service.ts b/src/app/features/product/services/product-service.ts new file mode 100644 index 0000000..df0123c --- /dev/null +++ b/src/app/features/product/services/product-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"; +import { ProductCollection, ProductModel } from "../../../core/models/product.model"; +import { map } from "rxjs"; + +@Injectable({ + providedIn: "root", +}) +export class ProductService { + http = inject(HttpClient); + apiUrl = inject(API_URL); + + getProducts() { + return this.http.get(`${this.apiUrl}/products`); + } + + getProduct(slug: string) { + return this.http.get<{ data: ProductModel }>(`${this.apiUrl}/products/${slug}`); + } +} diff --git a/src/app/features/product/product.css b/src/app/features/product/show-product/show-product.css similarity index 100% rename from src/app/features/product/product.css rename to src/app/features/product/show-product/show-product.css diff --git a/src/app/features/product/show-product/show-product.html b/src/app/features/product/show-product/show-product.html new file mode 100644 index 0000000..cd333a5 --- /dev/null +++ b/src/app/features/product/show-product/show-product.html @@ -0,0 +1,132 @@ +
+
+ + +
+
+
+ Product Image + + + +
+ +
+ @for (image of product()?.productImages; track image) { + + + } +
+
+ +
+
+

{{ product()?.title }}

+ + + +
+
+ +
+ 4.8 + + 112 Reviews +
+
+
+

Product Description

+

{{ product()?.description }}

+
+ +
+

Customer Reviews

+
    +
  • + "Rich in condensed nutrients using daily." + +
  • +
  • "Seedless & Eco-Peel it"
  • +
  • + "Longer Shelf Life has encounant" +
  • +
+
+
+ +
+
+
+
+ Rs.{{ product()?.actualPrice }} + Rs.{{ product()?.listPrice }} + +
+ +
+ +
+ + + +
+ +
+ + +
+
+
+
+
+
diff --git a/src/app/features/home/products/products.spec.ts b/src/app/features/product/show-product/show-product.spec.ts similarity index 58% rename from src/app/features/home/products/products.spec.ts rename to src/app/features/product/show-product/show-product.spec.ts index 0129469..31465ea 100644 --- a/src/app/features/home/products/products.spec.ts +++ b/src/app/features/product/show-product/show-product.spec.ts @@ -1,17 +1,17 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { Products } from "./products"; +import { ShowProduct } from "./show-product"; -describe("Products", () => { - let component: Products; - let fixture: ComponentFixture; +describe("ShowProduct", () => { + let component: ShowProduct; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [Products], + imports: [ShowProduct], }).compileComponents(); - fixture = TestBed.createComponent(Products); + fixture = TestBed.createComponent(ShowProduct); component = fixture.componentInstance; await fixture.whenStable(); }); diff --git a/src/app/features/product/show-product/show-product.ts b/src/app/features/product/show-product/show-product.ts new file mode 100644 index 0000000..3581d38 --- /dev/null +++ b/src/app/features/product/show-product/show-product.ts @@ -0,0 +1,40 @@ +import { Component, inject, Input, signal, WritableSignal } from "@angular/core"; +import { ProductModel } from "../../../core/models/product.model"; +import { ProductService } from "../services/product-service"; +import { LucideAngularModule, Heart, ArrowRight, ArrowLeft } from "lucide-angular"; + +@Component({ + selector: "app-show-product", + imports: [LucideAngularModule], + templateUrl: "./show-product.html", + styleUrl: "./show-product.css", +}) +export class ShowProduct { + @Input() slug?: string; + + HeartIcon = Heart; + ArrowRightIcon = ArrowRight; + ArrowLeftIcon = ArrowLeft; + productService = inject(ProductService); + product = signal(null); + activeImageIndex: WritableSignal = signal(0); + totalImageCount: number = 0; + + ngOnInit() { + if (!this.slug) return; + this.productService.getProduct(this.slug).subscribe((data) => { + this.product.set(data.data); + this.totalImageCount = this.product()?.productImages?.length || 0; + }); + } + + nextImage() { + this.activeImageIndex.update((index) => (index + 1) % this.totalImageCount); + } + prevImage() { + this.activeImageIndex.update( + (index) => + (index - 1 + this.product()?.productImages.length!) % this.product()?.productImages.length!, + ); + } +} diff --git a/src/styles.css b/src/styles.css index b38fa58..4ecbfd4 100644 --- a/src/styles.css +++ b/src/styles.css @@ -32,6 +32,10 @@ body { @apply text-gray-100 bg-gray-800 border border-gray-800 hover:bg-gray-200 hover:text-gray-800 hover:border-gray-400; } +.btn-primary { + @apply text-gray-100 bg-blue-700 border border-blue-800 hover:bg-blue-200 hover:text-blue-800 hover:border-blue-400; +} + .card { @apply bg-gray-50 rounded-xl border border-gray-300 hover:border-gray-400 p-4 hover:shadow-xl transition-all duration-300 ease-in-out; }