Merge branch 'feature/address-ui' into staging
This commit is contained in:
commit
a0e5cda432
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"angular-cli": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"@angular/cli",
|
||||||
|
"mcp"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
.phpactor.json
Normal file
3
.phpactor.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"indexer.exclude_patterns": ["/node_modules/**/*", "/backend/**/*"]
|
||||||
|
}
|
||||||
@ -19,4 +19,9 @@ export const routes: Routes = [
|
|||||||
canActivate: [authGuard, roleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: { roles: ["admin", "broker"] },
|
data: { roles: ["admin", "broker"] },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "checkout",
|
||||||
|
loadChildren: () =>
|
||||||
|
import("./features/checkout/checkout.routes").then((routes) => routes.checkoutRoutes),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { CanActivateFn, Router } from "@angular/router";
|
import { CanActivateFn, Router } from "@angular/router";
|
||||||
import { inject } from "@angular/core";
|
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) => {
|
export const authGuard: CanActivateFn = (route, state) => {
|
||||||
const authService = inject(AuthService);
|
const authService = inject(AuthService);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { CanActivateFn } from "@angular/router";
|
import { CanActivateFn } from "@angular/router";
|
||||||
import { inject } from "@angular/core";
|
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) => {
|
export const roleGuard: CanActivateFn = (route, state) => {
|
||||||
const authService = inject(AuthService);
|
const authService = inject(AuthService);
|
||||||
|
|||||||
@ -20,14 +20,14 @@
|
|||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<div class="flex text-gray-600">
|
<div class="flex text-gray-600">
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost py-1 px-2 rounded-r-none!"
|
class="btn btn-ghost py-1 px-3 rounded-r-none!"
|
||||||
popovertarget="popover-1"
|
popovertarget="popover-1"
|
||||||
style="anchor-name: --anchor-1"
|
style="anchor-name: --anchor-1"
|
||||||
>
|
>
|
||||||
<lucide-angular [img]="UserIcon" class="w-5" />
|
<lucide-angular [img]="UserIcon" class="w-5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost py-1 px-2 rounded-l-none! border-l-0! relative"
|
class="btn btn-ghost py-1 px-3 rounded-l-none! border-l-0! relative"
|
||||||
popovertarget="popover-2"
|
popovertarget="popover-2"
|
||||||
style="anchor-name: --anchor-2"
|
style="anchor-name: --anchor-2"
|
||||||
>
|
>
|
||||||
@ -40,11 +40,11 @@
|
|||||||
|
|
||||||
<ul class="dropdown" id="popover-1" popover style="position-anchor: --anchor-1">
|
<ul class="dropdown" id="popover-1" popover style="position-anchor: --anchor-1">
|
||||||
@if (authService.authState() === AuthState.Unauthenticated) {
|
@if (authService.authState() === AuthState.Unauthenticated) {
|
||||||
<li><a class="block h-full w-full" routerLink="/login">Login</a></li>
|
<li><a class="block h-full w-full" routerLink="/login">Login</a></li>
|
||||||
} @else if (authService.authState() === AuthState.Loading) {
|
} @else if (authService.authState() === AuthState.Loading) {
|
||||||
<li><a class="block h-full w-full">Loading</a></li>
|
<li><a class="block h-full w-full">Loading</a></li>
|
||||||
} @else {
|
} @else {
|
||||||
<li><a class="block h-full w-full" routerLink="/logout">Logout</a></li>
|
<li><a class="block h-full w-full" routerLink="/logout">Logout</a></li>
|
||||||
}
|
}
|
||||||
<li><a class="block h-full w-full" href="">My Account</a></li>
|
<li><a class="block h-full w-full" href="">My Account</a></li>
|
||||||
<li><a class="block h-full w-full" href="">Orders</a></li>
|
<li><a class="block h-full w-full" href="">Orders</a></li>
|
||||||
@ -53,7 +53,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<app-cart
|
<app-cart
|
||||||
[cart]="(cartItem$ | async)!"
|
[cart]="(cartItems$ | async)!"
|
||||||
id="popover-2"
|
id="popover-2"
|
||||||
class="dropdown"
|
class="dropdown"
|
||||||
popover
|
popover
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Component, inject } from "@angular/core";
|
import { Component, inject } from "@angular/core";
|
||||||
import { LucideAngularModule, Search, ShoppingCart, User } from "lucide-angular";
|
import { LucideAngularModule, Search, ShoppingCart, User } from "lucide-angular";
|
||||||
import { RouterLink } from "@angular/router";
|
import { AuthService, AuthState } from "@core/services/auth-service";
|
||||||
import { AuthService, AuthState } from "../../../features/auth/services/auth-service";
|
|
||||||
import { CartService } from "@app/core/services/cart-service";
|
import { CartService } from "@app/core/services/cart-service";
|
||||||
import { Cart } from "@app/shared/components/cart/cart";
|
import { Cart } from "@app/shared/components/cart/cart";
|
||||||
import { CartModel } from "@app/core/models/cart.model";
|
import { CartModel } from "@app/core/models/cart.model";
|
||||||
@ -10,7 +9,7 @@ import { AsyncPipe } from "@angular/common";
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-header",
|
selector: "app-header",
|
||||||
imports: [LucideAngularModule, RouterLink, Cart, AsyncPipe],
|
imports: [LucideAngularModule, Cart, AsyncPipe],
|
||||||
templateUrl: "./header.html",
|
templateUrl: "./header.html",
|
||||||
styleUrl: "./header.css",
|
styleUrl: "./header.css",
|
||||||
})
|
})
|
||||||
@ -22,6 +21,6 @@ export class Header {
|
|||||||
readonly cartService = inject(CartService);
|
readonly cartService = inject(CartService);
|
||||||
protected readonly AuthState = AuthState;
|
protected readonly AuthState = AuthState;
|
||||||
|
|
||||||
cartItem$ = this.cartService.cartItem$;
|
cartItems$ = this.cartService.cartItems$;
|
||||||
cartItemCount = this.cartItem$.pipe(map((cart: CartModel) => cart.itemsCount ?? 0));
|
cartItemCount = this.cartItems$.pipe(map((cart: CartModel) => cart.itemsCount ?? 0));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { computed, inject, Injectable, Signal, signal, WritableSignal } from "@angular/core";
|
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 { 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 { switchMap, tap } from "rxjs";
|
||||||
import { LocalStorageService } from "../../../core/services/local-storage.service";
|
import { LocalStorageService } from "../services/local-storage.service";
|
||||||
|
|
||||||
export enum AuthState {
|
export enum AuthState {
|
||||||
Loading = "loading",
|
Loading = "loading",
|
||||||
@ -2,7 +2,7 @@ import { HttpClient, HttpErrorResponse } from "@angular/common/http";
|
|||||||
import { effect, inject, Injectable, signal } from "@angular/core";
|
import { effect, inject, Injectable, signal } from "@angular/core";
|
||||||
import { API_URL } from "../tokens/api-url-tokens";
|
import { API_URL } from "../tokens/api-url-tokens";
|
||||||
import { CartItemModel, CartItemRequest, CartModel } from "../models/cart.model";
|
import { CartItemModel, CartItemRequest, CartModel } from "../models/cart.model";
|
||||||
import { AuthService, AuthState } from "@app/features/auth/services/auth-service";
|
import { AuthService, AuthState } from "@core/services/auth-service";
|
||||||
import { Cart } from "@app/shared/components/cart/cart";
|
import { Cart } from "@app/shared/components/cart/cart";
|
||||||
import { BehaviorSubject, tap } from "rxjs";
|
import { BehaviorSubject, tap } from "rxjs";
|
||||||
|
|
||||||
@ -15,23 +15,23 @@ export class CartService {
|
|||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
private apiUrl = inject(API_URL);
|
private apiUrl = inject(API_URL);
|
||||||
|
|
||||||
private _cartItem = new BehaviorSubject<CartModel>({} as CartModel);
|
private _cartItems = new BehaviorSubject<CartModel>({} as CartModel);
|
||||||
|
|
||||||
cartItem$ = this._cartItem.asObservable();
|
cartItems$ = this._cartItems.asObservable();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
if (this.authService.isAuthenticated()) {
|
if (this.authService.isAuthenticated()) {
|
||||||
this.fetchCart();
|
this.fetchCart();
|
||||||
} else {
|
} else {
|
||||||
this._cartItem.next({} as CartModel);
|
this._cartItems.next({} as CartModel);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchCart() {
|
private fetchCart() {
|
||||||
return this.http.get<CartModel>(this.apiUrl + "/cart").subscribe({
|
return this.http.get<CartModel>(this.apiUrl + "/cart").subscribe({
|
||||||
next: (data) => this._cartItem.next(data),
|
next: (data) => this._cartItems.next(data),
|
||||||
error: (error: HttpErrorResponse) => {
|
error: (error: HttpErrorResponse) => {
|
||||||
if (error.status === 401) {
|
if (error.status === 401) {
|
||||||
this.authService.purgeAuth();
|
this.authService.purgeAuth();
|
||||||
@ -44,18 +44,18 @@ export class CartService {
|
|||||||
addToCart(data: CartItemRequest) {
|
addToCart(data: CartItemRequest) {
|
||||||
return this.http
|
return this.http
|
||||||
.post<CartModel>(this.apiUrl + "/cart", data)
|
.post<CartModel>(this.apiUrl + "/cart", data)
|
||||||
.pipe(tap((updatedCart: CartModel) => this._cartItem.next(updatedCart)));
|
.pipe(tap((updatedCart: CartModel) => this._cartItems.next(updatedCart)));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCart(data: CartItemRequest) {
|
updateCart(data: CartItemRequest) {
|
||||||
return this.http
|
return this.http
|
||||||
.patch<CartModel>(this.apiUrl + "/cart", data)
|
.patch<CartModel>(this.apiUrl + "/cart", data)
|
||||||
.pipe(tap((updatedCart: CartModel) => this._cartItem.next(updatedCart)));
|
.pipe(tap((updatedCart: CartModel) => this._cartItems.next(updatedCart)));
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFromCart(productId: number) {
|
removeFromCart(productId: number) {
|
||||||
return this.http
|
return this.http
|
||||||
.delete<CartModel>(this.apiUrl + "/cart", { body: { productId: productId } })
|
.delete<CartModel>(this.apiUrl + "/cart", { body: { productId: productId } })
|
||||||
.pipe(tap((updatedCart: CartModel) => this._cartItem.next(updatedCart)));
|
.pipe(tap((updatedCart: CartModel) => this._cartItems.next(updatedCart)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Router, Routes } from "@angular/router";
|
|||||||
import { Login } from "./components/login/login";
|
import { Login } from "./components/login/login";
|
||||||
import { Register } from "./components/register/register";
|
import { Register } from "./components/register/register";
|
||||||
import { inject } from "@angular/core";
|
import { inject } from "@angular/core";
|
||||||
import { AuthService } from "./services/auth-service";
|
import { AuthService } from "@core/services/auth-service";
|
||||||
|
|
||||||
export const AuthRoutes: Routes = [
|
export const AuthRoutes: Routes = [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { Component, inject } from "@angular/core";
|
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 { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||||
import { AuthService } from "../../services/auth-service";
|
import { AuthService } from "@core/services/auth-service";
|
||||||
import { Error } from "../../../../shared/components/error/error";
|
import { Error } from "@app/shared/components/error/error";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-login",
|
selector: "app-login",
|
||||||
imports: [RouterLink, ReactiveFormsModule, Error],
|
imports: [ReactiveFormsModule, Error],
|
||||||
templateUrl: "./login.html",
|
templateUrl: "./login.html",
|
||||||
styleUrl: "./login.css",
|
styleUrl: "./login.css",
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { Component, inject, signal } from "@angular/core";
|
import { Component, inject, signal } from "@angular/core";
|
||||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||||
import { AuthService } from "../../services/auth-service";
|
import { AuthService } from "@core/services/auth-service";
|
||||||
import { RegisterUserRequest } from "../../../../core/models/user.model";
|
import { RegisterUserRequest } from "@core/models/user.model";
|
||||||
import { Error } from "../../../../shared/components/error/error";
|
import { Error } from "@shared/components/error/error";
|
||||||
import { Router, RouterLink } from "@angular/router";
|
import { Router, RouterLink } from "@angular/router";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
0
src/app/features/checkout/address/address.css
Normal file
0
src/app/features/checkout/address/address.css
Normal file
49
src/app/features/checkout/address/address.html
Normal file
49
src/app/features/checkout/address/address.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<app-address-select
|
||||||
|
class="flex-1"
|
||||||
|
[address]="address"
|
||||||
|
(addressUpdated)="updateAddress($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<app-address-form class="ml-5" (submitAddress)="createNewAddress($event)" />
|
||||||
|
</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>
|
||||||
22
src/app/features/checkout/address/address.spec.ts
Normal file
22
src/app/features/checkout/address/address.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
|
||||||
|
import { Address } from "./address";
|
||||||
|
|
||||||
|
describe("Address", () => {
|
||||||
|
let component: Address;
|
||||||
|
let fixture: ComponentFixture<Address>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Address],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Address);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
76
src/app/features/checkout/address/address.ts
Normal file
76
src/app/features/checkout/address/address.ts
Normal file
@ -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<AddressResponse[]>([]);
|
||||||
|
protected addressIdControl = new FormControl<number | null>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/app/features/checkout/checkout.css
Normal file
0
src/app/features/checkout/checkout.css
Normal file
6
src/app/features/checkout/checkout.html
Normal file
6
src/app/features/checkout/checkout.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<section class="wrapper my-10">
|
||||||
|
<div class="wrapper grid place-content-center w-full">
|
||||||
|
<app-stepper [steps]="steps" [currentStep]="1" />
|
||||||
|
</div>
|
||||||
|
<router-outlet />
|
||||||
|
</section>
|
||||||
16
src/app/features/checkout/checkout.routes.ts
Normal file
16
src/app/features/checkout/checkout.routes.ts
Normal file
@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
22
src/app/features/checkout/checkout.spec.ts
Normal file
22
src/app/features/checkout/checkout.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
|
||||||
|
import { Checkout } from "./checkout";
|
||||||
|
|
||||||
|
describe("Checkout", () => {
|
||||||
|
let component: Checkout;
|
||||||
|
let fixture: ComponentFixture<Checkout>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Checkout],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Checkout);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
18
src/app/features/checkout/checkout.ts
Normal file
18
src/app/features/checkout/checkout.ts
Normal file
@ -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" },
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
<details
|
||||||
|
class="card p-0!"
|
||||||
|
title="Click to add a new address"
|
||||||
|
[open]="isEditing()"
|
||||||
|
>
|
||||||
|
<summary class="p-6">
|
||||||
|
<label for="currentAddress" class="font-medium text-gray-600 ml-2"
|
||||||
|
>{{isEditing() ? 'Update address' : 'Add new address'}}</label
|
||||||
|
>
|
||||||
|
</summary>
|
||||||
|
<form
|
||||||
|
[formGroup]="addressForm"
|
||||||
|
(ngSubmit)="submitForm()"
|
||||||
|
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')" />
|
||||||
|
</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')" />
|
||||||
|
</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')" />
|
||||||
|
</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')" />
|
||||||
|
</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')" />
|
||||||
|
</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')" />
|
||||||
|
</fieldset>
|
||||||
|
</fieldset>
|
||||||
|
<div class="ml-auto flex space-x-4">
|
||||||
|
<button type="button" (click)="cancelEditing()" class="btn btn-ghost px-3 text-sm">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary px-3 text-sm">
|
||||||
|
{{isEditing() ? 'Update this address' : 'Use this address'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
|
||||||
|
import { AddressForm } from "./address-form";
|
||||||
|
|
||||||
|
describe("AddressForm", () => {
|
||||||
|
let component: AddressForm;
|
||||||
|
let fixture: ComponentFixture<AddressForm>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AddressForm],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AddressForm);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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<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);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
@if (!isEditing()) {
|
||||||
|
<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>
|
||||||
|
<p class="text-gray-400 text-sm">
|
||||||
|
{{`${address.street}, ${address.city}, ${address.pinCode}`}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<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)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
|
||||||
|
import { AddressSelect } from "./address-select";
|
||||||
|
|
||||||
|
describe("AddressSelect", () => {
|
||||||
|
let component: AddressSelect;
|
||||||
|
let fixture: ComponentFixture<AddressSelect>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AddressSelect],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AddressSelect);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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<AddressResponse> = new EventEmitter<AddressResponse>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
<div class="card">
|
||||||
|
<p class="text-gray-800 font-medium text-xl">Order Summery</p>
|
||||||
|
@if (cartItems | async; as cart) { @for (item of cart.items; track item.id) {
|
||||||
|
<article
|
||||||
|
class="mt-4 pb-4 border-b border-b-gray-400 border-dashed flex justify-between items-center"
|
||||||
|
>
|
||||||
|
<div class="flex space-x-4">
|
||||||
|
<div class="w-15 h-15 rounded-lg aspect-square relative overflow-hidden">
|
||||||
|
<img ngSrc="{{item.image}}" fill class="object-cover" alt="product image" />
|
||||||
|
</div>
|
||||||
|
<article>
|
||||||
|
<h2>{{item.title}}</h2>
|
||||||
|
<p class="text-gray-600 text-sm">Rs. {{item.price}} x {{item.quantity}}</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-800 font-medium">Rs. {{item.subtotal}}</p>
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
<article class="mt-4 flex justify-between items-center text-lg">
|
||||||
|
<p class="text-gray-800 font-medium">Total</p>
|
||||||
|
<p class="text-gray-800 font-medium">Rs. {{cart.totalPrice}}</p>
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
|
||||||
|
import { OrderSummery } from "./order-summery";
|
||||||
|
|
||||||
|
describe("OrderSummery", () => {
|
||||||
|
let component: OrderSummery;
|
||||||
|
let fixture: ComponentFixture<OrderSummery>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [OrderSummery],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(OrderSummery);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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<CartModel> | undefined;
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.cartItems = this.cartService.cartItems$;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app/features/checkout/services/address-service.spec.ts
Normal file
16
src/app/features/checkout/services/address-service.spec.ts
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
43
src/app/features/checkout/services/address-service.ts
Normal file
43
src/app/features/checkout/services/address-service.ts
Normal file
@ -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<PaginatedResponse<AddressResponse>>(
|
||||||
|
`${this.apiUrl}/user/${userId}/addresses`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
createAddress(userId: number, data: AddressRequest) {
|
||||||
|
return this.http.post<AddressResponse>(`${this.apiUrl}/user/${userId}/addresses`, data);
|
||||||
|
}
|
||||||
|
updateAddress(addressId: number, data: AddressRequest) {
|
||||||
|
return this.http.patch<AddressResponse>(`${this.apiUrl}/addresses/${addressId}`, data);
|
||||||
|
}
|
||||||
|
deleteAddress(userId: number, addressId: number) {
|
||||||
|
return this.http.delete<AddressResponse>(
|
||||||
|
`${this.apiUrl}/user/${userId}/addresses/${addressId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@
|
|||||||
[class.pointer-events-none]="isLoading()"
|
[class.pointer-events-none]="isLoading()"
|
||||||
[class.opacity-40]="isLoading()"
|
[class.opacity-40]="isLoading()"
|
||||||
[class.cursor-block]="isLoading()"
|
[class.cursor-block]="isLoading()"
|
||||||
|
class="rounded-none!"
|
||||||
>
|
>
|
||||||
@for (item of cart.items; track item.id) {
|
@for (item of cart.items; track item.id) {
|
||||||
<app-cart-item
|
<app-cart-item
|
||||||
@ -22,5 +23,11 @@
|
|||||||
<p>Total</p>
|
<p>Total</p>
|
||||||
<p>Rs. {{ cart.totalPrice }}</p>
|
<p>Rs. {{ cart.totalPrice }}</p>
|
||||||
</div>
|
</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
|
||||||
|
>
|
||||||
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import { Component, computed, inject, Input, signal } from "@angular/core";
|
import { Component, computed, inject, Input, signal } from "@angular/core";
|
||||||
import { CartItemModel, CartItemRequest, CartModel } from "@app/core/models/cart.model";
|
import { CartItemModel, CartItemRequest, CartModel } from "@app/core/models/cart.model";
|
||||||
import { CartItem } from "../cart-item/cart-item";
|
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 { CartService } from "@app/core/services/cart-service";
|
||||||
import { finalize, tap } from "rxjs";
|
import { finalize, tap } from "rxjs";
|
||||||
|
import { RouterLink } from "@angular/router";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-cart",
|
selector: "app-cart",
|
||||||
imports: [CartItem],
|
imports: [CartItem, RouterLink],
|
||||||
templateUrl: "./cart.html",
|
templateUrl: "./cart.html",
|
||||||
styleUrl: "./cart.css",
|
styleUrl: "./cart.css",
|
||||||
})
|
})
|
||||||
|
|||||||
0
src/app/shared/components/go-back/go-back.css
Normal file
0
src/app/shared/components/go-back/go-back.css
Normal file
4
src/app/shared/components/go-back/go-back.html
Normal file
4
src/app/shared/components/go-back/go-back.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<a [routerLink]="route" class="flex space-x-2 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>
|
||||||
22
src/app/shared/components/go-back/go-back.spec.ts
Normal file
22
src/app/shared/components/go-back/go-back.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
|
||||||
|
import { GoBack } from "./go-back";
|
||||||
|
|
||||||
|
describe("GoBack", () => {
|
||||||
|
let component: GoBack;
|
||||||
|
let fixture: ComponentFixture<GoBack>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [GoBack],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(GoBack);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
15
src/app/shared/components/go-back/go-back.ts
Normal file
15
src/app/shared/components/go-back/go-back.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
0
src/app/shared/components/stepper/stepper.css
Normal file
0
src/app/shared/components/stepper/stepper.css
Normal file
29
src/app/shared/components/stepper/stepper.html
Normal file
29
src/app/shared/components/stepper/stepper.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<ol class="flex">
|
||||||
|
@for (step of steps; track step) {
|
||||||
|
<li class="flex flex-col items-start">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<button
|
||||||
|
[class.bg-blue-600]="$index <= currentStep"
|
||||||
|
class="btn py-0! px-1 rounded-full! w-min"
|
||||||
|
>
|
||||||
|
<lucide-angular
|
||||||
|
[class.text-white]="$index <= currentStep"
|
||||||
|
[class.text-gray-400]="$index > currentStep"
|
||||||
|
[img]="$index <= currentStep ? CheckIcon : CirecleIcon"
|
||||||
|
class="w-4"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Connected line -->
|
||||||
|
@if (!$last) {
|
||||||
|
<hr
|
||||||
|
[class.border-blue-600]="$index < currentStep"
|
||||||
|
[class.border-gray-200]="$index >= currentStep"
|
||||||
|
class="border w-20"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-600">{{ step.label }}</p>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ol>
|
||||||
22
src/app/shared/components/stepper/stepper.spec.ts
Normal file
22
src/app/shared/components/stepper/stepper.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
|
||||||
|
import { Stepper } from "./stepper";
|
||||||
|
|
||||||
|
describe("Stepper", () => {
|
||||||
|
let component: Stepper;
|
||||||
|
let fixture: ComponentFixture<Stepper>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Stepper],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Stepper);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
20
src/app/shared/components/stepper/stepper.ts
Normal file
20
src/app/shared/components/stepper/stepper.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
8
src/app/shared/pipes/fullname-pipe.spec.ts
Normal file
8
src/app/shared/pipes/fullname-pipe.spec.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { FullnamePipe } from "./fullname-pipe";
|
||||||
|
|
||||||
|
describe("FullnamePipe", () => {
|
||||||
|
it("create an instance", () => {
|
||||||
|
const pipe = new FullnamePipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
13
src/app/shared/pipes/fullname-pipe.ts
Normal file
13
src/app/shared/pipes/fullname-pipe.ts
Normal file
@ -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(" "));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,7 +13,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-gray-100 antialiased m-0;
|
@apply bg-gray-50 antialiased m-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
@ -33,11 +33,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.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 {
|
.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 {
|
.fieldset {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user