From 6d8df154ec4e7fa8cbf062176a402481c7f4b20b Mon Sep 17 00:00:00 2001
From: "thibault.capt" <thibault.capt@etu.hesge.ch>
Date: Wed, 29 Jan 2025 17:00:44 +0100
Subject: [PATCH] (feat): add ngrx store

---
 garage-workspace/garage/debug.log             |  3 ++
 garage-workspace/garage/package.json          |  1 +
 garage-workspace/garage/pnpm-lock.yaml        | 19 ++++++++
 .../projects/garage-ui/src/app/app.config.ts  | 18 ++++----
 .../projects/garage-ui/src/app/app.routes.ts  |  2 +-
 .../cars/adapters/in-memory-car.gateway.ts    | 16 -------
 .../cars/adapters/in-memory-car.service.ts    | 20 ++++++++
 .../garage-ui/src/app/core/cars/car.store.ts  | 41 +++++++++++++++++
 .../ports/{car.gateway.ts => car.service.ts}  |  6 ++-
 .../src/app/features/cars/cars.component.html | 46 +++++++++++--------
 .../src/app/features/cars/cars.component.ts   | 10 ++--
 .../shared/services/request-status.feature.ts | 34 ++++++++++++++
 12 files changed, 163 insertions(+), 53 deletions(-)
 create mode 100644 garage-workspace/garage/debug.log
 delete mode 100644 garage-workspace/garage/projects/garage-ui/src/app/core/cars/adapters/in-memory-car.gateway.ts
 create mode 100644 garage-workspace/garage/projects/garage-ui/src/app/core/cars/adapters/in-memory-car.service.ts
 create mode 100644 garage-workspace/garage/projects/garage-ui/src/app/core/cars/car.store.ts
 rename garage-workspace/garage/projects/garage-ui/src/app/core/cars/ports/{car.gateway.ts => car.service.ts} (52%)
 create mode 100644 garage-workspace/garage/projects/garage-ui/src/app/shared/services/request-status.feature.ts

diff --git a/garage-workspace/garage/debug.log b/garage-workspace/garage/debug.log
new file mode 100644
index 000000000..82b825ed9
--- /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 52755a34e..39362edb4 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 a7f4ecc7f..89a1db08b 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 cad493aae..cd52a28bf 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 0ffb48756..ad5c38af6 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 6caf67f92..000000000
--- 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 000000000..a2cd598e1
--- /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 000000000..dbb4091d4
--- /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 ef890e4a0..291b8d243 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 3287aeb05..83cb6e06c 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 ddc06943a..55fed316b 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 000000000..aaf504bf5
--- /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 } };
+}
-- 
GitLab