diff --git a/src/app/features/product/product.css b/.ai/mcp/mcp.json
similarity index 100%
rename from src/app/features/product/product.css
rename to .ai/mcp/mcp.json
diff --git a/angular.json b/angular.json
index 77a0468..2131bae 100644
--- a/angular.json
+++ b/angular.json
@@ -2,7 +2,8 @@
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
- "packageManager": "npm"
+ "packageManager": "npm",
+ "analytics": false
},
"newProjectRoot": "projects",
"projects": {
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/core/services/favorite-service.spec.ts b/src/app/core/services/favorite-service.spec.ts
new file mode 100644
index 0000000..9bf2abe
--- /dev/null
+++ b/src/app/core/services/favorite-service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { FavoriteService } from './favorite-service';
+
+describe('FavoriteService', () => {
+ let service: FavoriteService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(FavoriteService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/core/services/favorite-service.ts b/src/app/core/services/favorite-service.ts
new file mode 100644
index 0000000..0df02da
--- /dev/null
+++ b/src/app/core/services/favorite-service.ts
@@ -0,0 +1,20 @@
+import { HttpClient } from '@angular/common/http';
+import { inject, Injectable } from '@angular/core';
+import { API_URL } from '../tokens/api-url-tokens';
+
+export interface FavoriteResponse {
+ message: string;
+ isFavorite: boolean;
+}
+
+@Injectable({
+ providedIn: 'root',
+})
+export class FavoriteService {
+ http = inject(HttpClient);
+ apiUrl = inject(API_URL);
+
+ toggle(productId: number) {
+ return this.http.post(`${this.apiUrl}/products/${productId}/favorite`, {});
+ }
+}
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 @@
-
-
-
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 @@
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..82ef41a
--- /dev/null
+++ b/src/app/features/product/components/product-card/product-card.html
@@ -0,0 +1,15 @@
+
+
+
+
+
![]()
+
+
+
{{ 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..cca01d7
--- /dev/null
+++ b/src/app/features/product/components/product-card/product-card.ts
@@ -0,0 +1,20 @@
+import { Component, inject, Input } from '@angular/core';
+import { ProductModel } from '../../../../core/models/product.model';
+import { Router } from '@angular/router';
+import { FavoriteButton } from '../../../../src/app/shared/components/favorite-button/favorite-button';
+
+@Component({
+ selector: 'app-product-card',
+ standalone: true,
+ imports: [FavoriteButton],
+ templateUrl: './product-card.html',
+})
+export class ProductCard {
+ 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!
+
+
+
+ @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..b56e5f1
--- /dev/null
+++ b/src/app/features/product/services/product-service.ts
@@ -0,0 +1,20 @@
+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';
+
+@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/show-product/show-product.css b/src/app/features/product/show-product/show-product.css
new file mode 100644
index 0000000..e69de29
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/app/src/app/shared/components/favorite-button/favorite-button.css b/src/app/src/app/shared/components/favorite-button/favorite-button.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/src/app/shared/components/favorite-button/favorite-button.html b/src/app/src/app/shared/components/favorite-button/favorite-button.html
new file mode 100644
index 0000000..cf185bf
--- /dev/null
+++ b/src/app/src/app/shared/components/favorite-button/favorite-button.html
@@ -0,0 +1,7 @@
+
+
diff --git a/src/app/src/app/shared/components/favorite-button/favorite-button.spec.ts b/src/app/src/app/shared/components/favorite-button/favorite-button.spec.ts
new file mode 100644
index 0000000..7f14936
--- /dev/null
+++ b/src/app/src/app/shared/components/favorite-button/favorite-button.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { FavoriteButton } from './favorite-button';
+
+describe('FavoriteButton', () => {
+ let component: FavoriteButton;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [FavoriteButton]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(FavoriteButton);
+ component = fixture.componentInstance;
+ await fixture.whenStable();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/src/app/shared/components/favorite-button/favorite-button.ts b/src/app/src/app/shared/components/favorite-button/favorite-button.ts
new file mode 100644
index 0000000..437e8d0
--- /dev/null
+++ b/src/app/src/app/shared/components/favorite-button/favorite-button.ts
@@ -0,0 +1,34 @@
+import { Component, inject, Input } from '@angular/core';
+import { HeartIcon, LucideAngularModule } from 'lucide-angular';
+import { FavoriteService } from '../../../../../core/services/favorite-service';
+
+@Component({
+ selector: 'app-favorite-button',
+ imports: [LucideAngularModule],
+ templateUrl: './favorite-button.html',
+ styleUrl: './favorite-button.css',
+})
+export class FavoriteButton {
+ @Input({ required: true }) productId!: number;
+ @Input() isFavorite = true;
+
+ favoriteService = inject(FavoriteService);
+
+ HeartIcon = HeartIcon;
+
+ toggleFavorite(event: Event) {
+ event.stopPropagation();
+ this.isFavorite = !this.isFavorite;
+
+ this.favoriteService.toggle(this.productId).subscribe({
+ next: (response) => {
+ this.isFavorite = response.isFavorite;
+ },
+ error: (err) => {
+ console.error(err);
+ // Revert the state incase of error
+ this.isFavorite = !this.isFavorite;
+ },
+ });
+ }
+}
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;
}