@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,137 @@
|
|
|
1
|
+
import { Injectable, signal, inject, computed } from '@angular/core';
|
|
2
|
+
import { HttpClient } from '@angular/common/http';
|
|
3
|
+
import { Router } from '@angular/router';
|
|
4
|
+
import { catchError, tap } from 'rxjs/operators';
|
|
5
|
+
import { of } from 'rxjs';
|
|
6
|
+
import { environment } from '../../../environments/environment';
|
|
7
|
+
|
|
8
|
+
export interface User {
|
|
9
|
+
id: string;
|
|
10
|
+
email: string;
|
|
11
|
+
name: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LoginResponse {
|
|
15
|
+
access_token: string;
|
|
16
|
+
user: User;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const TOKEN_KEY = 'auth_token';
|
|
20
|
+
const USER_KEY = 'auth_user';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Auth Service - handles authentication with backend API
|
|
24
|
+
* Uses signals for reactive state management (Angular 21 zoneless compatible)
|
|
25
|
+
*/
|
|
26
|
+
@Injectable({
|
|
27
|
+
providedIn: 'root'
|
|
28
|
+
})
|
|
29
|
+
export class AuthService {
|
|
30
|
+
private readonly http = inject(HttpClient);
|
|
31
|
+
private readonly router = inject(Router);
|
|
32
|
+
private readonly apiUrl = `${environment.apiUrl}/auth`;
|
|
33
|
+
|
|
34
|
+
// State signals
|
|
35
|
+
private readonly _currentUser = signal<User | null>(this.loadUserFromStorage());
|
|
36
|
+
private readonly _token = signal<string | null>(this.loadTokenFromStorage());
|
|
37
|
+
private readonly _loading = signal(false);
|
|
38
|
+
private readonly _error = signal<string | null>(null);
|
|
39
|
+
|
|
40
|
+
// Public readonly signals
|
|
41
|
+
readonly currentUser = this._currentUser.asReadonly();
|
|
42
|
+
readonly token = this._token.asReadonly();
|
|
43
|
+
readonly loading = this._loading.asReadonly();
|
|
44
|
+
readonly error = this._error.asReadonly();
|
|
45
|
+
|
|
46
|
+
// Computed signal for authentication status
|
|
47
|
+
readonly isAuthenticated = computed(() => !!this._token() && !!this._currentUser());
|
|
48
|
+
|
|
49
|
+
login(credentials: { email: string; password: string }) {
|
|
50
|
+
this._loading.set(true);
|
|
51
|
+
this._error.set(null);
|
|
52
|
+
|
|
53
|
+
return this.http.post<LoginResponse>(`${this.apiUrl}/login`, credentials).pipe(
|
|
54
|
+
tap(response => {
|
|
55
|
+
this.setSession(response);
|
|
56
|
+
this._loading.set(false);
|
|
57
|
+
}),
|
|
58
|
+
catchError(error => {
|
|
59
|
+
this._error.set(error.error?.message || 'Login failed');
|
|
60
|
+
this._loading.set(false);
|
|
61
|
+
return of(null);
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
signup(userData: { name: string; email: string; password: string }) {
|
|
67
|
+
this._loading.set(true);
|
|
68
|
+
this._error.set(null);
|
|
69
|
+
|
|
70
|
+
return this.http.post<LoginResponse>(`${this.apiUrl}/signup`, userData).pipe(
|
|
71
|
+
tap(response => {
|
|
72
|
+
this.setSession(response);
|
|
73
|
+
this._loading.set(false);
|
|
74
|
+
}),
|
|
75
|
+
catchError(error => {
|
|
76
|
+
this._error.set(error.error?.message || 'Signup failed');
|
|
77
|
+
this._loading.set(false);
|
|
78
|
+
return of(null);
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
logout() {
|
|
84
|
+
this.http.post(`${this.apiUrl}/logout`, {}).subscribe();
|
|
85
|
+
this.clearSession();
|
|
86
|
+
this.router.navigate(['/login']);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
refreshUser() {
|
|
90
|
+
if (!this._token()) return;
|
|
91
|
+
|
|
92
|
+
this.http.get<User>(`${this.apiUrl}/me`).pipe(
|
|
93
|
+
catchError(() => {
|
|
94
|
+
this.clearSession();
|
|
95
|
+
return of(null);
|
|
96
|
+
})
|
|
97
|
+
).subscribe(user => {
|
|
98
|
+
if (user) {
|
|
99
|
+
this._currentUser.set(user);
|
|
100
|
+
this.saveUserToStorage(user);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private setSession(response: LoginResponse) {
|
|
106
|
+
this._token.set(response.access_token);
|
|
107
|
+
this._currentUser.set(response.user);
|
|
108
|
+
this.saveTokenToStorage(response.access_token);
|
|
109
|
+
this.saveUserToStorage(response.user);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private clearSession() {
|
|
113
|
+
this._token.set(null);
|
|
114
|
+
this._currentUser.set(null);
|
|
115
|
+
localStorage.removeItem(TOKEN_KEY);
|
|
116
|
+
localStorage.removeItem(USER_KEY);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private loadTokenFromStorage(): string | null {
|
|
120
|
+
if (typeof localStorage === 'undefined') return null;
|
|
121
|
+
return localStorage.getItem(TOKEN_KEY);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private loadUserFromStorage(): User | null {
|
|
125
|
+
if (typeof localStorage === 'undefined') return null;
|
|
126
|
+
const userJson = localStorage.getItem(USER_KEY);
|
|
127
|
+
return userJson ? JSON.parse(userJson) : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private saveTokenToStorage(token: string) {
|
|
131
|
+
localStorage.setItem(TOKEN_KEY, token);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private saveUserToStorage(user: User) {
|
|
135
|
+
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Component, inject, ChangeDetectionStrategy, signal } from '@angular/core';
|
|
2
|
+
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
3
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
4
|
+
import { MatInputModule } from '@angular/material/input';
|
|
5
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
6
|
+
import { MatCardModule } from '@angular/material/card';
|
|
7
|
+
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
8
|
+
import { RouterLink } from '@angular/router';
|
|
9
|
+
import { AuthService } from './auth.service';
|
|
10
|
+
import { Router } from '@angular/router';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Login component - connects to backend API
|
|
14
|
+
*/
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'app-login',
|
|
17
|
+
imports: [
|
|
18
|
+
ReactiveFormsModule,
|
|
19
|
+
MatFormFieldModule,
|
|
20
|
+
MatInputModule,
|
|
21
|
+
MatButtonModule,
|
|
22
|
+
MatCardModule,
|
|
23
|
+
MatProgressSpinnerModule,
|
|
24
|
+
RouterLink
|
|
25
|
+
],
|
|
26
|
+
template: `
|
|
27
|
+
@let emailErrors = loginForm.get('email')?.errors;
|
|
28
|
+
@let passwordErrors = loginForm.get('password')?.errors;
|
|
29
|
+
@let isSubmitDisabled = !formValid() || authService.loading();
|
|
30
|
+
|
|
31
|
+
<div class="auth-container">
|
|
32
|
+
<mat-card class="auth-card">
|
|
33
|
+
<mat-card-header>
|
|
34
|
+
<mat-card-title>Login</mat-card-title>
|
|
35
|
+
<mat-card-subtitle>Sign in to your account</mat-card-subtitle>
|
|
36
|
+
</mat-card-header>
|
|
37
|
+
|
|
38
|
+
<mat-card-content>
|
|
39
|
+
@if (authService.error()) {
|
|
40
|
+
<div class="error-banner">{{ authService.error() }}</div>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
|
|
44
|
+
<mat-form-field appearance="outline">
|
|
45
|
+
<mat-label>Email</mat-label>
|
|
46
|
+
<input matInput formControlName="email" type="email" placeholder="Enter your email">
|
|
47
|
+
@if (emailErrors?.['required']) {
|
|
48
|
+
<mat-error>Email is required</mat-error>
|
|
49
|
+
}
|
|
50
|
+
@if (emailErrors?.['email']) {
|
|
51
|
+
<mat-error>Please enter a valid email</mat-error>
|
|
52
|
+
}
|
|
53
|
+
</mat-form-field>
|
|
54
|
+
|
|
55
|
+
<mat-form-field appearance="outline">
|
|
56
|
+
<mat-label>Password</mat-label>
|
|
57
|
+
<input matInput formControlName="password" type="password" placeholder="Enter your password">
|
|
58
|
+
@if (passwordErrors?.['required']) {
|
|
59
|
+
<mat-error>Password is required</mat-error>
|
|
60
|
+
}
|
|
61
|
+
</mat-form-field>
|
|
62
|
+
|
|
63
|
+
<button
|
|
64
|
+
mat-raised-button
|
|
65
|
+
color="primary"
|
|
66
|
+
type="submit"
|
|
67
|
+
[disabled]="isSubmitDisabled"
|
|
68
|
+
class="submit-btn">
|
|
69
|
+
@if (authService.loading()) {
|
|
70
|
+
<mat-spinner diameter="20"></mat-spinner>
|
|
71
|
+
<span>Logging in...</span>
|
|
72
|
+
} @else {
|
|
73
|
+
Login
|
|
74
|
+
}
|
|
75
|
+
</button>
|
|
76
|
+
</form>
|
|
77
|
+
|
|
78
|
+
<div class="auth-footer">
|
|
79
|
+
<p>Don't have an account? <a routerLink="/signup">Sign up</a></p>
|
|
80
|
+
<p class="demo-hint">Demo: admin@example.com / admin123</p>
|
|
81
|
+
</div>
|
|
82
|
+
</mat-card-content>
|
|
83
|
+
</mat-card>
|
|
84
|
+
</div>
|
|
85
|
+
`,
|
|
86
|
+
styles: `
|
|
87
|
+
.auth-container {
|
|
88
|
+
display: flex;
|
|
89
|
+
justify-content: center;
|
|
90
|
+
align-items: center;
|
|
91
|
+
min-height: 80vh;
|
|
92
|
+
padding: 1rem;
|
|
93
|
+
}
|
|
94
|
+
.auth-card {
|
|
95
|
+
width: 100%;
|
|
96
|
+
max-width: 400px;
|
|
97
|
+
}
|
|
98
|
+
form {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
gap: 16px;
|
|
102
|
+
margin-top: 16px;
|
|
103
|
+
}
|
|
104
|
+
.submit-btn {
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
justify-content: center;
|
|
108
|
+
gap: 8px;
|
|
109
|
+
height: 48px;
|
|
110
|
+
}
|
|
111
|
+
.error-banner {
|
|
112
|
+
background-color: #f44336;
|
|
113
|
+
color: white;
|
|
114
|
+
padding: 12px;
|
|
115
|
+
border-radius: 4px;
|
|
116
|
+
margin-bottom: 16px;
|
|
117
|
+
}
|
|
118
|
+
.auth-footer {
|
|
119
|
+
margin-top: 24px;
|
|
120
|
+
text-align: center;
|
|
121
|
+
|
|
122
|
+
a {
|
|
123
|
+
color: var(--color-primary);
|
|
124
|
+
text-decoration: none;
|
|
125
|
+
font-weight: 500;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.demo-hint {
|
|
129
|
+
margin-top: 8px;
|
|
130
|
+
font-size: 0.85rem;
|
|
131
|
+
color: var(--color-text-secondary);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
`,
|
|
135
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
136
|
+
})
|
|
137
|
+
export class LoginComponent {
|
|
138
|
+
private readonly fb = inject(FormBuilder);
|
|
139
|
+
protected readonly authService = inject(AuthService);
|
|
140
|
+
private readonly router = inject(Router);
|
|
141
|
+
|
|
142
|
+
readonly formValid = signal(false);
|
|
143
|
+
|
|
144
|
+
readonly loginForm = this.fb.group({
|
|
145
|
+
email: ['', [Validators.required, Validators.email]],
|
|
146
|
+
password: ['', [Validators.required]]
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
constructor() {
|
|
150
|
+
this.loginForm.statusChanges.subscribe(() => {
|
|
151
|
+
this.formValid.set(this.loginForm.valid);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
onSubmit(): void {
|
|
156
|
+
if (this.loginForm.valid) {
|
|
157
|
+
const { email, password } = this.loginForm.value;
|
|
158
|
+
this.authService.login({ email: email!, password: password! }).subscribe(response => {
|
|
159
|
+
if (response) {
|
|
160
|
+
this.router.navigate(['/']);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Component, inject, ChangeDetectionStrategy, signal } from '@angular/core';
|
|
2
|
+
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
3
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
4
|
+
import { MatInputModule } from '@angular/material/input';
|
|
5
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
6
|
+
import { MatCardModule } from '@angular/material/card';
|
|
7
|
+
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
8
|
+
import { RouterLink } from '@angular/router';
|
|
9
|
+
import { AuthService } from './auth.service';
|
|
10
|
+
import { Router } from '@angular/router';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Signup component - connects to backend API
|
|
14
|
+
*/
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'app-signup',
|
|
17
|
+
imports: [
|
|
18
|
+
ReactiveFormsModule,
|
|
19
|
+
MatFormFieldModule,
|
|
20
|
+
MatInputModule,
|
|
21
|
+
MatButtonModule,
|
|
22
|
+
MatCardModule,
|
|
23
|
+
MatProgressSpinnerModule,
|
|
24
|
+
RouterLink
|
|
25
|
+
],
|
|
26
|
+
template: `
|
|
27
|
+
@let nameErrors = signupForm.get('name')?.errors;
|
|
28
|
+
@let emailErrors = signupForm.get('email')?.errors;
|
|
29
|
+
@let passwordErrors = signupForm.get('password')?.errors;
|
|
30
|
+
@let isSubmitDisabled = !formValid() || authService.loading();
|
|
31
|
+
|
|
32
|
+
<div class="auth-container">
|
|
33
|
+
<mat-card class="auth-card">
|
|
34
|
+
<mat-card-header>
|
|
35
|
+
<mat-card-title>Sign Up</mat-card-title>
|
|
36
|
+
<mat-card-subtitle>Create a new account</mat-card-subtitle>
|
|
37
|
+
</mat-card-header>
|
|
38
|
+
|
|
39
|
+
<mat-card-content>
|
|
40
|
+
@if (authService.error()) {
|
|
41
|
+
<div class="error-banner">{{ authService.error() }}</div>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
|
|
45
|
+
<mat-form-field appearance="outline">
|
|
46
|
+
<mat-label>Name</mat-label>
|
|
47
|
+
<input matInput formControlName="name" placeholder="Enter your name">
|
|
48
|
+
@if (nameErrors?.['required']) {
|
|
49
|
+
<mat-error>Name is required</mat-error>
|
|
50
|
+
}
|
|
51
|
+
</mat-form-field>
|
|
52
|
+
|
|
53
|
+
<mat-form-field appearance="outline">
|
|
54
|
+
<mat-label>Email</mat-label>
|
|
55
|
+
<input matInput formControlName="email" type="email" placeholder="Enter your email">
|
|
56
|
+
@if (emailErrors?.['required']) {
|
|
57
|
+
<mat-error>Email is required</mat-error>
|
|
58
|
+
}
|
|
59
|
+
@if (emailErrors?.['email']) {
|
|
60
|
+
<mat-error>Please enter a valid email</mat-error>
|
|
61
|
+
}
|
|
62
|
+
</mat-form-field>
|
|
63
|
+
|
|
64
|
+
<mat-form-field appearance="outline">
|
|
65
|
+
<mat-label>Password</mat-label>
|
|
66
|
+
<input matInput formControlName="password" type="password" placeholder="Enter your password">
|
|
67
|
+
@if (passwordErrors?.['required']) {
|
|
68
|
+
<mat-error>Password is required</mat-error>
|
|
69
|
+
}
|
|
70
|
+
@if (passwordErrors?.['minlength']) {
|
|
71
|
+
<mat-error>Password must be at least 6 characters</mat-error>
|
|
72
|
+
}
|
|
73
|
+
</mat-form-field>
|
|
74
|
+
|
|
75
|
+
<button
|
|
76
|
+
mat-raised-button
|
|
77
|
+
color="primary"
|
|
78
|
+
type="submit"
|
|
79
|
+
[disabled]="isSubmitDisabled"
|
|
80
|
+
class="submit-btn">
|
|
81
|
+
@if (authService.loading()) {
|
|
82
|
+
<mat-spinner diameter="20"></mat-spinner>
|
|
83
|
+
<span>Creating account...</span>
|
|
84
|
+
} @else {
|
|
85
|
+
Sign Up
|
|
86
|
+
}
|
|
87
|
+
</button>
|
|
88
|
+
</form>
|
|
89
|
+
|
|
90
|
+
<div class="auth-footer">
|
|
91
|
+
<p>Already have an account? <a routerLink="/login">Login</a></p>
|
|
92
|
+
</div>
|
|
93
|
+
</mat-card-content>
|
|
94
|
+
</mat-card>
|
|
95
|
+
</div>
|
|
96
|
+
`,
|
|
97
|
+
styles: `
|
|
98
|
+
.auth-container {
|
|
99
|
+
display: flex;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
align-items: center;
|
|
102
|
+
min-height: 80vh;
|
|
103
|
+
padding: 1rem;
|
|
104
|
+
}
|
|
105
|
+
.auth-card {
|
|
106
|
+
width: 100%;
|
|
107
|
+
max-width: 400px;
|
|
108
|
+
}
|
|
109
|
+
form {
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-direction: column;
|
|
112
|
+
gap: 16px;
|
|
113
|
+
margin-top: 16px;
|
|
114
|
+
}
|
|
115
|
+
.submit-btn {
|
|
116
|
+
display: flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
justify-content: center;
|
|
119
|
+
gap: 8px;
|
|
120
|
+
height: 48px;
|
|
121
|
+
}
|
|
122
|
+
.error-banner {
|
|
123
|
+
background-color: #f44336;
|
|
124
|
+
color: white;
|
|
125
|
+
padding: 12px;
|
|
126
|
+
border-radius: 4px;
|
|
127
|
+
margin-bottom: 16px;
|
|
128
|
+
}
|
|
129
|
+
.auth-footer {
|
|
130
|
+
margin-top: 24px;
|
|
131
|
+
text-align: center;
|
|
132
|
+
|
|
133
|
+
a {
|
|
134
|
+
color: var(--color-primary);
|
|
135
|
+
text-decoration: none;
|
|
136
|
+
font-weight: 500;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
`,
|
|
140
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
141
|
+
})
|
|
142
|
+
export class SignupComponent {
|
|
143
|
+
private readonly fb = inject(FormBuilder);
|
|
144
|
+
protected readonly authService = inject(AuthService);
|
|
145
|
+
private readonly router = inject(Router);
|
|
146
|
+
|
|
147
|
+
readonly formValid = signal(false);
|
|
148
|
+
|
|
149
|
+
readonly signupForm = this.fb.group({
|
|
150
|
+
name: ['', [Validators.required]],
|
|
151
|
+
email: ['', [Validators.required, Validators.email]],
|
|
152
|
+
password: ['', [Validators.required, Validators.minLength(6)]]
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
constructor() {
|
|
156
|
+
this.signupForm.statusChanges.subscribe(() => {
|
|
157
|
+
this.formValid.set(this.signupForm.valid);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
onSubmit(): void {
|
|
162
|
+
if (this.signupForm.valid) {
|
|
163
|
+
const { name, email, password } = this.signupForm.value;
|
|
164
|
+
this.authService.signup({ name: name!, email: email!, password: password! }).subscribe(response => {
|
|
165
|
+
if (response) {
|
|
166
|
+
this.router.navigate(['/']);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Component, inject, computed } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
4
|
+
import { MatMenuModule } from '@angular/material/menu';
|
|
5
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
6
|
+
import { TranslateService } from '../translate/translate.service';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
selector: 'app-language-selector',
|
|
10
|
+
imports: [CommonModule, MatButtonModule, MatMenuModule, MatIconModule],
|
|
11
|
+
template: `
|
|
12
|
+
<button mat-button [matMenuTriggerFor]="menu" class="lang-button">
|
|
13
|
+
<mat-icon>translate</mat-icon>
|
|
14
|
+
<span class="lang-label">{{ currentLangLabel() }}</span>
|
|
15
|
+
<mat-icon class="dropdown-icon">arrow_drop_down</mat-icon>
|
|
16
|
+
</button>
|
|
17
|
+
|
|
18
|
+
<mat-menu #menu="matMenu" xPosition="before">
|
|
19
|
+
@for (lang of languages; track lang.code) {
|
|
20
|
+
<button mat-menu-item (click)="changeLang(lang.code)" [class.active-lang]="translate.currentLang() === lang.code">
|
|
21
|
+
<mat-icon *ngIf="translate.currentLang() === lang.code">check</mat-icon>
|
|
22
|
+
<span>{{ lang.label }}</span>
|
|
23
|
+
</button>
|
|
24
|
+
}
|
|
25
|
+
</mat-menu>
|
|
26
|
+
`,
|
|
27
|
+
styles: [
|
|
28
|
+
`
|
|
29
|
+
.lang-button {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
padding: 0 12px;
|
|
33
|
+
height: 40px;
|
|
34
|
+
border-radius: 20px;
|
|
35
|
+
background: rgba(255, 255, 255, 0.1);
|
|
36
|
+
color: white;
|
|
37
|
+
transition: background 0.3s ease;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.lang-button:hover {
|
|
41
|
+
background: rgba(255, 255, 255, 0.2);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.lang-label {
|
|
45
|
+
margin: 0 4px;
|
|
46
|
+
font-weight: 500;
|
|
47
|
+
text-transform: uppercase;
|
|
48
|
+
font-size: 0.85rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.dropdown-icon {
|
|
52
|
+
margin: 0;
|
|
53
|
+
width: 18px;
|
|
54
|
+
height: 18px;
|
|
55
|
+
font-size: 18px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.active-lang {
|
|
59
|
+
color: var(--primary-color, #3f51b5);
|
|
60
|
+
font-weight: bold;
|
|
61
|
+
}
|
|
62
|
+
`,
|
|
63
|
+
],
|
|
64
|
+
})
|
|
65
|
+
export class LanguageSelectorComponent {
|
|
66
|
+
translate = inject(TranslateService);
|
|
67
|
+
|
|
68
|
+
languages = [
|
|
69
|
+
{ code: 'en', label: 'English' },
|
|
70
|
+
{ code: 'fr', label: 'Français' },
|
|
71
|
+
{ code: 'es', label: 'Español' },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
currentLangLabel = computed(() => {
|
|
75
|
+
const current = this.translate.currentLang();
|
|
76
|
+
return this.languages.find((l) => l.code === current)?.label || current;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
changeLang(lang: string) {
|
|
80
|
+
this.translate.setLanguage(lang);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<mat-nav-list>
|
|
2
|
+
<div mat-subheader>Features</div>
|
|
3
|
+
@for (feature of features; track feature.path) {
|
|
4
|
+
<a mat-list-item [routerLink]="['/', feature.path]" routerLinkActive="selected-nav">
|
|
5
|
+
<mat-icon matListItemIcon>extension</mat-icon>
|
|
6
|
+
<span matListItemTitle>{{ feature.label }}</span>
|
|
7
|
+
</a>
|
|
8
|
+
}
|
|
9
|
+
@if (features.length === 0) {
|
|
10
|
+
<div class="no-features-msg">
|
|
11
|
+
No features generated yet.
|
|
12
|
+
</div>
|
|
13
|
+
}
|
|
14
|
+
</mat-nav-list>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Theme variables
|
|
2
|
+
@use '@angular/material' as mat;
|
|
3
|
+
|
|
4
|
+
:host {
|
|
5
|
+
display: block;
|
|
6
|
+
padding: 8px 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Subheader styling (e.g. "Features")
|
|
10
|
+
div[mat-subheader] {
|
|
11
|
+
color: var(--color-text-secondary) !important;
|
|
12
|
+
font-weight: 700;
|
|
13
|
+
text-transform: uppercase;
|
|
14
|
+
font-size: 0.75rem;
|
|
15
|
+
letter-spacing: 1.2px;
|
|
16
|
+
line-height: 24px;
|
|
17
|
+
margin-bottom: 8px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// List item styling
|
|
21
|
+
a[mat-list-item] {
|
|
22
|
+
margin: 0 8px;
|
|
23
|
+
border-radius: 8px;
|
|
24
|
+
transition: all 0.2s ease;
|
|
25
|
+
height: 48px;
|
|
26
|
+
|
|
27
|
+
[matListItemTitle] {
|
|
28
|
+
color: var(--color-text) !important;
|
|
29
|
+
font-weight: 500;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
mat-icon {
|
|
33
|
+
color: var(--color-text-secondary) !important;
|
|
34
|
+
transition: color 0.2s ease;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&:hover {
|
|
38
|
+
background-color: var(--color-bg-elevated);
|
|
39
|
+
|
|
40
|
+
mat-icon {
|
|
41
|
+
color: var(--color-text) !important;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
&.selected-nav {
|
|
46
|
+
background-color: rgba(63, 81, 181, 0.15);
|
|
47
|
+
border-left: 3px solid var(--color-primary);
|
|
48
|
+
|
|
49
|
+
[matListItemTitle] {
|
|
50
|
+
color: var(--color-primary) !important;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
mat-icon {
|
|
55
|
+
color: var(--color-primary) !important;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.no-features-msg {
|
|
61
|
+
padding: 16px;
|
|
62
|
+
color: var(--color-text-secondary);
|
|
63
|
+
font-style: italic;
|
|
64
|
+
font-size: 0.9rem;
|
|
65
|
+
text-align: center;
|
|
66
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
|
|
2
|
+
import { RouterModule, Router } from '@angular/router';
|
|
3
|
+
import { MatListModule } from '@angular/material/list';
|
|
4
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'app-menu',
|
|
8
|
+
imports: [RouterModule, MatListModule, MatIconModule],
|
|
9
|
+
templateUrl: './<%= name %>.component.html',
|
|
10
|
+
styleUrls: ['./<%= name %>.component.scss'],
|
|
11
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
12
|
+
})
|
|
13
|
+
export class <%= pascalName %>Component {
|
|
14
|
+
private readonly router = inject(Router);
|
|
15
|
+
|
|
16
|
+
get features() {
|
|
17
|
+
return this.router.config
|
|
18
|
+
.filter(route => route.path && route.path !== '')
|
|
19
|
+
.map(route => ({
|
|
20
|
+
path: route.path,
|
|
21
|
+
label: route.data?.['label'] || route.path?.split('-').map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(' ')
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
}
|