From 6d1cb81e6bec4140123e4f5995eb33280e7f1b66 Mon Sep 17 00:00:00 2001 From: kusowl Date: Fri, 13 Mar 2026 18:54:48 +0530 Subject: [PATCH 1/5] wip: checkout page - add button to go address page in cart ui - add template for address page --- .phpactor.json | 3 ++ src/app/app.routes.ts | 5 ++ src/app/core/layouts/header/header.html | 10 ++-- src/app/features/checkout/address/address.css | 0 .../features/checkout/address/address.html | 16 ++++++ .../features/checkout/address/address.spec.ts | 22 ++++++++ src/app/features/checkout/address/address.ts | 13 +++++ src/app/features/checkout/checkout.css | 0 src/app/features/checkout/checkout.html | 6 +++ src/app/features/checkout/checkout.routes.ts | 16 ++++++ src/app/features/checkout/checkout.spec.ts | 22 ++++++++ src/app/features/checkout/checkout.ts | 18 +++++++ .../components/address-form/address-form.css | 0 .../components/address-form/address-form.html | 50 +++++++++++++++++++ .../address-form/address-form.spec.ts | 22 ++++++++ .../components/address-form/address-form.ts | 20 ++++++++ .../address-select/address-select.css | 0 .../address-select/address-select.html | 10 ++++ .../address-select/address-select.spec.ts | 22 ++++++++ .../address-select/address-select.ts | 9 ++++ .../order-summery/order-summery.css | 0 .../order-summery/order-summery.html | 3 ++ .../order-summery/order-summery.spec.ts | 22 ++++++++ .../components/order-summery/order-summery.ts | 9 ++++ .../checkout/services/address-service.spec.ts | 16 ++++++ .../checkout/services/address-service.ts | 6 +++ src/app/shared/components/cart/cart.html | 3 ++ src/app/shared/components/cart/cart.ts | 3 +- src/app/shared/components/go-back/go-back.css | 0 .../shared/components/go-back/go-back.html | 4 ++ .../shared/components/go-back/go-back.spec.ts | 22 ++++++++ src/app/shared/components/go-back/go-back.ts | 15 ++++++ src/app/shared/components/stepper/stepper.css | 0 .../shared/components/stepper/stepper.html | 29 +++++++++++ .../shared/components/stepper/stepper.spec.ts | 22 ++++++++ src/app/shared/components/stepper/stepper.ts | 20 ++++++++ src/styles.css | 6 +-- 37 files changed, 435 insertions(+), 9 deletions(-) create mode 100644 .phpactor.json create mode 100644 src/app/features/checkout/address/address.css create mode 100644 src/app/features/checkout/address/address.html create mode 100644 src/app/features/checkout/address/address.spec.ts create mode 100644 src/app/features/checkout/address/address.ts create mode 100644 src/app/features/checkout/checkout.css create mode 100644 src/app/features/checkout/checkout.html create mode 100644 src/app/features/checkout/checkout.routes.ts create mode 100644 src/app/features/checkout/checkout.spec.ts create mode 100644 src/app/features/checkout/checkout.ts create mode 100644 src/app/features/checkout/components/address-form/address-form.css create mode 100644 src/app/features/checkout/components/address-form/address-form.html create mode 100644 src/app/features/checkout/components/address-form/address-form.spec.ts create mode 100644 src/app/features/checkout/components/address-form/address-form.ts create mode 100644 src/app/features/checkout/components/address-select/address-select.css create mode 100644 src/app/features/checkout/components/address-select/address-select.html create mode 100644 src/app/features/checkout/components/address-select/address-select.spec.ts create mode 100644 src/app/features/checkout/components/address-select/address-select.ts create mode 100644 src/app/features/checkout/components/order-summery/order-summery.css create mode 100644 src/app/features/checkout/components/order-summery/order-summery.html create mode 100644 src/app/features/checkout/components/order-summery/order-summery.spec.ts create mode 100644 src/app/features/checkout/components/order-summery/order-summery.ts create mode 100644 src/app/features/checkout/services/address-service.spec.ts create mode 100644 src/app/features/checkout/services/address-service.ts create mode 100644 src/app/shared/components/go-back/go-back.css create mode 100644 src/app/shared/components/go-back/go-back.html create mode 100644 src/app/shared/components/go-back/go-back.spec.ts create mode 100644 src/app/shared/components/go-back/go-back.ts create mode 100644 src/app/shared/components/stepper/stepper.css create mode 100644 src/app/shared/components/stepper/stepper.html create mode 100644 src/app/shared/components/stepper/stepper.spec.ts create mode 100644 src/app/shared/components/stepper/stepper.ts diff --git a/.phpactor.json b/.phpactor.json new file mode 100644 index 0000000..4933b4b --- /dev/null +++ b/.phpactor.json @@ -0,0 +1,3 @@ +{ + "indexer.exclude_patterns": ["/node_modules/**/*", "/backend/**/*"] +} diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index f6736f3..3e3354b 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -19,4 +19,9 @@ export const routes: Routes = [ canActivate: [authGuard, roleGuard], data: { roles: ["admin", "broker"] }, }, + { + path: "checkout", + loadChildren: () => + import("./features/checkout/checkout.routes").then((routes) => routes.checkoutRoutes), + }, ]; diff --git a/src/app/core/layouts/header/header.html b/src/app/core/layouts/header/header.html index 70f585c..7a74d77 100644 --- a/src/app/core/layouts/header/header.html +++ b/src/app/core/layouts/header/header.html @@ -20,14 +20,14 @@
+ +
+ + diff --git a/src/app/features/checkout/components/address-form/address-form.spec.ts b/src/app/features/checkout/components/address-form/address-form.spec.ts new file mode 100644 index 0000000..413f20a --- /dev/null +++ b/src/app/features/checkout/components/address-form/address-form.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { AddressForm } from "./address-form"; + +describe("AddressForm", () => { + let component: AddressForm; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AddressForm], + }).compileComponents(); + + fixture = TestBed.createComponent(AddressForm); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/checkout/components/address-form/address-form.ts b/src/app/features/checkout/components/address-form/address-form.ts new file mode 100644 index 0000000..12e9728 --- /dev/null +++ b/src/app/features/checkout/components/address-form/address-form.ts @@ -0,0 +1,20 @@ +import { Component } from "@angular/core"; +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; +import { Error } from "@app/shared/components/error/error"; + +@Component({ + selector: "app-address-form", + imports: [ReactiveFormsModule, Error], + templateUrl: "./address-form.html", + styleUrl: "./address-form.css", +}) +export class AddressForm { + addressForm = new FormGroup({ + firstName: new FormControl("", { validators: Validators.required }), + lastName: new FormControl("", { validators: Validators.required }), + streetAddress: new FormControl("", { validators: Validators.required }), + city: new FormControl("", { validators: Validators.required }), + state: new FormControl("", { validators: Validators.required }), + pinCode: new FormControl("", { validators: Validators.required }), + }); +} diff --git a/src/app/features/checkout/components/address-select/address-select.css b/src/app/features/checkout/components/address-select/address-select.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/features/checkout/components/address-select/address-select.html b/src/app/features/checkout/components/address-select/address-select.html new file mode 100644 index 0000000..1fb2a0e --- /dev/null +++ b/src/app/features/checkout/components/address-select/address-select.html @@ -0,0 +1,10 @@ +
+
+ +

