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.
Files changed (47) hide show
  1. package/CHANGELOG.md +81 -350
  2. package/README.md +93 -157
  3. package/lib/cli/index.js +29 -3
  4. package/lib/cli/interactive.js +26 -1
  5. package/lib/managers/ProjectManager.js +0 -4
  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/package.json +1 -1
  42. package/lib/templates/starter/features.js +0 -867
  43. package/lib/utils/ai-config.js +0 -641
  44. /package/lib/templates/{starter → base}/advanced-features.js +0 -0
  45. /package/lib/templates/{starter → base}/seo-assets.js +0 -0
  46. /package/lib/templates/{starter → base}/seo-features.js +0 -0
  47. /package/lib/templates/{starter → base}/ui-features.js +0 -0
@@ -1,867 +0,0 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
-
4
- async function createContactComponent(config) {
5
- const contactComponentTs = `import { Component, OnInit, inject } from '@angular/core';
6
- import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
7
- import { NgIconComponent, provideIcons } from '@ng-icons/core';
8
- import { TranslateModule } from '@ngx-translate/core';
9
-
10
- import {
11
- heroEnvelope,
12
- heroMapPin,
13
- heroClock,
14
- heroCheckCircle,
15
- heroPaperAirplane,
16
- } from '@ng-icons/heroicons/outline';
17
- import { ButtonComponent } from '@shared/components/button/button.component';
18
- import { SeoService } from '@core/services/seo.service';
19
-
20
- @Component({
21
- selector: 'app-contact',
22
- standalone: true,
23
- imports: [ReactiveFormsModule, NgIconComponent, ButtonComponent, TranslateModule],
24
- viewProviders: [provideIcons({
25
- heroEnvelope,
26
- heroMapPin,
27
- heroClock,
28
- heroCheckCircle,
29
- heroPaperAirplane,
30
- })],
31
- templateUrl: './contact.component.html'
32
- })
33
- export class ContactComponent implements OnInit {
34
- private fb = inject(FormBuilder);
35
- private seo = inject(SeoService);
36
-
37
- isSubmitting = false;
38
- submitted = false;
39
-
40
- ngOnInit(): void {
41
- // Set SEO meta tags for Contact page
42
- this.seo.updateMeta({
43
- title: 'Contact Us',
44
- description: 'Get in touch with us. We would love to hear from you and answer any questions you may have.',
45
- keywords: 'contact, support, get in touch, email',
46
- ogType: 'website'
47
- });
48
- }
49
-
50
- contactForm = this.fb.group({
51
- name: ['', [Validators.required, Validators.minLength(2)]],
52
- email: ['', [Validators.required, Validators.email]],
53
- subject: ['', Validators.required],
54
- message: ['', [Validators.required, Validators.minLength(10)]]
55
- });
56
-
57
- onSubmit(): void {
58
- if (this.contactForm.valid) {
59
- this.isSubmitting = true;
60
-
61
- // Simulate form submission
62
- setTimeout(() => {
63
- this.isSubmitting = false;
64
- this.submitted = true;
65
- this.contactForm.reset();
66
-
67
- // Hide success message after 5 seconds
68
- setTimeout(() => {
69
- this.submitted = false;
70
- }, 5000);
71
- }, 1500);
72
- } else {
73
- // Mark all fields as touched to show validation errors
74
- Object.keys(this.contactForm.controls).forEach(key => {
75
- this.contactForm.get(key)?.markAsTouched();
76
- });
77
- }
78
- }
79
- }`;
80
-
81
- const contactComponentHtml = `<!-- Hero Section -->
82
- <section class="relative py-20 bg-linear-to-br from-primary-600 via-secondary-600 to-accent-600 overflow-hidden">
83
- <div class="absolute inset-0 opacity-10">
84
- <div class="absolute top-0 -left-4 w-96 h-96 bg-white rounded-full mix-blend-multiply filter blur-3xl animate-blob"></div>
85
- <div class="absolute top-0 -right-4 w-96 h-96 bg-warning-200 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-2000"></div>
86
- <div class="absolute bottom-0 left-1/2 w-96 h-96 bg-success-200 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-4000"></div>
87
- </div>
88
-
89
- <div class="relative container mx-auto px-4 text-center">
90
- <div class="animate-fade-in">
91
- <div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-white/20 backdrop-blur-sm mb-6 shadow-xl">
92
- <ng-icon name="heroEnvelope" size="40" style="color: white;"></ng-icon>
93
- </div>
94
- <h1 class="text-5xl md:text-6xl lg:text-7xl font-bold text-white mb-6 drop-shadow-lg">
95
- {{ 'contact.title' | translate }}
96
- </h1>
97
- <p class="text-xl md:text-2xl text-secondary-50 max-w-3xl mx-auto leading-relaxed">
98
- {{ 'contact.subtitle' | translate }}
99
- </p>
100
- </div>
101
- </div>
102
- </section>
103
-
104
- <!-- Main Content -->
105
- <div class="bg-linear-to-br from-gray-50 to-primary-50 py-16 -mt-8">
106
- <div class="container mx-auto px-4 sm:px-6 lg:px-8 max-w-6xl">
107
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12">
108
-
109
- <!-- Contact Form Card -->
110
- <div class="bg-white rounded-2xl shadow-xl p-8 border border-gray-100 hover:shadow-2xl transition-shadow animate-fade-in">
111
- <div class="mb-6">
112
- <div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-linear-to-br from-primary-500 to-secondary-500 mb-4">
113
- <ng-icon name="heroPaperAirplane" size="24" style="color: white;"></ng-icon>
114
- </div>
115
- <h2 class="text-2xl font-bold text-gray-900">{{ 'contact.form.title' | translate }}</h2>
116
- <p class="text-gray-600 mt-1">{{ 'contact.form.description' | translate }}</p>
117
- </div>
118
-
119
- <form [formGroup]="contactForm" (ngSubmit)="onSubmit()" class="space-y-5">
120
- <!-- Name Field -->
121
- <div>
122
- <label for="name" class="block text-sm font-semibold text-gray-700 mb-2">
123
- {{ 'contact.form.name' | translate }} *
124
- </label>
125
- <input
126
- id="name"
127
- type="text"
128
- formControlName="name"
129
- autocomplete="name"
130
- placeholder="John Doe"
131
- class="w-full px-4 py-3 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all"
132
- [class.border-danger-500]="contactForm.get('name')?.invalid && contactForm.get('name')?.touched"
133
- [class.focus:ring-danger-500]="contactForm.get('name')?.invalid && contactForm.get('name')?.touched">
134
- @if (contactForm.get('name')?.invalid && contactForm.get('name')?.touched) {
135
- <div class="mt-2 text-sm text-danger-600 flex items-center gap-1">
136
- <svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
137
- <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
138
- </svg>
139
- @if (contactForm.get('name')?.errors?.['required']) {
140
- <span>{{ 'contact.form.errors.nameRequired' | translate }}</span>
141
- }
142
- @if (contactForm.get('name')?.errors?.['minlength']) {
143
- <span>{{ 'contact.form.errors.nameMinLength' | translate }}</span>
144
- }
145
- </div>
146
- }
147
- </div>
148
-
149
- <!-- Email Field -->
150
- <div>
151
- <label for="email" class="block text-sm font-semibold text-gray-700 mb-2">
152
- {{ 'contact.form.email' | translate }} *
153
- </label>
154
- <input
155
- id="email"
156
- type="email"
157
- formControlName="email"
158
- autocomplete="email"
159
- placeholder="john@example.com"
160
- class="w-full px-4 py-3 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all"
161
- [class.border-danger-500]="contactForm.get('email')?.invalid && contactForm.get('email')?.touched"
162
- [class.focus:ring-danger-500]="contactForm.get('email')?.invalid && contactForm.get('email')?.touched">
163
- @if (contactForm.get('email')?.invalid && contactForm.get('email')?.touched) {
164
- <div class="mt-2 text-sm text-danger-600 flex items-center gap-1">
165
- <svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
166
- <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
167
- </svg>
168
- @if (contactForm.get('email')?.errors?.['required']) {
169
- <span>{{ 'contact.form.errors.emailRequired' | translate }}</span>
170
- }
171
- @if (contactForm.get('email')?.errors?.['email']) {
172
- <span>{{ 'contact.form.errors.emailInvalid' | translate }}</span>
173
- }
174
- </div>
175
- }
176
- </div>
177
-
178
- <!-- Subject Field -->
179
- <div>
180
- <label for="subject" class="block text-sm font-semibold text-gray-700 mb-2">
181
- {{ 'contact.form.subject' | translate }} *
182
- </label>
183
- <input
184
- id="subject"
185
- type="text"
186
- formControlName="subject"
187
- placeholder="How can we help?"
188
- class="w-full px-4 py-3 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all"
189
- [class.border-danger-500]="contactForm.get('subject')?.invalid && contactForm.get('subject')?.touched"
190
- [class.focus:ring-danger-500]="contactForm.get('subject')?.invalid && contactForm.get('subject')?.touched">
191
- @if (contactForm.get('subject')?.invalid && contactForm.get('subject')?.touched) {
192
- <div class="mt-2 text-sm text-danger-600 flex items-center gap-1">
193
- <svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
194
- <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
195
- </svg>
196
- <span>{{ 'contact.form.errors.subjectRequired' | translate }}</span>
197
- </div>
198
- }
199
- </div>
200
-
201
- <!-- Message Field -->
202
- <div>
203
- <label for="message" class="block text-sm font-semibold text-gray-700 mb-2">
204
- {{ 'contact.form.message' | translate }} *
205
- </label>
206
- <textarea
207
- id="message"
208
- formControlName="message"
209
- rows="5"
210
- placeholder="Tell us more about your inquiry..."
211
- class="w-full px-4 py-3 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all resize-vertical"
212
- [class.border-danger-500]="contactForm.get('message')?.invalid && contactForm.get('message')?.touched"
213
- [class.focus:ring-danger-500]="contactForm.get('message')?.invalid && contactForm.get('message')?.touched"></textarea>
214
- @if (contactForm.get('message')?.invalid && contactForm.get('message')?.touched) {
215
- <div class="mt-2 text-sm text-danger-600 flex items-center gap-1">
216
- <svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
217
- <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
218
- </svg>
219
- @if (contactForm.get('message')?.errors?.['required']) {
220
- <span>{{ 'contact.form.errors.messageRequired' | translate }}</span>
221
- }
222
- @if (contactForm.get('message')?.errors?.['minlength']) {
223
- <span>{{ 'contact.form.errors.messageMinLength' | translate }}</span>
224
- }
225
- </div>
226
- }
227
- </div>
228
-
229
- <!-- Success Message -->
230
- @if (submitted && !contactForm.invalid) {
231
- <div class="p-4 bg-linear-to-r from-success-50 to-success-100 border-2 border-success-300 rounded-xl animate-fade-in">
232
- <div class="flex items-center gap-2">
233
- <ng-icon name="heroCheckCircle" size="20" style="color: var(--color-success-600);"></ng-icon>
234
- <p class="text-success-700 font-medium">
235
- {{ 'contact.form.success' | translate }}
236
- </p>
237
- </div>
238
- </div>
239
- }
240
-
241
- <!-- Submit Button -->
242
- <app-button
243
- type="submit"
244
- [fullWidth]="true"
245
- [loading]="isSubmitting"
246
- [disabled]="contactForm.invalid">
247
- <span class="flex items-center justify-center gap-2">
248
- @if (!isSubmitting) {
249
- <ng-icon name="heroPaperAirplane" size="18"></ng-icon>
250
- }
251
- {{ isSubmitting ? ('contact.form.sending' | translate) : ('contact.form.submit' | translate) }}
252
- </span>
253
- </app-button>
254
- </form>
255
- </div>
256
-
257
- <!-- Contact Info & Resources -->
258
- <div class="space-y-6 animate-fade-in animation-delay-200">
259
- <!-- Contact Information Card -->
260
- <div class="bg-linear-to-br from-primary-600 to-secondary-600 rounded-2xl shadow-xl p-8 text-white">
261
- <div class="mb-6">
262
- <div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-white/20 backdrop-blur-sm mb-4">
263
- <ng-icon name="heroMapPin" size="24" style="color: white;"></ng-icon>
264
- </div>
265
- <h2 class="text-2xl font-bold">{{ 'contact.info.title' | translate }}</h2>
266
- <p class="text-primary-100 mt-1">{{ 'contact.info.description' | translate }}</p>
267
- </div>
268
-
269
- <div class="space-y-6">
270
- <div class="flex items-start gap-4">
271
- <div class="shrink-0">
272
- <div class="h-12 w-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center">
273
- <ng-icon name="heroEnvelope" size="24" style="color: white;"></ng-icon>
274
- </div>
275
- </div>
276
- <div>
277
- <h3 class="text-lg font-semibold mb-1">{{ 'contact.info.email.label' | translate }}</h3>
278
- <p class="text-primary-100">hello&#64;example.com</p>
279
- <p class="text-sm text-primary-200 mt-1">{{ 'contact.info.email.description' | translate }}</p>
280
- </div>
281
- </div>
282
-
283
- <div class="flex items-start gap-4">
284
- <div class="shrink-0">
285
- <div class="h-12 w-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center">
286
- <ng-icon name="heroMapPin" size="24" style="color: white;"></ng-icon>
287
- </div>
288
- </div>
289
- <div>
290
- <h3 class="text-lg font-semibold mb-1">{{ 'contact.info.location.label' | translate }}</h3>
291
- <p class="text-primary-100">{{ 'contact.info.location.value' | translate }}</p>
292
- <p class="text-sm text-primary-200 mt-1">{{ 'contact.info.location.description' | translate }}</p>
293
- </div>
294
- </div>
295
-
296
- <div class="flex items-start gap-4">
297
- <div class="shrink-0">
298
- <div class="h-12 w-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center">
299
- <ng-icon name="heroClock" size="24" style="color: white;"></ng-icon>
300
- </div>
301
- </div>
302
- <div>
303
- <h3 class="text-lg font-semibold mb-1">{{ 'contact.info.responseTime.label' | translate }}</h3>
304
- <p class="text-primary-100">{{ 'contact.info.responseTime.value' | translate }}</p>
305
- <p class="text-sm text-primary-200 mt-1">{{ 'contact.info.responseTime.description' | translate }}</p>
306
- </div>
307
- </div>
308
- </div>
309
- </div>
310
-
311
- <!-- Resources Card -->
312
- <div class="bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
313
- <div class="mb-6">
314
- <div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-linear-to-br from-accent-500 to-secondary-500 mb-4">
315
- <svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
316
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path>
317
- </svg>
318
- </div>
319
- <h2 class="text-2xl font-bold text-gray-900">{{ 'contact.help.title' | translate }}</h2>
320
- <p class="text-gray-600 mt-1">{{ 'contact.help.subtitle' | translate }}</p>
321
- </div>
322
-
323
- <ul class="space-y-3">
324
- <li>
325
- <a href="https://angular.dev/" target="_blank" class="group flex items-center justify-between p-3 rounded-xl hover:bg-primary-50 transition-colors">
326
- <div class="flex items-center gap-3">
327
- <div class="h-10 w-10 bg-danger-100 rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform">
328
- <svg class="h-5 w-5 text-danger-600" fill="currentColor" viewBox="0 0 24 24">
329
- <path d="M12 2L2 6l10 5 10-5-10-4zm0 18.5l-7-3.5v-6l7 3.5 7-3.5v6l-7 3.5z"/>
330
- </svg>
331
- </div>
332
- <span class="font-medium text-gray-700 group-hover:text-primary-600 transition-colors">{{ 'contact.help.links.angular' | translate }}</span>
333
- </div>
334
- <svg class="h-5 w-5 text-gray-400 group-hover:text-primary-600 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
335
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
336
- </svg>
337
- </a>
338
- </li>
339
- <li>
340
- <a href="https://tailwindcss.com/" target="_blank" class="group flex items-center justify-between p-3 rounded-xl hover:bg-primary-50 transition-colors">
341
- <div class="flex items-center gap-3">
342
- <div class="h-10 w-10 bg-secondary-100 rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform">
343
- <svg class="h-5 w-5 text-secondary-600" fill="currentColor" viewBox="0 0 24 24">
344
- <path d="M12 6c-2.67 0-4.33 1.33-5 4 1-1.33 2.17-1.83 3.5-1.5.76.19 1.31.75 1.91 1.36.98 1 2.09 2.14 4.59 2.14 2.67 0 4.33-1.33 5-4-1 1.33-2.17 1.83-3.5 1.5-.76-.19-1.3-.75-1.91-1.36C15.61 7.14 14.5 6 12 6zM7 12c-2.67 0-4.33 1.33-5 4 1-1.33 2.17-1.83 3.5-1.5.76.19 1.3.75 1.91 1.36C8.39 16.86 9.5 18 12 18c2.67 0 4.33-1.33 5-4-1 1.33-2.17 1.83-3.5 1.5-.76-.19-1.3-.75-1.91-1.36C10.61 13.14 9.5 12 7 12z"/>
345
- </svg>
346
- </div>
347
- <span class="font-medium text-gray-700 group-hover:text-primary-600 transition-colors">{{ 'contact.help.links.tailwind' | translate }}</span>
348
- </div>
349
- <svg class="h-5 w-5 text-gray-400 group-hover:text-primary-600 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
350
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
351
- </svg>
352
- </a>
353
- </li>
354
- </ul>
355
- </div>
356
- </div>
357
- </div>
358
- </div>
359
- </div>
360
- `;
361
-
362
- await fs.writeFile(
363
- path.join(config.fullPath, 'src/app/features/contact/contact.component.ts'),
364
- contactComponentTs
365
- );
366
-
367
- await fs.writeFile(
368
- path.join(config.fullPath, 'src/app/features/contact/contact.component.html'),
369
- contactComponentHtml
370
- );
371
- }
372
-
373
- async function createRouting(config) {
374
- const routes = `import { Routes } from '@angular/router';
375
- // import { authGuard } from './core/guards/auth.guard';
376
-
377
- export const routes: Routes = [
378
- // Public routes
379
- {
380
- path: '',
381
- loadComponent: () => import('./features/home/home.component').then(c => c.HomeComponent)
382
- },
383
- {
384
- path: 'about',
385
- loadComponent: () => import('./features/about/about.component').then(c => c.AboutComponent)
386
- },
387
- {
388
- path: 'contact',
389
- loadComponent: () => import('./features/contact/contact.component').then(c => c.ContactComponent)
390
- },
391
-
392
- // Auth routes with layout
393
- {
394
- path: 'auth',
395
- loadComponent: () => import('./layout/auth/auth-layout.component').then(c => c.AuthLayoutComponent),
396
- children: [
397
- {
398
- path: '',
399
- redirectTo: 'login',
400
- pathMatch: 'full'
401
- },
402
- {
403
- path: 'login',
404
- loadComponent: () => import('./features/auth/login/login.component').then(c => c.LoginComponent)
405
- },
406
- {
407
- path: 'register',
408
- loadComponent: () => import('./features/auth/register/register.component').then(c => c.RegisterComponent)
409
- },
410
- {
411
- path: 'forgot-password',
412
- loadComponent: () => import('./features/auth/forgot-password/forgot-password.component').then(c => c.ForgotPasswordComponent)
413
- }
414
- ]
415
- },
416
-
417
- // Example protected route (uncomment authGuard import above when you need authentication)
418
- // {
419
- // path: 'dashboard',
420
- // loadComponent: () => import('./features/dashboard/dashboard.component').then(c => c.DashboardComponent),
421
- // canActivate: [authGuard]
422
- // },
423
-
424
- // Catch-all redirect
425
- {
426
- path: '**',
427
- redirectTo: ''
428
- }
429
- ];`;
430
-
431
- await fs.writeFile(path.join(config.fullPath, 'src/app/app.routes.ts'), routes);
432
- }
433
-
434
- async function createAppConfig(config) {
435
- // Check if SSR and zoneless are enabled
436
- const isSSR = config.ssr || false;
437
- const isZoneless = config.zoneless || false;
438
-
439
- // Build imports based on configuration
440
- const changeDetectionImport = isZoneless
441
- ? 'provideZonelessChangeDetection'
442
- : 'provideZoneChangeDetection';
443
-
444
- const coreImports = `import { ApplicationConfig, provideBrowserGlobalErrorListeners, ${changeDetectionImport}, isDevMode, importProvidersFrom } from '@angular/core';
445
- import { provideRouter } from '@angular/router';
446
- import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';${
447
- isSSR
448
- ? "\nimport { provideClientHydration, withEventReplay } from '@angular/platform-browser';"
449
- : ''
450
- }
451
- import { provideServiceWorker } from '@angular/service-worker';
452
- import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
453
- import { TranslateHttpLoader } from '@ngx-translate/http-loader';
454
- import { HttpClient } from '@angular/common/http';
455
-
456
- import { routes } from './app.routes';
457
- import { environment } from '@environments/environment';
458
- import { authInterceptor } from '@core/interceptors/auth.interceptor';
459
- import { errorInterceptor } from '@core/interceptors/error.interceptor';
460
- import { loadingInterceptor } from '@core/interceptors/loading.interceptor';
461
- import { cachingInterceptor } from '@core/interceptors/caching.interceptor';`;
462
-
463
- // Build providers array based on configuration
464
- const ssrProvider = isSSR ? '\n provideClientHydration(withEventReplay()),' : '';
465
- const changeDetectionProvider = isZoneless
466
- ? 'provideZonelessChangeDetection()'
467
- : 'provideZoneChangeDetection({ eventCoalescing: true })';
468
-
469
- const appConfig = `${coreImports}
470
-
471
- // Translation loader factory
472
- export function HttpLoaderFactory(http: HttpClient) {
473
- return new TranslateHttpLoader(http, './assets/i18n/', '.json');
474
- }
475
-
476
- export const appConfig: ApplicationConfig = {
477
- providers: [
478
- provideBrowserGlobalErrorListeners(),
479
- ${changeDetectionProvider},
480
- provideRouter(routes),${ssrProvider}
481
- provideHttpClient(
482
- withFetch(),
483
- withInterceptors([
484
- authInterceptor,
485
- cachingInterceptor,
486
- loadingInterceptor,
487
- errorInterceptor
488
- ])
489
- ),
490
- // Translation configuration
491
- importProvidersFrom(
492
- TranslateModule.forRoot({
493
- loader: {
494
- provide: TranslateLoader,
495
- useFactory: HttpLoaderFactory,
496
- deps: [HttpClient]
497
- }
498
- })
499
- ),
500
- provideServiceWorker('ngsw-worker.js', {
501
- enabled: !isDevMode(),
502
- registrationStrategy: 'registerWhenStable:30000'
503
- }),
504
- // Environment-based providers
505
- ...(environment.enableDevTools && isDevMode() ? [] : []),
506
- // Add more providers as needed based on environment configuration
507
- ]
508
- };`;
509
-
510
- await fs.writeFile(path.join(config.fullPath, 'src/app/app.config.ts'), appConfig);
511
- }
512
-
513
- async function createAppComponent(config) {
514
- const app = `import { Component, OnInit, inject } from '@angular/core';
515
- import { RouterOutlet } from '@angular/router';
516
- import { HeaderComponent } from './layout/header/header.component';
517
- import { FooterComponent } from './layout/footer/footer.component';
518
- import { ToastComponent } from '@shared/components/toast/toast.component';
519
- import { ModalComponent } from '@shared/components/modal/modal.component';
520
- import { TranslationService } from '@core/i18n/translation.service';
521
- import { SeoService } from '@core/services/seo.service';
522
- import { createOrganizationSchema, createWebSiteSchema } from '@core/utils/structured-data';
523
-
524
- @Component({
525
- selector: 'app-root',
526
- imports: [RouterOutlet, HeaderComponent, FooterComponent, ToastComponent, ModalComponent],
527
- template: \`
528
- <div class="min-h-screen flex flex-col bg-white">
529
- <app-header />
530
-
531
- <main class="flex-1">
532
- <router-outlet />
533
- </main>
534
-
535
- <app-footer />
536
-
537
- <!-- Global Toast Notifications -->
538
- <app-toast />
539
-
540
- <!-- Global Modal System -->
541
- <app-modal />
542
- </div>
543
- \`
544
- })
545
- export class App implements OnInit {
546
- // Inject services
547
- private translationService = inject(TranslationService);
548
- private seo = inject(SeoService);
549
-
550
- title = '${config.projectName}';
551
-
552
- ngOnInit(): void {
553
- // Add global Organization structured data
554
- const organizationData = createOrganizationSchema({
555
- name: '${config.projectName}',
556
- url: 'https://example.com', // TODO: Update with your production URL
557
- logo: '/assets/images/logo.svg',
558
- description: 'A modern Angular application built with best practices',
559
- sameAs: [
560
- // 'https://twitter.com/yourhandle',
561
- // 'https://facebook.com/yourpage',
562
- // 'https://linkedin.com/company/yourcompany'
563
- ]
564
- });
565
-
566
- const websiteData = createWebSiteSchema({
567
- name: '${config.projectName}',
568
- url: 'https://example.com', // TODO: Update with your production URL
569
- description: 'A modern Angular application with Tailwind CSS, SEO optimization, and i18n support'
570
- });
571
-
572
- // Add structured data to the page
573
- this.seo.addStructuredData({
574
- '@context': 'https://schema.org',
575
- '@graph': [organizationData, websiteData]
576
- });
577
- }
578
- }`;
579
-
580
- const appHtml = `<!-- This file is not used in standalone components -->
581
- <!-- The template is defined inline in app.component.ts -->`;
582
-
583
- await fs.writeFile(path.join(config.fullPath, 'src/app/app.ts'), app);
584
-
585
- await fs.writeFile(path.join(config.fullPath, 'src/app/app.html'), appHtml);
586
- }
587
-
588
- async function createStyles(config) {
589
- // Main styles with Tailwind v4 theme system
590
- const mainStyles = `@import "tailwindcss";
591
-
592
- /* ============================================
593
- TAILWIND V4 THEME CONFIGURATION
594
- ============================================
595
-
596
- This section defines the theme colors for your application.
597
- Tailwind v4 uses the @theme directive to create custom color scales
598
- that automatically generate utility classes (bg-*, text-*, border-*, etc.)
599
-
600
- To customize your theme:
601
- 1. Change the color values below to match your brand
602
- 2. All components will automatically use the new colors
603
- 3. Color format: Use hex colors (e.g., #3b82f6) or rgb (e.g., rgb(59 130 246))
604
-
605
- Example: Change primary-500 from #3b82f6 to #8b5cf6 for purple theme
606
- */
607
-
608
- @theme {
609
- /* PRIMARY COLOR SCALE - Main brand color */
610
- --color-primary-50: #eff6ff;
611
- --color-primary-100: #dbeafe;
612
- --color-primary-200: #bfdbfe;
613
- --color-primary-300: #93c5fd;
614
- --color-primary-400: #60a5fa;
615
- --color-primary-500: #3b82f6; /* Main primary color - CHANGE THIS for your brand */
616
- --color-primary-600: #2563eb; /* Primary hover state */
617
- --color-primary-700: #1d4ed8;
618
- --color-primary-800: #1e40af;
619
- --color-primary-900: #1e3a8a;
620
- --color-primary-950: #172554;
621
-
622
- /* SECONDARY COLOR SCALE - Secondary brand color */
623
- --color-secondary-50: #ecfeff;
624
- --color-secondary-100: #cffafe;
625
- --color-secondary-200: #a5f3fc;
626
- --color-secondary-300: #67e8f9;
627
- --color-secondary-400: #22d3ee;
628
- --color-secondary-500: #06b6d4; /* Main secondary color - CHANGE THIS for your brand */
629
- --color-secondary-600: #0891b2; /* Secondary hover state */
630
- --color-secondary-700: #0e7490;
631
- --color-secondary-800: #155e75;
632
- --color-secondary-900: #164e63;
633
- --color-secondary-950: #083344;
634
-
635
- /* ACCENT COLOR SCALE - Accent/highlight color */
636
- --color-accent-50: #faf5ff;
637
- --color-accent-100: #f3e8ff;
638
- --color-accent-200: #e9d5ff;
639
- --color-accent-300: #d8b4fe;
640
- --color-accent-400: #c084fc;
641
- --color-accent-500: #a855f7; /* Main accent color */
642
- --color-accent-600: #9333ea; /* Accent hover state */
643
- --color-accent-700: #7e22ce;
644
- --color-accent-800: #6b21a8;
645
- --color-accent-900: #581c87;
646
- --color-accent-950: #3b0764;
647
-
648
- /* SUCCESS COLOR SCALE - For success states */
649
- --color-success-50: #f0fdf4;
650
- --color-success-100: #dcfce7;
651
- --color-success-200: #bbf7d0;
652
- --color-success-300: #86efac;
653
- --color-success-400: #4ade80;
654
- --color-success-500: #22c55e; /* Main success color */
655
- --color-success-600: #16a34a;
656
- --color-success-700: #15803d;
657
- --color-success-800: #166534;
658
- --color-success-900: #14532d;
659
- --color-success-950: #052e16;
660
-
661
- /* DANGER COLOR SCALE - For error/danger states */
662
- --color-danger-50: #fef2f2;
663
- --color-danger-100: #fee2e2;
664
- --color-danger-200: #fecaca;
665
- --color-danger-300: #fca5a5;
666
- --color-danger-400: #f87171;
667
- --color-danger-500: #ef4444; /* Main danger color */
668
- --color-danger-600: #dc2626;
669
- --color-danger-700: #b91c1c;
670
- --color-danger-800: #991b1b;
671
- --color-danger-900: #7f1d1d;
672
- --color-danger-950: #450a0a;
673
-
674
- /* WARNING COLOR SCALE - For warning states */
675
- --color-warning-50: #fffbeb;
676
- --color-warning-100: #fef3c7;
677
- --color-warning-200: #fde68a;
678
- --color-warning-300: #fcd34d;
679
- --color-warning-400: #fbbf24;
680
- --color-warning-500: #f59e0b; /* Main warning color */
681
- --color-warning-600: #d97706;
682
- --color-warning-700: #b45309;
683
- --color-warning-800: #92400e;
684
- --color-warning-900: #78350f;
685
- --color-warning-950: #451a03;
686
-
687
- /* INFO COLOR SCALE - For informational states */
688
- --color-info-50: #eff6ff;
689
- --color-info-100: #dbeafe;
690
- --color-info-200: #bfdbfe;
691
- --color-info-300: #93c5fd;
692
- --color-info-400: #60a5fa;
693
- --color-info-500: #3b82f6; /* Main info color */
694
- --color-info-600: #2563eb;
695
- --color-info-700: #1d4ed8;
696
- --color-info-800: #1e40af;
697
- --color-info-900: #1e3a8a;
698
- --color-info-950: #172554;
699
- }
700
-
701
- /* ============================================
702
- USAGE EXAMPLES
703
- ============================================
704
-
705
- After defining colors in @theme, you can use them in your templates:
706
-
707
- - bg-primary-500 (primary background)
708
- - text-primary-600 (primary text)
709
- - border-primary-500 (primary border)
710
- - hover:bg-primary-600 (primary hover)
711
- - focus:ring-primary-500 (primary focus ring)
712
-
713
- - bg-secondary-500 (secondary background)
714
- - bg-accent-500 (accent background)
715
- - bg-success-500 (success background)
716
- - bg-danger-500 (danger background)
717
- - bg-warning-500 (warning background)
718
- - bg-info-500 (info background)
719
-
720
- All Tailwind utilities work: bg-*, text-*, border-*, ring-*, etc.
721
- */
722
-
723
- /* RTL Support */
724
- html[dir="rtl"] {
725
- direction: rtl;
726
- }
727
-
728
- html[dir="ltr"] {
729
- direction: ltr;
730
- }
731
-
732
- /* RTL-aware spacing utilities */
733
- html[dir="rtl"] .space-x-2 > * + * {
734
- margin-right: 0.5rem;
735
- margin-left: 0;
736
- }
737
-
738
- html[dir="rtl"] .space-x-3 > * + * {
739
- margin-right: 0.75rem;
740
- margin-left: 0;
741
- }
742
-
743
- html[dir="rtl"] .space-x-4 > * + * {
744
- margin-right: 1rem;
745
- margin-left: 0;
746
- }
747
-
748
- html[dir="rtl"] .space-x-8 > * + * {
749
- margin-right: 2rem;
750
- margin-left: 0;
751
- }
752
-
753
- /* RTL-aware text alignment */
754
- html[dir="rtl"] .text-left {
755
- text-align: right;
756
- }
757
-
758
- html[dir="rtl"] .text-right {
759
- text-align: left;
760
- }
761
-
762
- /* Custom utility classes */
763
- .container {
764
- @apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
765
- }
766
-
767
- /* Custom scrollbar */
768
- ::-webkit-scrollbar {
769
- width: 8px;
770
- height: 8px;
771
- }
772
-
773
- ::-webkit-scrollbar-track {
774
- @apply bg-gray-100;
775
- }
776
-
777
- ::-webkit-scrollbar-thumb {
778
- @apply bg-gray-300 rounded-full;
779
- }
780
-
781
- ::-webkit-scrollbar-thumb:hover {
782
- @apply bg-gray-400;
783
- }
784
-
785
- /* Focus styles for accessibility - now using theme colors */
786
- .focus-visible {
787
- @apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2;
788
- }
789
-
790
- /* Animation classes */
791
- .animate-fade-in {
792
- animation: fadeIn 0.6s ease-in-out;
793
- }
794
-
795
- .animate-slide-up {
796
- animation: slideUp 0.6s ease-out;
797
- }
798
-
799
- .animate-blob {
800
- animation: blob 7s infinite;
801
- }
802
-
803
- .animation-delay-2000 {
804
- animation-delay: 2s;
805
- }
806
-
807
- .animation-delay-4000 {
808
- animation-delay: 4s;
809
- }
810
-
811
- .animation-delay-200 {
812
- animation-delay: 0.2s;
813
- }
814
-
815
- @keyframes fadeIn {
816
- from { opacity: 0; }
817
- to { opacity: 1; }
818
- }
819
-
820
- @keyframes slideUp {
821
- from {
822
- opacity: 0;
823
- transform: translateY(20px);
824
- }
825
- to {
826
- opacity: 1;
827
- transform: translateY(0);
828
- }
829
- }
830
-
831
- @keyframes blob {
832
- 0% {
833
- transform: translate(0px, 0px) scale(1);
834
- }
835
- 33% {
836
- transform: translate(30px, -50px) scale(1.1);
837
- }
838
- 66% {
839
- transform: translate(-20px, 20px) scale(0.9);
840
- }
841
- 100% {
842
- transform: translate(0px, 0px) scale(1);
843
- }
844
- }
845
-
846
- /* Print styles */
847
- @media print {
848
- .no-print {
849
- display: none !important;
850
- }
851
- }
852
-
853
- /* RTL Transition smoothing */
854
- * {
855
- transition: margin 0.2s ease-in-out;
856
- }`;
857
-
858
- await fs.writeFile(path.join(config.fullPath, 'src/styles.css'), mainStyles);
859
- }
860
-
861
- module.exports = {
862
- createContactComponent,
863
- createRouting,
864
- createAppConfig,
865
- createAppComponent,
866
- createStyles,
867
- };