diff --git a/garage-workspace/garage/debug.log b/garage-workspace/garage/debug.log new file mode 100644 index 0000000000000000000000000000000000000000..82b825ed9242301a43850fe1912cb86422aef540 --- /dev/null +++ b/garage-workspace/garage/debug.log @@ -0,0 +1,3 @@ +[0129/170014.398:ERROR:crashpad_client_win.cc(810)] not connected +[0129/170022.383:ERROR:crashpad_client_win.cc(810)] not connected +[0129/170022.698:ERROR:crashpad_client_win.cc(810)] not connected diff --git a/garage-workspace/garage/package.json b/garage-workspace/garage/package.json index 52755a34e01019d2a39d316f3b59f7233b5df8e2..39362edb4e0a77bf13f59afd2d99267093fccc2e 100644 --- a/garage-workspace/garage/package.json +++ b/garage-workspace/garage/package.json @@ -24,6 +24,7 @@ "@fortawesome/free-brands-svg-icons": "^6.7.1", "@fortawesome/free-regular-svg-icons": "^6.7.1", "@fortawesome/free-solid-svg-icons": "^6.7.1", + "@ngrx/signals": "^19.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" diff --git a/garage-workspace/garage/pnpm-lock.yaml b/garage-workspace/garage/pnpm-lock.yaml index a7f4ecc7fbaf427bd6e07898d901490071b0a8c8..89a1db08b8f601d40b89b7aac3a00a2d5db72ca9 100644 --- a/garage-workspace/garage/pnpm-lock.yaml +++ b/garage-workspace/garage/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: '@fortawesome/free-solid-svg-icons': specifier: ^6.7.1 version: 6.7.2 + '@ngrx/signals': + specifier: ^19.0.0 + version: 19.0.0(@angular/core@19.1.3(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1) rxjs: specifier: ~7.8.0 version: 7.8.1 @@ -1423,6 +1426,15 @@ packages: resolution: {integrity: sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==} engines: {node: '>= 10'} + '@ngrx/signals@19.0.0': + resolution: {integrity: sha512-Ktgq+wwIVH9HdobLOhrYF6VArIJYZa5lkgajUpSB9QuudpOLja9f7W2RAHQsMUBpQuREgFkTgIEr1vKIzDrGMA==} + peerDependencies: + '@angular/core': ^19.0.0 + rxjs: ^6.5.3 || ^7.4.0 + peerDependenciesMeta: + rxjs: + optional: true + '@ngtools/webpack@19.1.4': resolution: {integrity: sha512-ZmUlbVqu/pz8abxVxNCKgKeY5g2MX1NsKxhM8rRV5tVV/MaAtSYNHgmFSYcKWA178v7k6BUuhnoNNxl5qqc1kw==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} @@ -6469,6 +6481,13 @@ snapshots: '@napi-rs/nice-win32-x64-msvc': 1.0.1 optional: true + '@ngrx/signals@19.0.0(@angular/core@19.1.3(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1)': + dependencies: + '@angular/core': 19.1.3(rxjs@7.8.1)(zone.js@0.15.0) + tslib: 2.8.1 + optionalDependencies: + rxjs: 7.8.1 + '@ngtools/webpack@19.1.4(@angular/compiler-cli@19.1.3(@angular/compiler@19.1.3(@angular/core@19.1.3(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.3))(typescript@5.7.3)(webpack@5.97.1(esbuild@0.24.2))': dependencies: '@angular/compiler-cli': 19.1.3(@angular/compiler@19.1.3(@angular/core@19.1.3(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.7.3) diff --git a/garage-workspace/garage/projects/garage-ui/src/app/app.config.ts b/garage-workspace/garage/projects/garage-ui/src/app/app.config.ts index cad493aae75e2dea0acbf7251a75be0370449c10..cd52a28bf0f2ed2151e6ba94e2d405ed84d794eb 100644 --- a/garage-workspace/garage/projects/garage-ui/src/app/app.config.ts +++ b/garage-workspace/garage/projects/garage-ui/src/app/app.config.ts @@ -6,8 +6,8 @@ import { provideRouter } from '@angular/router'; import { provideHttpClient } from '@angular/common/http'; import { routes } from './app.routes'; -import { CarGateway } from '@core/cars/ports/car.gateway'; -import { InMemoryCarGateway } from '@core/cars/adapters/in-memory-car.gateway'; +import { CarService } from '@core/cars/ports/car.service'; +import { InMemoryCarService } from '@core/cars/adapters/in-memory-car.service'; export const appConfig: ApplicationConfig = { providers: [ @@ -15,15 +15,13 @@ export const appConfig: ApplicationConfig = { provideRouter(routes), provideHttpClient(), { - provide: CarGateway, + provide: CarService, useFactory: () => - new InMemoryCarGateway().withCars( - [ - { id: 1, brand: 'Audi', model: 'A3', year: 2019, color: 'red' }, - { id: 2, brand: 'Audi', model: 'RS3', year: 2024, color: 'black' }, - { id: 3, brand: 'Audi', model: 'Q2', year: 2016, color: 'yellow' }, - ] - ), + new InMemoryCarService().withCars([ + { id: 1, brand: 'Audi', model: 'A3', year: 2019, color: 'red' }, + { id: 2, brand: 'Audi', model: 'RS3', year: 2024, color: 'black' }, + { id: 3, brand: 'Audi', model: 'Q2', year: 2016, color: 'yellow' }, + ]), }, ], }; diff --git a/garage-workspace/garage/projects/garage-ui/src/app/app.routes.ts b/garage-workspace/garage/projects/garage-ui/src/app/app.routes.ts index 0ffb48756834d80904e345078fe427289ddef994..ad5c38af6effece598b4aec625bad8df86369478 100644 --- a/garage-workspace/garage/projects/garage-ui/src/app/app.routes.ts +++ b/garage-workspace/garage/projects/garage-ui/src/app/app.routes.ts @@ -1,5 +1,5 @@ import type { Routes } from '@angular/router'; -import CarsComponent from "@features/cars/cars.component"; +import CarsComponent from '@features/cars/cars.component'; export const routes: Routes = [ { path: '', redirectTo: 'car', pathMatch: 'full' }, diff --git a/garage-workspace/garage/projects/garage-ui/src/app/core/cars/adapters/in-memory-car.gateway.ts b/garage-workspace/garage/projects/garage-ui/src/app/core/cars/adapters/in-memory-car.gateway.ts deleted file mode 100644 index 6caf67f92a713066f81bb683d93f95f8845ca6a9..0000000000000000000000000000000000000000 --- a/garage-workspace/garage/projects/garage-ui/src/app/core/cars/adapters/in-memory-car.gateway.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { CarGateway } from '@core/cars/ports/car.gateway'; -import { Observable, of } from 'rxjs'; -import { Cars } from '@core/cars/models/car.model'; - -export class InMemoryCarGateway extends CarGateway { - private cars: Cars = []; - - withCars(cars: Cars): InMemoryCarGateway { - this.cars = cars; - return this; - } - - getAll(): Observable<Cars> { - return of(this.cars); - } -} diff --git a/garage-workspace/garage/projects/garage-ui/src/app/core/cars/adapters/in-memory-car.service.ts b/garage-workspace/garage/projects/garage-ui/src/app/core/cars/adapters/in-memory-car.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..a2cd598e1ff3a8f5fde12ac79bbc5a43f129dca2 --- /dev/null +++ b/garage-workspace/garage/projects/garage-ui/src/app/core/cars/adapters/in-memory-car.service.ts @@ -0,0 +1,20 @@ +import { CarService } from '@core/cars/ports/car.service'; +import { Observable, of } from 'rxjs'; +import { Cars } from '@core/cars/models/car.model'; +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class InMemoryCarService extends CarService { + private cars: Cars = []; + + withCars(cars: Cars): InMemoryCarService { + this.cars = cars; + return this; + } + + getAll(): Observable<Cars> { + return of(this.cars); + } +} diff --git a/garage-workspace/garage/projects/garage-ui/src/app/core/cars/car.store.ts b/garage-workspace/garage/projects/garage-ui/src/app/core/cars/car.store.ts new file mode 100644 index 0000000000000000000000000000000000000000..dbb4091d44fbcf48ee61673508ced2ea8d3cb986 --- /dev/null +++ b/garage-workspace/garage/projects/garage-ui/src/app/core/cars/car.store.ts @@ -0,0 +1,41 @@ +import { Cars } from '@core/cars/models/car.model'; +import { + patchState, + signalStore, + withComputed, + withHooks, + withMethods, + withState, +} from '@ngrx/signals'; +import { CarService } from '@core/cars/ports/car.service'; +import { computed, inject } from '@angular/core'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { exhaustMap, pipe, tap } from 'rxjs'; + +interface CarState { + cars: Cars; +} + +const InitialCarState: CarState = { + cars: [], +}; + +export const CarStore = signalStore( + withState<CarState>(InitialCarState), + withComputed((store) => ({ + isEmpty: computed(() => store.cars().length === 0), + })), + withMethods((store, carGateway = inject(CarService)) => ({ + getCars: rxMethod<void>( + pipe( + exhaustMap(() => carGateway.getAll()), + tap((cars) => patchState(store, { cars })), + ), + ), + })), + withHooks((store) => ({ + onInit() { + store.getCars(); + }, + })), +); diff --git a/garage-workspace/garage/projects/garage-ui/src/app/core/cars/ports/car.gateway.ts b/garage-workspace/garage/projects/garage-ui/src/app/core/cars/ports/car.service.ts similarity index 52% rename from garage-workspace/garage/projects/garage-ui/src/app/core/cars/ports/car.gateway.ts rename to garage-workspace/garage/projects/garage-ui/src/app/core/cars/ports/car.service.ts index ef890e4a0fff0668f0ff3119348635ab00ab3cd8..291b8d2430fb4c7cd641c30368b3b01e58474eb7 100644 --- a/garage-workspace/garage/projects/garage-ui/src/app/core/cars/ports/car.gateway.ts +++ b/garage-workspace/garage/projects/garage-ui/src/app/core/cars/ports/car.service.ts @@ -1,6 +1,10 @@ import { Observable } from 'rxjs'; import { Cars } from '@core/cars/models/car.model'; +import { Injectable } from '@angular/core'; -export abstract class CarGateway { +@Injectable({ + providedIn: 'root', +}) +export abstract class CarService { abstract getAll(): Observable<Cars>; } diff --git a/garage-workspace/garage/projects/garage-ui/src/app/features/cars/cars.component.html b/garage-workspace/garage/projects/garage-ui/src/app/features/cars/cars.component.html index 3287aeb05c34d890d03c49fd103d3df8ef358480..83cb6e06cac984351ee0847323561f8044a2d672 100644 --- a/garage-workspace/garage/projects/garage-ui/src/app/features/cars/cars.component.html +++ b/garage-workspace/garage/projects/garage-ui/src/app/features/cars/cars.component.html @@ -1,28 +1,36 @@ +@let empty = this.store.isEmpty(); + <div class="container mx-auto py-6"> <h1 class="text-4xl font-bold text-center mb-6">My cars</h1> <div class="overflow-x-auto"> <table class="table table-zebra w-full"> <thead> - <tr> - <th>#</th> - <th>Brand</th> - <th>Model</th> - <th>Year</th> - <th>Color</th> - <th>Actions</th> - </tr> + <tr> + <th>#</th> + <th>Brand</th> + <th>Model</th> + <th>Year</th> + <th>Color</th> + <th>Actions</th> + </tr> </thead> - @for (car of cars(); track car.id) { - <tr> - <td>{{ car.id }}</td> - <td>{{ car.brand }}</td> - <td>{{ car.model }}</td> - <td>{{ car.year }}</td> - <td>{{ car.color }}</td> - <td>Modify | Delete</td> - </tr> - } + @if (!empty) { + @for (car of this.store.cars(); track car.id) { + <tr> + <td>{{ car.id }}</td> + <td>{{ car.brand }}</td> + <td>{{ car.model }}</td> + <td>{{ car.year }}</td> + <td>{{ car.color }}</td> + <td>Modify | Delete</td> + </tr> + } + } @else { + <tr> + <td colspan="6" class="text-center">No cars found</td> + </tr> + } </table> </div> -</div> \ No newline at end of file +</div> diff --git a/garage-workspace/garage/projects/garage-ui/src/app/features/cars/cars.component.ts b/garage-workspace/garage/projects/garage-ui/src/app/features/cars/cars.component.ts index ddc06943a60ef23f4b2fca29f55bee7b6834024d..55fed316b4d29d4b1bc4b4fd9c14ab8a6580ff01 100644 --- a/garage-workspace/garage/projects/garage-ui/src/app/features/cars/cars.component.ts +++ b/garage-workspace/garage/projects/garage-ui/src/app/features/cars/cars.component.ts @@ -1,15 +1,13 @@ -import { Component, inject } from "@angular/core"; -import { CarGateway } from "@core/cars/ports/car.gateway"; -import { toSignal } from "@angular/core/rxjs-interop"; -import { AsyncPipe } from "@angular/common"; +import { Component, inject } from '@angular/core'; +import { CarStore } from '@core/cars/car.store'; @Component({ selector: 'app-cars', imports: [], templateUrl: './cars.component.html', styleUrl: './cars.component.scss', + providers: [CarStore], }) export default class CarsComponent { - private readonly carGateway = inject(CarGateway); - cars = toSignal(this.carGateway.getAll()); + readonly store = inject(CarStore); } diff --git a/garage-workspace/garage/projects/garage-ui/src/app/shared/services/request-status.feature.ts b/garage-workspace/garage/projects/garage-ui/src/app/shared/services/request-status.feature.ts new file mode 100644 index 0000000000000000000000000000000000000000..aaf504bf58894dc2234966351e54098665820136 --- /dev/null +++ b/garage-workspace/garage/projects/garage-ui/src/app/shared/services/request-status.feature.ts @@ -0,0 +1,34 @@ +import { signalStoreFeature, withComputed, withState } from '@ngrx/signals'; +import { computed } from '@angular/core'; + +export type RequestStatus = 'idle' | 'pending' | 'success' | { error: string }; + +export interface RequestStatusState { + requestStatus: RequestStatus; +} + +export function withRequestStatus() { + return signalStoreFeature( + withState<RequestStatusState>({ requestStatus: 'idle' }), + withComputed(({ requestStatus }) => ({ + isPending: computed(() => requestStatus() === 'pending'), + isSuccess: computed(() => requestStatus() === 'success'), + error: computed(() => { + const status = requestStatus(); + return typeof status === 'object' ? status.error : null; + }), + })), + ); +} + +export function setPending(): RequestStatusState { + return { requestStatus: 'pending' }; +} + +export function setSuccess(): RequestStatusState { + return { requestStatus: 'success' }; +} + +export function setError(error: string): RequestStatusState { + return { requestStatus: { error } }; +}