create-ng-tailwind 3.0.1 → 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 -344
  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 +53 -4055
  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
@@ -0,0 +1,496 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+
4
+ /**
5
+ * Ecommerce Shared Components
6
+ * - Rating
7
+ * - PriceDisplay
8
+ * - QuantitySelector
9
+ * - ProductCard
10
+ * - ProductGrid
11
+ * - CartItem
12
+ * - ProductFilters
13
+ */
14
+
15
+ async function createComponents(config) {
16
+ // Rating Component
17
+ const ratingComponent = `import { Component, Input } from '@angular/core';
18
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
19
+ import { heroStarSolid } from '@ng-icons/heroicons/solid';
20
+ import { heroStar } from '@ng-icons/heroicons/outline';
21
+
22
+ @Component({
23
+ selector: 'app-rating',
24
+ standalone: true,
25
+ imports: [NgIconComponent],
26
+ viewProviders: [provideIcons({ heroStarSolid, heroStar })],
27
+ template: \`
28
+ <div class="flex items-center gap-1">
29
+ @for (star of stars; track $index) {
30
+ <ng-icon
31
+ [name]="star ? 'heroStarSolid' : 'heroStar'"
32
+ size="16"
33
+ [class]="star ? 'text-amber-400' : 'text-gray-300'">
34
+ </ng-icon>
35
+ }
36
+ @if (showCount && reviewCount > 0) {
37
+ <span class="ms-1 text-sm text-gray-500">({{ reviewCount }})</span>
38
+ }
39
+ </div>
40
+ \`,
41
+ })
42
+ export class RatingComponent {
43
+ @Input() rating = 0;
44
+ @Input() reviewCount = 0;
45
+ @Input() showCount = true;
46
+
47
+ get stars(): boolean[] {
48
+ return Array(5).fill(false).map((_, i) => i < Math.round(this.rating));
49
+ }
50
+ }`;
51
+
52
+ await fs.writeFile(
53
+ path.join(
54
+ config.fullPath,
55
+ "src/app/shared/components/rating/rating.component.ts"
56
+ ),
57
+ ratingComponent
58
+ );
59
+
60
+ // Price Display Component
61
+ const priceDisplayComponent = `import { Component, Input } from '@angular/core';
62
+ import { CurrencyPipe } from '@angular/common';
63
+
64
+ @Component({
65
+ selector: 'app-price-display',
66
+ standalone: true,
67
+ imports: [CurrencyPipe],
68
+ template: \`
69
+ <div class="flex items-center gap-2">
70
+ <span class="text-lg font-bold" [class]="priceClass">
71
+ {{ price | currency:currency }}
72
+ </span>
73
+ @if (originalPrice && originalPrice > price) {
74
+ <span class="text-sm text-gray-400 line-through">
75
+ {{ originalPrice | currency:currency }}
76
+ </span>
77
+ <span class="rounded-full bg-danger-100 px-2 py-0.5 text-xs font-semibold text-danger-700">
78
+ -{{ discountPercent }}%
79
+ </span>
80
+ }
81
+ </div>
82
+ \`,
83
+ })
84
+ export class PriceDisplayComponent {
85
+ @Input() price = 0;
86
+ @Input() originalPrice?: number;
87
+ @Input() currency = 'USD';
88
+ @Input() priceClass = 'text-gray-900';
89
+
90
+ get discountPercent(): number {
91
+ if (!this.originalPrice || this.originalPrice <= this.price) return 0;
92
+ return Math.round((1 - this.price / this.originalPrice) * 100);
93
+ }
94
+ }`;
95
+
96
+ await fs.writeFile(
97
+ path.join(
98
+ config.fullPath,
99
+ "src/app/shared/components/price-display/price-display.component.ts"
100
+ ),
101
+ priceDisplayComponent
102
+ );
103
+
104
+ // Quantity Selector Component
105
+ const quantitySelectorComponent = `import { Component, Input, Output, EventEmitter } from '@angular/core';
106
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
107
+ import { heroMinus, heroPlus } from '@ng-icons/heroicons/outline';
108
+
109
+ @Component({
110
+ selector: 'app-quantity-selector',
111
+ standalone: true,
112
+ imports: [NgIconComponent],
113
+ viewProviders: [provideIcons({ heroMinus, heroPlus })],
114
+ template: \`
115
+ <div class="flex items-center gap-2">
116
+ <button
117
+ type="button"
118
+ (click)="decrease()"
119
+ [disabled]="quantity <= min"
120
+ class="flex h-8 w-8 items-center justify-center rounded-lg border border-gray-300 text-gray-600 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50">
121
+ <ng-icon name="heroMinus" size="16"></ng-icon>
122
+ </button>
123
+ <span class="w-12 text-center font-medium">{{ quantity }}</span>
124
+ <button
125
+ type="button"
126
+ (click)="increase()"
127
+ [disabled]="quantity >= max"
128
+ class="flex h-8 w-8 items-center justify-center rounded-lg border border-gray-300 text-gray-600 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50">
129
+ <ng-icon name="heroPlus" size="16"></ng-icon>
130
+ </button>
131
+ </div>
132
+ \`,
133
+ })
134
+ export class QuantitySelectorComponent {
135
+ @Input() quantity = 1;
136
+ @Input() min = 1;
137
+ @Input() max = 99;
138
+ @Output() quantityChange = new EventEmitter<number>();
139
+
140
+ decrease(): void {
141
+ if (this.quantity > this.min) {
142
+ this.quantity--;
143
+ this.quantityChange.emit(this.quantity);
144
+ }
145
+ }
146
+
147
+ increase(): void {
148
+ if (this.quantity < this.max) {
149
+ this.quantity++;
150
+ this.quantityChange.emit(this.quantity);
151
+ }
152
+ }
153
+ }`;
154
+
155
+ await fs.writeFile(
156
+ path.join(
157
+ config.fullPath,
158
+ "src/app/shared/components/quantity-selector/quantity-selector.component.ts"
159
+ ),
160
+ quantitySelectorComponent
161
+ );
162
+
163
+ // Product Card Component
164
+ const productCardComponent = `import { Component, Input, inject } from '@angular/core';
165
+ import { RouterModule } from '@angular/router';
166
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
167
+ import { heroHeart, heroShoppingCart } from '@ng-icons/heroicons/outline';
168
+ import { heroHeartSolid } from '@ng-icons/heroicons/solid';
169
+ import { RatingComponent } from '../rating/rating.component';
170
+ import { PriceDisplayComponent } from '../price-display/price-display.component';
171
+ import { CartService } from '@core/services/cart.service';
172
+ import { WishlistService } from '@core/services/wishlist.service';
173
+ import { Product } from '@core/services/product.service';
174
+
175
+ @Component({
176
+ selector: 'app-product-card',
177
+ standalone: true,
178
+ imports: [RouterModule, NgIconComponent, RatingComponent, PriceDisplayComponent],
179
+ viewProviders: [provideIcons({ heroHeart, heroHeartSolid, heroShoppingCart })],
180
+ template: \`
181
+ <div class="group relative overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-sm transition-all duration-300 hover:shadow-xl hover:-translate-y-1">
182
+ <!-- Image -->
183
+ <a [routerLink]="['/products', product.id]" class="block aspect-square overflow-hidden bg-gray-100">
184
+ <img
185
+ [src]="product.image"
186
+ [alt]="product.name"
187
+ class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-110"
188
+ />
189
+ </a>
190
+
191
+ <!-- Wishlist Button -->
192
+ <button
193
+ type="button"
194
+ (click)="toggleWishlist()"
195
+ class="absolute end-3 top-3 flex h-10 w-10 items-center justify-center rounded-full bg-white/90 shadow-md backdrop-blur-sm transition-all hover:bg-white hover:scale-110">
196
+ <ng-icon
197
+ [name]="isInWishlist ? 'heroHeartSolid' : 'heroHeart'"
198
+ size="20"
199
+ [class]="isInWishlist ? 'text-danger-500' : 'text-gray-600'">
200
+ </ng-icon>
201
+ </button>
202
+
203
+ <!-- Badge -->
204
+ @if (product.badge) {
205
+ <span class="absolute start-3 top-3 rounded-full bg-primary-500 px-3 py-1 text-xs font-semibold text-white">
206
+ {{ product.badge }}
207
+ </span>
208
+ }
209
+
210
+ <!-- Content -->
211
+ <div class="p-4">
212
+ <a [routerLink]="['/products', product.id]" class="block">
213
+ <p class="text-sm text-gray-500">{{ product.category }}</p>
214
+ <h3 class="mt-1 font-semibold text-gray-900 line-clamp-2 hover:text-primary-600 transition-colors">
215
+ {{ product.name }}
216
+ </h3>
217
+ </a>
218
+
219
+ <div class="mt-2">
220
+ <app-rating [rating]="product.rating" [reviewCount]="product.reviewCount"></app-rating>
221
+ </div>
222
+
223
+ <div class="mt-3 flex items-center justify-between">
224
+ <app-price-display
225
+ [price]="product.price"
226
+ [originalPrice]="product.originalPrice">
227
+ </app-price-display>
228
+
229
+ <button
230
+ type="button"
231
+ (click)="addToCart()"
232
+ class="flex h-10 w-10 items-center justify-center rounded-full bg-primary-500 text-white shadow-lg shadow-primary-500/30 transition-all hover:bg-primary-600 hover:scale-110">
233
+ <ng-icon name="heroShoppingCart" size="18"></ng-icon>
234
+ </button>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ \`,
239
+ })
240
+ export class ProductCardComponent {
241
+ @Input() product!: Product;
242
+
243
+ private cartService = inject(CartService);
244
+ private wishlistService = inject(WishlistService);
245
+
246
+ get isInWishlist(): boolean {
247
+ return this.wishlistService.isInWishlist(this.product.id);
248
+ }
249
+
250
+ addToCart(): void {
251
+ this.cartService.addItem(this.product);
252
+ }
253
+
254
+ toggleWishlist(): void {
255
+ this.wishlistService.toggle(this.product);
256
+ }
257
+ }`;
258
+
259
+ await fs.writeFile(
260
+ path.join(
261
+ config.fullPath,
262
+ "src/app/shared/components/product-card/product-card.component.ts"
263
+ ),
264
+ productCardComponent
265
+ );
266
+
267
+ // Product Grid Component
268
+ const productGridComponent = `import { Component, Input } from '@angular/core';
269
+ import { ProductCardComponent } from '../product-card/product-card.component';
270
+ import { Product } from '@core/services/product.service';
271
+
272
+ @Component({
273
+ selector: 'app-product-grid',
274
+ standalone: true,
275
+ imports: [ProductCardComponent],
276
+ template: \`
277
+ <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
278
+ @for (product of products; track product.id) {
279
+ <app-product-card [product]="product"></app-product-card>
280
+ }
281
+ </div>
282
+
283
+ @if (products.length === 0) {
284
+ <div class="py-12 text-center">
285
+ <p class="text-gray-500">No products found</p>
286
+ </div>
287
+ }
288
+ \`,
289
+ })
290
+ export class ProductGridComponent {
291
+ @Input() products: Product[] = [];
292
+ }`;
293
+
294
+ await fs.writeFile(
295
+ path.join(
296
+ config.fullPath,
297
+ "src/app/shared/components/product-grid/product-grid.component.ts"
298
+ ),
299
+ productGridComponent
300
+ );
301
+
302
+ // Cart Item Component
303
+ const cartItemComponent = `import { Component, Input, inject } from '@angular/core';
304
+ import { RouterModule } from '@angular/router';
305
+ import { CurrencyPipe } from '@angular/common';
306
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
307
+ import { heroTrash } from '@ng-icons/heroicons/outline';
308
+ import { QuantitySelectorComponent } from '../quantity-selector/quantity-selector.component';
309
+ import { CartService, CartItem } from '@core/services/cart.service';
310
+
311
+ @Component({
312
+ selector: 'app-cart-item',
313
+ standalone: true,
314
+ imports: [RouterModule, CurrencyPipe, NgIconComponent, QuantitySelectorComponent],
315
+ viewProviders: [provideIcons({ heroTrash })],
316
+ template: \`
317
+ <div class="flex gap-4 rounded-xl border border-gray-100 bg-white p-4 shadow-sm">
318
+ <!-- Image -->
319
+ <a [routerLink]="['/products', item.product.id]" class="h-24 w-24 flex-shrink-0 overflow-hidden rounded-lg bg-gray-100">
320
+ <img [src]="item.product.image" [alt]="item.product.name" class="h-full w-full object-cover" />
321
+ </a>
322
+
323
+ <!-- Details -->
324
+ <div class="flex flex-1 flex-col">
325
+ <div class="flex items-start justify-between">
326
+ <div>
327
+ <a [routerLink]="['/products', item.product.id]" class="font-semibold text-gray-900 hover:text-primary-600">
328
+ {{ item.product.name }}
329
+ </a>
330
+ <p class="text-sm text-gray-500">{{ item.product.category }}</p>
331
+ </div>
332
+ <button
333
+ type="button"
334
+ (click)="removeItem()"
335
+ class="text-gray-400 hover:text-danger-500 transition-colors">
336
+ <ng-icon name="heroTrash" size="20"></ng-icon>
337
+ </button>
338
+ </div>
339
+
340
+ <div class="mt-auto flex items-center justify-between pt-2">
341
+ <app-quantity-selector
342
+ [quantity]="item.quantity"
343
+ (quantityChange)="updateQuantity($event)">
344
+ </app-quantity-selector>
345
+
346
+ <span class="font-bold text-gray-900">
347
+ {{ item.product.price * item.quantity | currency }}
348
+ </span>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ \`,
353
+ })
354
+ export class CartItemComponent {
355
+ @Input() item!: CartItem;
356
+
357
+ private cartService = inject(CartService);
358
+
359
+ updateQuantity(quantity: number): void {
360
+ this.cartService.updateQuantity(this.item.product.id, quantity);
361
+ }
362
+
363
+ removeItem(): void {
364
+ this.cartService.removeItem(this.item.product.id);
365
+ }
366
+ }`;
367
+
368
+ await fs.writeFile(
369
+ path.join(
370
+ config.fullPath,
371
+ "src/app/shared/components/cart-item/cart-item.component.ts"
372
+ ),
373
+ cartItemComponent
374
+ );
375
+
376
+ // Product Filters Component
377
+ const productFiltersComponent = `import { Component, Input, Output, EventEmitter } from '@angular/core';
378
+ import { FormsModule } from '@angular/forms';
379
+ import { TranslateModule } from '@ngx-translate/core';
380
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
381
+ import { heroFunnel, heroXMark } from '@ng-icons/heroicons/outline';
382
+
383
+ export interface ProductFilters {
384
+ category: string;
385
+ minPrice: number | null;
386
+ maxPrice: number | null;
387
+ sortBy: string;
388
+ }
389
+
390
+ @Component({
391
+ selector: 'app-product-filters',
392
+ standalone: true,
393
+ imports: [FormsModule, TranslateModule, NgIconComponent],
394
+ viewProviders: [provideIcons({ heroFunnel, heroXMark })],
395
+ template: \`
396
+ <div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
397
+ <div class="flex items-center justify-between mb-4">
398
+ <h3 class="font-semibold text-gray-900 flex items-center gap-2">
399
+ <ng-icon name="heroFunnel" size="20"></ng-icon>
400
+ {{ 'shop.filters' | translate }}
401
+ </h3>
402
+ <button
403
+ type="button"
404
+ (click)="clearFilters()"
405
+ class="text-sm text-primary-600 hover:text-primary-700">
406
+ {{ 'shop.clearAll' | translate }}
407
+ </button>
408
+ </div>
409
+
410
+ <!-- Category -->
411
+ <div class="mb-6">
412
+ <label class="block text-sm font-medium text-gray-700 mb-2">{{ 'shop.category' | translate }}</label>
413
+ <select
414
+ [(ngModel)]="filters.category"
415
+ (ngModelChange)="onFiltersChange()"
416
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
417
+ <option value="">{{ 'shop.allCategories' | translate }}</option>
418
+ @for (cat of categories; track cat) {
419
+ <option [value]="cat">{{ cat }}</option>
420
+ }
421
+ </select>
422
+ </div>
423
+
424
+ <!-- Price Range -->
425
+ <div class="mb-6">
426
+ <label class="block text-sm font-medium text-gray-700 mb-2">{{ 'shop.priceRange' | translate }}</label>
427
+ <div class="flex gap-2">
428
+ <input
429
+ type="number"
430
+ [(ngModel)]="filters.minPrice"
431
+ (ngModelChange)="onFiltersChange()"
432
+ [placeholder]="'shop.min' | translate"
433
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
434
+ />
435
+ <input
436
+ type="number"
437
+ [(ngModel)]="filters.maxPrice"
438
+ (ngModelChange)="onFiltersChange()"
439
+ [placeholder]="'shop.max' | translate"
440
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
441
+ />
442
+ </div>
443
+ </div>
444
+
445
+ <!-- Sort By -->
446
+ <div>
447
+ <label class="block text-sm font-medium text-gray-700 mb-2">{{ 'shop.sortBy' | translate }}</label>
448
+ <select
449
+ [(ngModel)]="filters.sortBy"
450
+ (ngModelChange)="onFiltersChange()"
451
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500">
452
+ <option value="newest">{{ 'shop.newest' | translate }}</option>
453
+ <option value="price-low">{{ 'shop.priceLowHigh' | translate }}</option>
454
+ <option value="price-high">{{ 'shop.priceHighLow' | translate }}</option>
455
+ <option value="rating">{{ 'shop.topRated' | translate }}</option>
456
+ </select>
457
+ </div>
458
+ </div>
459
+ \`,
460
+ })
461
+ export class ProductFiltersComponent {
462
+ @Input() categories: string[] = [];
463
+ @Output() filtersChange = new EventEmitter<ProductFilters>();
464
+
465
+ filters: ProductFilters = {
466
+ category: '',
467
+ minPrice: null,
468
+ maxPrice: null,
469
+ sortBy: 'newest',
470
+ };
471
+
472
+ onFiltersChange(): void {
473
+ this.filtersChange.emit({ ...this.filters });
474
+ }
475
+
476
+ clearFilters(): void {
477
+ this.filters = {
478
+ category: '',
479
+ minPrice: null,
480
+ maxPrice: null,
481
+ sortBy: 'newest',
482
+ };
483
+ this.filtersChange.emit({ ...this.filters });
484
+ }
485
+ }`;
486
+
487
+ await fs.writeFile(
488
+ path.join(
489
+ config.fullPath,
490
+ "src/app/shared/components/product-filters/product-filters.component.ts"
491
+ ),
492
+ productFiltersComponent
493
+ );
494
+ }
495
+
496
+ module.exports = { createComponents };