@devmed555/angular-clean-architecture-cli 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +425 -0
  2. package/bin/index.js +100 -2
  3. package/generators.json +19 -9
  4. package/package.json +1 -1
  5. package/src/generators/core/files/auth/auth.guard.ts.template +14 -0
  6. package/src/generators/core/files/auth/auth.service.ts.template +137 -0
  7. package/src/generators/core/files/auth/login.component.ts.template +165 -0
  8. package/src/generators/core/files/auth/signup.component.ts.template +171 -0
  9. package/src/generators/core/files/guard/__name__.guard.ts.template +6 -0
  10. package/src/generators/core/files/interceptor/__name__.interceptor.ts.template +6 -0
  11. package/src/generators/core/files/language-selector/language-selector.component.ts.template +82 -0
  12. package/src/generators/core/files/menu/__name__.component.html.template +14 -0
  13. package/src/generators/core/files/menu/__name__.component.scss.template +66 -0
  14. package/src/generators/core/files/menu/__name__.component.ts.template +24 -0
  15. package/src/generators/core/files/navbar/__name__.component.html.template +45 -0
  16. package/src/generators/core/files/navbar/__name__.component.scss.template +134 -0
  17. package/src/generators/core/files/navbar/__name__.component.ts.template +38 -0
  18. package/src/generators/core/files/service/__name__.service.ts.template +26 -0
  19. package/src/generators/core/files/theme-selector/theme-selector.component.ts.template +49 -0
  20. package/src/generators/core/files/translate/translate.pipe.ts.template +15 -0
  21. package/src/generators/core/files/translate/translate.service.ts.template +24 -0
  22. package/src/generators/core/generator.d.ts +7 -0
  23. package/src/generators/core/generator.js +49 -0
  24. package/src/generators/core/generator.js.map +1 -0
  25. package/src/generators/core/schema.json +34 -0
  26. package/src/generators/feature/blueprint.schema.json +34 -0
  27. package/src/generators/feature/files/application/store.ts.template +135 -0
  28. package/src/generators/feature/files/domain/model.ts.template +9 -0
  29. package/src/generators/{clean-feature → feature}/files/infrastructure/service.ts.template +5 -5
  30. package/src/generators/feature/files/ui/__singularName__.component.html.template +109 -0
  31. package/src/generators/feature/files/ui/__singularName__.component.scss.template +162 -0
  32. package/src/generators/feature/files/ui/__singularName__.component.ts.template +131 -0
  33. package/src/generators/feature/files/ui/_theme.scss.template +35 -0
  34. package/src/generators/feature/files/ui/form/form.component.ts.template +122 -0
  35. package/src/generators/feature/generator.d.ts +4 -0
  36. package/src/generators/feature/generator.js +209 -0
  37. package/src/generators/feature/generator.js.map +1 -0
  38. package/src/generators/feature/schema.d.ts +5 -0
  39. package/src/generators/{clean-feature → feature}/schema.json +25 -21
  40. package/src/generators/shared/files/ui/__name__.component.ts.template +57 -0
  41. package/src/generators/shared/files/util/__name__.ts.template +7 -0
  42. package/src/generators/shared/generator.d.ts +7 -0
  43. package/src/generators/shared/generator.js +31 -0
  44. package/src/generators/shared/generator.js.map +1 -0
  45. package/src/generators/shared/schema.json +23 -0
  46. package/src/index.js +1 -0
  47. package/src/utils/string-utils.d.ts +16 -0
  48. package/src/utils/string-utils.js +33 -0
  49. package/src/utils/string-utils.js.map +1 -0
  50. package/src/generators/clean-feature/files/application/store.ts.template +0 -6
  51. package/src/generators/clean-feature/files/domain/model.ts.template +0 -4
  52. package/src/generators/clean-feature/files/ui/component.ts.template +0 -44
  53. package/src/generators/clean-feature/generator.d.ts +0 -4
  54. package/src/generators/clean-feature/generator.js +0 -89
  55. package/src/generators/clean-feature/generator.js.map +0 -1
  56. package/src/generators/clean-feature/schema.d.ts +0 -4
