@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.
- package/README.md +425 -0
- package/bin/index.js +100 -2
- package/generators.json +19 -9
- package/package.json +1 -1
- package/src/generators/core/files/auth/auth.guard.ts.template +14 -0
- package/src/generators/core/files/auth/auth.service.ts.template +137 -0
- package/src/generators/core/files/auth/login.component.ts.template +165 -0
- package/src/generators/core/files/auth/signup.component.ts.template +171 -0
- package/src/generators/core/files/guard/__name__.guard.ts.template +6 -0
- package/src/generators/core/files/interceptor/__name__.interceptor.ts.template +6 -0
- package/src/generators/core/files/language-selector/language-selector.component.ts.template +82 -0
- package/src/generators/core/files/menu/__name__.component.html.template +14 -0
- package/src/generators/core/files/menu/__name__.component.scss.template +66 -0
- package/src/generators/core/files/menu/__name__.component.ts.template +24 -0
- package/src/generators/core/files/navbar/__name__.component.html.template +45 -0
- package/src/generators/core/files/navbar/__name__.component.scss.template +134 -0
- package/src/generators/core/files/navbar/__name__.component.ts.template +38 -0
- package/src/generators/core/files/service/__name__.service.ts.template +26 -0
- package/src/generators/core/files/theme-selector/theme-selector.component.ts.template +49 -0
- package/src/generators/core/files/translate/translate.pipe.ts.template +15 -0
- package/src/generators/core/files/translate/translate.service.ts.template +24 -0
- package/src/generators/core/generator.d.ts +7 -0
- package/src/generators/core/generator.js +49 -0
- package/src/generators/core/generator.js.map +1 -0
- package/src/generators/core/schema.json +34 -0
- package/src/generators/feature/blueprint.schema.json +34 -0
- package/src/generators/feature/files/application/store.ts.template +135 -0
- package/src/generators/feature/files/domain/model.ts.template +9 -0
- package/src/generators/{clean-feature → feature}/files/infrastructure/service.ts.template +5 -5
- package/src/generators/feature/files/ui/__singularName__.component.html.template +109 -0
- package/src/generators/feature/files/ui/__singularName__.component.scss.template +162 -0
- package/src/generators/feature/files/ui/__singularName__.component.ts.template +131 -0
- package/src/generators/feature/files/ui/_theme.scss.template +35 -0
- package/src/generators/feature/files/ui/form/form.component.ts.template +122 -0
- package/src/generators/feature/generator.d.ts +4 -0
- package/src/generators/feature/generator.js +209 -0
- package/src/generators/feature/generator.js.map +1 -0
- package/src/generators/feature/schema.d.ts +5 -0
- package/src/generators/{clean-feature → feature}/schema.json +25 -21
- package/src/generators/shared/files/ui/__name__.component.ts.template +57 -0
- package/src/generators/shared/files/util/__name__.ts.template +7 -0
- package/src/generators/shared/generator.d.ts +7 -0
- package/src/generators/shared/generator.js +31 -0
- package/src/generators/shared/generator.js.map +1 -0
- package/src/generators/shared/schema.json +23 -0
- package/src/index.js +1 -0
- package/src/utils/string-utils.d.ts +16 -0
- package/src/utils/string-utils.js +33 -0
- package/src/utils/string-utils.js.map +1 -0
- package/src/generators/clean-feature/files/application/store.ts.template +0 -6
- package/src/generators/clean-feature/files/domain/model.ts.template +0 -4
- package/src/generators/clean-feature/files/ui/component.ts.template +0 -44
- package/src/generators/clean-feature/generator.d.ts +0 -4
- package/src/generators/clean-feature/generator.js +0 -89
- package/src/generators/clean-feature/generator.js.map +0 -1
- 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
|
+
);
|
|
@@ -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
|
|
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
|
|