refactor: synchronize routes with checkout steps, add loading spinner in action buttons

This commit is contained in:
kusowl 2026-03-19 16:37:48 +05:30
parent bb3aafd89e
commit 4546d309b8
23 changed files with 368 additions and 144 deletions

View File

@ -2,11 +2,7 @@
"mcpServers": {
"angular-cli": {
"command": "npx",
"args": [
"-y",
"@angular/cli",
"mcp"
]
"args": ["-y", "@angular/cli", "mcp"]
}
}
}

View File

@ -1,49 +1,18 @@
<section class="my-10">
<div class="">
<app-go-back route="/" text="Home" />
<div class="grid grid-cols-3 gap-x-10">
<div class="col-span-2 flex flex-col space-y-4">
@for (address of addresses(); track address.id) {
<div class="flex space-x-2">
<input
type="radio"
name="address"
value="{{address.id}}"
[formControl]="addressIdControl"
/>
<input [formControl]="addressIdControl" name="address" type="radio" value="{{address.id}}" />
<app-address-select
class="flex-1"
[address]="address"
(addressUpdated)="updateAddress($event)"
[address]="address"
[isProcessing]="isProcessing()"
class="flex-1"
/>
</div>
}
<app-address-form class="ml-5" (submitAddress)="createNewAddress($event)" />
<app-address-form
(submitAddress)="createNewAddress($event)"
[isProcessing]="isProcessing()"
class="ml-5"
/>
</div>
<div>
<app-order-summery />
<div class="card mt-4">
<fieldset class="fieldset">
<legend class="fieldset-legend">Have any coupon ?</legend>
<div class="flex items-center space-x-2">
<input placeholder="Enter coupon here" type="text" class="input" />
<button class="btn btn-ghost px-4">Apply</button>
</div>
</fieldset>
@if (addressIdControl.invalid && addressIdControl.touched) {
<div class="text-red-500 text-sm p-4 mt-4 rounded-xl bg-red-50">
Please select an address
</div>
}
<button
class="btn btn-primary w-full mt-4"
(click)="proceedToPayment()"
[disabled]="addressIdControl.invalid && addressIdControl.touched"
>
Proceed to payment
</button>
</div>
</div>
</div>
</div>
</section>

View File

