feature: add to cart

- make the cart service dependable on BehavorialSubject, migrated from
siganls
- implement add to cart service
This commit is contained in:
kusowl 2026-03-11 19:00:24 +05:30
parent 27a04c6458
commit ad957efcf0
8 changed files with 50 additions and 36 deletions

View File

@ -32,7 +32,7 @@
style="anchor-name: --anchor-2" style="anchor-name: --anchor-2"
> >
<lucide-angular [img]="CartIcon" class="w-5" /> <lucide-angular [img]="CartIcon" class="w-5" />
<span class="absolute top-0 text-xs ml-1">{{ cartItemCount() }}</span> <span class="absolute top-0 text-xs ml-1">{{ cartItemCount | async }}</span>
</button> </button>
</div> </div>
</div> </div>
@ -53,7 +53,7 @@
</ul> </ul>
<app-cart <app-cart
[cart]="cartItem()" [cart]="(cartItem$ | async)!"
id="popover-2" id="popover-2"
class="dropdown" class="dropdown"
popover popover

View File

@ -1,13 +1,16 @@
import { Component, computed, inject } from "@angular/core"; import { Component, inject } from "@angular/core";
import { LucideAngularModule, Search, ShoppingCart, User } from "lucide-angular"; import { LucideAngularModule, Search, ShoppingCart, User } from "lucide-angular";
import { RouterLink } from "@angular/router"; import { RouterLink } from "@angular/router";
import { AuthService, AuthState } from "../../../features/auth/services/auth-service"; import { AuthService, AuthState } from "../../../features/auth/services/auth-service";
import { CartService } from "@app/core/services/cart-service"; import { CartService } from "@app/core/services/cart-service";
import { Cart } from "@app/shared/components/cart/cart"; import { Cart } from "@app/shared/components/cart/cart";
import { CartModel } from "@app/core/models/cart.model";
import { map } from "rxjs";
import { AsyncPipe } from "@angular/common";
@Component({ @Component({
selector: "app-header", selector: "app-header",
imports: [LucideAngularModule, RouterLink, Cart], imports: [LucideAngularModule, RouterLink, Cart, AsyncPipe],
templateUrl: "./header.html", templateUrl: "./header.html",
styleUrl: "./header.css", styleUrl: "./header.css",
}) })
@ -19,6 +22,6 @@ export class Header {
readonly cartService = inject(CartService); readonly cartService = inject(CartService);
protected readonly AuthState = AuthState; protected readonly AuthState = AuthState;
cartItem = this.cartService.cartItem; cartItem$ = this.cartService.cartItem$;
cartItemCount = computed(() => this.cartItem().itemsCount ?? 0); cartItemCount = this.cartItem$.pipe(map((cart: CartModel) => cart.itemsCount ?? 0));
} }

View File