@@ -0,0 +1,45 @@
1
+ <mat-toolbar color="primary" class="navbar">
2
+ <div class="navbar-left">
3
+ <button mat-icon-button (click)="onToggleSidenav()" aria-label="Toggle sidenav">
4
+ <mat-icon>menu</mat-icon>
5
+ </button>
6
+ <span class="brand-title">Clean Architecture</span>
7
+ </div>
8
+
9
+ <span class="spacer"></span>
10
+
11
+ <div class="navbar-right">
12
+ @if (authService.isAuthenticated()) {
13
+ <!-- Authenticated: Show user info and logout -->
14
+ @if (authService.currentUser(); as user) {
15
+ <div class="user-profile">
16
+ <div class="user-info">
17
+ <span class="user-label">Welcome</span>
18
+ <span class="user-email">{{ user.name || user.email }}</span>
19
+ </div>
20
+ <div class="user-avatar" [title]="user.email">
21
+ {{ (user.name || user.email).charAt(0).toUpperCase() }}
22
+ </div>
23
+ </div>
24
+ }
25
+ <button mat-icon-button (click)="onLogout()" matTooltip="Logout" class="logout-btn">
26
+ <mat-icon>logout</mat-icon>
27
+ </button>
28
+ } @else {
29
+ <!-- Not authenticated: Show login/signup buttons -->
30
+ <a mat-button routerLink="/login" class="auth-btn">
31
+ <mat-icon>login</mat-icon>
32
+ <span>Login</span>
33
+ </a>
34
+ <a mat-raised-button color="accent" routerLink="/signup" class="auth-btn signup-btn">
35
+ <mat-icon>person_add</mat-icon>
36
+ <span>Sign Up</span>
37
+ </a>
38
+ }
39
+
40
+ <div class="divider"></div>
41
+
42
+ <app-theme-selector />
43
+ <app-language-selector />
44
+ </div>
45
+ </mat-toolbar>
@@ -0,0 +1,134 @@
1
+ .navbar {
2
+ display: flex;
3
+ justify-content: space-between;
4
+ align-items: center;
5
+ padding: 0 16px;
6
+ // Premium Gradient Background
7
+ background: linear-gradient(135deg, var(--color-primary-dark), var(--color-primary)) !important;
8
+ color: white;
9
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
10
+ z-index: 1000;
11
+ position: relative;
12
+ }
13
+
14
+ .navbar-left {
15
+ display: flex;
16
+ align-items: center;
17
+ gap: 16px;
18
+ }
19
+
20
+ .brand-title {
21
+ font-weight: 700;
22
+ font-size: 1.25rem;
23
+ letter-spacing: 0.5px;
24
+ text-shadow: 0 1px 2px rgba(0,0,0,0.1);
25
+ }
26
+
27
+ .spacer {
28
+ flex: 1 1 auto;
29
+ }
30
+
31
+ .navbar-right {
32
+ display: flex;
33
+ align-items: center;
34
+ gap: 16px;
35
+ }
36
+
37
+ // Auth buttons
38
+ .auth-btn {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: 6px;
42
+ color: white !important;
43
+
44
+ mat-icon {
45
+ font-size: 20px;
46
+ width: 20px;
47
+ height: 20px;
48
+ }
49
+
50
+ @media (max-width: 600px) {
51
+ span {
52
+ display: none;
53
+ }
54
+ }
55
+ }
56
+
57
+ .signup-btn {
58
+ background: rgba(255, 255, 255, 0.2) !important;
59
+ border: 1px solid rgba(255, 255, 255, 0.3) !important;
60
+
61
+ &:hover {
62
+ background: rgba(255, 255, 255, 0.3) !important;
63
+ }
64
+ }
65
+
66
+ .logout-btn {
67
+ color: white !important;
68
+ opacity: 0.8;
69
+
70
+ &:hover {
71
+ opacity: 1;
72
+ background: rgba(255, 255, 255, 0.1);
73
+ }
74
+ }
75
+
76
+ .user-profile {
77
+ display: flex;
78
+ align-items: center;
79
+ gap: 12px;
80
+ padding: 6px 12px;
81
+ border-radius: 9999px;
82
+ background: rgba(255, 255, 255, 0.15);
83
+ border: 1px solid rgba(255, 255, 255, 0.1);
84
+ transition: all 0.3s ease;
85
+ cursor: default;
86
+
87
+ &:hover {
88
+ background: rgba(255, 255, 255, 0.25);
89
+ }
90
+ }
91
+
92
+ .user-info {
93
+ display: flex;
94
+ flex-direction: column;
95
+ align-items: flex-end;
96
+ line-height: 1.2;
97
+
98
+ @media (max-width: 600px) {
99
+ display: none;
100
+ }
101
+ }
102
+
103
+ .user-label {
104
+ font-size: 0.65rem;
105
+ opacity: 0.9;
106
+ text-transform: uppercase;
107
+ font-weight: 600;
108
+ letter-spacing: 0.5px;
109
+ }
110
+
111
+ .user-email {
112
+ font-size: 0.85rem;
113
+ font-weight: 500;
114
+ }
115
+
116
+ .user-avatar {
117
+ width: 36px;
118
+ height: 36px;
119
+ border-radius: 50%;
120
+ background: var(--color-bg);
121
+ color: var(--color-primary);
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ font-weight: 700;
126
+ font-size: 1.1rem;
127
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
128
+ }
129
+
130
+ .divider {
131
+ width: 1px;
132
+ height: 24px;
133
+ background: rgba(255, 255, 255, 0.3);
134
+ }
@@ -0,0 +1,38 @@
1
+ import { Component, inject, output, ChangeDetectionStrategy } from '@angular/core';
2
+ import { RouterLink } from '@angular/router';
3
+ import { MatToolbarModule } from '@angular/material/toolbar';
4
+ import { MatIconModule } from '@angular/material/icon';
5
+ import { MatButtonModule } from '@angular/material/button';
6
+ import { MatTooltipModule } from '@angular/material/tooltip';
7
+ import { LanguageSelectorComponent } from '../language-selector/language-selector.component';
8
+ import { ThemeSelectorComponent } from '../theme-selector/theme-selector.component';
9
+ import { AuthService } from '../auth/auth.service';
10
+
11
+ @Component({
12
+ selector: 'app-navbar',
13
+ imports: [
14
+ RouterLink,
15
+ MatToolbarModule,
16
+ MatIconModule,
17
+ MatButtonModule,
18
+ MatTooltipModule,
19
+ LanguageSelectorComponent,
20
+ ThemeSelectorComponent,
21
+ ],
22
+ templateUrl: './<%= name %>.component.html',
23
+ styleUrls: ['./<%= name %>.component.scss'],
24
+ changeDetection: ChangeDetectionStrategy.OnPush
25
+ })
26
+ export class <%= pascalName %>Component {
27
+ protected readonly authService = inject(AuthService);
28
+
29
+ readonly toggleSidenav = output<void>();
30
+
31
+ onToggleSidenav(): void {
32
+ this.toggleSidenav.emit();
33
+ }
34
+
35
+ onLogout(): void {
36
+ this.authService.logout();
37
+ }
38
+ }
@@ -0,0 +1,26 @@
1
+ import { Injectable } from '@angular/core';
2
+
3
+ @Injectable({
4
+ providedIn: 'root'
5
+ })
6
+ export class <%= pascalName %>Service {
7
+ private prefix = '[<%= pascalName %>]';
8
+
9
+ constructor() {}
10
+
11
+ log(message: any, ...args: any[]) {
12
+ console.log(`${this.prefix} ${message}`, ...args);
13
+ }
14
+
15
+ error(message: any, ...args: any[]) {
16
+ console.error(`${this.prefix} ${message}`, ...args);
17
+ }
18
+
19
+ warn(message: any, ...args: any[]) {
20
+ console.warn(`${this.prefix} ${message}`, ...args);
21
+ }
22
+
23
+ info(message: any, ...args: any[]) {
24
+ console.info(`${this.prefix} ${message}`, ...args);
25
+ }
26
+ }
@@ -0,0 +1,49 @@
1
+ import { Component, inject, ChangeDetectionStrategy, signal, effect } from '@angular/core';
2
+ import { MatIconModule } from '@angular/material/icon';
3
+ import { MatButtonModule } from '@angular/material/button';
4
+ import { MatTooltipModule } from '@angular/material/tooltip';
5
+
6
+ export type Theme = 'light' | 'dark';
7
+
8
+ @Component({
9
+ selector: 'app-theme-selector',
10
+ imports: [MatIconModule, MatButtonModule, MatTooltipModule],
11
+ template: `
12
+ <button
13
+ mat-icon-button
14
+ [matTooltip]="isDark() ? 'Switch to light mode' : 'Switch to dark mode'"
15
+ (click)="toggleTheme()">
16
+ <mat-icon>{{ isDark() ? 'light_mode' : 'dark_mode' }}</mat-icon>
17
+ </button>
18
+ `,
19
+ styles: `
20
+ button {
21
+ color: inherit;
22
+ }
23
+ `,
24
+ changeDetection: ChangeDetectionStrategy.OnPush
25
+ })
26
+ export class ThemeSelectorComponent {
27
+ readonly isDark = signal(this.getInitialTheme() === 'dark');
28
+
29
+ constructor() {
30
+ effect(() => {
31
+ this.applyTheme(this.isDark() ? 'dark' : 'light');
32
+ });
33
+ }
34
+
35
+ toggleTheme(): void {
36
+ this.isDark.update(dark => !dark);
37
+ }
38
+
39
+ private getInitialTheme(): Theme {
40
+ const stored = localStorage.getItem('theme') as Theme;
41
+ if (stored) return stored;
42
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
43
+ }
44
+
45
+ private applyTheme(theme: Theme): void {
46
+ document.documentElement.setAttribute('data-theme', theme);
47
+ localStorage.setItem('theme', theme);
48
+ }
49
+ }
@@ -0,0 +1,15 @@
1
+ import { Pipe, PipeTransform, inject } from '@angular/core';
2
+ import { TranslateService } from './translate.service';
3
+
4
+ @Pipe({
5
+ name: 'translate',
6
+ standalone: true,
7
+ pure: false
8
+ })
9
+ export class TranslatePipe implements PipeTransform {
10
+ private translateService = inject(TranslateService);
11
+
12
+ transform(value: string): string {
13
+ return this.translateService.get(value);
14
+ }
15
+ }
@@ -0,0 +1,24 @@
1
+ import { Injectable, signal } from '@angular/core';
2
+
3
+ @Injectable({
4
+ providedIn: 'root'
5
+ })
6
+ export class TranslateService {
7
+ public currentLang = signal('en');
8
+ private translations: Record<string, any> = {};
9
+
10
+ constructor() {}
11
+
12
+ setLanguage(lang: string) {
13
+ this.currentLang.set(lang);
14
+ // Here you would typically load the translation file for the language
15
+ }
16
+
17
+ get(key: string): string {
18
+ return this.translations[key] || key;
19
+ }
20
+
21
+ instant(key: string): string {
22
+ return this.get(key);
23
+ }
24
+ }
@@ -0,0 +1,7 @@
1
+ import { Tree } from '@nx/devkit';
2
+ export declare function coreGenerator(tree: Tree, options: CoreGeneratorSchema): Promise<void>;
3
+ export default coreGenerator;
4
+ export interface CoreGeneratorSchema {
5
+ name?: string;
6
+ type: 'all' | 'auth' | 'guard' | 'interceptor' | 'service' | 'translate' | 'language-selector' | 'theme-selector' | 'menu' | 'navbar';
7
+ }
@@ -0,0 +1,49 @@
1
+ import { formatFiles, generateFiles, joinPathFragments, } from '@nx/devkit';
2
+ import { toPascalCase, toCamelCase } from '../../utils/string-utils';
3
+ export async function coreGenerator(tree, options) {
4
+ // Default to 'all' if no type specified
5
+ if (!options.type) {
6
+ options.type = 'all';
7
+ }
8
+ if (options.type === 'all') {
9
+ const coreTypes = [
10
+ 'navbar',
11
+ 'menu',
12
+ 'theme-selector',
13
+ 'language-selector',
14
+ 'translate',
15
+ 'auth',
16
+ ];
17
+ console.log(`\n🚀 Generating ALL core assets: ${coreTypes.join(', ')}...`);
18
+ for (const type of coreTypes) {
19
+ // Use provided name or default to the type name (e.g. 'navbar')
20
+ const name = options.name || type;
21
+ await coreGenerator(tree, { ...options, type: type, name });
22
+ }
23
+ return;
24
+ }
25
+ // Ensure name is present for specific types (default to type name if missing)
26
+ if (!options.name) {
27
+ options.name = options.type;
28
+ }
29
+ const targetPath = options.type === 'auth' ||
30
+ options.type === 'translate' ||
31
+ options.type === 'language-selector' ||
32
+ options.type === 'theme-selector' ||
33
+ options.type === 'menu' ||
34
+ options.type === 'navbar'
35
+ ? joinPathFragments('apps/sandbox/src/app/core', options.type)
36
+ : joinPathFragments('apps/sandbox/src/app/core', options.type, options.name);
37
+ const pascalName = toPascalCase(options.name);
38
+ const camelName = toCamelCase(options.name);
39
+ generateFiles(tree, joinPathFragments(__dirname, 'files', options.type), targetPath, {
40
+ ...options,
41
+ pascalName,
42
+ camelName,
43
+ tmpl: '',
44
+ });
45
+ await formatFiles(tree);
46
+ console.log(`✓ Generated core ${options.type} "${options.name}" in ${targetPath}`);
47
+ }
48
+ export default coreGenerator;
49
+ //# sourceMappingURL=generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.js","sourceRoot":"","sources":["../../../../../../apps/cli/src/generators/core/generator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,aAAa,EACb,iBAAiB,GAElB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAErE,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAU,EAAE,OAA4B;IAC1E,wCAAwC;IACxC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAClB,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC;IACvB,CAAC;IAED,IAAI,OAAO,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QAC3B,MAAM,SAAS,GAAG;YAChB,QAAQ;YACR,MAAM;YACN,gBAAgB;YAChB,mBAAmB;YACnB,WAAW;YACX,MAAM;SACP,CAAC;QACF,OAAO,CAAC,GAAG,CAAC,oCAAoC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAE3E,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,gEAAgE;YAChE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC;YAClC,MAAM,aAAa,CAAC,IAAI,EAAE,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,IAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QACrE,CAAC;QACD,OAAO;IACT,CAAC;IAED,8EAA8E;IAC9E,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAClB,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC9B,CAAC;IAED,MAAM,UAAU,GACd,OAAO,CAAC,IAAI,KAAK,MAAM;QACvB,OAAO,CAAC,IAAI,KAAK,WAAW;QAC5B,OAAO,CAAC,IAAI,KAAK,mBAAmB;QACpC,OAAO,CAAC,IAAI,KAAK,gBAAgB;QACjC,OAAO,CAAC,IAAI,KAAK,MAAM;QACvB,OAAO,CAAC,IAAI,KAAK,QAAQ;QACvB,CAAC,CAAC,iBAAiB,CAAC,2BAA2B,EAAE,OAAO,CAAC,IAAI,CAAC;QAC9D,CAAC,CAAC,iBAAiB,CACf,2BAA2B,EAC3B,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,IAAI,CACb,CAAC;IAER,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,IAAK,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,WAAW,CAAC,OAAO,CAAC,IAAK,CAAC,CAAC;IAE7C,aAAa,CACX,IAAI,EACJ,iBAAiB,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,EACnD,UAAU,EACV;QACE,GAAG,OAAO;QACV,UAAU;QACV,SAAS;QACT,IAAI,EAAE,EAAE;KACT,CACF,CAAC;IAEF,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;IAExB,OAAO,CAAC,GAAG,CACT,oBAAoB,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,QAAQ,UAAU,EAAE,CACtE,CAAC;AACJ,CAAC;AAED,eAAe,aAAa,CAAC"}
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "CleanCoreGenerator",
4
+ "title": "Clean Core Generator",
5
+ "type": "object",
6
+ "properties": {
7
+ "name": {
8
+ "type": "string",
9
+ "description": "Name of the core asset",
10
+ "$default": {
11
+ "$source": "argv",
12
+ "index": 0
13
+ }
14
+ },
15
+ "type": {
16
+ "type": "string",
17
+ "description": "Type of core asset (defaults to 'all' if not specified)",
18
+ "enum": [
19
+ "all",
20
+ "auth",
21
+ "guard",
22
+ "interceptor",
23
+ "service",
24
+ "translate",
25
+ "language-selector",
26
+ "theme-selector",
27
+ "menu",
28
+ "navbar"
29
+ ],
30
+ "default": "all"
31
+ }
32
+ },
33
+ "required": []
34
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Clean Feature Blueprint",
4
+ "type": "object",
5
+ "properties": {
6
+ "name": {
7
+ "type": "string",
8
+ "description": "The name of the feature (e.g., product, user-profile)"
9
+ },
10
+ "models": {
11
+ "type": "array",
12
+ "items": {
13
+ "type": "object",
14
+ "properties": {
15
+ "name": { "type": "string" },
16
+ "attributes": {
17
+ "type": "array",
18
+ "items": {
19
+ "type": "object",
20
+ "properties": {
21
+ "name": { "type": "string" },
22
+ "type": { "type": "string" },
23
+ "required": { "type": "boolean", "default": true }
24
+ },
25
+ "required": ["name", "type"]
26
+ }
27
+ }
28
+ },
29
+ "required": ["name"]
30
+ }
31
+ }
32
+ },
33
+ "required": ["name"]
34
+ }
@@ -0,0 +1,135 @@
1
+ import { signalStore, withState, withMethods, withComputed, patchState } from '@ngrx/signals';
2
+ import { computed, inject } from '@angular/core';
3
+ import { rxMethod } from '@ngrx/signals/rxjs-interop';
4
+ import { pipe, tap, switchMap, catchError, of } from 'rxjs';
5
+ import { tapResponse } from '@ngrx/operators';
6
+ import { <%= pascalName %>Service } from '../infrastructure/service';
7
+ import { <%= pascalName %> } from '../domain/model';
8
+
9
+ interface <%= pascalName %>State {
10
+ items: <%= pascalName %>[];
11
+ loading: boolean;
12
+ error: string | null;
13
+ filter: string;
14
+ sortField: keyof <%= pascalName %>;
15
+ sortDirection: 'asc' | 'desc';
16
+ }
17
+
18
+ export const <%= pascalName %>Store = signalStore(
19
+ { providedIn: 'root' },
20
+ withState<<%= pascalName %>State>({
21
+ items: [],
22
+ loading: false,
23
+ error: null,
24
+ filter: '',
25
+ sortField: 'createdAt',
26
+ sortDirection: 'desc'
27
+ }),
28
+ withComputed((state) => ({
29
+ filteredAndSortedItems: computed(() => {
30
+ let items = [...state.items()];
31
+
32
+ // Apply filter
33
+ const filter = state.filter().toLowerCase();
34
+ if (filter) {
35
+ items = items.filter(item =>
36
+ Object.values(item).some(val =>
37
+ String(val).toLowerCase().includes(filter)
38
+ )
39
+ );
40
+ }
41
+
42
+ // Apply sort
43
+ const field = state.sortField();
44
+ const direction = state.sortDirection();
45
+ items.sort((a, b) => {
46
+ const aVal = a[field];
47
+ const bVal = b[field];
48
+ const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
49
+ return direction === 'asc' ? comparison : -comparison;
50
+ });
51
+
52
+ return items;
53
+ })
54
+ })),
55
+ withMethods((store, service = inject(<%= pascalName %>Service)) => ({
56
+ loadAll: rxMethod<void>(
57
+ pipe(
58
+ tap(() => patchState(store, { loading: true, error: null })),
59
+ switchMap(() =>
60
+ service.getAll().pipe(
61
+ tapResponse({
62
+ next: (items) => patchState(store, { items, loading: false }),
63
+ error: (error: Error) =>
64
+ patchState(store, { error: error.message, loading: false })
65
+ })
66
+ )
67
+ )
68
+ )
69
+ ),
70
+
71
+ create: rxMethod<Omit<<%= pascalName %>, 'id' | 'createdAt' | 'updatedAt'>>(
72
+ pipe(
73
+ tap(() => patchState(store, { loading: true, error: null })),
74
+ switchMap((data) =>
75
+ service.create(data).pipe(
76
+ tapResponse({
77
+ next: (item) =>
78
+ patchState(store, (state) => ({
79
+ items: [...state.items, item],
80
+ loading: false
81
+ })),
82
+ error: (error: Error) =>
83
+ patchState(store, { error: error.message, loading: false })
84
+ })
85
+ )
86
+ )
87
+ )
88
+ ),
89
+
90
+ update: rxMethod<{ id: string; data: Partial<<%= pascalName %>> }>(
91
+ pipe(
92
+ tap(() => patchState(store, { loading: true, error: null })),
93
+ switchMap(({ id, data }) =>
94
+ service.update(id, data).pipe(
95
+ tapResponse({
96
+ next: (updated) =>
97
+ patchState(store, (state) => ({
98
+ items: state.items.map((item) =>
99
+ item.id === id ? updated : item
100
+ ),
101
+ loading: false
102
+ })),
103
+ error: (error: Error) =>
104
+ patchState(store, { error: error.message, loading: false })
105
+ })
106
+ )
107
+ )
108
+ )
109
+ ),
110
+
111
+ delete: rxMethod<string>(
112
+ pipe(
113
+ tap(() => patchState(store, { loading: true, error: null })),
114
+ switchMap((id) =>
115
+ service.delete(id).pipe(
116
+ tapResponse({
117
+ next: () =>
118
+ patchState(store, (state) => ({
119
+ items: state.items.filter((item) => item.id !== id),
120
+ loading: false
121
+ })),
122
+ error: (error: Error) =>
123
+ patchState(store, { error: error.message, loading: false })
124
+ })
125
+ )
126
+ )
127
+ )
128
+ ),
129
+
130
+ setFilter: (filter: string) => patchState(store, { filter }),
131
+
132
+ setSort: (field: keyof <%= pascalName %>, direction: 'asc' | 'desc') =>
133
+ patchState(store, { sortField: field, sortDirection: direction })
134
+ }))
135
+ );
@@ -0,0 +1,9 @@
1
+ <% models.forEach(function(model) { %>
2
+ export interface <%= model.name %> {
3
+ id: string;
4
+ <% model.attributes.forEach(function(attr) { %> <%= attr.name %>: <%= attr.type %>;
5
+ <% }); %> createdAt: Date;
6
+ updatedAt: Date;
7
+ }
8
+
9
+ <% }); %>
@@ -1,13 +1,13 @@
1
- import { Injectable } from '@angular/core';
1
+ import { Injectable, inject } from '@angular/core';
2
2
  import { HttpClient } from '@angular/common/http';
