From 56f8477b9d3971faee40357a5c6bd0efbd8b08c7 Mon Sep 17 00:00:00 2001
From: jdnichollsc <jdnichollsc@hotmail.com>
Date: Mon, 19 Feb 2024 01:24:55 -0500
Subject: [PATCH] add page to display balance of the wallet

---
 .gitignore                                    |   3 +
 apps/webapp/.env.example                      |   1 +
 apps/webapp/project.json                      |  19 +-
 apps/webapp/src/app/app.config.ts             |   9 +-
 apps/webapp/src/app/app.routes.ts             |   7 +-
 .../layout/layout-container.component.html    |   4 +-
 .../layout/layout-container.component.ts      |   6 +-
 .../account.page.css}                         |   0
 .../src/app/pages/account/account.page.html   |  45 +
 .../app/pages/account/account.page.spec.ts    |  21 +
 .../src/app/pages/account/account.page.ts     |  19 +
 apps/webapp/src/app/pages/home/home.page.css  |   0
 .../src/app/pages/{ => home}/home.page.html   |   0
 .../app/pages/{ => home}/home.page.spec.ts    |   0
 .../src/app/pages/{ => home}/home.page.ts     |   2 +-
 apps/webapp/src/app/store/index.ts            |   1 +
 apps/webapp/src/app/store/wallet/index.ts     |   3 +
 apps/webapp/src/app/store/wallet/model.ts     |  16 +
 apps/webapp/src/app/store/wallet/service.ts   |  33 +
 apps/webapp/src/app/store/wallet/store.ts     |  32 +
 apps/webapp/src/env.d.ts                      |  51 +
 .../src/environments/environment.prod.ts      |   7 +
 apps/webapp/src/environments/environment.ts   |   7 +
 apps/webapp/src/index.html                    |  12 +-
 libs/ui/src/lib/header/header.component.html  |  14 +-
 libs/ui/src/lib/header/header.component.ts    |   9 +-
 nx.json                                       |   6 +-
 package-lock.json                             | 898 +++++++++++++++++-
 package.json                                  |   2 +
 29 files changed, 1187 insertions(+), 40 deletions(-)
 create mode 100644 apps/webapp/.env.example
 rename apps/webapp/src/app/pages/{home.page.css => account/account.page.css} (100%)
 create mode 100644 apps/webapp/src/app/pages/account/account.page.html
 create mode 100644 apps/webapp/src/app/pages/account/account.page.spec.ts
 create mode 100644 apps/webapp/src/app/pages/account/account.page.ts
 create mode 100644 apps/webapp/src/app/pages/home/home.page.css
 rename apps/webapp/src/app/pages/{ => home}/home.page.html (100%)
 rename apps/webapp/src/app/pages/{ => home}/home.page.spec.ts (100%)
 rename apps/webapp/src/app/pages/{ => home}/home.page.ts (75%)
 create mode 100644 apps/webapp/src/app/store/index.ts
 create mode 100644 apps/webapp/src/app/store/wallet/index.ts
 create mode 100644 apps/webapp/src/app/store/wallet/model.ts
 create mode 100644 apps/webapp/src/app/store/wallet/service.ts
 create mode 100644 apps/webapp/src/app/store/wallet/store.ts
 create mode 100644 apps/webapp/src/env.d.ts
 create mode 100644 apps/webapp/src/environments/environment.prod.ts
 create mode 100644 apps/webapp/src/environments/environment.ts

diff --git a/.gitignore b/.gitignore
index f9418f1..760070a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,6 @@ Thumbs.db
 
 .nx/cache
 .angular