@ -4,6 +4,7 @@ import { API_URL } from "../tokens/api-url-tokens";
import { CartItemModel, CartItemRequest, CartModel } from "../models/cart.model"; import { CartItemModel, CartItemRequest, CartModel } from "../models/cart.model";
import { AuthService, AuthState } from "@app/features/auth/services/auth-service"; import { AuthService, AuthState } from "@app/features/auth/services/auth-service";
import { Cart } from "@app/shared/components/cart/cart"; import { Cart } from "@app/shared/components/cart/cart";
import { BehaviorSubject, tap } from "rxjs";
@Injectable({ @Injectable({
providedIn: "root", providedIn: "root",
@ -14,21 +15,23 @@ export class CartService {
private http = inject(HttpClient); private http = inject(HttpClient);
private apiUrl = inject(API_URL); private apiUrl = inject(API_URL);
cartItem = signal<CartModel>({} as CartModel); private _cartItem = new BehaviorSubject<CartModel>({} as CartModel);
cartItem$ = this._cartItem.asObservable();
constructor() { constructor() {
effect(() => { effect(() => {
if (this.authService.isAuthenticated()) { if (this.authService.isAuthenticated()) {
this.fetchCart(); this.fetchCart();
} else { } else {
this.cartItem.set({} as CartModel); this._cartItem.next({} as CartModel);
} }
}); });
} }
fetchCart() { fetchCart() {
return this.http.get<CartModel>(this.apiUrl + "/cart").subscribe({ return this.http.get<CartModel>(this.apiUrl + "/cart").subscribe({
next: (data) => this.cartItem.set(data), next: (data) => this._cartItem.next(data),
error: (error: HttpErrorResponse) => { error: (error: HttpErrorResponse) => {
if (error.status === 401) { if (error.status === 401) {
this.authService.purgeAuth(); this.authService.purgeAuth();
@ -38,11 +41,21 @@ export class CartService {
}); });
} }
addToCart(data: CartItemRequest) {
return this.http
.post<CartModel>(this.apiUrl + "/cart", data)
.pipe(tap((updatedCart: CartModel) => this._cartItem.next(updatedCart)));
}
updateCart(data: CartItemRequest) { updateCart(data: CartItemRequest) {
return this.http.patch<CartModel>(this.apiUrl + "/cart", data); return this.http
.patch<CartModel>(this.apiUrl + "/cart", data)
.pipe(tap((updatedCart: CartModel) => this._cartItem.next(updatedCart)));
} }
removeFromCart(productId: number) { removeFromCart(productId: number) {
return this.http.delete<CartModel>(this.apiUrl + "/cart", { body: { productId: productId } }); return this.http
.delete<CartModel>(this.apiUrl + "/cart", { body: { productId: productId } })
.pipe(tap((updatedCart: CartModel) => this._cartItem.next(updatedCart)));
} }
} }

View File

@ -21,7 +21,7 @@
<p class="font-medium text-lg">Rs. {{ product.actualPrice }}</p> <p class="font-medium text-lg">Rs. {{ product.actualPrice }}</p>
</div> </div>
<div> <div>
<button class="btn btn-primary p-3" title="Add to cart"> <button (click)="addToCart($event)" class="btn btn-primary p-3" title="Add to cart">
<lucide-angular [img]="ShoppingCartIcon" class="w-4" /> <lucide-angular [img]="ShoppingCartIcon" class="w-4" />
</button> </button>
</div> </div>

View File

@ -3,6 +3,7 @@ import { ProductModel } from "../../../../core/models/product.model";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { FavoriteButton } from "../../../../shared/components/favorite-button/favorite-button"; import { FavoriteButton } from "../../../../shared/components/favorite-button/favorite-button";
import { LucideAngularModule, ShoppingCart } from "lucide-angular"; import { LucideAngularModule, ShoppingCart } from "lucide-angular";
import { CartService } from "@app/core/services/cart-service";
@Component({ @Component({
selector: "app-product-card", selector: "app-product-card",
@ -13,10 +14,16 @@ import { LucideAngularModule, ShoppingCart } from "lucide-angular";
export class ProductCard { export class ProductCard {
readonly router = inject(Router); readonly router = inject(Router);
readonly ShoppingCartIcon = ShoppingCart; readonly ShoppingCartIcon = ShoppingCart;
readonly cartService = inject(CartService);
@Input() product!: ProductModel; @Input() product!: ProductModel;
goToProductDetails() { goToProductDetails() {
this.router.navigate(["/products", this.product.slug]); this.router.navigate(["/products", this.product.slug]);
} }
addToCart(event: Event) {
event.stopPropagation();
this.cartService.addToCart({ productId: this.product.id, quantity: 1 }).subscribe();
}
} }

View File

@ -90,28 +90,16 @@
<div class="lg:col-span-3 flex flex-col gap-6"> <div class="lg:col-span-3 flex flex-col gap-6">
<div class="card"> <div class="card">
<div class="flex justify-between items-start mb-6"> <div class="flex justify-between items-start mb-6">
<div class="flex items-baseline gap-2"> <div class="">
<span class="text-xl text-gray-400 line-through decoration-1" <p class="text-sm text-gray-400 line-through decoration-1">
>Rs.{{ product()?.actualPrice }}</span Rs.{{ product()?.listPrice }}
> </p>
<span class="text-3xl font-bold text-gray-900">Rs.{{ product()?.listPrice }}</span> <p class="text-3xl font-bold text-gray-900">Rs.{{ product()?.actualPrice }}</p>
<span class="text-xs text-gray-400 ml-1"></span>
</div> </div>
</div> </div>
<div class="flex mb-6 overflow-hidden">
<button class="px-4 py-2 btn btn-ghost rounded-r-none!"></button>
<input
type="text"
value="1"
class="w-12 text-center border-y border-gray-300"
readonly
/>
<button class="px-4 py-2 btn btn-ghost rounded-l-none!">+</button>
</div>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<button class="w-full btn btn-ghost">Add to Cart</button> <button (click)="addToCart()" class="w-full btn btn-ghost">Add to Cart</button>
<button class="w-full btn btn-primary">Buy Now</button> <button class="w-full btn btn-primary">Buy Now</button>
</div> </div>
</div> </div>

View File

@ -3,6 +3,7 @@ import { ProductModel } from "../../../core/models/product.model";
import { ProductService } from "../services/product-service"; import { ProductService } from "../services/product-service";
import { LucideAngularModule, Heart, ArrowRight, ArrowLeft } from "lucide-angular"; import { LucideAngularModule, Heart, ArrowRight, ArrowLeft } from "lucide-angular";
import { FavoriteButton } from "../../../shared/components/favorite-button/favorite-button"; import { FavoriteButton } from "../../../shared/components/favorite-button/favorite-button";
import { CartService } from "@app/core/services/cart-service";
@Component({ @Component({
selector: "app-show-product", selector: "app-show-product",
@ -17,6 +18,7 @@ export class ShowProduct {
ArrowRightIcon = ArrowRight; ArrowRightIcon = ArrowRight;
ArrowLeftIcon = ArrowLeft; ArrowLeftIcon = ArrowLeft;
productService = inject(ProductService); productService = inject(ProductService);
cartService = inject(CartService);
product = signal<ProductModel | null>(null); product = signal<ProductModel | null>(null);
activeImageIndex: WritableSignal<number> = signal(0); activeImageIndex: WritableSignal<number> = signal(0);
totalImageCount: number = 0; totalImageCount: number = 0;
@ -29,9 +31,14 @@ export class ShowProduct {
}); });
} }
addToCart() {
this.cartService.addToCart({ productId: this.product()!.id, quantity: 1 }).subscribe();
}
nextImage() { nextImage() {
this.activeImageIndex.update((index) => (index + 1) % this.totalImageCount); this.activeImageIndex.update((index) => (index + 1) % this.totalImageCount);
} }
prevImage() { prevImage() {
this.activeImageIndex.update( this.activeImageIndex.update(
(index) => (index) =>

View File

@ -26,9 +26,7 @@ export class Cart {
this.cartService this.cartService
.updateCart(cartItem) .updateCart(cartItem)
.pipe(finalize(() => this.isLoading.set(false))) .pipe(finalize(() => this.isLoading.set(false)))
.subscribe((cartdata) => { .subscribe();
this.cart = cartdata;
});
} }
removeProduct(productId: number) { removeProduct(productId: number) {
@ -37,8 +35,6 @@ export class Cart {
this.cartService this.cartService
.removeFromCart(productId) .removeFromCart(productId)
.pipe(finalize(() => this.isLoading.set(false))) .pipe(finalize(() => this.isLoading.set(false)))
.subscribe((cartData) => { .subscribe();
this.cart = cartData;
});
} }
} }