create-ng-tailwind 3.1.0 → 4.0.0
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/CHANGELOG.md +81 -350
- package/README.md +93 -157
- package/lib/cli/index.js +29 -3
- package/lib/cli/interactive.js +26 -1
- package/lib/managers/ProjectManager.js +0 -4
- package/lib/templates/base/components.js +243 -0
- package/lib/templates/base/index.js +207 -0
- package/lib/templates/base/infrastructure.js +314 -0
- package/lib/templates/base/linting.js +359 -0
- package/lib/templates/base/pwa.js +103 -0
- package/lib/templates/base/services.js +362 -0
- package/lib/templates/blog/app.js +250 -0
- package/lib/templates/blog/components.js +360 -0
- package/lib/templates/blog/i18n.js +77 -0
- package/lib/templates/blog/index.js +126 -0
- package/lib/templates/blog/pages.js +554 -0
- package/lib/templates/blog/services.js +390 -0
- package/lib/templates/dashboard/app.js +320 -0
- package/lib/templates/dashboard/charts.js +305 -0
- package/lib/templates/dashboard/components.js +410 -0
- package/lib/templates/dashboard/i18n.js +340 -0
- package/lib/templates/dashboard/index.js +141 -0
- package/lib/templates/dashboard/layout.js +310 -0
- package/lib/templates/dashboard/pages.js +681 -0
- package/lib/templates/ecommerce/app.js +315 -0
- package/lib/templates/ecommerce/components.js +496 -0
- package/lib/templates/ecommerce/i18n.js +389 -0
- package/lib/templates/ecommerce/index.js +152 -0
- package/lib/templates/ecommerce/layout.js +270 -0
- package/lib/templates/ecommerce/pages.js +969 -0
- package/lib/templates/ecommerce/services.js +300 -0
- package/lib/templates/index.js +12 -0
- package/lib/templates/landing/index.js +1117 -0
- package/lib/templates/portfolio/index.js +1160 -0
- package/lib/templates/saas/index.js +1371 -0
- package/lib/templates/starter/app.js +364 -0
- package/lib/templates/starter/i18n.js +856 -0
- package/lib/templates/starter/index.js +52 -4060
- package/lib/templates/starter/layout.js +852 -0
- package/lib/templates/starter/pages.js +1241 -0
- package/package.json +1 -1
- package/lib/templates/starter/features.js +0 -867
- package/lib/utils/ai-config.js +0 -641
- /package/lib/templates/{starter → base}/advanced-features.js +0 -0
- /package/lib/templates/{starter → base}/seo-assets.js +0 -0
- /package/lib/templates/{starter → base}/seo-features.js +0 -0
- /package/lib/templates/{starter → base}/ui-features.js +0 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Starter Layout Components
|
|
6
|
+
* - Header with navigation and language switcher
|
|
7
|
+
* - Footer with links
|
|
8
|
+
* - Auth Layout with Login, Register, Forgot Password
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
async function createLayout(config) {
|
|
12
|
+
await createHeader(config);
|
|
13
|
+
await createFooter(config);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function createHeader(config) {
|
|
17
|
+
const headerComponentTs = `import { Component, inject, OnInit } from '@angular/core';
|
|
18
|
+
import { RouterModule, Router } from '@angular/router';
|
|
19
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
20
|
+
import { heroHome, heroInformationCircle, heroEnvelope, heroUser, heroBars3, heroXMark, heroChevronDown, heroLanguage } from '@ng-icons/heroicons/outline';
|
|
21
|
+
import { TranslateModule } from '@ngx-translate/core';
|
|
22
|
+
import { AuthService } from '@core/services/auth.service';
|
|
23
|
+
import { TranslationService, type SupportedLanguage, type LanguageOption } from '@core/i18n/translation.service';
|
|
24
|
+
|
|
25
|
+
interface User {
|
|
26
|
+
id: string;
|
|
27
|
+
email: string;
|
|
28
|
+
firstName: string;
|
|
29
|
+
lastName: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Component({
|
|
33
|
+
selector: 'app-header',
|
|
34
|
+
standalone: true,
|
|
35
|
+
imports: [RouterModule, NgIconComponent, TranslateModule],
|
|
36
|
+
viewProviders: [provideIcons({ heroHome, heroInformationCircle, heroEnvelope, heroUser, heroBars3, heroXMark, heroChevronDown, heroLanguage })],
|
|
37
|
+
templateUrl: './header.component.html'
|
|
38
|
+
})
|
|
39
|
+
export class HeaderComponent implements OnInit {
|
|
40
|
+
private authService = inject(AuthService);
|
|
41
|
+
private router = inject(Router);
|
|
42
|
+
public translationService = inject(TranslationService);
|
|
43
|
+
|
|
44
|
+
protected readonly projectName = '${config.projectName}';
|
|
45
|
+
|
|
46
|
+
mobileMenuOpen = false;
|
|
47
|
+
userMenuOpen = false;
|
|
48
|
+
langMenuOpen = false;
|
|
49
|
+
isAuthenticated = false;
|
|
50
|
+
currentUser: User | null = null;
|
|
51
|
+
currentLang: SupportedLanguage = 'en';
|
|
52
|
+
currentLanguage: LanguageOption = this.translationService.languages[0];
|
|
53
|
+
|
|
54
|
+
ngOnInit() {
|
|
55
|
+
// Subscribe to authentication state
|
|
56
|
+
this.authService.isAuthenticated$.subscribe(
|
|
57
|
+
isAuth => this.isAuthenticated = isAuth
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
this.authService.currentUser$.subscribe(
|
|
61
|
+
user => this.currentUser = user
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Subscribe to language changes
|
|
65
|
+
this.currentLang = this.translationService.getCurrentLanguage();
|
|
66
|
+
const lang = this.translationService.getLanguageOption(this.currentLang);
|
|
67
|
+
if (lang) {
|
|
68
|
+
this.currentLanguage = lang;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
changeLanguage(lang: SupportedLanguage) {
|
|
73
|
+
this.translationService.setLanguage(lang);
|
|
74
|
+
this.currentLang = lang;
|
|
75
|
+
const langOption = this.translationService.getLanguageOption(lang);
|
|
76
|
+
if (langOption) {
|
|
77
|
+
this.currentLanguage = langOption;
|
|
78
|
+
}
|
|
79
|
+
this.langMenuOpen = false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
onLogout() {
|
|
83
|
+
this.authService.logout();
|
|
84
|
+
this.userMenuOpen = false;
|
|
85
|
+
this.router.navigate(['/']);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
closeAllMenus() {
|
|
89
|
+
this.mobileMenuOpen = false;
|
|
90
|
+
this.userMenuOpen = false;
|
|
91
|
+
this.langMenuOpen = false;
|
|
92
|
+
}
|
|
93
|
+
}`;
|
|
94
|
+
|
|
95
|
+
const headerComponentHtml = `<header class="sticky top-0 z-50 bg-white/80 backdrop-blur-lg border-b border-gray-200/80">
|
|
96
|
+
<nav class="container mx-auto px-4 sm:px-6 lg:px-8">
|
|
97
|
+
<div class="flex items-center justify-between h-16">
|
|
98
|
+
|
|
99
|
+
<!-- Logo -->
|
|
100
|
+
<div class="flex items-center space-x-4">
|
|
101
|
+
<a routerLink="/" class="flex items-center space-x-3">
|
|
102
|
+
<div class="flex rounded-lg">
|
|
103
|
+
<img src="assets/images/logo.svg" alt="Logo" class="h-10 w-10" />
|
|
104
|
+
<span class="text-white font-bold text-sm">{{ projectName.charAt(0).toUpperCase() }}</span>
|
|
105
|
+
</div>
|
|
106
|
+
<span class="text-xl font-semibold text-gray-900">
|
|
107
|
+
{{ projectName }}
|
|
108
|
+
</span>
|
|
109
|
+
</a>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- Desktop Navigation -->
|
|
113
|
+
<div class="hidden md:block">
|
|
114
|
+
<div class="ml-10 flex items-center space-x-8">
|
|
115
|
+
<a
|
|
116
|
+
routerLink="/"
|
|
117
|
+
routerLinkActive="text-primary-600"
|
|
118
|
+
[routerLinkActiveOptions]="{exact: true}"
|
|
119
|
+
class="flex items-center gap-1.5 text-gray-700 hover:text-primary-600 transition-colors font-medium">
|
|
120
|
+
<ng-icon name="heroHome" size="18"></ng-icon>
|
|
121
|
+
{{ 'nav.home' | translate }}
|
|
122
|
+
</a>
|
|
123
|
+
<a
|
|
124
|
+
routerLink="/about"
|
|
125
|
+
routerLinkActive="text-primary-600"
|
|
126
|
+
class="flex items-center gap-1.5 text-gray-700 hover:text-primary-600 transition-colors font-medium">
|
|
127
|
+
<ng-icon name="heroInformationCircle" size="18"></ng-icon>
|
|
128
|
+
{{ 'nav.about' | translate }}
|
|
129
|
+
</a>
|
|
130
|
+
<a
|
|
131
|
+
routerLink="/contact"
|
|
132
|
+
routerLinkActive="text-primary-600"
|
|
133
|
+
class="flex items-center gap-1.5 text-gray-700 hover:text-primary-600 transition-colors font-medium">
|
|
134
|
+
<ng-icon name="heroEnvelope" size="18"></ng-icon>
|
|
135
|
+
{{ 'nav.contact' | translate }}
|
|
136
|
+
</a>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<!-- Right side actions -->
|
|
141
|
+
<div class="flex items-center space-x-4">
|
|
142
|
+
|
|
143
|
+
<!-- Language Switcher -->
|
|
144
|
+
<div class="hidden md:block relative">
|
|
145
|
+
<button
|
|
146
|
+
(click)="langMenuOpen = !langMenuOpen"
|
|
147
|
+
class="flex items-center space-x-2 text-gray-700 hover:text-primary-600 transition-colors px-3 py-2 rounded-lg hover:bg-gray-100"
|
|
148
|
+
[class.text-primary-600]="langMenuOpen"
|
|
149
|
+
[class.bg-gray-100]="langMenuOpen">
|
|
150
|
+
<ng-icon name="heroLanguage" size="20"></ng-icon>
|
|
151
|
+
<span class="font-medium text-sm">{{ currentLanguage.nativeName }}</span>
|
|
152
|
+
<ng-icon name="heroChevronDown" size="16"></ng-icon>
|
|
153
|
+
</button>
|
|
154
|
+
|
|
155
|
+
<!-- Language dropdown menu -->
|
|
156
|
+
@if (langMenuOpen) {
|
|
157
|
+
<div class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 z-50">
|
|
158
|
+
<div class="py-1">
|
|
159
|
+
@for (lang of translationService.languages; track lang.code) {
|
|
160
|
+
<button
|
|
161
|
+
(click)="changeLanguage(lang.code)"
|
|
162
|
+
class="w-full text-left px-4 py-2 text-sm transition-colors flex items-center justify-between"
|
|
163
|
+
[class.bg-primary-50]="currentLang === lang.code"
|
|
164
|
+
[class.text-primary-600]="currentLang === lang.code"
|
|
165
|
+
[class.text-gray-700]="currentLang !== lang.code"
|
|
166
|
+
[class.hover:bg-gray-100]="currentLang !== lang.code">
|
|
167
|
+
<span class="flex items-center space-x-2">
|
|
168
|
+
<span class="text-lg">{{ lang.flag }}</span>
|
|
169
|
+
<span>{{ lang.nativeName }}</span>
|
|
170
|
+
</span>
|
|
171
|
+
@if (currentLang === lang.code) {
|
|
172
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
173
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
174
|
+
</svg>
|
|
175
|
+
}
|
|
176
|
+
</button>
|
|
177
|
+
}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
}
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<!-- Authentication Section -->
|
|
184
|
+
<div class="hidden md:flex items-center space-x-3">
|
|
185
|
+
<!-- Logged out state -->
|
|
186
|
+
@if (!isAuthenticated) {
|
|
187
|
+
<a
|
|
188
|
+
routerLink="/auth/login"
|
|
189
|
+
class="text-gray-700 hover:text-primary-600 transition-colors font-medium">
|
|
190
|
+
{{ 'nav.login' | translate }}
|
|
191
|
+
</a>
|
|
192
|
+
<a
|
|
193
|
+
routerLink="/auth/register"
|
|
194
|
+
class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg transition-colors font-medium">
|
|
195
|
+
{{ 'nav.register' | translate }}
|
|
196
|
+
</a>
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
<!-- Logged in state -->
|
|
200
|
+
@if (isAuthenticated) {
|
|
201
|
+
<div class="relative">
|
|
202
|
+
<button
|
|
203
|
+
(click)="userMenuOpen = !userMenuOpen"
|
|
204
|
+
class="flex items-center space-x-2 text-gray-700 hover:text-primary-600 transition-colors"
|
|
205
|
+
[class.text-primary-600]="userMenuOpen">
|
|
206
|
+
<div class="h-8 w-8 bg-primary-600 rounded-full flex items-center justify-center">
|
|
207
|
+
<ng-icon name="heroUser" size="16" style="color: white;"></ng-icon>
|
|
208
|
+
</div>
|
|
209
|
+
<span class="font-medium">{{ currentUser?.firstName || 'User' }}</span>
|
|
210
|
+
<ng-icon name="heroChevronDown" size="16"></ng-icon>
|
|
211
|
+
</button>
|
|
212
|
+
|
|
213
|
+
<!-- User dropdown menu -->
|
|
214
|
+
@if (userMenuOpen) {
|
|
215
|
+
<div class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 z-50">
|
|
216
|
+
<div class="py-1">
|
|
217
|
+
<div class="px-4 py-2 text-sm text-gray-500 border-b border-gray-100">
|
|
218
|
+
{{ currentUser?.email }}
|
|
219
|
+
</div>
|
|
220
|
+
<button
|
|
221
|
+
class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors">
|
|
222
|
+
{{ 'nav.profile' | translate }}
|
|
223
|
+
</button>
|
|
224
|
+
<button
|
|
225
|
+
class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors">
|
|
226
|
+
{{ 'nav.account' | translate }}
|
|
227
|
+
</button>
|
|
228
|
+
<hr class="my-1">
|
|
229
|
+
<button
|
|
230
|
+
(click)="onLogout()"
|
|
231
|
+
class="w-full text-left px-4 py-2 text-sm text-danger-600 hover:bg-danger-50 transition-colors">
|
|
232
|
+
{{ 'nav.logout' | translate }}
|
|
233
|
+
</button>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
}
|
|
237
|
+
</div>
|
|
238
|
+
}
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<!-- Mobile menu button -->
|
|
242
|
+
<button
|
|
243
|
+
(click)="mobileMenuOpen = !mobileMenuOpen"
|
|
244
|
+
class="md:hidden p-2 rounded-lg hover:bg-gray-100 transition-colors">
|
|
245
|
+
@if (!mobileMenuOpen) {
|
|
246
|
+
<ng-icon name="heroBars3" size="24"></ng-icon>
|
|
247
|
+
}
|
|
248
|
+
@if (mobileMenuOpen) {
|
|
249
|
+
<ng-icon name="heroXMark" size="24"></ng-icon>
|
|
250
|
+
}
|
|
251
|
+
</button>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<!-- Mobile Navigation -->
|
|
256
|
+
@if (mobileMenuOpen) {
|
|
257
|
+
<div class="md:hidden pb-4">
|
|
258
|
+
<div class="flex flex-col space-y-2 mt-4">
|
|
259
|
+
<!-- Navigation Links -->
|
|
260
|
+
<a
|
|
261
|
+
routerLink="/"
|
|
262
|
+
routerLinkActive="text-primary-600 bg-primary-50"
|
|
263
|
+
[routerLinkActiveOptions]="{exact: true}"
|
|
264
|
+
(click)="mobileMenuOpen = false"
|
|
265
|
+
class="px-3 py-2 rounded-lg text-gray-700 hover:text-primary-600 hover:bg-gray-100 transition-colors font-medium">
|
|
266
|
+
{{ 'nav.home' | translate }}
|
|
267
|
+
</a>
|
|
268
|
+
<a
|
|
269
|
+
routerLink="/about"
|
|
270
|
+
routerLinkActive="text-primary-600 bg-primary-50"
|
|
271
|
+
(click)="mobileMenuOpen = false"
|
|
272
|
+
class="px-3 py-2 rounded-lg text-gray-700 hover:text-primary-600 hover:bg-gray-100 transition-colors font-medium">
|
|
273
|
+
{{ 'nav.about' | translate }}
|
|
274
|
+
</a>
|
|
275
|
+
<a
|
|
276
|
+
routerLink="/contact"
|
|
277
|
+
routerLinkActive="text-primary-600 bg-primary-50"
|
|
278
|
+
(click)="mobileMenuOpen = false"
|
|
279
|
+
class="px-3 py-2 rounded-lg text-gray-700 hover:text-primary-600 hover:bg-gray-100 transition-colors font-medium">
|
|
280
|
+
{{ 'nav.contact' | translate }}
|
|
281
|
+
</a>
|
|
282
|
+
|
|
283
|
+
<!-- Mobile Language Switcher -->
|
|
284
|
+
<hr class="my-2">
|
|
285
|
+
<div class="px-3 py-2">
|
|
286
|
+
<p class="text-xs font-semibold text-gray-500 uppercase mb-2">{{ 'language.select' | translate }}</p>
|
|
287
|
+
<div class="flex flex-col space-y-1">
|
|
288
|
+
@for (lang of translationService.languages; track lang.code) {
|
|
289
|
+
<button
|
|
290
|
+
(click)="changeLanguage(lang.code)"
|
|
291
|
+
class="w-full text-left px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between"
|
|
292
|
+
[class.bg-primary-50]="currentLang === lang.code"
|
|
293
|
+
[class.text-primary-600]="currentLang === lang.code"
|
|
294
|
+
[class.text-gray-700]="currentLang !== lang.code"
|
|
295
|
+
[class.hover:bg-gray-100]="currentLang !== lang.code">
|
|
296
|
+
<span class="flex items-center space-x-2">
|
|
297
|
+
<span class="text-lg">{{ lang.flag }}</span>
|
|
298
|
+
<span>{{ lang.nativeName }}</span>
|
|
299
|
+
</span>
|
|
300
|
+
@if (currentLang === lang.code) {
|
|
301
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
302
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
303
|
+
</svg>
|
|
304
|
+
}
|
|
305
|
+
</button>
|
|
306
|
+
}
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<!-- Mobile Auth Section -->
|
|
311
|
+
<hr class="my-2">
|
|
312
|
+
@if (!isAuthenticated) {
|
|
313
|
+
<a
|
|
314
|
+
routerLink="/auth/login"
|
|
315
|
+
(click)="mobileMenuOpen = false"
|
|
316
|
+
class="px-3 py-2 rounded-lg text-gray-700 hover:text-primary-600 hover:bg-gray-100 transition-colors font-medium">
|
|
317
|
+
{{ 'nav.login' | translate }}
|
|
318
|
+
</a>
|
|
319
|
+
<a
|
|
320
|
+
routerLink="/auth/register"
|
|
321
|
+
(click)="mobileMenuOpen = false"
|
|
322
|
+
class="px-3 py-2 rounded-lg bg-primary-600 text-white hover:bg-primary-700 transition-colors font-medium text-center">
|
|
323
|
+
{{ 'nav.register' | translate }}
|
|
324
|
+
</a>
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@if (isAuthenticated) {
|
|
328
|
+
<div class="px-3 py-2 text-sm text-gray-500">
|
|
329
|
+
Welcome, {{ currentUser?.firstName || 'User' }}!
|
|
330
|
+
</div>
|
|
331
|
+
<button
|
|
332
|
+
class="w-full text-left px-3 py-2 rounded-lg text-gray-700 hover:bg-gray-100 transition-colors">
|
|
333
|
+
{{ 'nav.profile' | translate }}
|
|
334
|
+
</button>
|
|
335
|
+
<button
|
|
336
|
+
(click)="onLogout(); mobileMenuOpen = false"
|
|
337
|
+
class="w-full text-left px-3 py-2 rounded-lg text-danger-600 hover:bg-danger-50 transition-colors">
|
|
338
|
+
{{ 'nav.logout' | translate }}
|
|
339
|
+
</button>
|
|
340
|
+
}
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
}
|
|
344
|
+
</nav>
|
|
345
|
+
</header>
|
|
346
|
+
|
|
347
|
+
<!-- Backdrop for mobile menu and user dropdown -->
|
|
348
|
+
@if (mobileMenuOpen || userMenuOpen || langMenuOpen) {
|
|
349
|
+
<button
|
|
350
|
+
(click)="closeAllMenus()"
|
|
351
|
+
(keydown.escape)="closeAllMenus()"
|
|
352
|
+
type="button"
|
|
353
|
+
aria-label="Close menu"
|
|
354
|
+
class="fixed inset-0 z-40 bg-black/20 backdrop-blur-sm cursor-default">
|
|
355
|
+
</button>
|
|
356
|
+
}
|
|
357
|
+
`;
|
|
358
|
+
|
|
359
|
+
await fs.writeFile(
|
|
360
|
+
path.join(config.fullPath, 'src/app/layout/header/header.component.ts'),
|
|
361
|
+
headerComponentTs
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
await fs.writeFile(
|
|
365
|
+
path.join(config.fullPath, 'src/app/layout/header/header.component.html'),
|
|
366
|
+
headerComponentHtml
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function createFooter(config) {
|
|
371
|
+
const footerComponentTs = `import { Component } from '@angular/core';
|
|
372
|
+
import { TranslateModule } from '@ngx-translate/core';
|
|
373
|
+
|
|
374
|
+
@Component({
|
|
375
|
+
selector: 'app-footer',
|
|
376
|
+
standalone: true,
|
|
377
|
+
imports: [TranslateModule],
|
|
378
|
+
templateUrl: './footer.component.html'
|
|
379
|
+
})
|
|
380
|
+
export class FooterComponent {
|
|
381
|
+
protected readonly projectName = '${config.projectName}';
|
|
382
|
+
protected readonly currentYear = new Date().getFullYear();
|
|
383
|
+
}`;
|
|
384
|
+
|
|
385
|
+
const footerComponentHtml = `<footer class="bg-white border-t border-gray-200">
|
|
386
|
+
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
387
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
388
|
+
|
|
389
|
+
<!-- Company Info -->
|
|
390
|
+
<div>
|
|
391
|
+
<div class="flex items-center space-x-3 mb-4">
|
|
392
|
+
<img src="assets/images/logo.svg" alt="Logo" class="h-10 w-10" />
|
|
393
|
+
<span class="text-xl font-semibold text-gray-900">
|
|
394
|
+
{{ projectName }}
|
|
395
|
+
</span>
|
|
396
|
+
</div>
|
|
397
|
+
<p class="text-gray-600 text-sm max-w-md">
|
|
398
|
+
{{ 'footer.description' | translate }}
|
|
399
|
+
</p>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<!-- Quick Links -->
|
|
403
|
+
<div>
|
|
404
|
+
<h3 class="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">
|
|
405
|
+
{{ 'footer.quickLinks' | translate }}
|
|
406
|
+
</h3>
|
|
407
|
+
<ul class="space-y-2">
|
|
408
|
+
<li>
|
|
409
|
+
<a href="/" class="text-gray-600 hover:text-primary-600 transition-colors text-sm">
|
|
410
|
+
{{ 'nav.home' | translate }}
|
|
411
|
+
</a>
|
|
412
|
+
</li>
|
|
413
|
+
<li>
|
|
414
|
+
<a href="/about" class="text-gray-600 hover:text-primary-600 transition-colors text-sm">
|
|
415
|
+
{{ 'nav.about' | translate }}
|
|
416
|
+
</a>
|
|
417
|
+
</li>
|
|
418
|
+
<li>
|
|
419
|
+
<a href="/contact" class="text-gray-600 hover:text-primary-600 transition-colors text-sm">
|
|
420
|
+
{{ 'nav.contact' | translate }}
|
|
421
|
+
</a>
|
|
422
|
+
</li>
|
|
423
|
+
</ul>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<!-- Tech Stack -->
|
|
427
|
+
<div>
|
|
428
|
+
<h3 class="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">
|
|
429
|
+
{{ 'footer.builtWith' | translate }}
|
|
430
|
+
</h3>
|
|
431
|
+
<ul class="space-y-2">
|
|
432
|
+
<li>
|
|
433
|
+
<a href="https://angular.dev" target="_blank" class="text-gray-600 hover:text-primary-600 transition-colors text-sm">
|
|
434
|
+
Angular
|
|
435
|
+
</a>
|
|
436
|
+
</li>
|
|
437
|
+
<li>
|
|
438
|
+
<a href="https://tailwindcss.com" target="_blank" class="text-gray-600 hover:text-primary-600 transition-colors text-sm">
|
|
439
|
+
Tailwind CSS
|
|
440
|
+
</a>
|
|
441
|
+
</li>
|
|
442
|
+
<li>
|
|
443
|
+
<a href="https://typescript.org" target="_blank" class="text-gray-600 hover:text-primary-600 transition-colors text-sm">
|
|
444
|
+
TypeScript
|
|
445
|
+
</a>
|
|
446
|
+
</li>
|
|
447
|
+
</ul>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
|
|
451
|
+
<!-- Bottom Bar -->
|
|
452
|
+
<div class="border-t border-gray-200 mt-8 pt-8">
|
|
453
|
+
<p class="text-gray-500 text-sm text-center">
|
|
454
|
+
{{ 'footer.copyright' | translate:{ year: currentYear, name: projectName } }}
|
|
455
|
+
</p>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
</footer>
|
|
459
|
+
`;
|
|
460
|
+
|
|
461
|
+
await fs.writeFile(
|
|
462
|
+
path.join(config.fullPath, 'src/app/layout/footer/footer.component.ts'),
|
|
463
|
+
footerComponentTs
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
await fs.writeFile(
|
|
467
|
+
path.join(config.fullPath, 'src/app/layout/footer/footer.component.html'),
|
|
468
|
+
footerComponentHtml
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function createAuthLayout(config) {
|
|
473
|
+
// Auth Layout Component
|
|
474
|
+
const authLayoutComponent = `import { Component } from '@angular/core';
|
|
475
|
+
import { RouterOutlet } from '@angular/router';
|
|
476
|
+
import { TranslateModule } from '@ngx-translate/core';
|
|
477
|
+
|
|
478
|
+
@Component({
|
|
479
|
+
selector: 'app-auth-layout',
|
|
480
|
+
standalone: true,
|
|
481
|
+
imports: [RouterOutlet, TranslateModule],
|
|
482
|
+
template: \`
|
|
483
|
+
<div class="min-h-screen bg-linear-to-br from-primary-500 via-accent-600 to-secondary-700 flex items-center justify-center p-4">
|
|
484
|
+
<div class="w-full max-w-md">
|
|
485
|
+
|
|
486
|
+
<!-- Logo/Brand -->
|
|
487
|
+
<div class="text-center mb-8">
|
|
488
|
+
<div class="inline-flex items-center justify-center h-16 w-16 bg-white/20 backdrop-blur-lg rounded-2xl mb-4">
|
|
489
|
+
<span class="text-2xl font-bold text-white">{{ firstLetter }}</span>
|
|
490
|
+
</div>
|
|
491
|
+
<h1 class="text-2xl font-bold text-white">{{ projectName }}</h1>
|
|
492
|
+
<p class="text-primary-100 mt-2">{{ 'auth.layout.welcome' | translate }}</p>
|
|
493
|
+
</div>
|
|
494
|
+
|
|
495
|
+
<!-- Auth Form Container -->
|
|
496
|
+
<div class="bg-white/95 backdrop-blur-sm rounded-2xl shadow-xl p-8">
|
|
497
|
+
<router-outlet />
|
|
498
|
+
</div>
|
|
499
|
+
|
|
500
|
+
<!-- Footer -->
|
|
501
|
+
<div class="text-center mt-8 text-primary-100 text-sm">
|
|
502
|
+
<p>{{ 'auth.layout.copyright' | translate:{ year: currentYear, name: projectName } }}</p>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
\`
|
|
507
|
+
})
|
|
508
|
+
export class AuthLayoutComponent {
|
|
509
|
+
protected readonly projectName = '${config.projectName}';
|
|
510
|
+
protected readonly firstLetter = '${config.projectName}'.charAt(0).toUpperCase();
|
|
511
|
+
protected readonly currentYear = new Date().getFullYear();
|
|
512
|
+
}`;
|
|
513
|
+
|
|
514
|
+
await fs.writeFile(
|
|
515
|
+
path.join(config.fullPath, 'src/app/layout/auth/auth-layout.component.ts'),
|
|
516
|
+
authLayoutComponent
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
// Create auth pages
|
|
520
|
+
await createLoginComponent(config);
|
|
521
|
+
await createRegisterComponent(config);
|
|
522
|
+
await createForgotPasswordComponent(config);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function createLoginComponent(config) {
|
|
526
|
+
const loginComponentTs = `import { Component, inject } from '@angular/core';
|
|
527
|
+
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
|
|
528
|
+
import { Router, RouterModule } from '@angular/router';
|
|
529
|
+
import { TranslateModule } from '@ngx-translate/core';
|
|
530
|
+
import { ButtonComponent } from '@shared/components/button/button.component';
|
|
531
|
+
|
|
532
|
+
@Component({
|
|
533
|
+
selector: 'app-login',
|
|
534
|
+
standalone: true,
|
|
535
|
+
imports: [ReactiveFormsModule, RouterModule, TranslateModule, ButtonComponent],
|
|
536
|
+
templateUrl: './login.component.html'
|
|
537
|
+
})
|
|
538
|
+
export class LoginComponent {
|
|
539
|
+
private fb = inject(FormBuilder);
|
|
540
|
+
private router = inject(Router);
|
|
541
|
+
|
|
542
|
+
showPassword = false;
|
|
543
|
+
isLoading = false;
|
|
544
|
+
errorMessage = '';
|
|
545
|
+
|
|
546
|
+
loginForm = this.fb.group({
|
|
547
|
+
email: ['', [Validators.required, Validators.email]],
|
|
548
|
+
password: ['', [Validators.required, Validators.minLength(6)]],
|
|
549
|
+
rememberMe: [false]
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
togglePassword(): void {
|
|
553
|
+
this.showPassword = !this.showPassword;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
onSubmit(): void {
|
|
557
|
+
if (this.loginForm.valid) {
|
|
558
|
+
this.isLoading = true;
|
|
559
|
+
this.errorMessage = '';
|
|
560
|
+
|
|
561
|
+
// Simulate login API call
|
|
562
|
+
setTimeout(() => {
|
|
563
|
+
this.isLoading = false;
|
|
564
|
+
|
|
565
|
+
// Mock login logic - replace with actual authentication service
|
|
566
|
+
const { email, password } = this.loginForm.value;
|
|
567
|
+
|
|
568
|
+
if (email === 'demo@example.com' && password === 'password') {
|
|
569
|
+
// Store token (mock)
|
|
570
|
+
localStorage.setItem('auth_token', 'mock_token_12345');
|
|
571
|
+
this.router.navigate(['/']);
|
|
572
|
+
} else {
|
|
573
|
+
this.errorMessage = 'Invalid email or password. Try demo@example.com / password';
|
|
574
|
+
}
|
|
575
|
+
}, 1500);
|
|
576
|
+
} else {
|
|
577
|
+
// Mark all fields as touched to show validation errors
|
|
578
|
+
Object.keys(this.loginForm.controls).forEach(key => {
|
|
579
|
+
this.loginForm.get(key)?.markAsTouched();
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}`;
|
|
584
|
+
|
|
585
|
+
const loginComponentHtml = `<div class="space-y-6">
|
|
586
|
+
<div class="text-center">
|
|
587
|
+
<h2 class="text-2xl font-bold text-gray-900">{{ 'auth.login.title' | translate }}</h2>
|
|
588
|
+
<p class="text-gray-600 mt-2">{{ 'auth.login.subtitle' | translate }}</p>
|
|
589
|
+
</div>
|
|
590
|
+
|
|
591
|
+
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()" class="space-y-4">
|
|
592
|
+
|
|
593
|
+
<!-- Email Field -->
|
|
594
|
+
<div>
|
|
595
|
+
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
|
|
596
|
+
{{ 'auth.login.email' | translate }}
|
|
597
|
+
</label>
|
|
598
|
+
<input
|
|
599
|
+
id="email"
|
|
600
|
+
type="email"
|
|
601
|
+
formControlName="email"
|
|
602
|
+
autocomplete="email"
|
|
603
|
+
[placeholder]="'auth.login.email' | translate"
|
|
604
|
+
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-colors"
|
|
605
|
+
[class.border-danger-500]="loginForm.get('email')?.invalid && loginForm.get('email')?.touched">
|
|
606
|
+
@if (loginForm.get('email')?.invalid && loginForm.get('email')?.touched) {
|
|
607
|
+
<div class="mt-1 text-sm text-danger-600">
|
|
608
|
+
@if (loginForm.get('email')?.errors?.['required']) {
|
|
609
|
+
<span>{{ 'auth.validation.emailRequired' | translate }}</span>
|
|
610
|
+
}
|
|
611
|
+
@if (loginForm.get('email')?.errors?.['email']) {
|
|
612
|
+
<span>{{ 'auth.validation.emailInvalid' | translate }}</span>
|
|
613
|
+
}
|
|
614
|
+
</div>
|
|
615
|
+
}
|
|
616
|
+
</div>
|
|
617
|
+
|
|
618
|
+
<!-- Password Field -->
|
|
619
|
+
<div>
|
|
620
|
+
<label for="password" class="block text-sm font-medium text-gray-700 mb-2">
|
|
621
|
+
{{ 'auth.login.password' | translate }}
|
|
622
|
+
</label>
|
|
623
|
+
<div class="relative">
|
|
624
|
+
<input
|
|
625
|
+
id="password"
|
|
626
|
+
[type]="showPassword ? 'text' : 'password'"
|
|
627
|
+
formControlName="password"
|
|
628
|
+
autocomplete="current-password"
|
|
629
|
+
[placeholder]="'auth.login.password' | translate"
|
|
630
|
+
class="w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-colors"
|
|
631
|
+
[class.border-danger-500]="loginForm.get('password')?.invalid && loginForm.get('password')?.touched">
|
|
632
|
+
<button
|
|
633
|
+
type="button"
|
|
634
|
+
(click)="togglePassword()"
|
|
635
|
+
[attr.aria-label]="showPassword ? ('auth.login.hidePassword' | translate) : ('auth.login.showPassword' | translate)"
|
|
636
|
+
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500 hover:text-gray-700">
|
|
637
|
+
@if (!showPassword) {
|
|
638
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
639
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
640
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
|
641
|
+
</svg>
|
|
642
|
+
}
|
|
643
|
+
@if (showPassword) {
|
|
644
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
645
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"></path>
|
|
646
|
+
</svg>
|
|
647
|
+
}
|
|
648
|
+
</button>
|
|
649
|
+
</div>
|
|
650
|
+
@if (loginForm.get('password')?.invalid && loginForm.get('password')?.touched) {
|
|
651
|
+
<div class="mt-1 text-sm text-danger-600">
|
|
652
|
+
@if (loginForm.get('password')?.errors?.['required']) {
|
|
653
|
+
<span>{{ 'auth.validation.passwordRequired' | translate }}</span>
|
|
654
|
+
}
|
|
655
|
+
@if (loginForm.get('password')?.errors?.['minlength']) {
|
|
656
|
+
<span>{{ 'auth.validation.passwordMinLength' | translate }}</span>
|
|
657
|
+
}
|
|
658
|
+
</div>
|
|
659
|
+
}
|
|
660
|
+
</div>
|
|
661
|
+
|
|
662
|
+
<!-- Remember Me & Forgot Password -->
|
|
663
|
+
<div class="flex items-center justify-between">
|
|
664
|
+
<label class="flex items-center">
|
|
665
|
+
<input
|
|
666
|
+
type="checkbox"
|
|
667
|
+
formControlName="rememberMe"
|
|
668
|
+
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded">
|
|
669
|
+
<span class="ml-2 rtl:mr-2 text-sm text-gray-600">{{ 'auth.login.rememberMe' | translate }}</span>
|
|
670
|
+
</label>
|
|
671
|
+
<a routerLink="/auth/forgot-password" class="text-sm text-primary-600 hover:text-primary-700">
|
|
672
|
+
{{ 'auth.login.forgotPassword' | translate }}
|
|
673
|
+
</a>
|
|
674
|
+
</div>
|
|
675
|
+
|
|
676
|
+
<!-- Submit Button -->
|
|
677
|
+
<app-button
|
|
678
|
+
type="submit"
|
|
679
|
+
[fullWidth]="true"
|
|
680
|
+
[loading]="isLoading"
|
|
681
|
+
[disabled]="loginForm.invalid">
|
|
682
|
+
{{ (isLoading ? 'auth.login.signing' : 'auth.login.submit') | translate }}
|
|
683
|
+
</app-button>
|
|
684
|
+
|
|
685
|
+
<!-- Error Message -->
|
|
686
|
+
@if (errorMessage) {
|
|
687
|
+
<div class="p-3 bg-danger-100 border border-danger-300 text-danger-700 rounded-lg text-sm">
|
|
688
|
+
{{ errorMessage }}
|
|
689
|
+
</div>
|
|
690
|
+
}
|
|
691
|
+
</form>
|
|
692
|
+
|
|
693
|
+
<!-- Sign Up Link -->
|
|
694
|
+
<div class="text-center pt-4 border-t border-gray-200">
|
|
695
|
+
<p class="text-sm text-gray-600">
|
|
696
|
+
{{ 'auth.login.noAccount' | translate }}
|
|
697
|
+
<a routerLink="/auth/register" class="text-primary-600 hover:text-primary-700 font-medium">
|
|
698
|
+
{{ 'auth.login.createAccount' | translate }}
|
|
699
|
+
</a>
|
|
700
|
+
</p>
|
|
701
|
+
</div>
|
|
702
|
+
</div>
|
|
703
|
+
`;
|
|
704
|
+
|
|
705
|
+
await fs.writeFile(
|
|
706
|
+
path.join(config.fullPath, 'src/app/features/auth/login/login.component.ts'),
|
|
707
|
+
loginComponentTs
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
await fs.writeFile(
|
|
711
|
+
path.join(config.fullPath, 'src/app/features/auth/login/login.component.html'),
|
|
712
|
+
loginComponentHtml
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function createRegisterComponent(config) {
|
|
717
|
+
const registerComponentTs = `import { Component, inject } from '@angular/core';
|
|
718
|
+
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
|
|
719
|
+
import { RouterModule } from '@angular/router';
|
|
720
|
+
import { TranslateModule } from '@ngx-translate/core';
|
|
721
|
+
import { ButtonComponent } from '@shared/components/button/button.component';
|
|
722
|
+
|
|
723
|
+
@Component({
|
|
724
|
+
selector: 'app-register',
|
|
725
|
+
standalone: true,
|
|
726
|
+
imports: [ReactiveFormsModule, RouterModule, TranslateModule, ButtonComponent],
|
|
727
|
+
templateUrl: './register.component.html'
|
|
728
|
+
})
|
|
729
|
+
export class RegisterComponent {
|
|
730
|
+
private fb = inject(FormBuilder);
|
|
731
|
+
registerForm = this.fb.group({
|
|
732
|
+
firstName: ['', Validators.required],
|
|
733
|
+
lastName: ['', Validators.required],
|
|
734
|
+
email: ['', [Validators.required, Validators.email]],
|
|
735
|
+
password: ['', [Validators.required, Validators.minLength(6)]]
|
|
736
|
+
});
|
|
737
|
+
onSubmit() { /* Implementation */ }
|
|
738
|
+
}`;
|
|
739
|
+
|
|
740
|
+
const registerComponentHtml = `<div class="space-y-6">
|
|
741
|
+
<div class="text-center">
|
|
742
|
+
<h2 class="text-2xl font-bold text-gray-900">{{ 'auth.register.title' | translate }}</h2>
|
|
743
|
+
<p class="text-gray-600 mt-2">{{ 'auth.register.subtitle' | translate }}</p>
|
|
744
|
+
</div>
|
|
745
|
+
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()" class="space-y-4">
|
|
746
|
+
<div class="grid grid-cols-2 gap-4">
|
|
747
|
+
<input
|
|
748
|
+
type="text"
|
|
749
|
+
formControlName="firstName"
|
|
750
|
+
autocomplete="given-name"
|
|
751
|
+
[placeholder]="'auth.register.firstName' | translate"
|
|
752
|
+
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
|
|
753
|
+
<input
|
|
754
|
+
type="text"
|
|
755
|
+
formControlName="lastName"
|
|
756
|
+
autocomplete="family-name"
|
|
757
|
+
[placeholder]="'auth.register.lastName' | translate"
|
|
758
|
+
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
|
|
759
|
+
</div>
|
|
760
|
+
<input
|
|
761
|
+
type="email"
|
|
762
|
+
formControlName="email"
|
|
763
|
+
autocomplete="email"
|
|
764
|
+
[placeholder]="'auth.register.email' | translate"
|
|
765
|
+
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
|
|
766
|
+
<input
|
|
767
|
+
type="password"
|
|
768
|
+
formControlName="password"
|
|
769
|
+
autocomplete="new-password"
|
|
770
|
+
[placeholder]="'auth.register.password' | translate"
|
|
771
|
+
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
|
|
772
|
+
<app-button type="submit" [fullWidth]="true" [disabled]="registerForm.invalid">
|
|
773
|
+
{{ 'auth.register.submit' | translate }}
|
|
774
|
+
</app-button>
|
|
775
|
+
<div class="text-center">
|
|
776
|
+
<a routerLink="/auth/login" class="text-primary-600 hover:text-primary-700">
|
|
777
|
+
{{ 'auth.register.hasAccount' | translate }}
|
|
778
|
+
</a>
|
|
779
|
+
</div>
|
|
780
|
+
</form>
|
|
781
|
+
</div>
|
|
782
|
+
`;
|
|
783
|
+
|
|
784
|
+
await fs.writeFile(
|
|
785
|
+
path.join(config.fullPath, 'src/app/features/auth/register/register.component.ts'),
|
|
786
|
+
registerComponentTs
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
await fs.writeFile(
|
|
790
|
+
path.join(config.fullPath, 'src/app/features/auth/register/register.component.html'),
|
|
791
|
+
registerComponentHtml
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function createForgotPasswordComponent(config) {
|
|
796
|
+
const forgotPasswordComponentTs = `import { Component, inject } from '@angular/core';
|
|
797
|
+
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
|
|
798
|
+
import { RouterModule } from '@angular/router';
|
|
799
|
+
import { TranslateModule } from '@ngx-translate/core';
|
|
800
|
+
import { ButtonComponent } from '@shared/components/button/button.component';
|
|
801
|
+
|
|
802
|
+
@Component({
|
|
803
|
+
selector: 'app-forgot-password',
|
|
804
|
+
standalone: true,
|
|
805
|
+
imports: [ReactiveFormsModule, RouterModule, TranslateModule, ButtonComponent],
|
|
806
|
+
templateUrl: './forgot-password.component.html'
|
|
807
|
+
})
|
|
808
|
+
export class ForgotPasswordComponent {
|
|
809
|
+
private fb = inject(FormBuilder);
|
|
810
|
+
forgotForm = this.fb.group({ email: ['', [Validators.required, Validators.email]] });
|
|
811
|
+
onSubmit() { /* Implementation */ }
|
|
812
|
+
}`;
|
|
813
|
+
|
|
814
|
+
const forgotPasswordComponentHtml = `<div class="space-y-6">
|
|
815
|
+
<div class="text-center">
|
|
816
|
+
<h2 class="text-2xl font-bold text-gray-900">{{ 'auth.forgot.title' | translate }}</h2>
|
|
817
|
+
<p class="text-gray-600 mt-2">{{ 'auth.forgot.subtitle' | translate }}</p>
|
|
818
|
+
</div>
|
|
819
|
+
<form [formGroup]="forgotForm" (ngSubmit)="onSubmit()" class="space-y-4">
|
|
820
|
+
<input
|
|
821
|
+
type="email"
|
|
822
|
+
formControlName="email"
|
|
823
|
+
autocomplete="email"
|
|
824
|
+
[placeholder]="'auth.forgot.email' | translate"
|
|
825
|
+
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
|
|
826
|
+
<app-button type="submit" [fullWidth]="true" [disabled]="forgotForm.invalid">
|
|
827
|
+
{{ 'auth.forgot.submit' | translate }}
|
|
828
|
+
</app-button>
|
|
829
|
+
<div class="text-center">
|
|
830
|
+
<a routerLink="/auth/login" class="text-primary-600 hover:text-primary-700">
|
|
831
|
+
{{ 'auth.forgot.backToLogin' | translate }}
|
|
832
|
+
</a>
|
|
833
|
+
</div>
|
|
834
|
+
</form>
|
|
835
|
+
</div>
|
|
836
|
+
`;
|
|
837
|
+
|
|
838
|
+
await fs.writeFile(
|
|
839
|
+
path.join(config.fullPath, 'src/app/features/auth/forgot-password/forgot-password.component.ts'),
|
|
840
|
+
forgotPasswordComponentTs
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
await fs.writeFile(
|
|
844
|
+
path.join(config.fullPath, 'src/app/features/auth/forgot-password/forgot-password.component.html'),
|
|
845
|
+
forgotPasswordComponentHtml
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
module.exports = {
|
|
850
|
+
createLayout,
|
|
851
|
+
createAuthLayout,
|
|
852
|
+
};
|