+
+# Environment Variables
+.env
\ No newline at end of file
diff --git a/apps/webapp/.env.example b/apps/webapp/.env.example
new file mode 100644
index 0000000..afe0c07
--- /dev/null
+++ b/apps/webapp/.env.example
@@ -0,0 +1 @@
+NG_APP_SHYFT_API_KEY=fill-this-in
\ No newline at end of file
diff --git a/apps/webapp/project.json b/apps/webapp/project.json
index 8e05677..af6a560 100644
--- a/apps/webapp/project.json
+++ b/apps/webapp/project.json
@@ -7,7 +7,7 @@
   "tags": [],
   "targets": {
     "build": {
-      "executor": "@angular-devkit/build-angular:application",
+      "executor": "@ngx-env/builder:application",
       "outputs": [
         "{options.outputPath}"
       ],
@@ -16,7 +16,6 @@
         "index": "apps/webapp/src/index.html",
         "browser": "apps/webapp/src/main.ts",
         "polyfills": [
-          "zone.js",
           "apps/webapp/src/polyfills.ts"
         ],
         "tsConfig": "apps/webapp/tsconfig.app.json",
@@ -30,13 +29,17 @@
         ],
         "scripts": [],
         "server": "apps/webapp/src/main.server.ts",
-        "prerender": true,
-        "ssr": {
-          "entry": "apps/webapp/server.ts"
-        }
+        "prerender": false,
+        "ssr": false
       },
       "configurations": {
         "production": {
+          "fileReplacements": [
+            {
+              "replace": "apps/webapp/src/environments/environment.ts",
+              "with": "apps/webapp/src/environments/environment.prod.ts"
+            }
+          ],
           "budgets": [
             {
               "type": "initial",
@@ -60,7 +63,7 @@
       "defaultConfiguration": "production"
     },
     "serve": {
-      "executor": "@angular-devkit/build-angular:dev-server",
+      "executor": "@ngx-env/builder:dev-server",
       "configurations": {
         "production": {
           "buildTarget": "webapp:build:production"
@@ -72,7 +75,7 @@
       "defaultConfiguration": "development"
     },
     "extract-i18n": {
-      "executor": "@angular-devkit/build-angular:extract-i18n",
+      "executor": "@ngx-env/builder:extract-i18n",
       "options": {
         "buildTarget": "webapp:build"
       }
diff --git a/apps/webapp/src/app/app.config.ts b/apps/webapp/src/app/app.config.ts
index 017f0d5..8d6355a 100644
--- a/apps/webapp/src/app/app.config.ts
+++ b/apps/webapp/src/app/app.config.ts
@@ -1,3 +1,4 @@
+import { provideHttpClient } from '@angular/common/http';
 import { ApplicationConfig } from '@angular/core';
 import { provideRouter } from '@angular/router';
 import { provideClientHydration } from '@angular/platform-browser';
@@ -7,5 +8,11 @@ import { provideWalletAdapter } from '@heavy-duty/wallet-adapter';
 import { appRoutes } from './app.routes';
 
 export const appConfig: ApplicationConfig = {
-  providers: [provideClientHydration(), provideRouter(appRoutes), provideAnimationsAsync(), provideWalletAdapter()],
+  providers: [
+    provideClientHydration(),
+    provideRouter(appRoutes),
+    provideAnimationsAsync(),
+    provideWalletAdapter(),
+    provideHttpClient(),
+  ],
 };
diff --git a/apps/webapp/src/app/app.routes.ts b/apps/webapp/src/app/app.routes.ts
index 7c27e93..24d6c46 100644
--- a/apps/webapp/src/app/app.routes.ts
+++ b/apps/webapp/src/app/app.routes.ts
@@ -7,7 +7,12 @@ export const appRoutes: Route[] = [
       {
         path: 'home',
         loadComponent: () =>
-          import('./pages/home.page').then((m) => m.HomePage),
+          import('./pages/home/home.page').then((m) => m.HomePage),
+      },
+      {
+        path: 'account',
+        loadComponent: () =>
+          import('./pages/account/account.page').then((m) => m.AccountPage),
       },
       {
         path: '',
diff --git a/apps/webapp/src/app/containers/layout/layout-container.component.html b/apps/webapp/src/app/containers/layout/layout-container.component.html
index 9772432..f997092 100644
--- a/apps/webapp/src/app/containers/layout/layout-container.component.html
+++ b/apps/webapp/src/app/containers/layout/layout-container.component.html
@@ -1,5 +1,5 @@
 <section class="min-h-screen flex flex-col">
-  <projectx-header [title]="title">
+  <projectx-header [title]="title" [links]="headerLinks">
     <div right>
       <hd-wallet-multi-button></hd-wallet-multi-button>
     </div>
@@ -7,5 +7,5 @@
   <main class="flex flex-grow">
     <ng-content></ng-content>
   </main>
-  <projectx-footer [navigation]="navigationLinks" />
+  <projectx-footer [navigation]="footerLinks" />
 </section>
\ No newline at end of file
diff --git a/apps/webapp/src/app/containers/layout/layout-container.component.ts b/apps/webapp/src/app/containers/layout/layout-container.component.ts
index 3c2896e..860415e 100644
--- a/apps/webapp/src/app/containers/layout/layout-container.component.ts
+++ b/apps/webapp/src/app/containers/layout/layout-container.component.ts
@@ -22,7 +22,11 @@ import { HdWalletMultiButtonComponent } from '@heavy-duty/wallet-adapter-materia
 export class LayoutContainerComponent {
   @Input() title?: string = 'Jam Sessions';
 
-  navigationLinks = [
+  headerLinks = [
+    { label: 'Account', href: '/account' },
+  ];
+
+  footerLinks = [
     {
       label: 'ProjectX on Facebook',
       href: 'https://facebook.com/projectx',
diff --git a/apps/webapp/src/app/pages/home.page.css b/apps/webapp/src/app/pages/account/account.page.css
similarity index 100%
rename from apps/webapp/src/app/pages/home.page.css
rename to apps/webapp/src/app/pages/account/account.page.css
diff --git a/apps/webapp/src/app/pages/account/account.page.html b/apps/webapp/src/app/pages/account/account.page.html
new file mode 100644
index 0000000..ad7a58f
--- /dev/null
+++ b/apps/webapp/src/app/pages/account/account.page.html
@@ -0,0 +1,45 @@
+<webapp-layout-container>
+  <section class="mx-auto w-full max-w-7xl px-4 py-16 sm:px-6 lg:px-8 lg:pb-24">
+    <div class="max-w-xl">
+      <h1 class="text-2xl font-bold tracking-tight text-gray-900 sm:text-3xl">Account</h1>
+      <p class="mt-1 text-sm text-gray-500">
+        Check the status of your account
+      </p>
+    </div>
+
+    <section aria-labelledby="recent-heading" class="mt-16">
+      <h2 id="recent-heading" class="sr-only">
+        Wallet Status
+      </h2>
+
+      <div class="space-y-20">
+        <div *ngIf="(account$ | async) as account">
+          <div
+            class="rounded-lg bg-gray-50 px-4 py-6 sm:flex sm:items-center sm:justify-between sm:space-x-6 sm:px-6 lg:space-x-8">
+            <dl
+              class="flex-auto space-y-6 divide-y divide-gray-200 text-sm text-gray-600 sm:grid sm:grid-cols-3 sm:gap-x-6 sm:space-y-0 sm:divide-y-0 lg:w-1/2 lg:flex-none lg:gap-x-8">
+              <div class="flex justify-between sm:block">
+                <dd class="sm:mt-1">
+                  <img [src]="account.info?.image" alt="account.info?.name || token"
+                    class="h-16 w-16 rounded object-cover object-center" />
+                </dd>
+              </div>
+              <div class="flex justify-between pt-6 sm:block sm:pt-0">
+                <dt class="font-medium text-gray-900">Balance</dt>
+                <dd class="sm:mt-1">
+                  {{ account.balance }}
+                </dd>
+              </div>
+              <div class="flex justify-between pt-6 font-medium text-gray-900 sm:block sm:pt-0">
+                <dt>Is Frozen</dt>
+                <dd class="sm:mt-1">
+                  {{ account.isFrozen ? 'Yes' : 'No' }}
+                </dd>
+              </div>
+            </dl>
+          </div>
+        </div>
+      </div>
+    </section>
+  </section>
+</webapp-layout-container>
\ No newline at end of file
diff --git a/apps/webapp/src/app/pages/account/account.page.spec.ts b/apps/webapp/src/app/pages/account/account.page.spec.ts
new file mode 100644
index 0000000..79195b8
--- /dev/null
+++ b/apps/webapp/src/app/pages/account/account.page.spec.ts
@@ -0,0 +1,21 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { AccountPage } from './account.page';
+
+describe('AccountPage', () => {
+  let component: AccountPage;
+  let fixture: ComponentFixture<AccountPage>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [AccountPage],
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(AccountPage);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/apps/webapp/src/app/pages/account/account.page.ts b/apps/webapp/src/app/pages/account/account.page.ts
new file mode 100644
index 0000000..f127306
--- /dev/null
+++ b/apps/webapp/src/app/pages/account/account.page.ts
@@ -0,0 +1,19 @@
+import { Component, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { LayoutContainerComponent } from '../../containers/layout/layout-container.component';
+import { WalletStore } from '../../store';
+
+@Component({
+  selector: 'webapp-account',
+  standalone: true,
+  imports: [CommonModule, LayoutContainerComponent],
+  templateUrl: './account.page.html',
+  styleUrl: './account.page.css',
+  providers: [WalletStore],
+})
+export class AccountPage {
+  
+  readonly walletStore = inject(WalletStore);
+  readonly account$ = this.walletStore.account()
+}
diff --git a/apps/webapp/src/app/pages/home/home.page.css b/apps/webapp/src/app/pages/home/home.page.css
new file mode 100644
index 0000000..e69de29
diff --git a/apps/webapp/src/app/pages/home.page.html b/apps/webapp/src/app/pages/home/home.page.html
similarity index 100%
rename from apps/webapp/src/app/pages/home.page.html
rename to apps/webapp/src/app/pages/home/home.page.html
diff --git a/apps/webapp/src/app/pages/home.page.spec.ts b/apps/webapp/src/app/pages/home/home.page.spec.ts
similarity index 100%
rename from apps/webapp/src/app/pages/home.page.spec.ts
rename to apps/webapp/src/app/pages/home/home.page.spec.ts
diff --git a/apps/webapp/src/app/pages/home.page.ts b/apps/webapp/src/app/pages/home/home.page.ts
similarity index 75%
rename from apps/webapp/src/app/pages/home.page.ts
rename to apps/webapp/src/app/pages/home/home.page.ts
index 71c2c11..e734e5d 100644
--- a/apps/webapp/src/app/pages/home.page.ts
+++ b/apps/webapp/src/app/pages/home/home.page.ts
@@ -1,7 +1,7 @@
 import { Component } from '@angular/core';
 import { CommonModule } from '@angular/common';
 
-import { LayoutContainerComponent } from '../containers/layout/layout-container.component';
+import { LayoutContainerComponent } from '../../containers/layout/layout-container.component';
 
 @Component({
   selector: 'webapp-home',
diff --git a/apps/webapp/src/app/store/index.ts b/apps/webapp/src/app/store/index.ts
new file mode 100644
index 0000000..3c5958c
--- /dev/null
+++ b/apps/webapp/src/app/store/index.ts
@@ -0,0 +1 @@
+export * from './wallet';
diff --git a/apps/webapp/src/app/store/wallet/index.ts b/apps/webapp/src/app/store/wallet/index.ts
new file mode 100644
index 0000000..ac69ddf
--- /dev/null
+++ b/apps/webapp/src/app/store/wallet/index.ts
@@ -0,0 +1,3 @@
+export * from './store'
+export * from './model'
+export * from './service'
diff --git a/apps/webapp/src/app/store/wallet/model.ts b/apps/webapp/src/app/store/wallet/model.ts
new file mode 100644
index 0000000..b5a5925
--- /dev/null
+++ b/apps/webapp/src/app/store/wallet/model.ts
@@ -0,0 +1,16 @@
+export type TokenBalanceResponse = {
+  success: boolean;
+  message: string;
+  result: {
+    address: string;
+    balance: number;
+    associated_account: string;
+    info: {
+      name: string;
+      symbol: string;
+      image: string;
+      decimals: number;
+    };
+    isFrozen: false;
+  };
+};
diff --git a/apps/webapp/src/app/store/wallet/service.ts b/apps/webapp/src/app/store/wallet/service.ts
new file mode 100644
index 0000000..e238eeb
--- /dev/null
+++ b/apps/webapp/src/app/store/wallet/service.ts
@@ -0,0 +1,33 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable, inject } from '@angular/core';
+import { map, of } from 'rxjs';
+
+import { environment } from '../../../environments/environment';
+import { TokenBalanceResponse } from './model';
+
+@Injectable({ providedIn: 'root' })
+export class ShyftApiService {
+  private readonly http = inject(HttpClient);
+
+  getAccount(publicKey?: string) {
+    if (!publicKey) {
+      return of(null);
+    }
+
+    const url = new URL(
+      '/sol/v1/wallet/token_balance',
+      environment.shyftApiUrl
+    );
+    url.searchParams.append('network', environment.walletNetwork);
+    url.searchParams.append('wallet', publicKey);
+    url.searchParams.append('token', environment.mintUSDC);
+
+    return this.http
+      .get<TokenBalanceResponse>(url.toString(), {
+        headers: {
+          'x-api-key': environment.shyftApiKey,
+        },
+      })
+      .pipe(map((res) => res.result));
+  }
+}
diff --git a/apps/webapp/src/app/store/wallet/store.ts b/apps/webapp/src/app/store/wallet/store.ts
new file mode 100644
index 0000000..b6555b6
--- /dev/null
+++ b/apps/webapp/src/app/store/wallet/store.ts
@@ -0,0 +1,32 @@
+import { computed, inject } from '@angular/core';
+import { signalStore, withComputed, withState } from '@ngrx/signals';
+import { WalletStore as WalletAdapterStore } from '@heavy-duty/wallet-adapter';
+
+import { ShyftApiService } from './service';
+import { switchMap } from 'rxjs';
+
+type WalletState = {
+  isLoading: boolean;
+  wallet: WalletAdapterStore;
+  error?: string;
+};
+
+export const WalletStore = signalStore(
+  withState(
+    () =>
+      <WalletState>{
+        wallet: inject(WalletAdapterStore),
+      }
+  ),
+  withComputed((store, walletService = inject(ShyftApiService)) => ({
+    account: computed(() => {
+      return store
+        .wallet()
+        .publicKey$.pipe(
+          switchMap((publicKey) =>
+            walletService.getAccount(publicKey?.toBase58())
+          )
+        );
+    }),
+  }))
+);
diff --git a/apps/webapp/src/env.d.ts b/apps/webapp/src/env.d.ts
new file mode 100644
index 0000000..3499374
--- /dev/null
+++ b/apps/webapp/src/env.d.ts
@@ -0,0 +1,51 @@
+interface ImportMeta {
+  readonly env: ImportMetaEnv;
+}
+
+interface ImportMetaEnv {
+  /**
+   * Built-in environment variable.
+   * @see Docs https://github.com/chihab/dotenv-run/packages/angular#node_env.
+   */
+  readonly NODE_ENV: string;
+  // Add your environment variables below
+  readonly NG_APP_SHYFT_API_KEY: string;
+  [key: string]: unknown;
+}
+
+/*
+ * Remove all the deprecated code below if you're using import.meta.env (recommended)
+ */
+
+/****************************** DEPREACTED **************************/
+/**
+ * @deprecated process.env usage
+ * prefer using import.meta.env
+ * */
+// declare var process: {
+//   env: {
+//     NODE_ENV: string;
+//     [key: string]: any;
+//   };
+// };
+
+// If your project references @types/node directly (in you) or indirectly (as in RxJS < 7.6.0),
+// you might need to use the following declaration merging.
+// declare namespace NodeJS {
+//   export interface ProcessEnv {
+//     readonly NODE_ENV: string;
+//     // Add your environment variables below
+//   }
+// }
+
+// If you're using Angular Universal and process.env notation, you'll need to add the following to your tsconfig.server.json:
+/* In your tsconfig.server.json */
+// {
+//   "extends": "./tsconfig.app.json",
+//   ...
+//   "exclude": [
+//     "src/env.d.ts"
+//   ]
+// }
+
+/*********************************************************************/
diff --git a/apps/webapp/src/environments/environment.prod.ts b/apps/webapp/src/environments/environment.prod.ts
new file mode 100644
index 0000000..e744a38
--- /dev/null
+++ b/apps/webapp/src/environments/environment.prod.ts
@@ -0,0 +1,7 @@
+export const environment = {
+  production: import.meta.env.NODE_ENV === 'production',
+  shyftApiKey: import.meta.env.NG_APP_SHYFT_API_KEY,
+  shyftApiUrl: 'https://api.shyft.to',
+  walletNetwork: 'mainnet-beta',
+  mintUSDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
+};
\ No newline at end of file
diff --git a/apps/webapp/src/environments/environment.ts b/apps/webapp/src/environments/environment.ts
new file mode 100644
index 0000000..6e7ced9
--- /dev/null
+++ b/apps/webapp/src/environments/environment.ts
@@ -0,0 +1,7 @@
+export const environment = {
+  production: false,
+  shyftApiKey: import.meta.env.NG_APP_SHYFT_API_KEY,
+  shyftApiUrl: 'https://api.shyft.to',
+  walletNetwork: 'devnet',
+  mintUSDC: '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU'
+};
\ No newline at end of file
diff --git a/apps/webapp/src/index.html b/apps/webapp/src/index.html
index 0f5361a..f03d8ad 100644
--- a/apps/webapp/src/index.html
+++ b/apps/webapp/src/index.html
@@ -1,12 +1,14 @@
 <!DOCTYPE html>
 <html lang="en">
+
   <head>
     <meta charset="utf-8" />
     <title>webapp</title>
     <base href="/" />
 
     <meta name="color-scheme" content="light dark" />
-    <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <meta name="viewport"
+      content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
     <meta name="format-detection" content="telephone=no" />
     <meta name="msapplication-tap-highlight" content="no" />
 
@@ -18,8 +20,10 @@
 
     <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
     <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
-</head>
-  <body class="mat-app-background">
+  </head>
+
+  <body>
     <webapp-root></webapp-root>
   </body>
-</html>
+
+</html>
\ No newline at end of file
diff --git a/libs/ui/src/lib/header/header.component.html b/libs/ui/src/lib/header/header.component.html
index e2e8eab..1595ac4 100644
--- a/libs/ui/src/lib/header/header.component.html
+++ b/libs/ui/src/lib/header/header.component.html
@@ -31,20 +31,12 @@
       class="relative flex justify-end sm:gap-4 md:flex-[0.2_0_auto] md:gap-8"
     >
       <ul class="list-reset hidden flex-1 items-center justify-end xl:flex">
-        <li class="mr-3">
+        <li class="mr-3" *ngFor="let link of links">
           <a
             class="inline-block px-4 py-2 text-gray-800 no-underline dark:text-dark"
-            href="/artists"
+            [href]="link.href"
           >
-            Artists
-          </a>
-        </li>
-        <li class="mr-3">
-          <a
-            class="inline-block px-4 py-2 text-gray-800 no-underline dark:text-dark"
-            href="/events"
-          >
-            Events
+            {{ link.label }}
           </a>
         </li>
       </ul>
diff --git a/libs/ui/src/lib/header/header.component.ts b/libs/ui/src/lib/header/header.component.ts
index bdaf346..4cd27c8 100644
--- a/libs/ui/src/lib/header/header.component.ts
+++ b/libs/ui/src/lib/header/header.component.ts
@@ -1,4 +1,4 @@
-import { Component, Input } from '@angular/core';
+import { Component, EventEmitter, Input, Output } from '@angular/core';
 import { CommonModule } from '@angular/common';
 
 import { ThemeButtonComponent } from '../theme-button/theme-button.component';
@@ -12,9 +12,10 @@ import { ThemeButtonComponent } from '../theme-button/theme-button.component';
 })
 export class HeaderComponent {
   @Input() title?: string;
-  themeEmitted = ''
+  @Input() links?: Array<{ label: string; href: string }> = [];
+  @Output() setTheme = new EventEmitter<string>();
 
-  onSetTheme(event: string) {
-    this.themeEmitted = event;
+  onSetTheme(theme: string) {
+    this.setTheme.emit(theme);
   }
 }
diff --git a/nx.json b/nx.json
index 0c8288b..9bd9f37 100644
--- a/nx.json
+++ b/nx.json
@@ -81,7 +81,11 @@
   "tasksRunnerOptions": {
     "default": {
       "options": {
-        "cacheableOperations": ["build-storybook"]
+        "cacheableOperations": ["build-storybook"],
+        "runtimeCacheInputs": [
+          "echo $NODE_ENV",
+          "echo $NG_APP_SHYFT_API_KEY"
+        ]
       }
     }
   }
diff --git a/package-lock.json b/package-lock.json
index 402349e..9041ebd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33,6 +33,7 @@
         "@tailwindcss/forms": "^0.5.7",
         "@tailwindcss/typography": "^0.5.10",
         "express": "~4.18.2",
+        "ngxtension": "^2.0.0",
         "rxjs": "~7.8.0",
         "tslib": "^2.3.0",
         "zone.js": "~0.14.3"
@@ -47,6 +48,7 @@
         "@angular/cli": "~17.1.0",
         "@angular/compiler-cli": "~17.1.0",
         "@angular/language-service": "~17.1.0",
+        "@ngx-env/builder": "^17.1.3",
         "@nx/cypress": "18.0.3",
         "@nx/eslint": "18.0.3",
         "@nx/eslint-plugin": "18.0.3",
@@ -4397,6 +4399,76 @@
         "node": ">=10.0.0"
       }
     },
+    "node_modules/@dotenv-run/core": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/@dotenv-run/core/-/core-1.3.4.tgz",
+      "integrity": "sha512-U/jCYwpzaRjKPhuMj5VdIiIFiIBtiX7lqkyqA+WjpG6S1j91N+5c6LOPUYC54b/N6bKr+fmTj9f05CpWRIuPXw==",
+      "dev": true,
+      "dependencies": {
+        "chalk": "^4.1.0",
+        "dotenv": "^16.1.4",
+        "dotenv-expand": "^10.0.0",
+        "find-up": "^5.0.0"
+      }
+    },
+    "node_modules/@dotenv-run/core/node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@dotenv-run/core/node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@dotenv-run/core/node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@dotenv-run/webpack": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/@dotenv-run/webpack/-/webpack-1.3.4.tgz",
+      "integrity": "sha512-2QAH9q2iXhYk9l9lV1htAw5Qvwyyjp7Cg26t8j4RsFcO9NRr+TguKsoYUWI+HALvhFiEb/T07cFCUvaxv9APeQ==",
+      "dev": true,
+      "dependencies": {
+        "@dotenv-run/core": "^1.3.4"
+      },
+      "peerDependencies": {
+        "webpack": "^5.0.0"
+      }
+    },
     "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
@@ -6472,10 +6544,467 @@
         "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
         "yarn": ">= 1.13.0"
       },
-      "peerDependencies": {
-        "@angular/compiler-cli": "^17.0.0",
-        "typescript": ">=5.2 <5.4",
-        "webpack": "^5.54.0"
+      "peerDependencies": {
+        "@angular/compiler-cli": "^17.0.0",
+        "typescript": ">=5.2 <5.4",
+        "webpack": "^5.54.0"
+      }
+    },
+    "node_modules/@ngx-env/builder": {
+      "version": "17.1.3",
+      "resolved": "https://registry.npmjs.org/@ngx-env/builder/-/builder-17.1.3.tgz",
+      "integrity": "sha512-q+U0+2YTRWHUGIGPUnGZqOaQnWkToxWTCGvYOzPqPfYPATS7OozMiIpZKdY9BMmqdCCtf7p/PGVwYOOGqcuM5w==",
+      "dev": true,
+      "dependencies": {
+        "@dotenv-run/esbuild": "^1.3.4",
+        "@dotenv-run/webpack": "^1.3.4",
+        "glob": "^10.3.10"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@dotenv-run/esbuild": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/@dotenv-run/esbuild/-/esbuild-1.3.4.tgz",
+      "integrity": "sha512-wc7dhnST+PrzliNZilyDCaLFjYbvlvaL8wafSLb311/ylFafJLLv+i8sABUfG+/iEKZerrTMAJj5rw55VxZsNA==",
+      "dev": true,
+      "dependencies": {
+        "@dotenv-run/core": "^1.3.4"
+      },
+      "peerDependencies": {
+        "esbuild": "0.19.5"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/android-arm": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.5.tgz",
+      "integrity": "sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/android-arm64": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.5.tgz",
+      "integrity": "sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/android-x64": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.5.tgz",
+      "integrity": "sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/darwin-arm64": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.5.tgz",
+      "integrity": "sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/darwin-x64": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.5.tgz",
+      "integrity": "sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.5.tgz",
+      "integrity": "sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/freebsd-x64": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.5.tgz",
+      "integrity": "sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/linux-arm": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.5.tgz",
+      "integrity": "sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/linux-arm64": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.5.tgz",
+      "integrity": "sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/linux-ia32": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.5.tgz",
+      "integrity": "sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/linux-loong64": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.5.tgz",
+      "integrity": "sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/linux-mips64el": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.5.tgz",
+      "integrity": "sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/linux-ppc64": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.5.tgz",
+      "integrity": "sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/linux-riscv64": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.5.tgz",
+      "integrity": "sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/linux-s390x": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.5.tgz",
+      "integrity": "sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/linux-x64": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz",
+      "integrity": "sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/netbsd-x64": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.5.tgz",
+      "integrity": "sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/openbsd-x64": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.5.tgz",
+      "integrity": "sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/sunos-x64": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.5.tgz",
+      "integrity": "sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/win32-arm64": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.5.tgz",
+      "integrity": "sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/win32-ia32": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.5.tgz",
+      "integrity": "sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/@esbuild/win32-x64": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.5.tgz",
+      "integrity": "sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/esbuild": {
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.5.tgz",
+      "integrity": "sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ==",
+      "dev": true,
+      "hasInstallScript": true,
+      "peer": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/android-arm": "0.19.5",
+        "@esbuild/android-arm64": "0.19.5",
+        "@esbuild/android-x64": "0.19.5",
+        "@esbuild/darwin-arm64": "0.19.5",
+        "@esbuild/darwin-x64": "0.19.5",
+        "@esbuild/freebsd-arm64": "0.19.5",
+        "@esbuild/freebsd-x64": "0.19.5",
+        "@esbuild/linux-arm": "0.19.5",
+        "@esbuild/linux-arm64": "0.19.5",
+        "@esbuild/linux-ia32": "0.19.5",
+        "@esbuild/linux-loong64": "0.19.5",
+        "@esbuild/linux-mips64el": "0.19.5",
+        "@esbuild/linux-ppc64": "0.19.5",
+        "@esbuild/linux-riscv64": "0.19.5",
+        "@esbuild/linux-s390x": "0.19.5",
+        "@esbuild/linux-x64": "0.19.5",
+        "@esbuild/netbsd-x64": "0.19.5",
+        "@esbuild/openbsd-x64": "0.19.5",
+        "@esbuild/sunos-x64": "0.19.5",
+        "@esbuild/win32-arm64": "0.19.5",
+        "@esbuild/win32-ia32": "0.19.5",
+        "@esbuild/win32-x64": "0.19.5"
+      }
+    },
+    "node_modules/@ngx-env/builder/node_modules/glob": {
+      "version": "10.3.10",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+      "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+      "dev": true,
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^2.3.5",
+        "minimatch": "^9.0.1",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+        "path-scurry": "^1.10.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
       }
     },
     "node_modules/@noble/curves": {
@@ -13422,6 +13951,57 @@
         "node": ">=10.13.0"
       }
     },
+    "node_modules/@ts-morph/common": {
+      "version": "0.22.0",
+      "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.22.0.tgz",
+      "integrity": "sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==",
+      "dependencies": {
+        "fast-glob": "^3.3.2",
+        "minimatch": "^9.0.3",
+        "mkdirp": "^3.0.1",
+        "path-browserify": "^1.0.1"
+      }
+    },
+    "node_modules/@ts-morph/common/node_modules/fast-glob": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+      "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/@ts-morph/common/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/@ts-morph/common/node_modules/mkdirp": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
+      "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
+      "bin": {
+        "mkdirp": "dist/cjs/src/bin.js"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/@tsconfig/node10": {
       "version": "1.0.9",
       "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
@@ -14143,6 +14723,21 @@
         "url": "https://opencollective.com/typescript-eslint"
       }
     },
+    "node_modules/@use-gesture/core": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.0.tgz",
+      "integrity": "sha512-rh+6MND31zfHcy9VU3dOZCqGY511lvGcfyJenN4cWZe0u1BH6brBpBddLVXhF2r4BMqWbvxfsbL7D287thJU2A==",
+      "peer": true
+    },
+    "node_modules/@use-gesture/vanilla": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@use-gesture/vanilla/-/vanilla-10.3.0.tgz",
+      "integrity": "sha512-hehZWLaNyNc+TWAbhJpj84yumD8ZBp/eet6HGg3xztPcchuNNTGEu5LEEdSg69SXHzS7exWE6j5VnsZ3VXVFxQ==",
+      "peer": true,
+      "dependencies": {
+        "@use-gesture/core": "10.3.0"
+      }
+    },
     "node_modules/@vitejs/plugin-basic-ssl": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.2.tgz",
@@ -16337,6 +16932,11 @@
         "node": ">= 0.12.0"
       }
     },
+    "node_modules/code-block-writer": {
+      "version": "12.0.0",
+      "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz",
+      "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w=="
+    },
     "node_modules/collect-v8-coverage": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
@@ -25862,6 +26462,284 @@
       "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
       "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
     },
+    "node_modules/ngxtension": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ngxtension/-/ngxtension-2.0.0.tgz",
+      "integrity": "sha512-S/MJ2rifemopwmq4rexfWmxnURGZeBxUwivG6ymbLusHsfHuQo821RzMl+fmJwMr4Yt5nLwUsralM7Vipd7/nQ==",
+      "dependencies": {
+        "@nx/devkit": "^17.0.0",
+        "nx": "^17.0.0",
+        "ts-morph": "^21.0.1",
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@angular/common": ">=16.0.0",
+        "@angular/core": ">=16.0.0",
+        "@use-gesture/vanilla": "^10.0.0",
+        "rxjs": "^6.0.0 || ^7.0.0"
+      }
+    },
+    "node_modules/ngxtension/node_modules/@nrwl/devkit": {
+      "version": "17.3.2",
+      "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-17.3.2.tgz",
+      "integrity": "sha512-31wh7dDZPM1YUCfhhk/ioHnUeoPIlKYLFLW0fGdw76Ow2nmTqrmxha2m0CSIR1/9En9GpYut2IdUdNh9CctNlA==",
+      "dependencies": {
+        "@nx/devkit": "17.3.2"
+      }
+    },
+    "node_modules/ngxtension/node_modules/@nrwl/tao": {
+      "version": "17.3.2",
+      "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-17.3.2.tgz",
+      "integrity": "sha512-5uvpSmij0J9tteFV/0M/024K+H/o3XAlqtSdU8j03Auj1IleclSLF2yCTuIo7pYXhG3cgx1+nR+3nMs1QVAdUA==",
+      "dependencies": {
+        "nx": "17.3.2",
+        "tslib": "^2.3.0"
+      },
+      "bin": {
+        "tao": "index.js"
+      }
+    },
+    "node_modules/ngxtension/node_modules/@nx/devkit": {
+      "version": "17.3.2",
+      "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-17.3.2.tgz",
+      "integrity": "sha512-gbOIhwrZKCSSFFbh6nE6LLCvAU7mhSdBSnRiS14YBwJJMu4CRJ0IcaFz58iXqGWZefMivKtkNFtx+zqwUC4ziw==",
+      "dependencies": {
+        "@nrwl/devkit": "17.3.2",
+        "ejs": "^3.1.7",
+        "enquirer": "~2.3.6",
+        "ignore": "^5.0.4",
+        "semver": "^7.5.3",
+        "tmp": "~0.2.1",
+        "tslib": "^2.3.0",
+        "yargs-parser": "21.1.1"
+      },
+      "peerDependencies": {
+        "nx": ">= 16 <= 18"
+      }
+    },
+    "node_modules/ngxtension/node_modules/@nx/nx-darwin-arm64": {
+      "version": "17.3.2",
+      "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-17.3.2.tgz",
+      "integrity": "sha512-hn12o/tt26Pf4wG+8rIBgNIEZq5BFlHLv3scNrgKbd5SancHlTbY4RveRGct737UQ/78GCMCgMDRgNdagbCr6w==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/ngxtension/node_modules/@nx/nx-darwin-x64": {
+      "version": "17.3.2",
+      "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-17.3.2.tgz",
+      "integrity": "sha512-5F28wrfE7yU60MzEXGjndy1sPJmNMIaV2W/g82kTXzxAbGHgSjwrGFmrJsrexzLp9oDlWkbc6YmInKV8gmmIaQ==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/ngxtension/node_modules/@nx/nx-freebsd-x64": {
+      "version": "17.3.2",
+      "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-17.3.2.tgz",
+      "integrity": "sha512-07MMTfsJooONqL1Vrm5L6qk/gzmSrYLazjkiTmJz+9mrAM61RdfSYfO3mSyAoyfgWuQ5yEvfI56P036mK8aoPg==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/ngxtension/node_modules/@nx/nx-linux-arm-gnueabihf": {
+      "version": "17.3.2",
+      "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-17.3.2.tgz",
+      "integrity": "sha512-gQxMF6U/h18Rz+FZu50DZCtfOdk27hHghNh3d3YTeVsrJTd1SmUQbYublmwU/ia1HhFS8RVI8GvkaKt5ph0HoA==",
+      "cpu": [
+        "arm"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/ngxtension/node_modules/@nx/nx-linux-arm64-gnu": {
+      "version": "17.3.2",
+      "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-17.3.2.tgz",
+      "integrity": "sha512-X20wiXtXmKlC01bpVEREsRls1uVOM22xDTpqILvVty6+P+ytEYFR3Vs5EjDtzBKF51wjrwf03rEoToZbmgM8MA==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/ngxtension/node_modules/@nx/nx-linux-arm64-musl": {
+      "version": "17.3.2",
+      "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-17.3.2.tgz",
+      "integrity": "sha512-yko3Xsezkn4tjeudZYLjxFl07X/YB84K+DLK7EFyh9elRWV/8VjFcQmBAKUS2r9LfaEMNXq8/vhWMOWYyWBrIA==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/ngxtension/node_modules/@nx/nx-linux-x64-gnu": {
+      "version": "17.3.2",
+      "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-17.3.2.tgz",
+      "integrity": "sha512-RiPvvQMmlZmDu9HdT6n6sV0+fEkyAqR5VocrD5ZAzEzFIlh4dyVLripFR3+MD+QhIhXyPt/hpri1kq9sgs4wnw==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/ngxtension/node_modules/@nx/nx-linux-x64-musl": {
+      "version": "17.3.2",
+      "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-17.3.2.tgz",
+      "integrity": "sha512-PWfVGmFsFJi+N1Nljg/jTKLHdufpGuHlxyfHqhDso/o4Qc0exZKSeZ1C63WkD7eTcT5kInifTQ/PffLiIDE3MA==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/ngxtension/node_modules/@nx/nx-win32-arm64-msvc": {
+      "version": "17.3.2",
+      "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-17.3.2.tgz",
+      "integrity": "sha512-O+4FFPbQz1mqaIj+SVE02ppe7T9ELj7Z5soQct5TbRRhwjGaw5n5xaPPBW7jUuQe2L5htid1E82LJyq3JpVc8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/ngxtension/node_modules/@nx/nx-win32-x64-msvc": {
+      "version": "17.3.2",
+      "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-17.3.2.tgz",
+      "integrity": "sha512-4hQm+7coy+hBqGY9J709hz/tUPijhf/WS7eML2r2xBmqBew3PMHfeZuaAAYWN690nIsu0WX3wyDsNjulR8HGPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/ngxtension/node_modules/nx": {
+      "version": "17.3.2",
+      "resolved": "https://registry.npmjs.org/nx/-/nx-17.3.2.tgz",
+      "integrity": "sha512-QjF1gnwKebQISvATrSbW7dsmIcLbA0fcyDyxLo5wVHx/MIlcaIb/lLYaPTld73ZZ6svHEZ6n2gOkhMitmkIPQA==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "@nrwl/tao": "17.3.2",
+        "@yarnpkg/lockfile": "^1.1.0",
+        "@yarnpkg/parsers": "3.0.0-rc.46",
+        "@zkochan/js-yaml": "0.0.6",
+        "axios": "^1.6.0",
+        "chalk": "^4.1.0",
+        "cli-cursor": "3.1.0",
+        "cli-spinners": "2.6.1",
+        "cliui": "^8.0.1",
+        "dotenv": "~16.3.1",
+        "dotenv-expand": "~10.0.0",
+        "enquirer": "~2.3.6",
+        "figures": "3.2.0",
+        "flat": "^5.0.2",
+        "fs-extra": "^11.1.0",
+        "ignore": "^5.0.4",
+        "jest-diff": "^29.4.1",
+        "js-yaml": "4.1.0",
+        "jsonc-parser": "3.2.0",
+        "lines-and-columns": "~2.0.3",
+        "minimatch": "9.0.3",
+        "node-machine-id": "1.1.12",
+        "npm-run-path": "^4.0.1",
+        "open": "^8.4.0",
+        "ora": "5.3.0",
+        "semver": "^7.5.3",
+        "string-width": "^4.2.3",
+        "strong-log-transformer": "^2.1.0",
+        "tar-stream": "~2.2.0",
+        "tmp": "~0.2.1",
+        "tsconfig-paths": "^4.1.2",
+        "tslib": "^2.3.0",
+        "yargs": "^17.6.2",
+        "yargs-parser": "21.1.1"
+      },
+      "bin": {
+        "nx": "bin/nx.js",
+        "nx-cloud": "bin/nx-cloud.js"
+      },
+      "optionalDependencies": {
+        "@nx/nx-darwin-arm64": "17.3.2",
+        "@nx/nx-darwin-x64": "17.3.2",
+        "@nx/nx-freebsd-x64": "17.3.2",
+        "@nx/nx-linux-arm-gnueabihf": "17.3.2",
+        "@nx/nx-linux-arm64-gnu": "17.3.2",
+        "@nx/nx-linux-arm64-musl": "17.3.2",
+        "@nx/nx-linux-x64-gnu": "17.3.2",
+        "@nx/nx-linux-x64-musl": "17.3.2",
+        "@nx/nx-win32-arm64-msvc": "17.3.2",
+        "@nx/nx-win32-x64-msvc": "17.3.2"
+      },
+      "peerDependencies": {
+        "@swc-node/register": "^1.6.7",
+        "@swc/core": "^1.3.85"
+      },
+      "peerDependenciesMeta": {
+        "@swc-node/register": {
+          "optional": true
+        },
+        "@swc/core": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/nice-napi": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",
@@ -27136,8 +28014,7 @@
     "node_modules/path-browserify": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
-      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
-      "dev": true
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
     },
     "node_modules/path-exists": {
       "version": "4.0.0",
@@ -31431,6 +32308,15 @@
         "webpack": "^5.0.0"
       }
     },
+    "node_modules/ts-morph": {
+      "version": "21.0.1",
+      "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-21.0.1.tgz",
+      "integrity": "sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==",
+      "dependencies": {
+        "@ts-morph/common": "~0.22.0",
+        "code-block-writer": "^12.0.0"
+      }
+    },
     "node_modules/ts-node": {
       "version": "10.9.1",
       "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
diff --git a/package.json b/package.json
index 987905b..89db095 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
     "@tailwindcss/forms": "^0.5.7",
     "@tailwindcss/typography": "^0.5.10",
     "express": "~4.18.2",
+    "ngxtension": "^2.0.0",
     "rxjs": "~7.8.0",
     "tslib": "^2.3.0",
     "zone.js": "~0.14.3"
@@ -52,6 +53,7 @@
     "@angular/cli": "~17.1.0",
     "@angular/compiler-cli": "~17.1.0",
     "@angular/language-service": "~17.1.0",
+    "@ngx-env/builder": "^17.1.3",
     "@nx/cypress": "18.0.3",
     "@nx/eslint": "18.0.3",
     "@nx/eslint-plugin": "18.0.3",