3
3
  import { Observable } from 'rxjs';
4
4
  import { <%= pascalName %> } from '../domain/model';
5
+ import { environment } from '../../../../environments/environment';
5
6
 
6
7
  @Injectable({ providedIn: 'root' })
7
8
  export class <%= pascalName %>Service {
8
- private readonly apiUrl = '/api/<%= name %>s';
9
-
10
- constructor(private http: HttpClient) {}
9
+ private readonly http = inject(HttpClient);
10
+ private readonly apiUrl = `${environment.apiUrl}/features/<%= name %>`;
11
11
 
12
12
  getAll(): Observable<<%= pascalName %>[]> {
13
13
  return this.http.get<<%= pascalName %>[]>(this.apiUrl);
@@ -17,7 +17,7 @@ export class <%= pascalName %>Service {
17
17
  return this.http.get<<%= pascalName %>>(`${this.apiUrl}/${id}`);
18
18
  }
19
19
 
20
- create(data: Omit<<%= pascalName %>, 'id'>): Observable<<%= pascalName %>> {
20
+ create(data: Omit<<%= pascalName %>, 'id' | 'createdAt' | 'updatedAt'>): Observable<<%= pascalName %>> {
21
21
  return this.http.post<<%= pascalName %>>(this.apiUrl, data);
22
22
  }
23
23