@ -1,8 +1,6 @@
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,
@ -10,26 +8,26 @@ import {
} 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 { finalize, of, switchMap } from "rxjs";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
import { ReactiveFormsModule } from "@angular/forms";
@Component({
selector: "app-address",
imports: [AddressSelect, AddressForm, GoBack, OrderSummery, ReactiveFormsModule],
imports: [AddressSelect, AddressForm, ReactiveFormsModule],
templateUrl: "./address.html",
styleUrl: "./address.css",
})
export class Address implements OnInit {
addressService = inject(AddressService);
authService = inject(AuthService);
addressIdControl = this.addressService.addressIdControl;
isProcessing = signal(false);
// I am subscribing to the observable instead of using toSignal(),
// i have to destroy the subscription manually.
destroyRef = inject(DestroyRef);
protected addresses = signal<AddressResponse[]>([]);
protected addressIdControl = new FormControl<number | null>(null, Validators.required);
private user: User | undefined;
ngOnInit(): void {
@ -53,24 +51,30 @@ export class Address implements OnInit {
}
protected createNewAddress(addressData: AddressRequest) {
this.addressService.createAddress(this.user!.id, addressData).subscribe({
this.isProcessing.set(true);
this.addressService
.createAddress(this.user!.id, addressData)
.pipe(
takeUntilDestroyed(this.destroyRef),
finalize(() => this.isProcessing.set(false)),
)
.subscribe({
next: (address) => this.addresses.update((addresses) => [...addresses, address]),
});
}
protected updateAddress(addressData: AddressResponse) {
console.log(addressData);
this.addressService.updateAddress(addressData.id, addressData).subscribe({
this.isProcessing.set(true);
this.addressService
.updateAddress(addressData.id, addressData)
.pipe(
takeUntilDestroyed(this.destroyRef),
finalize(() => this.isProcessing.set(false)),
)
.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;
}
}
}

View File

@ -1,6 +1,46 @@
<section class="wrapper my-10">
<div class="wrapper grid place-content-center w-full">
<app-stepper [steps]="steps" [currentStep]="1" />
<app-stepper [currentStep]="currentStepNumber()" [steps]="steps" />
</div>
<section class="mt-10">
@if (currentStepNumber() > 0) {
<app-go-back
[route]="steps[currentStepNumber() - 1].route"
[text]="steps[currentStepNumber() - 1 ].label"
class="ml-4 block"
/>
}
<div class="grid md:grid-cols-6 gap-10">
<div class="md:col-span-4">
<router-outlet />
</div>
<div class="md:col-span-2">
<app-order-summery class="hidden md:block" />
<div class="card mt-4">
<fieldset class="fieldset">
<legend class="fieldset-legend">Have any coupon ?</legend>
<div class="flex items-center space-x-2">
<input class="input" placeholder="Enter coupon here" type="text" />
<button class="btn btn-ghost px-4">Apply</button>
</div>
</fieldset>
@if (addressIdControl.invalid && addressIdControl.touched) {
<div class="text-red-500 text-sm p-4 mt-4 rounded-xl bg-red-50">
Please select an address
</div>
}
<button
(click)="proceedToPayment()"
[disabled]="addressIdControl.invalid && addressIdControl.touched || orderCreationLoading()"
class="btn btn-primary w-full mt-4"
>
<app-loading-spinner [isLoading]="orderCreationLoading()">
Proceed to payment
</app-loading-spinner>
</button>
</div>
</div>
</div>
</section>
</section>

View File

@ -1,6 +1,7 @@
import { Routes } from "@angular/router";
import { Address } from "./address/address";
import { Checkout } from "./checkout";
import { Payment } from "@app/features/checkout/payment/payment";
export const checkoutRoutes: Routes = [
{
@ -8,9 +9,13 @@ export const checkoutRoutes: Routes = [
component: Checkout,
children: [
{
path: "address/:cartId",
path: "address",
component: Address,
},
{
path: "payment",
component: Payment,
},
],
},
];

View File

@ -1,18 +1,77 @@
import { Component } from "@angular/core";
import { RouterOutlet } from "@angular/router";
import { Component, DestroyRef, inject, OnInit, signal } from "@angular/core";
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
import { Stepper, Steps } from "@app/shared/components/stepper/stepper";
import { OrderSummery } from "@app/features/checkout/components/order-summery/order-summery";
import { GoBack } from "@shared/components/go-back/go-back";
import { AddressService } from "@app/features/checkout/services/address-service";
import { LoadingSpinner } from "@shared/components/loading-spinner/loading-spinner";
import { OrderService } from "@app/features/checkout/services/order-service";
import { CartService } from "@core/services/cart-service";
import { CartModel } from "@core/models/cart.model";
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
import { filter, finalize, map } from "rxjs";
@Component({
selector: "app-checkout",
imports: [RouterOutlet, Stepper],
providers: [AddressService],
imports: [RouterOutlet, Stepper, OrderSummery, GoBack, LoadingSpinner],
templateUrl: "./checkout.html",
styleUrl: "./checkout.css",
})
export class Checkout {
export class Checkout implements OnInit {
steps: Steps[] = [
{ label: "Cart" },
{ label: "Address" },
{ label: "Payment" },
{ label: "Confirm" },
{ label: "Cart", route: "" },
{ label: "Address", route: "/checkout/address" },
{ label: "Payment", route: "/checkout/payment" },
{ label: "Confirm", route: "/checkout/address" },
];
destroyRef = inject(DestroyRef);
orderCreationLoading = signal(false);
private addressService = inject(AddressService);
addressIdControl = this.addressService.addressIdControl;
private orderService = inject(OrderService);
private cartService = inject(CartService);
private cart: CartModel | undefined;
private router = inject(Router);
protected currentStepNumber = toSignal(
this.router.events.pipe(
filter((event): event is NavigationEnd => event instanceof NavigationEnd), // Added TS type guard
map((event: NavigationEnd) => {
// Strip query params and fragments to ensure exact matching
const cleanUrl = event.urlAfterRedirects.split("?")[0].split("#")[0];
// Exact match comparison
const activeIndex = this.steps.findIndex(
(step) => cleanUrl === step.route || (step.route !== "" && cleanUrl.endsWith(step.route)),
);
return activeIndex !== -1 ? activeIndex : 0;
}),
),
{ initialValue: 1 },
);
ngOnInit(): void {
this.cartService.cartItems$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((cart) => {
this.cart = cart;
});
}
protected proceedToPayment() {
if (this.addressIdControl.invalid) {
this.addressIdControl.markAsTouched();
return;
}
this.orderCreationLoading.set(true);
this.orderService
.createOrder({ cartId: this.cart!.id, addressId: this.addressIdControl.value! })
.pipe(
finalize(() => this.orderCreationLoading.set(false)),
takeUntilDestroyed(this.destroyRef),
)
.subscribe(async () => {
await this.router.navigate(["/checkout/payment"]);
});
}
}

View File

@ -1,58 +1,56 @@
<details
class="card p-0!"
title="Click to add a new address"
[open]="isEditing()"
>
<details [open]="isEditing()" class="card p-0!" title="Click to add a new address">
<summary class="p-6">
<label for="currentAddress" class="font-medium text-gray-600 ml-2"
<label class="font-medium text-gray-600 ml-2" for="currentAddress"
>{{isEditing() ? 'Update address' : 'Add new address'}}</label
>
</summary>
<form
[formGroup]="addressForm"
(ngSubmit)="submitForm()"
[formGroup]="addressForm"
class="w-full flex flex-col gap-y-2 pt-0 p-4"
>
<fieldset class="flex space-x-4 w-full">
<fieldset class="fieldset w-full">
<legend class="fieldset-legend">First Name</legend>
<input type="text" formControlName="firstName" class="input" placeholder="Example: Jhon" />
<app-error fieldName="First name" [control]="addressForm.get('firstName')" />
<input class="input" formControlName="firstName" placeholder="Example: Jhon" type="text" />
<app-error [control]="addressForm.get('firstName')" fieldName="First name" />
</fieldset>
<fieldset class="fieldset w-full">
<legend class="fieldset-legend">Last Name</legend>
<input type="text" class="input" formControlName="lastName" placeholder="Example: Doe" />
<app-error fieldName="Last name" [control]="addressForm.get('lastName')" />
<input class="input" formControlName="lastName" placeholder="Example: Doe" type="text" />
<app-error [control]="addressForm.get('lastName')" fieldName="Last name" />
</fieldset>
</fieldset>
<fieldset class="fieldset w-full">
<legend class="fieldset-legend">Street Address</legend>
<input type="text" class="input" formControlName="street" placeholder="Your street address" />
<app-error fieldName="Street address" [control]="addressForm.get('street')" />
<input class="input" formControlName="street" placeholder="Your street address" type="text" />
<app-error [control]="addressForm.get('street')" fieldName="Street address" />
</fieldset>
<fieldset class="flex space-x-4 w-full">
<fieldset class="fieldset w-full">
<legend class="fieldset-legend">City</legend>
<input type="text" class="input" formControlName="city" placeholder="Your city" />
<app-error fieldName="City" [control]="addressForm.get('city')" />
<input class="input" formControlName="city" placeholder="Your city" type="text" />
<app-error [control]="addressForm.get('city')" fieldName="City" />
</fieldset>
<fieldset class="fieldset w-full">
<legend class="fieldset-legend">State</legend>
<input type="text" class="input" formControlName="state" placeholder="State Name" />
<app-error fieldName="State" [control]="addressForm.get('state')" />
<input class="input" formControlName="state" placeholder="State Name" type="text" />
<app-error [control]="addressForm.get('state')" fieldName="State" />
</fieldset>
<fieldset class="fieldset w-full">
<legend class="fieldset-legend">Pin Code</legend>
<input type="text" class="input" formControlName="pinCode" placeholder="7XX XX1" />
<app-error fieldName="Pin Code" [control]="addressForm.get('pinCode')" />
<input class="input" formControlName="pinCode" placeholder="7XX XX1" type="text" />
<app-error [control]="addressForm.get('pinCode')" fieldName="Pin Code" />
</fieldset>
</fieldset>
<div class="ml-auto flex space-x-4">
<button type="button" (click)="cancelEditing()" class="btn btn-ghost px-3 text-sm">
<button (click)="cancelEditing()" class="btn btn-ghost px-3 text-sm" type="button">
Cancel
</button>
<button class="btn btn-primary px-3 text-sm">
<button class="btn btn-primary min-w-50 px-3 text-sm">
<app-loading-spinner [isLoading]="isProcessing()">
{{isEditing() ? 'Update this address' : 'Use this address'}}
</app-loading-spinner>
</button>
</div>
</form>

View File

@ -1,28 +1,21 @@
import { Component, EventEmitter, Input, Output, signal } from "@angular/core";
import { Component, EventEmitter, input, 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";
import { LoadingSpinner } from "@shared/components/loading-spinner/loading-spinner";
@Component({
selector: "app-address-form",
imports: [ReactiveFormsModule, Error],
imports: [ReactiveFormsModule, Error, LoadingSpinner],
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<AddressRequest> = new EventEmitter<AddressRequest>();
@Output() updateAddress: EventEmitter<AddressResponse> = new EventEmitter<AddressResponse>();
@Output() editingCanceled: EventEmitter<void> = new EventEmitter<void>();
protected isEditing = signal(false);
protected address = signal<AddressResponse | null>(null);
isProcessing = input<boolean>(false);
addressForm = new FormGroup({
firstName: new FormControl("", {
@ -38,6 +31,16 @@ export class AddressForm {
validators: [Validators.required, Validators.pattern("^[0-9]{6}$")],
}),
});
protected isEditing = signal(false);
protected address = signal<AddressResponse | null>(null);
@Input() set initialData(address: AddressResponse) {
if (address) {
this.addressForm.patchValue(address);
this.address.set(address);
this.isEditing.set(true);
}
}
submitForm() {
if (this.addressForm.invalid) {
@ -46,11 +49,9 @@ export class AddressForm {
}
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);

View File

@ -1,4 +1,11 @@
@if (!isEditing()) {
@if (isEditing()) {
<app-address-form
(editingCanceled)="cancelEditing()"
(updateAddress)="updateAddress($event)"
[initialData]="address"
[isProcessing]="isProcessing()"
/>
} @else{
<div class="flex justify-between card">
<div class="flex space-x-4 items-center">
<p class="text-gray-600 font-medium">{{[address.firstName, address.lastName] | fullname}}</p>
@ -10,10 +17,4 @@
<button (click)="editForm()" class="btn btn-ghost text-sm px-2">Edit</button>
</div>
</div>
} @else{
<app-address-form
[initialData]="address"
(editingCanceled)="cancelEditing()"
(updateAddress)="updateAddress($event)"
/>
}

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output, signal } from "@angular/core";
import { Component, effect, EventEmitter, Input, 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";
@ -11,10 +11,25 @@ import { AddressForm } from "@app/features/checkout/components/address-form/addr
})
export class AddressSelect {
@Input() address!: AddressResponse;
@Output() addressUpdated: EventEmitter<AddressResponse> = new EventEmitter<AddressResponse>();
@Output() addressUpdated = new EventEmitter<AddressResponse>();
isProcessing = input<boolean>(false);
protected isEditing = signal(false);
// Track if THIS specific component triggered the API call
private isLocallySubmitted = false;
constructor() {
effect(() => {
const processing = this.isProcessing();
if (!processing && this.isLocallySubmitted) {
this.isEditing.set(false);
this.isLocallySubmitted = false;
}
});
}
editForm() {
this.isEditing.set(true);
}
@ -24,7 +39,7 @@ export class AddressSelect {
}
updateAddress(address: AddressResponse) {
this.isEditing.set(false);
this.isLocallySubmitted = true;
this.addressUpdated.emit(address);
}
}

View File

@ -0,0 +1 @@
<p>payment works!</p>

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { Payment } from "./payment";
describe("Payment", () => {
let component: Payment;
let fixture: ComponentFixture<Payment>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Payment],
}).compileComponents();
fixture = TestBed.createComponent(Payment);
component = fixture.componentInstance;
await fixture.whenStable();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,9 @@
import { Component } from "@angular/core";
@Component({
selector: "app-payment",
imports: [],
templateUrl: "./payment.html",
styleUrl: "./payment.css",
})
export class Payment {}

View File

@ -2,6 +2,7 @@ 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";
import { FormControl, Validators } from "@angular/forms";
export interface AddressRequest {
firstName: string;
@ -16,13 +17,13 @@ export interface AddressResponse extends AddressRequest {
id: number;
}
@Injectable({
providedIn: "root",
})
@Injectable()
export class AddressService {
http = inject(HttpClient);
apiUrl = inject(API_URL);
addressIdControl = new FormControl<number | null>(null, Validators.required);
fetchAddresses(userId: number) {
return this.http.get<PaginatedResponse<AddressResponse>>(
`${this.apiUrl}/user/${userId}/addresses`,

View File

@ -0,0 +1,16 @@
import { TestBed } from "@angular/core/testing";
import { OrderService } from "./order-service";
describe("OrderService", () => {
let service: OrderService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(OrderService);
});
it("should be created", () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,23 @@
import { inject, Injectable } from "@angular/core";
import { API_URL } from "@core/tokens/api-url-tokens";
import { HttpClient } from "@angular/common/http";
import { AuthService } from "@core/services/auth-service";
export interface OrderRequest {
addressId: number;
cartId: number;
}
@Injectable({
providedIn: "root",
})
export class OrderService {
http = inject(HttpClient);
apiUrl = inject(API_URL);
private authService = inject(AuthService);
private user = this.authService.user;
createOrder(data: OrderRequest) {
return this.http.post(`${this.apiUrl}/users/${this.user()?.id}/orders`, data);
}
}

View File

@ -25,9 +25,7 @@
</div>
<li class="pt-4! mt-4 border-t border-gray-200 rounded-none!">
<a [routerLink]="`/checkout/address/${cart.id}`" class="btn btn-primary px-4"
>Proceed to checkout</a
>
<a [routerLink]="`/checkout/address`" class="btn btn-primary px-4">Proceed to checkout</a>
</li>
}
</ul>

View File

@ -1,4 +1,4 @@
<a [routerLink]="route" class="flex space-x-2 my-4 text-gray-600 hover:text-blue-500 text-sm">
<a [routerLink]="route" class="flex space-x-2 w-min my-4 text-gray-600 hover:text-blue-500 text-sm">
<lucide-angular [img]="MoveLeftIcon" class="w-4" />
<p class="font-medium">{{ text }}</p>
</a>

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { LoadingSpinner } from "./loading-spinner";
describe("LoadingSpinner", () => {
let component: LoadingSpinner;
let fixture: ComponentFixture<LoadingSpinner>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LoadingSpinner],
}).compileComponents();
fixture = TestBed.createComponent(LoadingSpinner);
component = fixture.componentInstance;
await fixture.whenStable();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,40 @@
import { Component, input } from "@angular/core";
@Component({
selector: "app-loading-spinner",
standalone: true,
imports: [],
template: `
@if (isLoading()) {
<div class="grid w-full place-items-center overflow-x-scroll lg:overflow-visible">
<svg
class="text-gray-300 animate-spin w-6"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M32 3C35.8083 3 39.5794 3.75011 43.0978 5.20749C46.6163 6.66488 49.8132 8.80101 52.5061 11.4939C55.199 14.1868 57.3351 17.3837 58.7925 20.9022C60.2499 24.4206 61 28.1917 61 32C61 35.8083 60.2499 39.5794 58.7925 43.0978C57.3351 46.6163 55.199 49.8132 52.5061 52.5061C49.8132 55.199 46.6163 57.3351 43.0978 58.7925C39.5794 60.2499 35.8083 61 32 61C28.1917 61 24.4206 60.2499 20.9022 58.7925C17.3837 57.3351 14.1868 55.199 11.4939 52.5061C8.801 49.8132 6.66487 46.6163 5.20749 43.0978C3.7501 39.5794 3 35.8083 3 32C3 28.1917 3.75011 24.4206 5.2075 20.9022C6.66489 17.3837 8.80101 14.1868 11.4939 11.4939C14.1868 8.80099 17.3838 6.66487 20.9022 5.20749C24.4206 3.7501 28.1917 3 32 3L32 3Z"
stroke="currentColor"
stroke-width="5"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M32 3C36.5778 3 41.0906 4.08374 45.1692 6.16256C49.2477 8.24138 52.7762 11.2562 55.466 14.9605C58.1558 18.6647 59.9304 22.9531 60.6448 27.4748C61.3591 31.9965 60.9928 36.6232 59.5759 40.9762"
stroke="currentColor"
stroke-width="5"
stroke-linecap="round"
stroke-linejoin="round"
class="text-gray-900"
></path>
</svg>
</div>
} @else {
<ng-content />
}
`,
})
export class LoadingSpinner {
isLoading = input<boolean>(false);
}

View File

@ -2,7 +2,9 @@
@for (step of steps; track step) {
<li class="flex flex-col items-start">
<div class="flex items-center">
<button
<a
[routerLink]="$index <= currentStep ? step.route : ''"
[class.cursor-not-allowed]="$index > currentStep"
[class.bg-blue-600]="$index <= currentStep"
class="btn py-0! px-1 rounded-full! w-min"
>
@ -12,7 +14,7 @@
[img]="$index <= currentStep ? CheckIcon : CirecleIcon"
class="w-4"
/>
</button>
</a>
<!-- Connected line -->
@if (!$last) {

View File

@ -1,13 +1,15 @@
import { Component, Input } from "@angular/core";
import { Check, Circle, LucideAngularModule } from "lucide-angular";
import { RouterLink } from "@angular/router";
export type Steps = {
label: string;
route: string;
};
@Component({
selector: "app-stepper",
imports: [LucideAngularModule],
imports: [LucideAngularModule, RouterLink],
templateUrl: "./stepper.html",
styleUrl: "./stepper.css",
})