Kushal Saha

+

48 St, Park Avenue, New Towm, 700021

+
+
+ +
+
diff --git a/src/app/features/checkout/components/address-select/address-select.spec.ts b/src/app/features/checkout/components/address-select/address-select.spec.ts new file mode 100644 index 0000000..d9d477d --- /dev/null +++ b/src/app/features/checkout/components/address-select/address-select.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { AddressSelect } from "./address-select"; + +describe("AddressSelect", () => { + let component: AddressSelect; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AddressSelect], + }).compileComponents(); + + fixture = TestBed.createComponent(AddressSelect); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/checkout/components/address-select/address-select.ts b/src/app/features/checkout/components/address-select/address-select.ts new file mode 100644 index 0000000..18a7490 --- /dev/null +++ b/src/app/features/checkout/components/address-select/address-select.ts @@ -0,0 +1,9 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-address-select", + imports: [], + templateUrl: "./address-select.html", + styleUrl: "./address-select.css", +}) +export class AddressSelect {} diff --git a/src/app/features/checkout/components/order-summery/order-summery.css b/src/app/features/checkout/components/order-summery/order-summery.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/features/checkout/components/order-summery/order-summery.html b/src/app/features/checkout/components/order-summery/order-summery.html new file mode 100644 index 0000000..4d21641 --- /dev/null +++ b/src/app/features/checkout/components/order-summery/order-summery.html @@ -0,0 +1,3 @@ +
+

Order Summery

+
diff --git a/src/app/features/checkout/components/order-summery/order-summery.spec.ts b/src/app/features/checkout/components/order-summery/order-summery.spec.ts new file mode 100644 index 0000000..aca8d57 --- /dev/null +++ b/src/app/features/checkout/components/order-summery/order-summery.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { OrderSummery } from "./order-summery"; + +describe("OrderSummery", () => { + let component: OrderSummery; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [OrderSummery], + }).compileComponents(); + + fixture = TestBed.createComponent(OrderSummery); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/checkout/components/order-summery/order-summery.ts b/src/app/features/checkout/components/order-summery/order-summery.ts new file mode 100644 index 0000000..3a03579 --- /dev/null +++ b/src/app/features/checkout/components/order-summery/order-summery.ts @@ -0,0 +1,9 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-order-summery", + imports: [], + templateUrl: "./order-summery.html", + styleUrl: "./order-summery.css", +}) +export class OrderSummery {} diff --git a/src/app/features/checkout/services/address-service.spec.ts b/src/app/features/checkout/services/address-service.spec.ts new file mode 100644 index 0000000..b8c5e22 --- /dev/null +++ b/src/app/features/checkout/services/address-service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from "@angular/core/testing"; + +import { AddressService } from "./address-service"; + +describe("AddressService", () => { + let service: AddressService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AddressService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/features/checkout/services/address-service.ts b/src/app/features/checkout/services/address-service.ts new file mode 100644 index 0000000..1ddc4c6 --- /dev/null +++ b/src/app/features/checkout/services/address-service.ts @@ -0,0 +1,6 @@ +import { Injectable } from "@angular/core"; + +@Injectable({ + providedIn: "root", +}) +export class AddressService {} diff --git a/src/app/shared/components/cart/cart.html b/src/app/shared/components/cart/cart.html index df526c5..8ee49df 100644 --- a/src/app/shared/components/cart/cart.html +++ b/src/app/shared/components/cart/cart.html @@ -24,3 +24,6 @@
} +Proceed to checkout diff --git a/src/app/shared/components/cart/cart.ts b/src/app/shared/components/cart/cart.ts index 121b0ef..6334750 100644 --- a/src/app/shared/components/cart/cart.ts +++ b/src/app/shared/components/cart/cart.ts @@ -4,10 +4,11 @@ import { CartItem } from "../cart-item/cart-item"; import { AuthService, AuthState } from "@app/features/auth/services/auth-service"; import { CartService } from "@app/core/services/cart-service"; import { finalize, tap } from "rxjs"; +import { RouterLink } from "@angular/router"; @Component({ selector: "app-cart", - imports: [CartItem], + imports: [CartItem, RouterLink], templateUrl: "./cart.html", styleUrl: "./cart.css", }) diff --git a/src/app/shared/components/go-back/go-back.css b/src/app/shared/components/go-back/go-back.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/components/go-back/go-back.html b/src/app/shared/components/go-back/go-back.html new file mode 100644 index 0000000..c21331c --- /dev/null +++ b/src/app/shared/components/go-back/go-back.html @@ -0,0 +1,4 @@ + + +

