create-ng-tailwind 1.0.0 → 2.0.1
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 +209 -8
- package/CLAUDE.md +178 -0
- package/LICENSE +1 -1
- package/README.md +151 -246
- package/bin/create-ng-tailwind.js +5 -407
- package/lib/cli/index.js +222 -0
- package/lib/cli/interactive.js +26 -0
- package/lib/cli/validators.js +23 -0
- package/lib/config/defaults.js +7 -0
- package/lib/managers/ProjectManager.js +97 -0
- package/lib/managers/TailwindManager.js +100 -0
- package/lib/managers/TemplateManager.js +39 -0
- package/lib/templates/index.js +7 -0
- package/lib/templates/minimal/index.js +24 -0
- package/lib/templates/starter/advanced-features.js +528 -0
- package/lib/templates/starter/features.js +700 -0
- package/lib/templates/starter/index.js +4117 -0
- package/lib/templates/starter/ui-features.js +437 -0
- package/lib/utils/ai-config.js +606 -0
- package/lib/utils/constants.js +14 -0
- package/lib/utils/helpers.js +60 -0
- package/lib/utils/logger.js +109 -0
- package/package.json +6 -15
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
async function createContactComponent(config) {
|
|
5
|
+
const contactComponentTs = `import { Component, inject } from '@angular/core';
|
|
6
|
+
import { CommonModule } from '@angular/common';
|
|
7
|
+
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
|
|
8
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
9
|
+
import { TranslateModule } from '@ngx-translate/core';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
heroEnvelope,
|
|
13
|
+
heroMapPin,
|
|
14
|
+
heroClock,
|
|
15
|
+
heroCheckCircle,
|
|
16
|
+
heroPaperAirplane,
|
|
17
|
+
} from '@ng-icons/heroicons/outline';
|
|
18
|
+
import { ButtonComponent } from '@shared/components/button/button.component';
|
|
19
|
+
|
|
20
|
+
@Component({
|
|
21
|
+
selector: 'app-contact',
|
|
22
|
+
standalone: true,
|
|
23
|
+
imports: [CommonModule, 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 {
|
|
34
|
+
private fb = inject(FormBuilder);
|
|
35
|
+
|
|
36
|
+
isSubmitting = false;
|
|
37
|
+
submitted = false;
|
|
38
|
+
|
|
39
|
+
contactForm = this.fb.group({
|
|
40
|
+
name: ['', [Validators.required, Validators.minLength(2)]],
|
|
41
|
+
email: ['', [Validators.required, Validators.email]],
|
|
42
|
+
subject: ['', Validators.required],
|
|
43
|
+
message: ['', [Validators.required, Validators.minLength(10)]]
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
onSubmit(): void {
|
|
47
|
+
if (this.contactForm.valid) {
|
|
48
|
+
this.isSubmitting = true;
|
|
49
|
+
|
|
50
|
+
// Simulate form submission
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
this.isSubmitting = false;
|
|
53
|
+
this.submitted = true;
|
|
54
|
+
this.contactForm.reset();
|
|
55
|
+
|
|
56
|
+
// Hide success message after 5 seconds
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
this.submitted = false;
|
|
59
|
+
}, 5000);
|
|
60
|
+
}, 1500);
|
|
61
|
+
} else {
|
|
62
|
+
// Mark all fields as touched to show validation errors
|
|
63
|
+
Object.keys(this.contactForm.controls).forEach(key => {
|
|
64
|
+
this.contactForm.get(key)?.markAsTouched();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}`;
|
|
69
|
+
|
|
70
|
+
const contactComponentHtml = `<!-- Hero Section -->
|
|
71
|
+
<section class="relative py-20 bg-linear-to-br from-blue-600 via-cyan-600 to-teal-600 overflow-hidden">
|
|
72
|
+
<div class="absolute inset-0 opacity-10">
|
|
73
|
+
<div class="absolute top-0 -left-4 w-96 h-96 bg-white rounded-full mix-blend-multiply filter blur-3xl animate-blob"></div>
|
|
74
|
+
<div class="absolute top-0 -right-4 w-96 h-96 bg-yellow-200 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-2000"></div>
|
|
75
|
+
<div class="absolute bottom-0 left-1/2 w-96 h-96 bg-green-200 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-4000"></div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="relative container mx-auto px-4 text-center">
|
|
79
|
+
<div class="animate-fade-in">
|
|
80
|
+
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-white/20 backdrop-blur-sm mb-6 shadow-xl">
|
|
81
|
+
<ng-icon name="heroEnvelope" size="40" style="color: white;"></ng-icon>
|
|
82
|
+
</div>
|
|
83
|
+
<h1 class="text-5xl md:text-6xl lg:text-7xl font-bold text-white mb-6 drop-shadow-lg">
|
|
84
|
+
{{ 'contact.title' | translate }}
|
|
85
|
+
</h1>
|
|
86
|
+
<p class="text-xl md:text-2xl text-cyan-50 max-w-3xl mx-auto leading-relaxed">
|
|
87
|
+
{{ 'contact.subtitle' | translate }}
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</section>
|
|
92
|
+
|
|
93
|
+
<!-- Main Content -->
|
|
94
|
+
<div class="bg-linear-to-br from-gray-50 to-blue-50 py-16 -mt-8">
|
|
95
|
+
<div class="container mx-auto px-4 sm:px-6 lg:px-8 max-w-6xl">
|
|
96
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12">
|
|
97
|
+
|
|
98
|
+
<!-- Contact Form Card -->
|
|
99
|
+
<div class="bg-white rounded-2xl shadow-xl p-8 border border-gray-100 hover:shadow-2xl transition-shadow animate-fade-in">
|
|
100
|
+
<div class="mb-6">
|
|
101
|
+
<div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-linear-to-br from-blue-500 to-cyan-500 mb-4">
|
|
102
|
+
<ng-icon name="heroPaperAirplane" size="24" style="color: white;"></ng-icon>
|
|
103
|
+
</div>
|
|
104
|
+
<h2 class="text-2xl font-bold text-gray-900">{{ 'contact.form.title' | translate }}</h2>
|
|
105
|
+
<p class="text-gray-600 mt-1">{{ 'contact.form.description' | translate }}</p>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<form [formGroup]="contactForm" (ngSubmit)="onSubmit()" class="space-y-5">
|
|
109
|
+
<!-- Name Field -->
|
|
110
|
+
<div>
|
|
111
|
+
<label for="name" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
112
|
+
{{ 'contact.form.name' | translate }} *
|
|
113
|
+
</label>
|
|
114
|
+
<input
|
|
115
|
+
id="name"
|
|
116
|
+
type="text"
|
|
117
|
+
formControlName="name"
|
|
118
|
+
placeholder="John Doe"
|
|
119
|
+
class="w-full px-4 py-3 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
|
120
|
+
[class.border-red-500]="contactForm.get('name')?.invalid && contactForm.get('name')?.touched"
|
|
121
|
+
[class.focus:ring-red-500]="contactForm.get('name')?.invalid && contactForm.get('name')?.touched">
|
|
122
|
+
@if (contactForm.get('name')?.invalid && contactForm.get('name')?.touched) {
|
|
123
|
+
<div class="mt-2 text-sm text-red-600 flex items-center gap-1">
|
|
124
|
+
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
|
125
|
+
<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"/>
|
|
126
|
+
</svg>
|
|
127
|
+
@if (contactForm.get('name')?.errors?.['required']) {
|
|
128
|
+
<span>{{ 'contact.form.errors.nameRequired' | translate }}</span>
|
|
129
|
+
}
|
|
130
|
+
@if (contactForm.get('name')?.errors?.['minlength']) {
|
|
131
|
+
<span>{{ 'contact.form.errors.nameMinLength' | translate }}</span>
|
|
132
|
+
}
|
|
133
|
+
</div>
|
|
134
|
+
}
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<!-- Email Field -->
|
|
138
|
+
<div>
|
|
139
|
+
<label for="email" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
140
|
+
{{ 'contact.form.email' | translate }} *
|
|
141
|
+
</label>
|
|
142
|
+
<input
|
|
143
|
+
id="email"
|
|
144
|
+
type="email"
|
|
145
|
+
formControlName="email"
|
|
146
|
+
placeholder="john@example.com"
|
|
147
|
+
class="w-full px-4 py-3 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
|
148
|
+
[class.border-red-500]="contactForm.get('email')?.invalid && contactForm.get('email')?.touched"
|
|
149
|
+
[class.focus:ring-red-500]="contactForm.get('email')?.invalid && contactForm.get('email')?.touched">
|
|
150
|
+
@if (contactForm.get('email')?.invalid && contactForm.get('email')?.touched) {
|
|
151
|
+
<div class="mt-2 text-sm text-red-600 flex items-center gap-1">
|
|
152
|
+
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
|
153
|
+
<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"/>
|
|
154
|
+
</svg>
|
|
155
|
+
@if (contactForm.get('email')?.errors?.['required']) {
|
|
156
|
+
<span>{{ 'contact.form.errors.emailRequired' | translate }}</span>
|
|
157
|
+
}
|
|
158
|
+
@if (contactForm.get('email')?.errors?.['email']) {
|
|
159
|
+
<span>{{ 'contact.form.errors.emailInvalid' | translate }}</span>
|
|
160
|
+
}
|
|
161
|
+
</div>
|
|
162
|
+
}
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<!-- Subject Field -->
|
|
166
|
+
<div>
|
|
167
|
+
<label for="subject" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
168
|
+
{{ 'contact.form.subject' | translate }} *
|
|
169
|
+
</label>
|
|
170
|
+
<input
|
|
171
|
+
id="subject"
|
|
172
|
+
type="text"
|
|
173
|
+
formControlName="subject"
|
|
174
|
+
placeholder="How can we help?"
|
|
175
|
+
class="w-full px-4 py-3 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
|
176
|
+
[class.border-red-500]="contactForm.get('subject')?.invalid && contactForm.get('subject')?.touched"
|
|
177
|
+
[class.focus:ring-red-500]="contactForm.get('subject')?.invalid && contactForm.get('subject')?.touched">
|
|
178
|
+
@if (contactForm.get('subject')?.invalid && contactForm.get('subject')?.touched) {
|
|
179
|
+
<div class="mt-2 text-sm text-red-600 flex items-center gap-1">
|
|
180
|
+
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
|
181
|
+
<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"/>
|
|
182
|
+
</svg>
|
|
183
|
+
<span>{{ 'contact.form.errors.subjectRequired' | translate }}</span>
|
|
184
|
+
</div>
|
|
185
|
+
}
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<!-- Message Field -->
|
|
189
|
+
<div>
|
|
190
|
+
<label for="message" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
191
|
+
{{ 'contact.form.message' | translate }} *
|
|
192
|
+
</label>
|
|
193
|
+
<textarea
|
|
194
|
+
id="message"
|
|
195
|
+
formControlName="message"
|
|
196
|
+
rows="5"
|
|
197
|
+
placeholder="Tell us more about your inquiry..."
|
|
198
|
+
class="w-full px-4 py-3 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all resize-vertical"
|
|
199
|
+
[class.border-red-500]="contactForm.get('message')?.invalid && contactForm.get('message')?.touched"
|
|
200
|
+
[class.focus:ring-red-500]="contactForm.get('message')?.invalid && contactForm.get('message')?.touched">
|
|
201
|
+
</textarea>
|
|
202
|
+
@if (contactForm.get('message')?.invalid && contactForm.get('message')?.touched) {
|
|
203
|
+
<div class="mt-2 text-sm text-red-600 flex items-center gap-1">
|
|
204
|
+
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
|
205
|
+
<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"/>
|
|
206
|
+
</svg>
|
|
207
|
+
@if (contactForm.get('message')?.errors?.['required']) {
|
|
208
|
+
<span>{{ 'contact.form.errors.messageRequired' | translate }}</span>
|
|
209
|
+
}
|
|
210
|
+
@if (contactForm.get('message')?.errors?.['minlength']) {
|
|
211
|
+
<span>{{ 'contact.form.errors.messageMinLength' | translate }}</span>
|
|
212
|
+
}
|
|
213
|
+
</div>
|
|
214
|
+
}
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<!-- Success Message -->
|
|
218
|
+
@if (submitted && !contactForm.invalid) {
|
|
219
|
+
<div class="p-4 bg-linear-to-r from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl animate-fade-in">
|
|
220
|
+
<div class="flex items-center gap-2">
|
|
221
|
+
<ng-icon name="heroCheckCircle" size="20" style="color: #059669;"></ng-icon>
|
|
222
|
+
<p class="text-green-700 font-medium">
|
|
223
|
+
{{ 'contact.form.success' | translate }}
|
|
224
|
+
</p>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
<!-- Submit Button -->
|
|
230
|
+
<app-button
|
|
231
|
+
type="submit"
|
|
232
|
+
[fullWidth]="true"
|
|
233
|
+
[loading]="isSubmitting"
|
|
234
|
+
[disabled]="contactForm.invalid">
|
|
235
|
+
<span class="flex items-center justify-center gap-2">
|
|
236
|
+
@if (!isSubmitting) {
|
|
237
|
+
<ng-icon name="heroPaperAirplane" size="18"></ng-icon>
|
|
238
|
+
}
|
|
239
|
+
{{ isSubmitting ? ('contact.form.sending' | translate) : ('contact.form.submit' | translate) }}
|
|
240
|
+
</span>
|
|
241
|
+
</app-button>
|
|
242
|
+
</form>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<!-- Contact Info & Resources -->
|
|
246
|
+
<div class="space-y-6 animate-fade-in animation-delay-200">
|
|
247
|
+
<!-- Contact Information Card -->
|
|
248
|
+
<div class="bg-linear-to-br from-blue-600 to-cyan-600 rounded-2xl shadow-xl p-8 text-white">
|
|
249
|
+
<div class="mb-6">
|
|
250
|
+
<div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-white/20 backdrop-blur-sm mb-4">
|
|
251
|
+
<ng-icon name="heroMapPin" size="24" style="color: white;"></ng-icon>
|
|
252
|
+
</div>
|
|
253
|
+
<h2 class="text-2xl font-bold">{{ 'contact.info.title' | translate }}</h2>
|
|
254
|
+
<p class="text-cyan-100 mt-1">{{ 'contact.info.description' | translate }}</p>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<div class="space-y-6">
|
|
258
|
+
<div class="flex items-start gap-4">
|
|
259
|
+
<div class="shrink-0">
|
|
260
|
+
<div class="h-12 w-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center">
|
|
261
|
+
<ng-icon name="heroEnvelope" size="24" style="color: white;"></ng-icon>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
<div>
|
|
265
|
+
<h3 class="text-lg font-semibold mb-1">{{ 'contact.info.email.label' | translate }}</h3>
|
|
266
|
+
<p class="text-cyan-100">hello@example.com</p>
|
|
267
|
+
<p class="text-sm text-cyan-200 mt-1">{{ 'contact.info.email.description' | translate }}</p>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<div class="flex items-start gap-4">
|
|
272
|
+
<div class="shrink-0">
|
|
273
|
+
<div class="h-12 w-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center">
|
|
274
|
+
<ng-icon name="heroMapPin" size="24" style="color: white;"></ng-icon>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
<div>
|
|
278
|
+
<h3 class="text-lg font-semibold mb-1">{{ 'contact.info.location.label' | translate }}</h3>
|
|
279
|
+
<p class="text-cyan-100">{{ 'contact.info.location.value' | translate }}</p>
|
|
280
|
+
<p class="text-sm text-cyan-200 mt-1">{{ 'contact.info.location.description' | translate }}</p>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<div class="flex items-start gap-4">
|
|
285
|
+
<div class="shrink-0">
|
|
286
|
+
<div class="h-12 w-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center">
|
|
287
|
+
<ng-icon name="heroClock" size="24" style="color: white;"></ng-icon>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
<div>
|
|
291
|
+
<h3 class="text-lg font-semibold mb-1">{{ 'contact.info.responseTime.label' | translate }}</h3>
|
|
292
|
+
<p class="text-cyan-100">{{ 'contact.info.responseTime.value' | translate }}</p>
|
|
293
|
+
<p class="text-sm text-cyan-200 mt-1">{{ 'contact.info.responseTime.description' | translate }}</p>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
<!-- Resources Card -->
|
|
300
|
+
<div class="bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
|
|
301
|
+
<div class="mb-6">
|
|
302
|
+
<div class="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-linear-to-br from-purple-500 to-pink-500 mb-4">
|
|
303
|
+
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
304
|
+
<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>
|
|
305
|
+
</svg>
|
|
306
|
+
</div>
|
|
307
|
+
<h2 class="text-2xl font-bold text-gray-900">{{ 'contact.help.title' | translate }}</h2>
|
|
308
|
+
<p class="text-gray-600 mt-1">{{ 'contact.help.subtitle' | translate }}</p>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<ul class="space-y-3">
|
|
312
|
+
<li>
|
|
313
|
+
<a href="https://angular.dev/" target="_blank" class="group flex items-center justify-between p-3 rounded-xl hover:bg-blue-50 transition-colors">
|
|
314
|
+
<div class="flex items-center gap-3">
|
|
315
|
+
<div class="h-10 w-10 bg-red-100 rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform">
|
|
316
|
+
<svg class="h-5 w-5 text-red-600" fill="currentColor" viewBox="0 0 24 24">
|
|
317
|
+
<path d="M12 2L2 6l10 5 10-5-10-4zm0 18.5l-7-3.5v-6l7 3.5 7-3.5v6l-7 3.5z"/>
|
|
318
|
+
</svg>
|
|
319
|
+
</div>
|
|
320
|
+
<span class="font-medium text-gray-700 group-hover:text-blue-600 transition-colors">{{ 'contact.help.links.angular' | translate }}</span>
|
|
321
|
+
</div>
|
|
322
|
+
<svg class="h-5 w-5 text-gray-400 group-hover:text-blue-600 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
323
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
324
|
+
</svg>
|
|
325
|
+
</a>
|
|
326
|
+
</li>
|
|
327
|
+
<li>
|
|
328
|
+
<a href="https://tailwindcss.com/" target="_blank" class="group flex items-center justify-between p-3 rounded-xl hover:bg-blue-50 transition-colors">
|
|
329
|
+
<div class="flex items-center gap-3">
|
|
330
|
+
<div class="h-10 w-10 bg-cyan-100 rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform">
|
|
331
|
+
<svg class="h-5 w-5 text-cyan-600" fill="currentColor" viewBox="0 0 24 24">
|
|
332
|
+
<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"/>
|
|
333
|
+
</svg>
|
|
334
|
+
</div>
|
|
335
|
+
<span class="font-medium text-gray-700 group-hover:text-blue-600 transition-colors">{{ 'contact.help.links.tailwind' | translate }}</span>
|
|
336
|
+
</div>
|
|
337
|
+
<svg class="h-5 w-5 text-gray-400 group-hover:text-blue-600 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
338
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
339
|
+
</svg>
|
|
340
|
+
</a>
|
|
341
|
+
</li>
|
|
342
|
+
</ul>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
`;
|
|
349
|
+
|
|
350
|
+
await fs.writeFile(
|
|
351
|
+
path.join(config.fullPath, 'src/app/features/contact/contact.component.ts'),
|
|
352
|
+
contactComponentTs
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
await fs.writeFile(
|
|
356
|
+
path.join(config.fullPath, 'src/app/features/contact/contact.component.html'),
|
|
357
|
+
contactComponentHtml
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function createRouting(config) {
|
|
362
|
+
const routes = `import { Routes } from '@angular/router';
|
|
363
|
+
// import { authGuard } from './core/guards/auth.guard';
|
|
364
|
+
|
|
365
|
+
export const routes: Routes = [
|
|
366
|
+
// Public routes
|
|
367
|
+
{
|
|
368
|
+
path: '',
|
|
369
|
+
loadComponent: () => import('./features/home/home.component').then(c => c.HomeComponent)
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
path: 'about',
|
|
373
|
+
loadComponent: () => import('./features/about/about.component').then(c => c.AboutComponent)
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
path: 'contact',
|
|
377
|
+
loadComponent: () => import('./features/contact/contact.component').then(c => c.ContactComponent)
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
// Auth routes with layout
|
|
381
|
+
{
|
|
382
|
+
path: 'auth',
|
|
383
|
+
loadComponent: () => import('./layout/auth/auth-layout.component').then(c => c.AuthLayoutComponent),
|
|
384
|
+
children: [
|
|
385
|
+
{
|
|
386
|
+
path: '',
|
|
387
|
+
redirectTo: 'login',
|
|
388
|
+
pathMatch: 'full'
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
path: 'login',
|
|
392
|
+
loadComponent: () => import('./features/auth/login/login.component').then(c => c.LoginComponent)
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
path: 'register',
|
|
396
|
+
loadComponent: () => import('./features/auth/register/register.component').then(c => c.RegisterComponent)
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
path: 'forgot-password',
|
|
400
|
+
loadComponent: () => import('./features/auth/forgot-password/forgot-password.component').then(c => c.ForgotPasswordComponent)
|
|
401
|
+
}
|
|
402
|
+
]
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
// Example protected route (uncomment authGuard import above when you need authentication)
|
|
406
|
+
// {
|
|
407
|
+
// path: 'dashboard',
|
|
408
|
+
// loadComponent: () => import('./features/dashboard/dashboard.component').then(c => c.DashboardComponent),
|
|
409
|
+
// canActivate: [authGuard]
|
|
410
|
+
// },
|
|
411
|
+
|
|
412
|
+
// Catch-all redirect
|
|
413
|
+
{
|
|
414
|
+
path: '**',
|
|
415
|
+
redirectTo: ''
|
|
416
|
+
}
|
|
417
|
+
];`;
|
|
418
|
+
|
|
419
|
+
await fs.writeFile(path.join(config.fullPath, 'src/app/app.routes.ts'), routes);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function createAppConfig(config) {
|
|
423
|
+
// Check if SSR and zoneless are enabled
|
|
424
|
+
const isSSR = config.ssr || false;
|
|
425
|
+
const isZoneless = config.zoneless || false;
|
|
426
|
+
|
|
427
|
+
// Build imports based on configuration
|
|
428
|
+
const changeDetectionImport = isZoneless
|
|
429
|
+
? 'provideZonelessChangeDetection'
|
|
430
|
+
: 'provideZoneChangeDetection';
|
|
431
|
+
|
|
432
|
+
const coreImports = `import { ApplicationConfig, provideBrowserGlobalErrorListeners, ${changeDetectionImport}, isDevMode, importProvidersFrom } from '@angular/core';
|
|
433
|
+
import { provideRouter } from '@angular/router';
|
|
434
|
+
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';${
|
|
435
|
+
isSSR
|
|
436
|
+
? "\nimport { provideClientHydration, withEventReplay } from '@angular/platform-browser';"
|
|
437
|
+
: ''
|
|
438
|
+
}
|
|
439
|
+
import { provideServiceWorker } from '@angular/service-worker';
|
|
440
|
+
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
|
441
|
+
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
|
442
|
+
import { HttpClient } from '@angular/common/http';
|
|
443
|
+
|
|
444
|
+
import { routes } from './app.routes';
|
|
445
|
+
import { environment } from '@environments/environment';
|
|
446
|
+
import { authInterceptor } from '@core/interceptors/auth.interceptor';
|
|
447
|
+
import { errorInterceptor } from '@core/interceptors/error.interceptor';
|
|
448
|
+
import { loadingInterceptor } from '@core/interceptors/loading.interceptor';
|
|
449
|
+
import { cachingInterceptor } from '@core/interceptors/caching.interceptor';`;
|
|
450
|
+
|
|
451
|
+
// Build providers array based on configuration
|
|
452
|
+
const ssrProvider = isSSR ? '\n provideClientHydration(withEventReplay()),' : '';
|
|
453
|
+
const changeDetectionProvider = isZoneless
|
|
454
|
+
? 'provideZonelessChangeDetection()'
|
|
455
|
+
: 'provideZoneChangeDetection({ eventCoalescing: true })';
|
|
456
|
+
|
|
457
|
+
const appConfig = `${coreImports}
|
|
458
|
+
|
|
459
|
+
// Translation loader factory
|
|
460
|
+
export function HttpLoaderFactory(http: HttpClient) {
|
|
461
|
+
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export const appConfig: ApplicationConfig = {
|
|
465
|
+
providers: [
|
|
466
|
+
provideBrowserGlobalErrorListeners(),
|
|
467
|
+
${changeDetectionProvider},
|
|
468
|
+
provideRouter(routes),${ssrProvider}
|
|
469
|
+
provideHttpClient(
|
|
470
|
+
withFetch(),
|
|
471
|
+
withInterceptors([
|
|
472
|
+
authInterceptor,
|
|
473
|
+
cachingInterceptor,
|
|
474
|
+
loadingInterceptor,
|
|
475
|
+
errorInterceptor
|
|
476
|
+
])
|
|
477
|
+
),
|
|
478
|
+
// Translation configuration
|
|
479
|
+
importProvidersFrom(
|
|
480
|
+
TranslateModule.forRoot({
|
|
481
|
+
loader: {
|
|
482
|
+
provide: TranslateLoader,
|
|
483
|
+
useFactory: HttpLoaderFactory,
|
|
484
|
+
deps: [HttpClient]
|
|
485
|
+
}
|
|
486
|
+
})
|
|
487
|
+
),
|
|
488
|
+
provideServiceWorker('ngsw-worker.js', {
|
|
489
|
+
enabled: !isDevMode(),
|
|
490
|
+
registrationStrategy: 'registerWhenStable:30000'
|
|
491
|
+
}),
|
|
492
|
+
// Environment-based providers
|
|
493
|
+
...(environment.enableDevTools && isDevMode() ? [] : []),
|
|
494
|
+
// Add more providers as needed based on environment configuration
|
|
495
|
+
]
|
|
496
|
+
};`;
|
|
497
|
+
|
|
498
|
+
await fs.writeFile(path.join(config.fullPath, 'src/app/app.config.ts'), appConfig);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function createAppComponent(config) {
|
|
502
|
+
const app = `import { Component, inject } from '@angular/core';
|
|
503
|
+
import { CommonModule } from '@angular/common';
|
|
504
|
+
import { RouterOutlet } from '@angular/router';
|
|
505
|
+
import { HeaderComponent } from './layout/header/header.component';
|
|
506
|
+
import { FooterComponent } from './layout/footer/footer.component';
|
|
507
|
+
import { ToastComponent } from '@shared/components/toast/toast.component';
|
|
508
|
+
import { ModalComponent } from '@shared/components/modal/modal.component';
|
|
509
|
+
import { TranslationService } from '@core/i18n/translation.service';
|
|
510
|
+
|
|
511
|
+
@Component({
|
|
512
|
+
selector: 'app-root',
|
|
513
|
+
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, ToastComponent, ModalComponent],
|
|
514
|
+
template: \`
|
|
515
|
+
<div class="min-h-screen flex flex-col bg-white">
|
|
516
|
+
<app-header />
|
|
517
|
+
|
|
518
|
+
<main class="flex-1">
|
|
519
|
+
<router-outlet />
|
|
520
|
+
</main>
|
|
521
|
+
|
|
522
|
+
<app-footer />
|
|
523
|
+
|
|
524
|
+
<!-- Global Toast Notifications -->
|
|
525
|
+
<app-toast />
|
|
526
|
+
|
|
527
|
+
<!-- Global Modal System -->
|
|
528
|
+
<app-modal />
|
|
529
|
+
</div>
|
|
530
|
+
\`
|
|
531
|
+
})
|
|
532
|
+
export class App {
|
|
533
|
+
// Inject translation service to initialize it (auto-detects and loads language)
|
|
534
|
+
private translationService = inject(TranslationService);
|
|
535
|
+
title = '${config.projectName}';
|
|
536
|
+
}`;
|
|
537
|
+
|
|
538
|
+
const appHtml = `<!-- This file is not used in standalone components -->
|
|
539
|
+
<!-- The template is defined inline in app.component.ts -->`;
|
|
540
|
+
|
|
541
|
+
await fs.writeFile(path.join(config.fullPath, 'src/app/app.ts'), app);
|
|
542
|
+
|
|
543
|
+
await fs.writeFile(path.join(config.fullPath, 'src/app/app.html'), appHtml);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function createStyles(config) {
|
|
547
|
+
// Main styles
|
|
548
|
+
const mainStyles = `@import "tailwindcss";
|
|
549
|
+
|
|
550
|
+
/* Custom CSS variables for theming */
|
|
551
|
+
:root {
|
|
552
|
+
--color-primary: 59 130 246; /* blue-500 */
|
|
553
|
+
--color-primary-dark: 37 99 235; /* blue-600 */
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/* RTL Support */
|
|
557
|
+
html[dir="rtl"] {
|
|
558
|
+
direction: rtl;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
html[dir="ltr"] {
|
|
562
|
+
direction: ltr;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/* RTL-aware spacing utilities */
|
|
566
|
+
html[dir="rtl"] .space-x-2 > * + * {
|
|
567
|
+
margin-right: 0.5rem;
|
|
568
|
+
margin-left: 0;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
html[dir="rtl"] .space-x-3 > * + * {
|
|
572
|
+
margin-right: 0.75rem;
|
|
573
|
+
margin-left: 0;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
html[dir="rtl"] .space-x-4 > * + * {
|
|
577
|
+
margin-right: 1rem;
|
|
578
|
+
margin-left: 0;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
html[dir="rtl"] .space-x-8 > * + * {
|
|
582
|
+
margin-right: 2rem;
|
|
583
|
+
margin-left: 0;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/* RTL-aware text alignment */
|
|
587
|
+
html[dir="rtl"] .text-left {
|
|
588
|
+
text-align: right;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
html[dir="rtl"] .text-right {
|
|
592
|
+
text-align: left;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/* Custom utility classes */
|
|
596
|
+
.container {
|
|
597
|
+
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/* Custom scrollbar */
|
|
601
|
+
::-webkit-scrollbar {
|
|
602
|
+
width: 8px;
|
|
603
|
+
height: 8px;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
::-webkit-scrollbar-track {
|
|
607
|
+
@apply bg-gray-100;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
::-webkit-scrollbar-thumb {
|
|
611
|
+
@apply bg-gray-300 rounded-full;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
::-webkit-scrollbar-thumb:hover {
|
|
615
|
+
@apply bg-gray-400;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/* Focus styles for accessibility */
|
|
619
|
+
.focus-visible {
|
|
620
|
+
@apply focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/* Animation classes */
|
|
624
|
+
.animate-fade-in {
|
|
625
|
+
animation: fadeIn 0.6s ease-in-out;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.animate-slide-up {
|
|
629
|
+
animation: slideUp 0.6s ease-out;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.animate-blob {
|
|
633
|
+
animation: blob 7s infinite;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.animation-delay-2000 {
|
|
637
|
+
animation-delay: 2s;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.animation-delay-4000 {
|
|
641
|
+
animation-delay: 4s;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.animation-delay-200 {
|
|
645
|
+
animation-delay: 0.2s;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
@keyframes fadeIn {
|
|
649
|
+
from { opacity: 0; }
|
|
650
|
+
to { opacity: 1; }
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
@keyframes slideUp {
|
|
654
|
+
from {
|
|
655
|
+
opacity: 0;
|
|
656
|
+
transform: translateY(20px);
|
|
657
|
+
}
|
|
658
|
+
to {
|
|
659
|
+
opacity: 1;
|
|
660
|
+
transform: translateY(0);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
@keyframes blob {
|
|
665
|
+
0% {
|
|
666
|
+
transform: translate(0px, 0px) scale(1);
|
|
667
|
+
}
|
|
668
|
+
33% {
|
|
669
|
+
transform: translate(30px, -50px) scale(1.1);
|
|
670
|
+
}
|
|
671
|
+
66% {
|
|
672
|
+
transform: translate(-20px, 20px) scale(0.9);
|
|
673
|
+
}
|
|
674
|
+
100% {
|
|
675
|
+
transform: translate(0px, 0px) scale(1);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/* Print styles */
|
|
680
|
+
@media print {
|
|
681
|
+
.no-print {
|
|
682
|
+
display: none !important;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/* RTL Transition smoothing */
|
|
687
|
+
* {
|
|
688
|
+
transition: margin 0.2s ease-in-out;
|
|
689
|
+
}`;
|
|
690
|
+
|
|
691
|
+
await fs.writeFile(path.join(config.fullPath, 'src/styles.css'), mainStyles);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
module.exports = {
|
|
695
|
+
createContactComponent,
|
|
696
|
+
createRouting,
|
|
697
|
+
createAppConfig,
|
|
698
|
+
createAppComponent,
|
|
699
|
+
createStyles,
|
|
700
|
+
};
|