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.
- package/CHANGELOG.md +96 -341
- package/README.md +111 -157
- package/lib/cli/index.js +74 -3
- package/lib/cli/interactive.js +26 -1
- package/lib/managers/ProjectManager.js +2 -5
- package/lib/templates/base/components.js +243 -0
- package/lib/templates/base/index.js +207 -0
- package/lib/templates/base/infrastructure.js +314 -0
- package/lib/templates/base/linting.js +359 -0
- package/lib/templates/base/pwa.js +103 -0
- package/lib/templates/base/services.js +362 -0
- package/lib/templates/blog/app.js +250 -0
- package/lib/templates/blog/components.js +360 -0
- package/lib/templates/blog/i18n.js +77 -0
- package/lib/templates/blog/index.js +126 -0
- package/lib/templates/blog/pages.js +554 -0
- package/lib/templates/blog/services.js +390 -0
- package/lib/templates/dashboard/app.js +320 -0
- package/lib/templates/dashboard/charts.js +305 -0
- package/lib/templates/dashboard/components.js +410 -0
- package/lib/templates/dashboard/i18n.js +340 -0
- package/lib/templates/dashboard/index.js +141 -0
- package/lib/templates/dashboard/layout.js +310 -0
- package/lib/templates/dashboard/pages.js +681 -0
- package/lib/templates/ecommerce/app.js +315 -0
- package/lib/templates/ecommerce/components.js +496 -0
- package/lib/templates/ecommerce/i18n.js +389 -0
- package/lib/templates/ecommerce/index.js +152 -0
- package/lib/templates/ecommerce/layout.js +270 -0
- package/lib/templates/ecommerce/pages.js +969 -0
- package/lib/templates/ecommerce/services.js +300 -0
- package/lib/templates/index.js +12 -0
- package/lib/templates/landing/index.js +1117 -0
- package/lib/templates/portfolio/index.js +1160 -0
- package/lib/templates/saas/index.js +1371 -0
- package/lib/templates/starter/app.js +364 -0
- package/lib/templates/starter/i18n.js +856 -0
- package/lib/templates/starter/index.js +52 -4060
- package/lib/templates/starter/layout.js +852 -0
- package/lib/templates/starter/pages.js +1241 -0
- package/lib/utils/nodeCompat.js +85 -0
- package/package.json +1 -1
- package/lib/templates/starter/features.js +0 -867
- package/lib/utils/ai-config.js +0 -641
- /package/lib/templates/{starter → base}/advanced-features.js +0 -0
- /package/lib/templates/{starter → base}/seo-assets.js +0 -0
- /package/lib/templates/{starter → base}/seo-features.js +0 -0
- /package/lib/templates/{starter → base}/ui-features.js +0 -0
|
@@ -0,0 +1,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 };
|