{{ text }}

+
diff --git a/src/app/shared/components/go-back/go-back.spec.ts b/src/app/shared/components/go-back/go-back.spec.ts new file mode 100644 index 0000000..8b2a9b0 --- /dev/null +++ b/src/app/shared/components/go-back/go-back.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { GoBack } from "./go-back"; + +describe("GoBack", () => { + let component: GoBack; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [GoBack], + }).compileComponents(); + + fixture = TestBed.createComponent(GoBack); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/go-back/go-back.ts b/src/app/shared/components/go-back/go-back.ts new file mode 100644 index 0000000..b690ada --- /dev/null +++ b/src/app/shared/components/go-back/go-back.ts @@ -0,0 +1,15 @@ +import { Component, Input } from "@angular/core"; +import { RouterLink } from "@angular/router"; +import { LucideAngularModule, MoveLeft } from "lucide-angular"; + +@Component({ + selector: "app-go-back", + imports: [RouterLink, LucideAngularModule], + templateUrl: "./go-back.html", + styleUrl: "./go-back.css", +}) +export class GoBack { + @Input() route: string = "#"; + @Input() text: string = ""; + MoveLeftIcon = MoveLeft; +} diff --git a/src/app/shared/components/stepper/stepper.css b/src/app/shared/components/stepper/stepper.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/components/stepper/stepper.html b/src/app/shared/components/stepper/stepper.html new file mode 100644 index 0000000..91b4b16 --- /dev/null +++ b/src/app/shared/components/stepper/stepper.html @@ -0,0 +1,29 @@ +
    + @for (step of steps; track step) { +
  1. +
    + + + + @if (!$last) { +
    + } +
    +

    {{ step.label }}

    +
  2. + } +
