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,969 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Ecommerce Pages
6
+ * - Home (hero, features, featured products, categories)
7
+ * - Products (with filters)
8
+ * - ProductDetail (with gallery, related products)
9
+ * - Cart (with order summary)
10
+ * - Checkout (with form validation)
11
+ * - Wishlist
12
+ */
13
+
14
+ async function createPages(config) {
15
+ // Home Page - Clean design with key sections
16
+ const homeComponent = `import { Component, inject } from '@angular/core';
17
+ import { RouterModule } from '@angular/router';
18
+ import { TranslateModule } from '@ngx-translate/core';
19
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
20
+ import {
21
+ heroArrowRight,
22
+ heroTruck,
23
+ heroShieldCheck,
24
+ heroArrowPath,
25
+ heroSparkles,
26
+ heroDevicePhoneMobile,
27
+ heroComputerDesktop,
28
+ heroShoppingBag,
29
+ heroHome,
30
+ heroHeart,
31
+ heroBolt,
32
+ } from '@ng-icons/heroicons/outline';
33
+ import { heroStarSolid } from '@ng-icons/heroicons/solid';
34
+ import { ProductGridComponent } from '@shared/components/product-grid/product-grid.component';
35
+ import { ProductService } from '@core/services/product.service';
36
+
37
+ interface CategoryItem {
38
+ name: string;
39
+ icon: string;
40
+ color: string;
41
+ bgColor: string;
42
+ }
43
+
44
+ @Component({
45
+ selector: 'app-home',
46
+ standalone: true,
47
+ imports: [RouterModule, TranslateModule, NgIconComponent, ProductGridComponent],
48
+ viewProviders: [
49
+ provideIcons({
50
+ heroArrowRight,
51
+ heroTruck,
52
+ heroShieldCheck,
53
+ heroArrowPath,
54
+ heroSparkles,
55
+ heroStarSolid,
56
+ heroDevicePhoneMobile,
57
+ heroComputerDesktop,
58
+ heroShoppingBag,
59
+ heroHome,
60
+ heroHeart,
61
+ heroBolt,
62
+ }),
63
+ ],
64
+ template: \`
65
+ <div>
66
+ <!-- Hero Section - Enhanced Dark Design -->
67
+ <section class="relative min-h-[600px] overflow-hidden bg-linear-to-br from-slate-900 via-purple-900 to-slate-900">
68
+ <!-- Animated Background Elements -->
69
+ <div class="absolute inset-0">
70
+ <div class="animate-blob absolute -left-4 top-0 h-72 w-72 rounded-full bg-purple-500 opacity-20 mix-blend-multiply blur-xl filter"></div>
71
+ <div class="animate-blob animation-delay-2000 absolute -right-4 top-0 h-72 w-72 rounded-full bg-yellow-500 opacity-20 mix-blend-multiply blur-xl filter"></div>
72
+ <div class="animate-blob animation-delay-4000 absolute -bottom-8 left-20 h-72 w-72 rounded-full bg-pink-500 opacity-20 mix-blend-multiply blur-xl filter"></div>
73
+ </div>
74
+
75
+ <div class="relative mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8 lg:py-24">
76
+ <div class="grid grid-cols-1 items-center gap-12 lg:grid-cols-2">
77
+ <!-- Left Content -->
78
+ <div class="text-center lg:text-start">
79
+ <!-- Sale Badge -->
80
+ <div class="mb-6 inline-flex items-center gap-2 rounded-full bg-gradient-to-r from-amber-500 to-orange-500 px-4 py-2 text-sm font-semibold text-white shadow-lg">
81
+ <span class="relative flex h-2 w-2">
82
+ <span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-white opacity-75"></span>
83
+ <span class="relative inline-flex h-2 w-2 rounded-full bg-white"></span>
84
+ </span>
85
+ {{ 'home.saleBadge' | translate }}
86
+ </div>
87
+
88
+ <h1 class="text-4xl font-bold tracking-tight text-white sm:text-5xl lg:text-6xl">
89
+ {{ 'home.heroTitle' | translate }}
90
+ </h1>
91
+ <p class="mt-6 text-lg text-gray-300">
92
+ {{ 'home.heroSubtitle' | translate }}
93
+ </p>
94
+
95
+ <!-- Stats -->
96
+ <div class="mt-8 flex flex-wrap justify-center gap-8 lg:justify-start">
97
+ <div class="text-center">
98
+ <div class="text-3xl font-bold text-white">50K+</div>
99
+ <div class="text-sm text-gray-400">{{ 'home.happyCustomers' | translate }}</div>
100
+ </div>
101
+ <div class="text-center">
102
+ <div class="text-3xl font-bold text-white">10K+</div>
103
+ <div class="text-sm text-gray-400">{{ 'home.products' | translate }}</div>
104
+ </div>
105
+ <div class="text-center">
106
+ <div class="text-3xl font-bold text-white">4.9</div>
107
+ <div class="flex items-center justify-center gap-1 text-sm text-gray-400">
108
+ <ng-icon name="heroStarSolid" size="14" class="text-amber-400"></ng-icon>
109
+ {{ 'home.rating' | translate }}
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ <!-- CTA Buttons -->
115
+ <div class="mt-10 flex flex-wrap justify-center gap-4 lg:justify-start">
116
+ <a routerLink="/products" class="group inline-flex items-center gap-2 rounded-full bg-white px-8 py-4 font-semibold text-gray-900 shadow-xl transition-all hover:bg-gray-100 hover:shadow-2xl">
117
+ {{ 'home.shopNow' | translate }}
118
+ <ng-icon name="heroArrowRight" size="20" class="transition-transform group-hover:translate-x-1"></ng-icon>
119
+ </a>
120
+ <a routerLink="/products" [queryParams]="{sale: true}" class="inline-flex items-center gap-2 rounded-full border-2 border-white/30 px-8 py-4 font-semibold text-white backdrop-blur-sm transition-all hover:border-white hover:bg-white/10">
121
+ {{ 'home.viewDeals' | translate }}
122
+ </a>
123
+ </div>
124
+ </div>
125
+
126
+ <!-- Right Content - Hero Image with Floating Elements -->
127
+ <div class="relative hidden lg:block">
128
+ <div class="relative">
129
+ <img
130
+ src="https://images.unsplash.com/photo-1607082348824-0a96f2a4b9da?w=600"
131
+ alt="Shopping"
132
+ class="rounded-3xl shadow-2xl"
133
+ />
134
+ <!-- Floating Discount Badge -->
135
+ <div class="absolute -left-6 top-8 animate-bounce rounded-2xl bg-gradient-to-r from-rose-500 to-pink-500 p-4 shadow-xl">
136
+ <div class="text-center text-white">
137
+ <div class="text-2xl font-bold">-50%</div>
138
+ <div class="text-xs">{{ 'home.sale' | translate }}</div>
139
+ </div>
140
+ </div>
141
+ <!-- Floating Product Card -->
142
+ <div class="absolute -bottom-4 -right-4 rounded-2xl bg-white p-4 shadow-xl">
143
+ <div class="flex items-center gap-3">
144
+ <div class="h-12 w-12 rounded-xl bg-linear-to-br from-purple-500 to-pink-500"></div>
145
+ <div>
146
+ <div class="text-sm font-semibold text-gray-900">{{ 'home.newCollection' | translate }}</div>
147
+ <div class="text-xs text-gray-500">{{ 'home.justArrived' | translate }}</div>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </section>
156
+
157
+ <!-- Features Bar -->
158
+ <section class="border-b border-gray-200 bg-white">
159
+ <div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
160
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
161
+ <div class="flex items-center justify-center gap-3 p-4">
162
+ <div class="flex h-10 w-10 items-center justify-center rounded-full bg-primary-100 text-primary-600">
163
+ <ng-icon name="heroTruck" size="20"></ng-icon>
164
+ </div>
165
+ <div>
166
+ <p class="font-semibold text-gray-900">{{ 'home.freeShipping' | translate }}</p>
167
+ <p class="text-sm text-gray-500">{{ 'home.freeShippingDesc' | translate }}</p>
168
+ </div>
169
+ </div>
170
+ <div class="flex items-center justify-center gap-3 p-4">
171
+ <div class="flex h-10 w-10 items-center justify-center rounded-full bg-success-100 text-success-600">
172
+ <ng-icon name="heroShieldCheck" size="20"></ng-icon>
173
+ </div>
174
+ <div>
175
+ <p class="font-semibold text-gray-900">{{ 'home.securePayment' | translate }}</p>
176
+ <p class="text-sm text-gray-500">{{ 'home.securePaymentDesc' | translate }}</p>
177
+ </div>
178
+ </div>
179
+ <div class="flex items-center justify-center gap-3 p-4">
180
+ <div class="flex h-10 w-10 items-center justify-center rounded-full bg-warning-100 text-warning-600">
181
+ <ng-icon name="heroArrowPath" size="20"></ng-icon>
182
+ </div>
183
+ <div>
184
+ <p class="font-semibold text-gray-900">{{ 'home.easyReturns' | translate }}</p>
185
+ <p class="text-sm text-gray-500">{{ 'home.easyReturnsDesc' | translate }}</p>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ </div>
190
+ </section>
191
+
192
+ <!-- Shop by Category -->
193
+ <section class="py-16 bg-gray-50">
194
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
195
+ <div class="text-center mb-12">
196
+ <h2 class="text-3xl font-bold text-gray-900">{{ 'home.shopByCategory' | translate }}</h2>
197
+ <p class="mt-2 text-gray-600">{{ 'home.categorySubtitle' | translate }}</p>
198
+ </div>
199
+
200
+ <div class="grid grid-cols-2 gap-6 sm:grid-cols-3 lg:grid-cols-6">
201
+ @for (cat of categoryItems; track cat.name) {
202
+ <a
203
+ [routerLink]="['/products']"
204
+ [queryParams]="{category: cat.name}"
205
+ class="group relative overflow-hidden rounded-2xl bg-white p-6 shadow-sm hover:shadow-xl transition-all duration-300 hover:-translate-y-2">
206
+ <div class="flex flex-col items-center gap-4">
207
+ <div
208
+ class="flex h-16 w-16 items-center justify-center rounded-2xl transition-transform group-hover:scale-110"
209
+ [class]="cat.bgColor">
210
+ <ng-icon [name]="cat.icon" size="32" [class]="cat.color"></ng-icon>
211
+ </div>
212
+ <span class="font-semibold text-gray-900 text-center">{{ cat.name }}</span>
213
+ </div>
214
+ <div class="absolute inset-0 bg-gradient-to-t from-primary-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity"></div>
215
+ </a>
216
+ }
217
+ </div>
218
+ </div>
219
+ </section>
220
+
221
+ <!-- Featured Products -->
222
+ <section class="py-16">
223
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
224
+ <div class="flex items-center justify-between mb-8">
225
+ <h2 class="text-2xl font-bold text-gray-900">{{ 'home.featuredProducts' | translate }}</h2>
226
+ <a routerLink="/products" class="inline-flex items-center gap-2 text-primary-600 hover:text-primary-700 font-medium">
227
+ {{ 'home.viewAll' | translate }}
228
+ <ng-icon name="heroArrowRight" size="16"></ng-icon>
229
+ </a>
230
+ </div>
231
+ <app-product-grid [products]="featuredProducts"></app-product-grid>
232
+ </div>
233
+ </section>
234
+
235
+ <!-- New Arrivals -->
236
+ <section class="py-16 bg-gray-50">
237
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
238
+ <div class="flex items-center justify-between mb-8">
239
+ <div class="flex items-center gap-3">
240
+ <div class="flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-r from-green-500 to-emerald-500">
241
+ <ng-icon name="heroSparkles" size="20" class="text-white"></ng-icon>
242
+ </div>
243
+ <div>
244
+ <h2 class="text-2xl font-bold text-gray-900">{{ 'home.newArrivals' | translate }}</h2>
245
+ <p class="text-gray-600">{{ 'home.newArrivalsSubtitle' | translate }}</p>
246
+ </div>
247
+ </div>
248
+ <a routerLink="/products" [queryParams]="{sort: 'newest'}" class="hidden sm:inline-flex items-center gap-2 text-primary-600 hover:text-primary-700 font-medium">
249
+ {{ 'home.seeMore' | translate }}
250
+ <ng-icon name="heroArrowRight" size="16"></ng-icon>
251
+ </a>
252
+ </div>
253
+ <app-product-grid [products]="newArrivals"></app-product-grid>
254
+ </div>
255
+ </section>
256
+
257
+ <!-- Testimonials -->
258
+ <section class="py-16 bg-white">
259
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
260
+ <div class="text-center mb-12">
261
+ <h2 class="text-3xl font-bold text-gray-900">{{ 'home.testimonials' | translate }}</h2>
262
+ <p class="mt-2 text-gray-600">{{ 'home.testimonialsSubtitle' | translate }}</p>
263
+ </div>
264
+
265
+ <div class="grid grid-cols-1 gap-8 md:grid-cols-3">
266
+ @for (testimonial of testimonials; track testimonial.name) {
267
+ <div class="rounded-2xl border border-gray-200 bg-white p-8 shadow-sm hover:shadow-lg transition-shadow">
268
+ <div class="flex gap-1 mb-4">
269
+ @for (star of [1, 2, 3, 4, 5]; track star) {
270
+ <ng-icon name="heroStarSolid" size="20" class="text-amber-400"></ng-icon>
271
+ }
272
+ </div>
273
+ <p class="text-gray-600 mb-6">"{{ testimonial.text }}"</p>
274
+ <div class="flex items-center gap-4">
275
+ <div class="h-12 w-12 rounded-full bg-linear-to-br" [class]="testimonial.avatarColor"></div>
276
+ <div>
277
+ <p class="font-semibold text-gray-900">{{ testimonial.name }}</p>
278
+ <p class="text-sm text-gray-500">{{ testimonial.role }}</p>
279
+ </div>
280
+ </div>
281
+ </div>
282
+ }
283
+ </div>
284
+ </div>
285
+ </section>
286
+ </div>
287
+ \`,
288
+ })
289
+ export class HomeComponent {
290
+ private productService = inject(ProductService);
291
+
292
+ featuredProducts = this.productService.getProducts().slice(0, 4);
293
+ newArrivals = this.productService.getProducts().slice(4, 8);
294
+ categories = this.productService.getCategories();
295
+
296
+ categoryItems: CategoryItem[] = [
297
+ { name: 'Electronics', icon: 'heroDevicePhoneMobile', color: 'text-blue-600', bgColor: 'bg-blue-100' },
298
+ { name: 'Computers', icon: 'heroComputerDesktop', color: 'text-purple-600', bgColor: 'bg-purple-100' },
299
+ { name: 'Fashion', icon: 'heroShoppingBag', color: 'text-pink-600', bgColor: 'bg-pink-100' },
300
+ { name: 'Home', icon: 'heroHome', color: 'text-amber-600', bgColor: 'bg-amber-100' },
301
+ { name: 'Beauty', icon: 'heroHeart', color: 'text-rose-600', bgColor: 'bg-rose-100' },
302
+ { name: 'Sports', icon: 'heroBolt', color: 'text-green-600', bgColor: 'bg-green-100' },
303
+ ];
304
+
305
+ testimonials = [
306
+ {
307
+ name: 'Sarah Johnson',
308
+ role: 'Verified Buyer',
309
+ text: 'Amazing quality products and super fast delivery. The customer service is top-notch!',
310
+ avatarColor: 'from-pink-400 to-rose-400',
311
+ },
312
+ {
313
+ name: 'Michael Chen',
314
+ role: 'Verified Buyer',
315
+ text: 'Best shopping experience ever. Great prices and the products always exceed expectations.',
316
+ avatarColor: 'from-blue-400 to-indigo-400',
317
+ },
318
+ {
319
+ name: 'Emily Davis',
320
+ role: 'Verified Buyer',
321
+ text: 'Love the variety of products available. Returns are hassle-free and shipping is always on time.',
322
+ avatarColor: 'from-green-400 to-emerald-400',
323
+ },
324
+ ];
325
+ }`;
326
+
327
+ await fs.writeFile(
328
+ path.join(config.fullPath, 'src/app/features/home/home.component.ts'),
329
+ homeComponent
330
+ );
331
+
332
+ // Products Page
333
+ const productsComponent = `import { Component, inject, signal, computed } from '@angular/core';
334
+ import { ActivatedRoute } from '@angular/router';
335
+ import { TranslateModule } from '@ngx-translate/core';
336
+ import { ProductGridComponent } from '@shared/components/product-grid/product-grid.component';
337
+ import { ProductFiltersComponent, ProductFilters } from '@shared/components/product-filters/product-filters.component';
338
+ import { ProductService, Product } from '@core/services/product.service';
339
+
340
+ @Component({
341
+ selector: 'app-products',
342
+ standalone: true,
343
+ imports: [TranslateModule, ProductGridComponent, ProductFiltersComponent],
344
+ template: \`
345
+ <div class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
346
+ <div class="mb-8">
347
+ <h1 class="text-3xl font-bold text-gray-900">{{ 'shop.title' | translate }}</h1>
348
+ <p class="mt-2 text-gray-600">{{ 'shop.subtitle' | translate }}</p>
349
+ </div>
350
+
351
+ <div class="grid grid-cols-1 gap-8 lg:grid-cols-4">
352
+ <!-- Filters Sidebar -->
353
+ <aside class="lg:col-span-1">
354
+ <app-product-filters
355
+ [categories]="categories"
356
+ (filtersChange)="onFiltersChange($event)">
357
+ </app-product-filters>
358
+ </aside>
359
+
360
+ <!-- Products Grid -->
361
+ <div class="lg:col-span-3">
362
+ <div class="mb-4 text-sm text-gray-500">
363
+ {{ filteredProducts().length }} {{ 'shop.productsFound' | translate }}
364
+ </div>
365
+ <app-product-grid [products]="filteredProducts()"></app-product-grid>
366
+ </div>
367
+ </div>
368
+ </div>
369
+ \`,
370
+ })
371
+ export class ProductsComponent {
372
+ private productService = inject(ProductService);
373
+ private route = inject(ActivatedRoute);
374
+
375
+ categories = this.productService.getCategories();
376
+ private allProducts = this.productService.getProducts();
377
+
378
+ private filters = signal<ProductFilters>({
379
+ category: '',
380
+ minPrice: null,
381
+ maxPrice: null,
382
+ sortBy: 'newest',
383
+ });
384
+
385
+ filteredProducts = computed(() => {
386
+ let products = [...this.allProducts];
387
+ const f = this.filters();
388
+
389
+ // Filter by category
390
+ if (f.category) {
391
+ products = products.filter(p => p.category === f.category);
392
+ }
393
+
394
+ // Filter by price
395
+ if (f.minPrice !== null) {
396
+ products = products.filter(p => p.price >= f.minPrice!);
397
+ }
398
+ if (f.maxPrice !== null) {
399
+ products = products.filter(p => p.price <= f.maxPrice!);
400
+ }
401
+
402
+ // Sort
403
+ switch (f.sortBy) {
404
+ case 'price-low':
405
+ products.sort((a, b) => a.price - b.price);
406
+ break;
407
+ case 'price-high':
408
+ products.sort((a, b) => b.price - a.price);
409
+ break;
410
+ case 'rating':
411
+ products.sort((a, b) => b.rating - a.rating);
412
+ break;
413
+ }
414
+
415
+ return products;
416
+ });
417
+
418
+ constructor() {
419
+ // Check for category query param
420
+ this.route.queryParams.subscribe(params => {
421
+ if (params['category']) {
422
+ this.filters.update(f => ({ ...f, category: params['category'] }));
423
+ }
424
+ });
425
+ }
426
+
427
+ onFiltersChange(newFilters: ProductFilters): void {
428
+ this.filters.set(newFilters);
429
+ }
430
+ }`;
431
+
432
+ await fs.writeFile(
433
+ path.join(config.fullPath, 'src/app/features/products/products.component.ts'),
434
+ productsComponent
435
+ );
436
+
437
+ // Product Detail Page
438
+ const productDetailComponent = `import { Component, inject, signal } from '@angular/core';
439
+ import { ActivatedRoute, Router, RouterModule } from '@angular/router';
440
+ import { CurrencyPipe } from '@angular/common';
441
+ import { TranslateModule } from '@ngx-translate/core';
442
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
443
+ import { heroHeart, heroShoppingCart, heroCheck } from '@ng-icons/heroicons/outline';
444
+ import { heroHeartSolid } from '@ng-icons/heroicons/solid';
445
+ import { RatingComponent } from '@shared/components/rating/rating.component';
446
+ import { QuantitySelectorComponent } from '@shared/components/quantity-selector/quantity-selector.component';
447
+ import { ProductGridComponent } from '@shared/components/product-grid/product-grid.component';
448
+ import { ProductService, Product } from '@core/services/product.service';
449
+ import { CartService } from '@core/services/cart.service';
450
+ import { WishlistService } from '@core/services/wishlist.service';
451
+
452
+ @Component({
453
+ selector: 'app-product-detail',
454
+ standalone: true,
455
+ imports: [RouterModule, CurrencyPipe, TranslateModule, NgIconComponent, RatingComponent, QuantitySelectorComponent, ProductGridComponent],
456
+ viewProviders: [provideIcons({ heroHeart, heroHeartSolid, heroShoppingCart, heroCheck })],
457
+ template: \`
458
+ @if (product) {
459
+ <div class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
460
+ <div class="grid grid-cols-1 gap-8 lg:grid-cols-2">
461
+ <!-- Image Gallery -->
462
+ <div class="space-y-4">
463
+ <div class="aspect-square overflow-hidden rounded-2xl bg-gray-100">
464
+ <img [src]="selectedImage()" [alt]="product.name" class="h-full w-full object-cover" />
465
+ </div>
466
+ @if (product.images && product.images.length > 1) {
467
+ <div class="flex gap-2">
468
+ @for (img of product.images; track img) {
469
+ <button
470
+ (click)="selectedImage.set(img)"
471
+ class="h-20 w-20 overflow-hidden rounded-lg border-2 transition-all"
472
+ [class.border-primary-500]="selectedImage() === img"
473
+ [class.border-transparent]="selectedImage() !== img">
474
+ <img [src]="img" [alt]="product.name" class="h-full w-full object-cover" />
475
+ </button>
476
+ }
477
+ </div>
478
+ }
479
+ </div>
480
+
481
+ <!-- Product Info -->
482
+ <div class="space-y-6">
483
+ <div>
484
+ <p class="text-sm text-gray-500">{{ product.category }}</p>
485
+ <h1 class="mt-1 text-3xl font-bold text-gray-900">{{ product.name }}</h1>
486
+ </div>
487
+
488
+ <div class="flex items-center gap-4">
489
+ <app-rating [rating]="product.rating" [reviewCount]="product.reviewCount"></app-rating>
490
+ </div>
491
+
492
+ <div class="flex items-center gap-4">
493
+ <span class="text-3xl font-bold text-gray-900">{{ product.price | currency }}</span>
494
+ @if (product.originalPrice) {
495
+ <span class="text-xl text-gray-400 line-through">{{ product.originalPrice | currency }}</span>
496
+ <span class="rounded-full bg-danger-100 px-3 py-1 text-sm font-semibold text-danger-700">
497
+ -{{ discountPercent }}%
498
+ </span>
499
+ }
500
+ </div>
501
+
502
+ <p class="text-gray-600">{{ product.description }}</p>
503
+
504
+ @if (product.features) {
505
+ <div>
506
+ <h3 class="font-semibold text-gray-900 mb-2">{{ 'product.features' | translate }}</h3>
507
+ <ul class="space-y-2">
508
+ @for (feature of product.features; track feature) {
509
+ <li class="flex items-center gap-2 text-gray-600">
510
+ <ng-icon name="heroCheck" size="18" class="text-success-500"></ng-icon>
511
+ {{ feature }}
512
+ </li>
513
+ }
514
+ </ul>
515
+ </div>
516
+ }
517
+
518
+ <div class="flex items-center gap-4">
519
+ <app-quantity-selector
520
+ [quantity]="quantity"
521
+ (quantityChange)="quantity = $event">
522
+ </app-quantity-selector>
523
+
524
+ <button
525
+ (click)="addToCart()"
526
+ class="flex-1 flex items-center justify-center gap-2 rounded-full bg-primary-500 px-8 py-3 font-semibold text-white shadow-lg shadow-primary-500/30 hover:bg-primary-600 transition-all">
527
+ <ng-icon name="heroShoppingCart" size="20"></ng-icon>
528
+ {{ 'product.addToCart' | translate }}
529
+ </button>
530
+
531
+ <button
532
+ (click)="toggleWishlist()"
533
+ class="flex h-12 w-12 items-center justify-center rounded-full border-2 transition-all"
534
+ [class.border-danger-500]="isInWishlist"
535
+ [class.border-gray-300]="!isInWishlist">
536
+ <ng-icon
537
+ [name]="isInWishlist ? 'heroHeartSolid' : 'heroHeart'"
538
+ size="24"
539
+ [class]="isInWishlist ? 'text-danger-500' : 'text-gray-600'">
540
+ </ng-icon>
541
+ </button>
542
+ </div>
543
+
544
+ @if (product.inStock) {
545
+ <p class="flex items-center gap-2 text-success-600">
546
+ <ng-icon name="heroCheck" size="18"></ng-icon>
547
+ {{ 'product.inStock' | translate }}
548
+ </p>
549
+ }
550
+ </div>
551
+ </div>
552
+
553
+ <!-- Related Products -->
554
+ <div class="mt-16">
555
+ <h2 class="text-2xl font-bold text-gray-900 mb-8">{{ 'product.relatedProducts' | translate }}</h2>
556
+ <app-product-grid [products]="relatedProducts"></app-product-grid>
557
+ </div>
558
+ </div>
559
+ }
560
+ \`,
561
+ })
562
+ export class ProductDetailComponent {
563
+ private route = inject(ActivatedRoute);
564
+ private router = inject(Router);
565
+ private productService = inject(ProductService);
566
+ private cartService = inject(CartService);
567
+ private wishlistService = inject(WishlistService);
568
+
569
+ product?: Product;
570
+ relatedProducts: Product[] = [];
571
+ quantity = 1;
572
+ selectedImage = signal('');
573
+
574
+ constructor() {
575
+ this.route.params.subscribe(params => {
576
+ const id = Number(params['id']);
577
+ this.product = this.productService.getProductById(id);
578
+
579
+ if (this.product) {
580
+ this.selectedImage.set(this.product.image);
581
+ this.relatedProducts = this.productService
582
+ .getProductsByCategory(this.product.category)
583
+ .filter(p => p.id !== id)
584
+ .slice(0, 4);
585
+ } else {
586
+ this.router.navigate(['/products']);
587
+ }
588
+ });
589
+ }
590
+
591
+ get discountPercent(): number {
592
+ if (!this.product?.originalPrice) return 0;
593
+ return Math.round((1 - this.product.price / this.product.originalPrice) * 100);
594
+ }
595
+
596
+ get isInWishlist(): boolean {
597
+ return this.product ? this.wishlistService.isInWishlist(this.product.id) : false;
598
+ }
599
+
600
+ addToCart(): void {
601
+ if (this.product) {
602
+ this.cartService.addItem(this.product, this.quantity);
603
+ }
604
+ }
605
+
606
+ toggleWishlist(): void {
607
+ if (this.product) {
608
+ this.wishlistService.toggle(this.product);
609
+ }
610
+ }
611
+ }`;
612
+
613
+ await fs.writeFile(
614
+ path.join(config.fullPath, 'src/app/features/product-detail/product-detail.component.ts'),
615
+ productDetailComponent
616
+ );
617
+
618
+ // Cart Page
619
+ const cartComponent = `import { Component, inject } from '@angular/core';
620
+ import { RouterModule } from '@angular/router';
621
+ import { CurrencyPipe } from '@angular/common';
622
+ import { TranslateModule } from '@ngx-translate/core';
623
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
624
+ import { heroShoppingBag, heroArrowRight } from '@ng-icons/heroicons/outline';
625
+ import { CartItemComponent } from '@shared/components/cart-item/cart-item.component';
626
+ import { CartService } from '@core/services/cart.service';
627
+
628
+ @Component({
629
+ selector: 'app-cart',
630
+ standalone: true,
631
+ imports: [RouterModule, CurrencyPipe, TranslateModule, NgIconComponent, CartItemComponent],
632
+ viewProviders: [provideIcons({ heroShoppingBag, heroArrowRight })],
633
+ template: \`
634
+ <div class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
635
+ <h1 class="text-3xl font-bold text-gray-900 mb-8">{{ 'cart.title' | translate }}</h1>
636
+
637
+ @if (cartService.itemCount() > 0) {
638
+ <div class="grid grid-cols-1 gap-8 lg:grid-cols-3">
639
+ <!-- Cart Items -->
640
+ <div class="lg:col-span-2 space-y-4">
641
+ @for (item of cartService.cartItems(); track item.product.id) {
642
+ <app-cart-item [item]="item"></app-cart-item>
643
+ }
644
+ </div>
645
+
646
+ <!-- Order Summary -->
647
+ <div class="lg:col-span-1">
648
+ <div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm sticky top-24">
649
+ <h2 class="text-lg font-semibold text-gray-900 mb-4">{{ 'cart.orderSummary' | translate }}</h2>
650
+
651
+ <div class="space-y-3">
652
+ <div class="flex justify-between text-gray-600">
653
+ <span>{{ 'cart.subtotal' | translate }}</span>
654
+ <span>{{ cartService.subtotal() | currency }}</span>
655
+ </div>
656
+ <div class="flex justify-between text-gray-600">
657
+ <span>{{ 'cart.shipping' | translate }}</span>
658
+ <span>{{ cartService.shipping() === 0 ? ('cart.free' | translate) : (cartService.shipping() | currency) }}</span>
659
+ </div>
660
+ <div class="flex justify-between text-gray-600">
661
+ <span>{{ 'cart.tax' | translate }}</span>
662
+ <span>{{ cartService.tax() | currency }}</span>
663
+ </div>
664
+ <div class="border-t border-gray-200 pt-3 flex justify-between font-semibold text-gray-900">
665
+ <span>{{ 'cart.total' | translate }}</span>
666
+ <span>{{ cartService.total() | currency }}</span>
667
+ </div>
668
+ </div>
669
+
670
+ <a
671
+ routerLink="/checkout"
672
+ class="mt-6 flex w-full items-center justify-center gap-2 rounded-full bg-primary-500 px-8 py-3 font-semibold text-white shadow-lg shadow-primary-500/30 hover:bg-primary-600 transition-all">
673
+ {{ 'cart.checkout' | translate }}
674
+ <ng-icon name="heroArrowRight" size="20"></ng-icon>
675
+ </a>
676
+
677
+ <a
678
+ routerLink="/products"
679
+ class="mt-4 flex w-full items-center justify-center gap-2 rounded-full border border-gray-300 px-8 py-3 font-semibold text-gray-700 hover:bg-gray-50 transition-all">
680
+ {{ 'cart.continueShopping' | translate }}
681
+ </a>
682
+ </div>
683
+ </div>
684
+ </div>
685
+ } @else {
686
+ <div class="py-16 text-center">
687
+ <div class="mx-auto h-24 w-24 rounded-full bg-gray-100 flex items-center justify-center mb-6">
688
+ <ng-icon name="heroShoppingBag" size="40" class="text-gray-400"></ng-icon>
689
+ </div>
690
+ <h2 class="text-xl font-semibold text-gray-900 mb-2">{{ 'cart.empty' | translate }}</h2>
691
+ <p class="text-gray-500 mb-6">{{ 'cart.emptyDesc' | translate }}</p>
692
+ <a
693
+ routerLink="/products"
694
+ class="inline-flex items-center gap-2 rounded-full bg-primary-500 px-8 py-3 font-semibold text-white shadow-lg shadow-primary-500/30 hover:bg-primary-600 transition-all">
695
+ {{ 'cart.startShopping' | translate }}
696
+ <ng-icon name="heroArrowRight" size="20"></ng-icon>
697
+ </a>
698
+ </div>
699
+ }
700
+ </div>
701
+ \`,
702
+ })
703
+ export class CartComponent {
704
+ cartService = inject(CartService);
705
+ }`;
706
+
707
+ await fs.writeFile(
708
+ path.join(config.fullPath, 'src/app/features/cart/cart.component.ts'),
709
+ cartComponent
710
+ );
711
+
712
+ // Checkout Page
713
+ const checkoutComponent = `import { Component, inject } from '@angular/core';
714
+ import { Router, RouterModule } from '@angular/router';
715
+ import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
716
+ import { CurrencyPipe } from '@angular/common';
717
+ import { TranslateModule } from '@ngx-translate/core';
718
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
719
+ import { heroLockClosed, heroCheck } from '@ng-icons/heroicons/outline';
720
+ import { CartService } from '@core/services/cart.service';
721
+
722
+ @Component({
723
+ selector: 'app-checkout',
724
+ standalone: true,
725
+ imports: [RouterModule, ReactiveFormsModule, CurrencyPipe, TranslateModule, NgIconComponent],
726
+ viewProviders: [provideIcons({ heroLockClosed, heroCheck })],
727
+ template: \`
728
+ <div class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
729
+ <h1 class="text-3xl font-bold text-gray-900 mb-8">{{ 'checkout.title' | translate }}</h1>
730
+
731
+ @if (!orderPlaced) {
732
+ <form [formGroup]="checkoutForm" (ngSubmit)="placeOrder()" class="grid grid-cols-1 gap-8 lg:grid-cols-2">
733
+ <!-- Checkout Form -->
734
+ <div class="space-y-6">
735
+ <!-- Contact -->
736
+ <div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
737
+ <h2 class="text-lg font-semibold text-gray-900 mb-4">{{ 'checkout.contact' | translate }}</h2>
738
+ <div class="space-y-4">
739
+ <div>
740
+ <label for="email" class="block text-sm font-medium text-gray-700">{{ 'checkout.email' | translate }}</label>
741
+ <input id="email" type="email" formControlName="email"
742
+ class="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
743
+ [class.border-danger-500]="checkoutForm.get('email')?.invalid && checkoutForm.get('email')?.touched" />
744
+ </div>
745
+ <div>
746
+ <label for="phone" class="block text-sm font-medium text-gray-700">{{ 'checkout.phone' | translate }}</label>
747
+ <input id="phone" type="tel" formControlName="phone"
748
+ class="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500" />
749
+ </div>
750
+ </div>
751
+ </div>
752
+
753
+ <!-- Shipping -->
754
+ <div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
755
+ <h2 class="text-lg font-semibold text-gray-900 mb-4">{{ 'checkout.shipping' | translate }}</h2>
756
+ <div class="space-y-4">
757
+ <div class="grid grid-cols-2 gap-4">
758
+ <div>
759
+ <label for="firstName" class="block text-sm font-medium text-gray-700">{{ 'checkout.firstName' | translate }}</label>
760
+ <input id="firstName" type="text" formControlName="firstName"
761
+ class="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500" />
762
+ </div>
763
+ <div>
764
+ <label for="lastName" class="block text-sm font-medium text-gray-700">{{ 'checkout.lastName' | translate }}</label>
765
+ <input id="lastName" type="text" formControlName="lastName"
766
+ class="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500" />
767
+ </div>
768
+ </div>
769
+ <div>
770
+ <label for="address" class="block text-sm font-medium text-gray-700">{{ 'checkout.address' | translate }}</label>
771
+ <input id="address" type="text" formControlName="address"
772
+ class="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500" />
773
+ </div>
774
+ <div class="grid grid-cols-2 gap-4">
775
+ <div>
776
+ <label for="city" class="block text-sm font-medium text-gray-700">{{ 'checkout.city' | translate }}</label>
777
+ <input id="city" type="text" formControlName="city"
778
+ class="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500" />
779
+ </div>
780
+ <div>
781
+ <label for="zip" class="block text-sm font-medium text-gray-700">{{ 'checkout.zip' | translate }}</label>
782
+ <input id="zip" type="text" formControlName="zip"
783
+ class="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500" />
784
+ </div>
785
+ </div>
786
+ </div>
787
+ </div>
788
+
789
+ <!-- Payment -->
790
+ <div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
791
+ <h2 class="text-lg font-semibold text-gray-900 mb-4">{{ 'checkout.payment' | translate }}</h2>
792
+ <div class="space-y-4">
793
+ <div>
794
+ <label for="cardNumber" class="block text-sm font-medium text-gray-700">{{ 'checkout.cardNumber' | translate }}</label>
795
+ <input id="cardNumber" type="text" formControlName="cardNumber" placeholder="1234 5678 9012 3456"
796
+ class="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500" />
797
+ </div>
798
+ <div class="grid grid-cols-2 gap-4">
799
+ <div>
800
+ <label for="expiry" class="block text-sm font-medium text-gray-700">{{ 'checkout.expiry' | translate }}</label>
801
+ <input id="expiry" type="text" formControlName="expiry" placeholder="MM/YY"
802
+ class="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500" />
803
+ </div>
804
+ <div>
805
+ <label for="cvv" class="block text-sm font-medium text-gray-700">{{ 'checkout.cvv' | translate }}</label>
806
+ <input id="cvv" type="text" formControlName="cvv" placeholder="123"
807
+ class="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500" />
808
+ </div>
809
+ </div>
810
+ </div>
811
+ </div>
812
+ </div>
813
+
814
+ <!-- Order Summary -->
815
+ <div class="lg:col-span-1">
816
+ <div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm sticky top-24">
817
+ <h2 class="text-lg font-semibold text-gray-900 mb-4">{{ 'cart.orderSummary' | translate }}</h2>
818
+
819
+ <!-- Items -->
820
+ <div class="space-y-3 mb-4">
821
+ @for (item of cartService.cartItems(); track item.product.id) {
822
+ <div class="flex items-center gap-3">
823
+ <img [src]="item.product.image" [alt]="item.product.name" class="h-12 w-12 rounded-lg object-cover" />
824
+ <div class="flex-1">
825
+ <p class="text-sm font-medium text-gray-900 line-clamp-1">{{ item.product.name }}</p>
826
+ <p class="text-sm text-gray-500">{{ 'cart.qty' | translate }}: {{ item.quantity }}</p>
827
+ </div>
828
+ <span class="text-sm font-medium">{{ item.product.price * item.quantity | currency }}</span>
829
+ </div>
830
+ }
831
+ </div>
832
+
833
+ <div class="border-t border-gray-200 pt-4 space-y-3">
834
+ <div class="flex justify-between text-gray-600">
835
+ <span>{{ 'cart.subtotal' | translate }}</span>
836
+ <span>{{ cartService.subtotal() | currency }}</span>
837
+ </div>
838
+ <div class="flex justify-between text-gray-600">
839
+ <span>{{ 'cart.shipping' | translate }}</span>
840
+ <span>{{ cartService.shipping() === 0 ? ('cart.free' | translate) : (cartService.shipping() | currency) }}</span>
841
+ </div>
842
+ <div class="flex justify-between text-gray-600">
843
+ <span>{{ 'cart.tax' | translate }}</span>
844
+ <span>{{ cartService.tax() | currency }}</span>
845
+ </div>
846
+ <div class="border-t border-gray-200 pt-3 flex justify-between font-semibold text-gray-900 text-lg">
847
+ <span>{{ 'cart.total' | translate }}</span>
848
+ <span>{{ cartService.total() | currency }}</span>
849
+ </div>
850
+ </div>
851
+
852
+ <button
853
+ type="submit"
854
+ [disabled]="checkoutForm.invalid"
855
+ class="mt-6 flex w-full items-center justify-center gap-2 rounded-full bg-primary-500 px-8 py-3 font-semibold text-white shadow-lg shadow-primary-500/30 hover:bg-primary-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
856
+ <ng-icon name="heroLockClosed" size="20"></ng-icon>
857
+ {{ 'checkout.placeOrder' | translate }}
858
+ </button>
859
+
860
+ <p class="mt-4 text-xs text-center text-gray-500">
861
+ {{ 'checkout.secureNote' | translate }}
862
+ </p>
863
+ </div>
864
+ </div>
865
+ </form>
866
+ } @else {
867
+ <!-- Order Confirmation -->
868
+ <div class="py-16 text-center">
869
+ <div class="mx-auto h-24 w-24 rounded-full bg-success-100 flex items-center justify-center mb-6">
870
+ <ng-icon name="heroCheck" size="40" class="text-success-600"></ng-icon>
871
+ </div>
872
+ <h2 class="text-2xl font-bold text-gray-900 mb-2">{{ 'checkout.orderPlaced' | translate }}</h2>
873
+ <p class="text-gray-500 mb-2">{{ 'checkout.orderNumber' | translate }}: #{{ orderNumber }}</p>
874
+ <p class="text-gray-500 mb-8">{{ 'checkout.confirmationEmail' | translate }}</p>
875
+ <a
876
+ routerLink="/"
877
+ class="inline-flex items-center gap-2 rounded-full bg-primary-500 px-8 py-3 font-semibold text-white shadow-lg shadow-primary-500/30 hover:bg-primary-600 transition-all">
878
+ {{ 'checkout.backToHome' | translate }}
879
+ </a>
880
+ </div>
881
+ }
882
+ </div>
883
+ \`,
884
+ })
885
+ export class CheckoutComponent {
886
+ private fb = inject(FormBuilder);
887
+ private router = inject(Router);
888
+ cartService = inject(CartService);
889
+
890
+ orderPlaced = false;
891
+ orderNumber = '';
892
+
893
+ checkoutForm = this.fb.group({
894
+ email: ['', [Validators.required, Validators.email]],
895
+ phone: [''],
896
+ firstName: ['', Validators.required],
897
+ lastName: ['', Validators.required],
898
+ address: ['', Validators.required],
899
+ city: ['', Validators.required],
900
+ zip: ['', Validators.required],
901
+ cardNumber: ['', Validators.required],
902
+ expiry: ['', Validators.required],
903
+ cvv: ['', Validators.required],
904
+ });
905
+
906
+ placeOrder(): void {
907
+ if (this.checkoutForm.valid) {
908
+ // Generate order number
909
+ this.orderNumber = Math.random().toString(36).substr(2, 9).toUpperCase();
910
+ this.orderPlaced = true;
911
+ this.cartService.clearCart();
912
+ }
913
+ }
914
+ }`;
915
+
916
+ await fs.writeFile(
917
+ path.join(config.fullPath, 'src/app/features/checkout/checkout.component.ts'),
918
+ checkoutComponent
919
+ );
920
+
921
+ // Wishlist Page
922
+ const wishlistComponent = `import { Component, inject } from '@angular/core';
923
+ import { RouterModule } from '@angular/router';
924
+ import { TranslateModule } from '@ngx-translate/core';
925
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
926
+ import { heroHeart, heroArrowRight } from '@ng-icons/heroicons/outline';
927
+ import { ProductGridComponent } from '@shared/components/product-grid/product-grid.component';
928
+ import { WishlistService } from '@core/services/wishlist.service';
929
+
930
+ @Component({
931
+ selector: 'app-wishlist',
932
+ standalone: true,
933
+ imports: [RouterModule, TranslateModule, NgIconComponent, ProductGridComponent],
934
+ viewProviders: [provideIcons({ heroHeart, heroArrowRight })],
935
+ template: \`
936
+ <div class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
937
+ <h1 class="text-3xl font-bold text-gray-900 mb-8">{{ 'wishlist.title' | translate }}</h1>
938
+
939
+ @if (wishlistService.itemCount() > 0) {
940
+ <app-product-grid [products]="wishlistService.wishlistItems()"></app-product-grid>
941
+ } @else {
942
+ <div class="py-16 text-center">
943
+ <div class="mx-auto h-24 w-24 rounded-full bg-gray-100 flex items-center justify-center mb-6">
944
+ <ng-icon name="heroHeart" size="40" class="text-gray-400"></ng-icon>
945
+ </div>
946
+ <h2 class="text-xl font-semibold text-gray-900 mb-2">{{ 'wishlist.empty' | translate }}</h2>
947
+ <p class="text-gray-500 mb-6">{{ 'wishlist.emptyDesc' | translate }}</p>
948
+ <a
949
+ routerLink="/products"
950
+ class="inline-flex items-center gap-2 rounded-full bg-primary-500 px-8 py-3 font-semibold text-white shadow-lg shadow-primary-500/30 hover:bg-primary-600 transition-all">
951
+ {{ 'wishlist.browseProducts' | translate }}
952
+ <ng-icon name="heroArrowRight" size="20"></ng-icon>
953
+ </a>
954
+ </div>
955
+ }
956
+ </div>
957
+ \`,
958
+ })
959
+ export class WishlistComponent {
960
+ wishlistService = inject(WishlistService);
961
+ }`;
962
+
963
+ await fs.writeFile(
964
+ path.join(config.fullPath, 'src/app/features/wishlist/wishlist.component.ts'),
965
+ wishlistComponent
966
+ );
967
+ }
968
+
969
+ module.exports = { createPages };