create-ng-tailwind 3.1.0 → 4.1.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.

Potentially problematic release.


This version of create-ng-tailwind might be problematic. Click here for more details.

Files changed (48) hide show
  1. package/CHANGELOG.md +96 -341
  2. package/README.md +111 -157
  3. package/lib/cli/index.js +74 -3
  4. package/lib/cli/interactive.js +26 -1
  5. package/lib/managers/ProjectManager.js +2 -5
  6. package/lib/templates/base/components.js +243 -0
  7. package/lib/templates/base/index.js +207 -0
  8. package/lib/templates/base/infrastructure.js +314 -0
  9. package/lib/templates/base/linting.js +359 -0
  10. package/lib/templates/base/pwa.js +103 -0
  11. package/lib/templates/base/services.js +362 -0
  12. package/lib/templates/blog/app.js +250 -0
  13. package/lib/templates/blog/components.js +360 -0
  14. package/lib/templates/blog/i18n.js +77 -0
  15. package/lib/templates/blog/index.js +126 -0
  16. package/lib/templates/blog/pages.js +554 -0
  17. package/lib/templates/blog/services.js +390 -0
  18. package/lib/templates/dashboard/app.js +320 -0
  19. package/lib/templates/dashboard/charts.js +305 -0
  20. package/lib/templates/dashboard/components.js +410 -0
  21. package/lib/templates/dashboard/i18n.js +340 -0
  22. package/lib/templates/dashboard/index.js +141 -0
  23. package/lib/templates/dashboard/layout.js +310 -0
  24. package/lib/templates/dashboard/pages.js +681 -0
  25. package/lib/templates/ecommerce/app.js +315 -0
  26. package/lib/templates/ecommerce/components.js +496 -0
  27. package/lib/templates/ecommerce/i18n.js +389 -0
  28. package/lib/templates/ecommerce/index.js +152 -0
  29. package/lib/templates/ecommerce/layout.js +270 -0
  30. package/lib/templates/ecommerce/pages.js +969 -0
  31. package/lib/templates/ecommerce/services.js +300 -0
  32. package/lib/templates/index.js +12 -0
  33. package/lib/templates/landing/index.js +1117 -0
  34. package/lib/templates/portfolio/index.js +1160 -0
  35. package/lib/templates/saas/index.js +1371 -0
  36. package/lib/templates/starter/app.js +364 -0
  37. package/lib/templates/starter/i18n.js +856 -0
  38. package/lib/templates/starter/index.js +52 -4060
  39. package/lib/templates/starter/layout.js +852 -0
  40. package/lib/templates/starter/pages.js +1241 -0
  41. package/lib/utils/nodeCompat.js +85 -0
  42. package/package.json +1 -1
  43. package/lib/templates/starter/features.js +0 -867
  44. package/lib/utils/ai-config.js +0 -641
  45. /package/lib/templates/{starter → base}/advanced-features.js +0 -0
  46. /package/lib/templates/{starter → base}/seo-assets.js +0 -0
  47. /package/lib/templates/{starter → base}/seo-features.js +0 -0
  48. /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
+ };