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/.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/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.html b/src/app/core/layouts/header/header.html index 70f585c..ea798f2 100644 --- a/src/app/core/layouts/header/header.html +++ b/src/app/core/layouts/header/header.html @@ -20,14 +20,14 @@
+
+ + @if (addressIdControl.invalid && addressIdControl.touched) { +
+ Please select an address +
+ } + +
+ + + + diff --git a/src/app/features/checkout/address/address.spec.ts b/src/app/features/checkout/address/address.spec.ts new file mode 100644 index 0000000..8fbd5ee --- /dev/null +++ b/src/app/features/checkout/address/address.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { Address } from "./address"; + +describe("Address", () => { + let component: Address; + let fixture: ComponentFixture
; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Address], + }).compileComponents(); + + fixture = TestBed.createComponent(Address); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/checkout/address/address.ts b/src/app/features/checkout/address/address.ts new file mode 100644 index 0000000..e74443c --- /dev/null +++ b/src/app/features/checkout/address/address.ts @@ -0,0 +1,76 @@ +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"; +import { OrderSummery } from "../components/order-summery/order-summery"; +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 { 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, ReactiveFormsModule], + templateUrl: "./address.html", + styleUrl: "./address.css", +}) +export class Address implements OnInit { + addressService = inject(AddressService); + authService = inject(AuthService); + + // 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 + .getCurrentUser() + .pipe( + switchMap((user) => { + this.user = user; + if (user?.id) { + return this.addressService.fetchAddresses(user.id); + } + return of({ data: [] }); + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe({ + next: (addresses) => { + this.addresses.set(addresses.data); + }, + }); + } + + protected createNewAddress(addressData: AddressRequest) { + this.addressService.createAddress(this.user!.id, addressData).subscribe({ + next: (address) => this.addresses.update((addresses) => [...addresses, address]), + }); + } + protected 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)), + ), + }); + } + + protected proceedToPayment() { + if (this.addressIdControl.invalid) { + this.addressIdControl.markAsTouched(); + return; + } + } +} diff --git a/src/app/features/checkout/checkout.css b/src/app/features/checkout/checkout.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/features/checkout/checkout.html b/src/app/features/checkout/checkout.html new file mode 100644 index 0000000..8d4ae2c --- /dev/null +++ b/src/app/features/checkout/checkout.html @@ -0,0 +1,6 @@ +
+
+ +
+ +
diff --git a/src/app/features/checkout/checkout.routes.ts b/src/app/features/checkout/checkout.routes.ts new file mode 100644 index 0000000..0817b56 --- /dev/null +++ b/src/app/features/checkout/checkout.routes.ts @@ -0,0 +1,16 @@ +import { Routes } from "@angular/router"; +import { Address } from "./address/address"; +import { Checkout } from "./checkout"; + +export const checkoutRoutes: Routes = [ + { + path: "", + component: Checkout, + children: [ + { + path: "address/:cartId", + component: Address, + }, + ], + }, +]; diff --git a/src/app/features/checkout/checkout.spec.ts b/src/app/features/checkout/checkout.spec.ts new file mode 100644 index 0000000..6a6a18b --- /dev/null +++ b/src/app/features/checkout/checkout.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { Checkout } from "./checkout"; + +describe("Checkout", () => { + let component: Checkout; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Checkout], + }).compileComponents(); + + fixture = TestBed.createComponent(Checkout); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/checkout/checkout.ts b/src/app/features/checkout/checkout.ts new file mode 100644 index 0000000..50ee05a --- /dev/null +++ b/src/app/features/checkout/checkout.ts @@ -0,0 +1,18 @@ +import { Component } from "@angular/core"; +import { RouterOutlet } from "@angular/router"; +import { Stepper, Steps } from "@app/shared/components/stepper/stepper"; + +@Component({ + selector: "app-checkout", + imports: [RouterOutlet, Stepper], + templateUrl: "./checkout.html", + styleUrl: "./checkout.css", +}) +export class Checkout { + steps: Steps[] = [ + { label: "Cart" }, + { label: "Address" }, + { label: "Payment" }, + { label: "Confirm" }, + ]; +} diff --git a/src/app/features/checkout/components/address-form/address-form.css b/src/app/features/checkout/components/address-form/address-form.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/features/checkout/components/address-form/address-form.html b/src/app/features/checkout/components/address-form/address-form.html new file mode 100644 index 0000000..ac5ddf5 --- /dev/null +++ b/src/app/features/checkout/components/address-form/address-form.html @@ -0,0 +1,59 @@ +
+ + + +
+
+
+ First Name + + +
+
+ Last Name + + +
+
+
+ Street Address + + +
+
+
+ City + + +
+
+ State + + +
+
+ Pin Code + + +
+
+
+ + +
+
+
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..0250074 --- /dev/null +++ b/src/app/features/checkout/components/address-form/address-form.ts @@ -0,0 +1,64 @@ +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", + imports: [ReactiveFormsModule, Error], + templateUrl: "./address-form.html", + 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, 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, 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.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..fc22c74 --- /dev/null +++ b/src/app/features/checkout/components/address-select/address-select.html @@ -0,0 +1,19 @@ +@if (!isEditing()) { +
+
+

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

+

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

+
+
+ +
+
+} @else{ + +} 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..64cc7ea --- /dev/null +++ b/src/app/features/checkout/components/address-select/address-select.ts @@ -0,0 +1,30 @@ +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: [FullnamePipe, AddressForm], + templateUrl: "./address-select.html", + styleUrl: "./address-select.css", +}) +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/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..49b9257 --- /dev/null +++ b/src/app/features/checkout/components/order-summery/order-summery.html @@ -0,0 +1,24 @@ +
+

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.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..2bcdf1b --- /dev/null +++ b/src/app/features/checkout/components/order-summery/order-summery.ts @@ -0,0 +1,19 @@ +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: [AsyncPipe, NgOptimizedImage], + templateUrl: "./order-summery.html", + styleUrl: "./order-summery.css", +}) +export class OrderSummery implements OnInit { + cartService = inject(CartService); + cartItems: Observable | undefined; + ngOnInit(): void { + this.cartItems = this.cartService.cartItems$; + } +} 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..db6001e --- /dev/null +++ b/src/app/features/checkout/services/address-service.ts @@ -0,0 +1,43 @@ +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", +}) +export class AddressService { + http = inject(HttpClient); + apiUrl = inject(API_URL); + + fetchAddresses(userId: number) { + 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/components/cart/cart.html b/src/app/shared/components/cart/cart.html index df526c5..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 +
  • } diff --git a/src/app/shared/components/cart/cart.ts b/src/app/shared/components/cart/cart.ts index 121b0ef..6319fd3 100644 --- a/src/app/shared/components/cart/cart.ts +++ b/src/app/shared/components/cart/cart.ts @@ -1,13 +1,14 @@ 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"; @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/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(" ")); + } +} 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 {