diff --git a/src/app/shared/components/stepper/stepper.spec.ts b/src/app/shared/components/stepper/stepper.spec.ts new file mode 100644 index 0000000..53af7c4 --- /dev/null +++ b/src/app/shared/components/stepper/stepper.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { Stepper } from "./stepper"; + +describe("Stepper", () => { + let component: Stepper; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Stepper], + }).compileComponents(); + + fixture = TestBed.createComponent(Stepper); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/stepper/stepper.ts b/src/app/shared/components/stepper/stepper.ts new file mode 100644 index 0000000..af56031 --- /dev/null +++ b/src/app/shared/components/stepper/stepper.ts @@ -0,0 +1,20 @@ +import { Component, Input } from "@angular/core"; +import { Check, Circle, LucideAngularModule } from "lucide-angular"; + +export type Steps = { + label: string; +}; + +@Component({ + selector: "app-stepper", + imports: [LucideAngularModule], + templateUrl: "./stepper.html", + styleUrl: "./stepper.css", +}) +export class Stepper { + @Input() currentStep: number = 0; + @Input() steps: Steps[] = []; + + CheckIcon = Check; + CirecleIcon = Circle; +} diff --git a/src/styles.css b/src/styles.css index 0f9fd15..09b81b8 100644 --- a/src/styles.css +++ b/src/styles.css @@ -13,7 +13,7 @@ } body { - @apply bg-gray-100 antialiased m-0; + @apply bg-gray-50 antialiased m-0; } .wrapper { @@ -33,11 +33,11 @@ body { } .btn-primary { - @apply text-blue-100 bg-blue-600 border border-b-3 border-blue-900 hover:bg-blue-700; + @apply text-blue-100 bg-blue-600 border-b border-b-3 border-blue-900 hover:bg-blue-700; } .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; + @apply bg-white rounded-xl border-2 border-gray-200 p-4; } .fieldset { From 3059a923b4a495272713a663caf3f1d44fbfb1bd Mon Sep 17 00:00:00 2001 From: kusowl Date: Mon, 16 Mar 2026 13:05:06 +0530 Subject: [PATCH 2/5] refactor: move auth service to core --- src/app/core/guards/auth-guard.ts | 2 +- src/app/core/guards/role-guard.ts | 2 +- src/app/core/layouts/header/header.ts | 5 ++--- .../auth => core}/services/auth-service.spec.ts | 0 .../auth => core}/services/auth-service.ts | 6 +++--- src/app/core/services/cart-service.ts | 2 +- src/app/features/auth/auth.routes.ts | 2 +- src/app/features/auth/components/login/login.ts | 8 ++++---- .../features/auth/components/register/register.ts | 6 +++--- src/app/features/checkout/address/address.ts | 9 +++++++-- .../features/checkout/services/address-service.ts | 13 +++++++++++-- src/app/shared/components/cart/cart.html | 10 +++++++--- src/app/shared/components/cart/cart.ts | 2 +- 13 files changed, 42 insertions(+), 25 deletions(-) rename src/app/{features/auth => core}/services/auth-service.spec.ts (100%) rename src/app/{features/auth => core}/services/auth-service.ts (92%) diff --git a/src/app/core/guards/auth-guard.ts b/src/app/core/guards/auth-guard.ts index 8b3e945..5eeaf9c 100644 --- a/src/app/core/guards/auth-guard.ts +++ b/src/app/core/guards/auth-guard.ts @@ -1,6 +1,6 @@ import { CanActivateFn, Router } from "@angular/router"; import { inject } from "@angular/core"; -import { AuthService } from "../../features/auth/services/auth-service"; +import { AuthService } from "@core/services/auth-service"; export const authGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); diff --git a/src/app/core/guards/role-guard.ts b/src/app/core/guards/role-guard.ts index dfd7a3c..48d4074 100644 --- a/src/app/core/guards/role-guard.ts +++ b/src/app/core/guards/role-guard.ts @@ -1,6 +1,6 @@ import { CanActivateFn } from "@angular/router"; import { inject } from "@angular/core"; -import { AuthService } from "../../features/auth/services/auth-service"; +import { AuthService } from "@core/services/auth-service"; export const roleGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); diff --git a/src/app/core/layouts/header/header.ts b/src/app/core/layouts/header/header.ts index 8c4ad69..d0121cb 100644 --- a/src/app/core/layouts/header/header.ts +++ b/src/app/core/layouts/header/header.ts @@ -1,7 +1,6 @@ import { Component, inject } from "@angular/core"; import { LucideAngularModule, Search, ShoppingCart, User } from "lucide-angular"; -import { RouterLink } from "@angular/router"; -import { AuthService, AuthState } from "../../../features/auth/services/auth-service"; +import { AuthService, AuthState } from "@core/services/auth-service"; import { CartService } from "@app/core/services/cart-service"; import { Cart } from "@app/shared/components/cart/cart"; import { CartModel } from "@app/core/models/cart.model"; @@ -10,7 +9,7 @@ import { AsyncPipe } from "@angular/common"; @Component({ selector: "app-header", - imports: [LucideAngularModule, RouterLink, Cart, AsyncPipe], + imports: [LucideAngularModule, Cart, AsyncPipe], templateUrl: "./header.html", styleUrl: "./header.css", }) diff --git a/src/app/features/auth/services/auth-service.spec.ts b/src/app/core/services/auth-service.spec.ts similarity index 100% rename from src/app/features/auth/services/auth-service.spec.ts rename to src/app/core/services/auth-service.spec.ts diff --git a/src/app/features/auth/services/auth-service.ts b/src/app/core/services/auth-service.ts similarity index 92% rename from src/app/features/auth/services/auth-service.ts rename to src/app/core/services/auth-service.ts index ca38560..e5f6f52 100644 --- a/src/app/features/auth/services/auth-service.ts +++ b/src/app/core/services/auth-service.ts @@ -1,9 +1,9 @@ import { computed, inject, Injectable, Signal, signal, WritableSignal } from "@angular/core"; -import { RegisterUserRequest, User } from "../../../core/models/user.model"; +import { RegisterUserRequest, User } from "../models/user.model"; import { HttpClient, HttpErrorResponse } from "@angular/common/http"; -import { API_URL, BACKEND_URL } from "../../../core/tokens/api-url-tokens"; +import { API_URL, BACKEND_URL } from "../tokens/api-url-tokens"; import { switchMap, tap } from "rxjs"; -import { LocalStorageService } from "../../../core/services/local-storage.service"; +import { LocalStorageService } from "../services/local-storage.service"; export enum AuthState { Loading = "loading", diff --git a/src/app/core/services/cart-service.ts b/src/app/core/services/cart-service.ts index f58d354..9c0e1c4 100644 --- a/src/app/core/services/cart-service.ts +++ b/src/app/core/services/cart-service.ts @@ -2,7 +2,7 @@ import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { effect, inject, Injectable, signal } from "@angular/core"; import { API_URL } from "../tokens/api-url-tokens"; import { CartItemModel, CartItemRequest, CartModel } from "../models/cart.model"; -import { AuthService, AuthState } from "@app/features/auth/services/auth-service"; +import { AuthService, AuthState } from "@core/services/auth-service"; import { Cart } from "@app/shared/components/cart/cart"; import { BehaviorSubject, tap } from "rxjs"; diff --git a/src/app/features/auth/auth.routes.ts b/src/app/features/auth/auth.routes.ts index 6f010bf..ca836bf 100644 --- a/src/app/features/auth/auth.routes.ts +++ b/src/app/features/auth/auth.routes.ts @@ -2,7 +2,7 @@ import { Router, Routes } from "@angular/router"; import { Login } from "./components/login/login"; import { Register } from "./components/register/register"; import { inject } from "@angular/core"; -import { AuthService } from "./services/auth-service"; +import { AuthService } from "@core/services/auth-service"; export const AuthRoutes: Routes = [ { diff --git a/src/app/features/auth/components/login/login.ts b/src/app/features/auth/components/login/login.ts index e01d973..680c505 100644 --- a/src/app/features/auth/components/login/login.ts +++ b/src/app/features/auth/components/login/login.ts @@ -1,12 +1,12 @@ import { Component, inject } from "@angular/core"; -import { Router, RouterLink } from "@angular/router"; +import { Router } from "@angular/router"; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; -import { AuthService } from "../../services/auth-service"; -import { Error } from "../../../../shared/components/error/error"; +import { AuthService } from "@core/services/auth-service"; +import { Error } from "@app/shared/components/error/error"; @Component({ selector: "app-login", - imports: [RouterLink, ReactiveFormsModule, Error], + imports: [ReactiveFormsModule, Error], templateUrl: "./login.html", styleUrl: "./login.css", }) diff --git a/src/app/features/auth/components/register/register.ts b/src/app/features/auth/components/register/register.ts index 30ba022..408588b 100644 --- a/src/app/features/auth/components/register/register.ts +++ b/src/app/features/auth/components/register/register.ts @@ -1,8 +1,8 @@ import { Component, inject, signal } from "@angular/core"; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; -import { AuthService } from "../../services/auth-service"; -import { RegisterUserRequest } from "../../../../core/models/user.model"; -import { Error } from "../../../../shared/components/error/error"; +import { AuthService } from "@core/services/auth-service"; +import { RegisterUserRequest } from "@core/models/user.model"; +import { Error } from "@shared/components/error/error"; import { Router, RouterLink } from "@angular/router"; @Component({ diff --git a/src/app/features/checkout/address/address.ts b/src/app/features/checkout/address/address.ts index bf85115..ed76393 100644 --- a/src/app/features/checkout/address/address.ts +++ b/src/app/features/checkout/address/address.ts @@ -1,8 +1,9 @@ -import { Component } from "@angular/core"; +import { Component, inject, OnInit } from "@angular/core"; import { AddressForm } from "../components/address-form/address-form"; import { GoBack } from "@app/shared/components/go-back/go-back"; import { AddressSelect } from "../components/address-select/address-select"; import { OrderSummery } from "../components/order-summery/order-summery"; +import { AddressService } from "@app/features/checkout/services/address-service"; @Component({ selector: "app-address", @@ -10,4 +11,8 @@ import { OrderSummery } from "../components/order-summery/order-summery"; templateUrl: "./address.html", styleUrl: "./address.css", }) -export class Address {} +export class Address implements OnInit { + addressService = inject(AddressService); + + ngOnInit() {} +} diff --git a/src/app/features/checkout/services/address-service.ts b/src/app/features/checkout/services/address-service.ts index 1ddc4c6..4aacac9 100644 --- a/src/app/features/checkout/services/address-service.ts +++ b/src/app/features/checkout/services/address-service.ts @@ -1,6 +1,15 @@ -import { Injectable } from "@angular/core"; +import { inject, Injectable } from "@angular/core"; +import { HttpClient } from "@angular/common/http"; +import { API_URL } from "@core/tokens/api-url-tokens"; @Injectable({ providedIn: "root", }) -export class AddressService {} +export class AddressService { + http = inject(HttpClient); + apiUrl = inject(API_URL); + + fetchAddresses(userId: number) { + return this.http.get(`${this.apiUrl}/user/${userId}/addresses`); + } +} diff --git a/src/app/shared/components/cart/cart.html b/src/app/shared/components/cart/cart.html index 8ee49df..3aa4062 100644 --- a/src/app/shared/components/cart/cart.html +++ b/src/app/shared/components/cart/cart.html @@ -8,6 +8,7 @@ [class.pointer-events-none]="isLoading()" [class.opacity-40]="isLoading()" [class.cursor-block]="isLoading()" + class="rounded-none!" > @for (item of cart.items; track item.id) { Total

Rs. {{ cart.totalPrice }}

+ +
  • + Proceed to checkout +
  • } -Proceed to checkout diff --git a/src/app/shared/components/cart/cart.ts b/src/app/shared/components/cart/cart.ts index 6334750..6319fd3 100644 --- a/src/app/shared/components/cart/cart.ts +++ b/src/app/shared/components/cart/cart.ts @@ -1,7 +1,7 @@ import { Component, computed, inject, Input, signal } from "@angular/core"; import { CartItemModel, CartItemRequest, CartModel } from "@app/core/models/cart.model"; import { CartItem } from "../cart-item/cart-item"; -import { AuthService, AuthState } from "@app/features/auth/services/auth-service"; +import { AuthService, AuthState } from "@core/services/auth-service"; import { CartService } from "@app/core/services/cart-service"; import { finalize, tap } from "rxjs"; import { RouterLink } from "@angular/router"; From 24bdfe9cc622e70f6c50564afb71701308622cc5 Mon Sep 17 00:00:00 2001 From: kusowl Date: Tue, 17 Mar 2026 10:58:22 +0530 Subject: [PATCH 3/5] feature: fetch, edit and add address - fetch existing addresses from api, - user can edit existing address - user can add new address --- .../features/checkout/address/address.html | 8 +-- src/app/features/checkout/address/address.ts | 48 +++++++++++++++-- .../components/address-form/address-form.html | 31 ++++++----- .../components/address-form/address-form.ts | 54 +++++++++++++++++-- .../address-select/address-select.html | 16 ++++-- .../address-select/address-select.ts | 27 ++++++++-- .../checkout/services/address-service.ts | 30 ++++++++++- src/app/shared/pipes/fullname-pipe.spec.ts | 8 +++ src/app/shared/pipes/fullname-pipe.ts | 13 +++++ 9 files changed, 203 insertions(+), 32 deletions(-) create mode 100644 src/app/shared/pipes/fullname-pipe.spec.ts create mode 100644 src/app/shared/pipes/fullname-pipe.ts diff --git a/src/app/features/checkout/address/address.html b/src/app/features/checkout/address/address.html index 6ae9bf5..dadbf33 100644 --- a/src/app/features/checkout/address/address.html +++ b/src/app/features/checkout/address/address.html @@ -3,10 +3,10 @@
    - - - - + @for (address of addresses(); track address.id) { + + } +
    diff --git a/src/app/features/checkout/address/address.ts b/src/app/features/checkout/address/address.ts index ed76393..56404fd 100644 --- a/src/app/features/checkout/address/address.ts +++ b/src/app/features/checkout/address/address.ts @@ -1,9 +1,16 @@ -import { Component, inject, OnInit } from "@angular/core"; +import { Component, inject, OnInit, signal } from "@angular/core"; import { AddressForm } from "../components/address-form/address-form"; import { GoBack } from "@app/shared/components/go-back/go-back"; import { AddressSelect } from "../components/address-select/address-select"; import { OrderSummery } from "../components/order-summery/order-summery"; -import { AddressService } from "@app/features/checkout/services/address-service"; +import { + AddressRequest, + AddressResponse, + AddressService, +} from "@app/features/checkout/services/address-service"; +import { AuthService } from "@core/services/auth-service"; +import { User } from "@core/models/user.model"; +import { switchMap } from "rxjs"; @Component({ selector: "app-address", @@ -13,6 +20,41 @@ import { AddressService } from "@app/features/checkout/services/address-service" }) export class Address implements OnInit { addressService = inject(AddressService); + authService = inject(AuthService); + private user: User | undefined; + protected addresses = signal([]); - ngOnInit() {} + ngOnInit(): void { + this.authService + .getCurrentUser() + .pipe( + switchMap((user) => { + this.user = user; + if (user?.id) { + return this.addressService.fetchAddresses(user.id); + } + return []; + }), + ) + .subscribe({ + next: (addresses) => { + this.addresses.set(addresses.data); + }, + }); + } + + createNewAddress(addressData: AddressRequest) { + this.addressService.createAddress(this.user!.id, addressData).subscribe({ + next: (address) => this.addresses.update((addresses) => [...addresses, address]), + }); + } + updateAddress(addressData: AddressResponse) { + console.log(addressData); + this.addressService.updateAddress(addressData.id, addressData).subscribe({ + next: (address) => + this.addresses.update((addresses) => + addresses.map((a) => (a.id === address.id ? address : a)), + ), + }); + } } diff --git a/src/app/features/checkout/components/address-form/address-form.html b/src/app/features/checkout/components/address-form/address-form.html index d5a240f..6ac7da9 100644 --- a/src/app/features/checkout/components/address-form/address-form.html +++ b/src/app/features/checkout/components/address-form/address-form.html @@ -1,8 +1,14 @@ -
    - - +
    + + -
    +
    First Name @@ -17,13 +23,8 @@
    Street Address - - + +
    @@ -43,8 +44,12 @@
    - - + +
    diff --git a/src/app/features/checkout/components/address-form/address-form.ts b/src/app/features/checkout/components/address-form/address-form.ts index 12e9728..0250074 100644 --- a/src/app/features/checkout/components/address-form/address-form.ts +++ b/src/app/features/checkout/components/address-form/address-form.ts @@ -1,6 +1,7 @@ -import { Component } from "@angular/core"; +import { Component, EventEmitter, Input, Output, signal } from "@angular/core"; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; import { Error } from "@app/shared/components/error/error"; +import { AddressRequest, AddressResponse } from "@app/features/checkout/services/address-service"; @Component({ selector: "app-address-form", @@ -9,12 +10,55 @@ import { Error } from "@app/shared/components/error/error"; styleUrl: "./address-form.css", }) export class AddressForm { + @Input() set initialData(address: AddressResponse) { + if (address) { + this.addressForm.patchValue(address); + this.address.set(address); + this.isEditing.set(true); + } + } + @Output() submitAddress: EventEmitter = new EventEmitter(); + @Output() updateAddress: EventEmitter = new EventEmitter(); + @Output() editingCanceled: EventEmitter = new EventEmitter(); + + protected isEditing = signal(false); + protected address = signal(null); + addressForm = new FormGroup({ - firstName: new FormControl("", { validators: Validators.required }), - lastName: new FormControl("", { validators: Validators.required }), - streetAddress: new FormControl("", { validators: Validators.required }), + firstName: new FormControl("", { + validators: [Validators.required, Validators.pattern("^[a-zA-Z]\\S+$")], + }), + lastName: new FormControl("", { + validators: [Validators.required, Validators.pattern("^[a-zA-Z]\\S+$")], + }), + street: new FormControl("", { validators: Validators.required }), city: new FormControl("", { validators: Validators.required }), state: new FormControl("", { validators: Validators.required }), - pinCode: new FormControl("", { validators: Validators.required }), + pinCode: new FormControl("", { + validators: [Validators.required, Validators.pattern("^[0-9]{6}$")], + }), }); + + submitForm() { + if (this.addressForm.invalid) { + this.addressForm.markAllAsTouched(); + return; + } + + const emittedData = this.addressForm.getRawValue() as AddressRequest; + this.addressForm.reset(); + + if (this.isEditing()) { + const mergedData = { ...this.address(), ...emittedData }; + console.log(mergedData); + this.updateAddress.emit(mergedData as unknown as AddressResponse); + } else { + this.submitAddress.emit(emittedData); + } + } + + cancelEditing() { + this.addressForm.reset(); + this.editingCanceled.emit(); + } } diff --git a/src/app/features/checkout/components/address-select/address-select.html b/src/app/features/checkout/components/address-select/address-select.html index 1fb2a0e..da89163 100644 --- a/src/app/features/checkout/components/address-select/address-select.html +++ b/src/app/features/checkout/components/address-select/address-select.html @@ -1,10 +1,20 @@ +@if (!isEditing()) {
    -

    Kushal Saha

    -

    48 St, Park Avenue, New Towm, 700021

    +

    {{[address.firstName, address.lastName] | fullname}}

    +

    + {{`${address.street}, ${address.city}, ${address.pinCode}`}} +

    - +
    +} @else{ + +} diff --git a/src/app/features/checkout/components/address-select/address-select.ts b/src/app/features/checkout/components/address-select/address-select.ts index 18a7490..64cc7ea 100644 --- a/src/app/features/checkout/components/address-select/address-select.ts +++ b/src/app/features/checkout/components/address-select/address-select.ts @@ -1,9 +1,30 @@ -import { Component } from "@angular/core"; +import { Component, EventEmitter, Input, Output, signal } from "@angular/core"; +import { AddressResponse } from "@app/features/checkout/services/address-service"; +import { FullnamePipe } from "@shared/pipes/fullname-pipe"; +import { AddressForm } from "@app/features/checkout/components/address-form/address-form"; @Component({ selector: "app-address-select", - imports: [], + imports: [FullnamePipe, AddressForm], templateUrl: "./address-select.html", styleUrl: "./address-select.css", }) -export class AddressSelect {} +export class AddressSelect { + @Input() address!: AddressResponse; + @Output() addressUpdated: EventEmitter = new EventEmitter(); + + protected isEditing = signal(false); + + editForm() { + this.isEditing.set(true); + } + + cancelEditing() { + this.isEditing.set(false); + } + + updateAddress(address: AddressResponse) { + this.isEditing.set(false); + this.addressUpdated.emit(address); + } +} diff --git a/src/app/features/checkout/services/address-service.ts b/src/app/features/checkout/services/address-service.ts index 4aacac9..db6001e 100644 --- a/src/app/features/checkout/services/address-service.ts +++ b/src/app/features/checkout/services/address-service.ts @@ -1,6 +1,20 @@ import { inject, Injectable } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { API_URL } from "@core/tokens/api-url-tokens"; +import { PaginatedResponse } from "@core/models/paginated.model"; + +export interface AddressRequest { + firstName: string; + lastName: string; + street: string; + city: string; + state: string; + pinCode: string; +} + +export interface AddressResponse extends AddressRequest { + id: number; +} @Injectable({ providedIn: "root", @@ -10,6 +24,20 @@ export class AddressService { apiUrl = inject(API_URL); fetchAddresses(userId: number) { - return this.http.get(`${this.apiUrl}/user/${userId}/addresses`); + return this.http.get>( + `${this.apiUrl}/user/${userId}/addresses`, + ); + } + + createAddress(userId: number, data: AddressRequest) { + return this.http.post(`${this.apiUrl}/user/${userId}/addresses`, data); + } + updateAddress(addressId: number, data: AddressRequest) { + return this.http.patch(`${this.apiUrl}/addresses/${addressId}`, data); + } + deleteAddress(userId: number, addressId: number) { + return this.http.delete( + `${this.apiUrl}/user/${userId}/addresses/${addressId}`, + ); } } diff --git a/src/app/shared/pipes/fullname-pipe.spec.ts b/src/app/shared/pipes/fullname-pipe.spec.ts new file mode 100644 index 0000000..8032c7e --- /dev/null +++ b/src/app/shared/pipes/fullname-pipe.spec.ts @@ -0,0 +1,8 @@ +import { FullnamePipe } from "./fullname-pipe"; + +describe("FullnamePipe", () => { + it("create an instance", () => { + const pipe = new FullnamePipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/app/shared/pipes/fullname-pipe.ts b/src/app/shared/pipes/fullname-pipe.ts new file mode 100644 index 0000000..ff153bd --- /dev/null +++ b/src/app/shared/pipes/fullname-pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { TitleCasePipe } from "@angular/common"; + +@Pipe({ + name: "fullname", +}) +export class FullnamePipe implements PipeTransform { + titlecase = new TitleCasePipe(); + + transform(values: string[]): unknown { + return this.titlecase.transform(values.join(" ")); + } +} From 419e8281e2595d5dbb247d04cd5b7d1c55a16a57 Mon Sep 17 00:00:00 2001 From: kusowl Date: Tue, 17 Mar 2026 16:10:05 +0530 Subject: [PATCH 4/5] BREAKING CHANGE: change obervable name from cartItem$ to cartItems$ --- src/app/core/layouts/header/header.html | 2 +- src/app/core/layouts/header/header.ts | 4 ++-- src/app/core/services/cart-service.ts | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app/core/layouts/header/header.html b/src/app/core/layouts/header/header.html index 7a74d77..ea798f2 100644 --- a/src/app/core/layouts/header/header.html +++ b/src/app/core/layouts/header/header.html @@ -53,7 +53,7 @@ cart.itemsCount ?? 0)); + cartItems$ = this.cartService.cartItems$; + cartItemCount = this.cartItems$.pipe(map((cart: CartModel) => cart.itemsCount ?? 0)); } diff --git a/src/app/core/services/cart-service.ts b/src/app/core/services/cart-service.ts index 9c0e1c4..b7987af 100644 --- a/src/app/core/services/cart-service.ts +++ b/src/app/core/services/cart-service.ts @@ -15,23 +15,23 @@ export class CartService { private http = inject(HttpClient); private apiUrl = inject(API_URL); - private _cartItem = new BehaviorSubject({} as CartModel); + private _cartItems = new BehaviorSubject({} as CartModel); - cartItem$ = this._cartItem.asObservable(); + cartItems$ = this._cartItems.asObservable(); constructor() { effect(() => { if (this.authService.isAuthenticated()) { this.fetchCart(); } else { - this._cartItem.next({} as CartModel); + this._cartItems.next({} as CartModel); } }); } - fetchCart() { + private fetchCart() { return this.http.get(this.apiUrl + "/cart").subscribe({ - next: (data) => this._cartItem.next(data), + next: (data) => this._cartItems.next(data), error: (error: HttpErrorResponse) => { if (error.status === 401) { this.authService.purgeAuth(); @@ -44,18 +44,18 @@ export class CartService { addToCart(data: CartItemRequest) { return this.http .post(this.apiUrl + "/cart", data) - .pipe(tap((updatedCart: CartModel) => this._cartItem.next(updatedCart))); + .pipe(tap((updatedCart: CartModel) => this._cartItems.next(updatedCart))); } updateCart(data: CartItemRequest) { return this.http .patch(this.apiUrl + "/cart", data) - .pipe(tap((updatedCart: CartModel) => this._cartItem.next(updatedCart))); + .pipe(tap((updatedCart: CartModel) => this._cartItems.next(updatedCart))); } removeFromCart(productId: number) { return this.http .delete(this.apiUrl + "/cart", { body: { productId: productId } }) - .pipe(tap((updatedCart: CartModel) => this._cartItem.next(updatedCart))); + .pipe(tap((updatedCart: CartModel) => this._cartItems.next(updatedCart))); } } From bb3aafd89ed3a993c8ff2a6f7ae240a9f2923497 Mon Sep 17 00:00:00 2001 From: kusowl Date: Tue, 17 Mar 2026 16:10:43 +0530 Subject: [PATCH 5/5] feature: show order summary on address page --- .ai/mcp/mcp.json | 12 ++++++ .../features/checkout/address/address.html | 37 ++++++++++++++++++- src/app/features/checkout/address/address.ts | 30 +++++++++++---- .../components/address-form/address-form.html | 6 ++- .../address-select/address-select.html | 1 - .../order-summery/order-summery.html | 25 ++++++++++++- .../components/order-summery/order-summery.ts | 16 ++++++-- 7 files changed, 111 insertions(+), 16 deletions(-) diff --git a/.ai/mcp/mcp.json b/.ai/mcp/mcp.json index e69de29..bb3f251 100644 --- a/.ai/mcp/mcp.json +++ b/.ai/mcp/mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "angular-cli": { + "command": "npx", + "args": [ + "-y", + "@angular/cli", + "mcp" + ] + } + } +} \ No newline at end of file diff --git a/src/app/features/checkout/address/address.html b/src/app/features/checkout/address/address.html index dadbf33..ca829e0 100644 --- a/src/app/features/checkout/address/address.html +++ b/src/app/features/checkout/address/address.html @@ -4,12 +4,45 @@
    @for (address of addresses(); track address.id) { - +
    + + +
    } - +
    +
    +
    + Have any coupon ? +
    + + +
    +
    + @if (addressIdControl.invalid && addressIdControl.touched) { +
    + Please select an address +
    + } + +
    diff --git a/src/app/features/checkout/address/address.ts b/src/app/features/checkout/address/address.ts index 56404fd..e74443c 100644 --- a/src/app/features/checkout/address/address.ts +++ b/src/app/features/checkout/address/address.ts @@ -1,4 +1,4 @@ -import { Component, inject, OnInit, signal } from "@angular/core"; +import { Component, DestroyRef, inject, OnInit, signal } from "@angular/core"; import { AddressForm } from "../components/address-form/address-form"; import { GoBack } from "@app/shared/components/go-back/go-back"; import { AddressSelect } from "../components/address-select/address-select"; @@ -10,19 +10,27 @@ import { } from "@app/features/checkout/services/address-service"; import { AuthService } from "@core/services/auth-service"; import { User } from "@core/models/user.model"; -import { switchMap } from "rxjs"; +import { of, switchMap } from "rxjs"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl, ReactiveFormsModule, Validators } from "@angular/forms"; @Component({ selector: "app-address", - imports: [AddressSelect, AddressForm, GoBack, OrderSummery], + imports: [AddressSelect, AddressForm, GoBack, OrderSummery, ReactiveFormsModule], templateUrl: "./address.html", styleUrl: "./address.css", }) export class Address implements OnInit { addressService = inject(AddressService); authService = inject(AuthService); - private user: User | undefined; + + // I am subscribing to the observable instead of using toSignal(), + // i have to destroy the subscription manually. + destroyRef = inject(DestroyRef); + protected addresses = signal([]); + protected addressIdControl = new FormControl(null, Validators.required); + private user: User | undefined; ngOnInit(): void { this.authService @@ -33,8 +41,9 @@ export class Address implements OnInit { if (user?.id) { return this.addressService.fetchAddresses(user.id); } - return []; + return of({ data: [] }); }), + takeUntilDestroyed(this.destroyRef), ) .subscribe({ next: (addresses) => { @@ -43,12 +52,12 @@ export class Address implements OnInit { }); } - createNewAddress(addressData: AddressRequest) { + protected createNewAddress(addressData: AddressRequest) { this.addressService.createAddress(this.user!.id, addressData).subscribe({ next: (address) => this.addresses.update((addresses) => [...addresses, address]), }); } - updateAddress(addressData: AddressResponse) { + protected updateAddress(addressData: AddressResponse) { console.log(addressData); this.addressService.updateAddress(addressData.id, addressData).subscribe({ next: (address) => @@ -57,4 +66,11 @@ export class Address implements OnInit { ), }); } + + protected proceedToPayment() { + if (this.addressIdControl.invalid) { + this.addressIdControl.markAsTouched(); + return; + } + } } diff --git a/src/app/features/checkout/components/address-form/address-form.html b/src/app/features/checkout/components/address-form/address-form.html index 6ac7da9..ac5ddf5 100644 --- a/src/app/features/checkout/components/address-form/address-form.html +++ b/src/app/features/checkout/components/address-form/address-form.html @@ -1,4 +1,8 @@ -
    +
    -

    {{[address.firstName, address.lastName] | fullname}}

    {{`${address.street}, ${address.city}, ${address.pinCode}`}} diff --git a/src/app/features/checkout/components/order-summery/order-summery.html b/src/app/features/checkout/components/order-summery/order-summery.html index 4d21641..49b9257 100644 --- a/src/app/features/checkout/components/order-summery/order-summery.html +++ b/src/app/features/checkout/components/order-summery/order-summery.html @@ -1,3 +1,24 @@ -

    -

    Order Summery

    +
    +

    Order Summery

    + @if (cartItems | async; as cart) { @for (item of cart.items; track item.id) { +
    +
    +
    + product image +
    +
    +

    {{item.title}}

    +

    Rs. {{item.price}} x {{item.quantity}}

    +
    +
    +

    Rs. {{item.subtotal}}

    +
    + } +
    +

    Total

    +

    Rs. {{cart.totalPrice}}

    +
    + }
    diff --git a/src/app/features/checkout/components/order-summery/order-summery.ts b/src/app/features/checkout/components/order-summery/order-summery.ts index 3a03579..2bcdf1b 100644 --- a/src/app/features/checkout/components/order-summery/order-summery.ts +++ b/src/app/features/checkout/components/order-summery/order-summery.ts @@ -1,9 +1,19 @@ -import { Component } from "@angular/core"; +import { Component, inject, OnInit } from "@angular/core"; +import { CartService } from "@core/services/cart-service"; +import { CartModel } from "@core/models/cart.model"; +import { Observable } from "rxjs"; +import { AsyncPipe, NgOptimizedImage } from "@angular/common"; @Component({ selector: "app-order-summery", - imports: [], + imports: [AsyncPipe, NgOptimizedImage], templateUrl: "./order-summery.html", styleUrl: "./order-summery.css", }) -export class OrderSummery {} +export class OrderSummery implements OnInit { + cartService = inject(CartService); + cartItems: Observable | undefined; + ngOnInit(): void { + this.cartItems = this.cartService.cartItems$; + } +}