create-ng-tailwind 3.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +81 -350
  2. package/README.md +93 -157
  3. package/lib/cli/index.js +29 -3
  4. package/lib/cli/interactive.js +26 -1
  5. package/lib/managers/ProjectManager.js +0 -4
  6. package/lib/templates/base/components.js +243 -0
  7. package/lib/templates/base/index.js +207 -0
  8. package/lib/templates/base/infrastructure.js +314 -0
  9. package/lib/templates/base/linting.js +359 -0
  10. package/lib/templates/base/pwa.js +103 -0
  11. package/lib/templates/base/services.js +362 -0
  12. package/lib/templates/blog/app.js +250 -0
  13. package/lib/templates/blog/components.js +360 -0
  14. package/lib/templates/blog/i18n.js +77 -0
  15. package/lib/templates/blog/index.js +126 -0
  16. package/lib/templates/blog/pages.js +554 -0
  17. package/lib/templates/blog/services.js +390 -0
  18. package/lib/templates/dashboard/app.js +320 -0
  19. package/lib/templates/dashboard/charts.js +305 -0
  20. package/lib/templates/dashboard/components.js +410 -0
  21. package/lib/templates/dashboard/i18n.js +340 -0
  22. package/lib/templates/dashboard/index.js +141 -0
  23. package/lib/templates/dashboard/layout.js +310 -0
  24. package/lib/templates/dashboard/pages.js +681 -0
  25. package/lib/templates/ecommerce/app.js +315 -0
  26. package/lib/templates/ecommerce/components.js +496 -0
  27. package/lib/templates/ecommerce/i18n.js +389 -0
  28. package/lib/templates/ecommerce/index.js +152 -0
  29. package/lib/templates/ecommerce/layout.js +270 -0
  30. package/lib/templates/ecommerce/pages.js +969 -0
  31. package/lib/templates/ecommerce/services.js +300 -0
  32. package/lib/templates/index.js +12 -0
  33. package/lib/templates/landing/index.js +1117 -0
  34. package/lib/templates/portfolio/index.js +1160 -0
  35. package/lib/templates/saas/index.js +1371 -0
  36. package/lib/templates/starter/app.js +364 -0
  37. package/lib/templates/starter/i18n.js +856 -0
  38. package/lib/templates/starter/index.js +52 -4060
  39. package/lib/templates/starter/layout.js +852 -0
  40. package/lib/templates/starter/pages.js +1241 -0
  41. package/package.json +1 -1
  42. package/lib/templates/starter/features.js +0 -867
  43. package/lib/utils/ai-config.js +0 -641
  44. /package/lib/templates/{starter → base}/advanced-features.js +0 -0
  45. /package/lib/templates/{starter → base}/seo-assets.js +0 -0
  46. /package/lib/templates/{starter → base}/seo-features.js +0 -0
  47. /package/lib/templates/{starter → base}/ui-features.js +0 -0
@@ -0,0 +1,554 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+
4
+ /**
5
+ * Create Blog Pages
6
+ */
7
+ async function createPages(config) {
8
+ // Post List Page (Home)
9
+ const postList = `import { Component, inject, signal, computed } from '@angular/core';
10
+ import { RouterModule } from '@angular/router';
11
+ import { TranslateModule } from '@ngx-translate/core';
12
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
13
+ import { heroMagnifyingGlass } from '@ng-icons/heroicons/outline';
14
+ import { PostCardComponent } from '@shared/components/post-card/post-card.component';
15
+ import { PaginationComponent } from '@shared/components/pagination/pagination.component';
16
+ import { BlogService } from '@core/services/blog.service';
17
+
18
+ @Component({
19
+ selector: 'app-post-list',
20
+ standalone: true,
21
+ imports: [RouterModule, TranslateModule, NgIconComponent, PostCardComponent, PaginationComponent],
22
+ viewProviders: [provideIcons({ heroMagnifyingGlass })],
23
+ template: \`
24
+ <div class="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
25
+ <!-- Hero Section -->
26
+ <div class="mb-12 text-center">
27
+ <h1 class="text-4xl font-bold text-gray-900 sm:text-5xl">{{ 'blog.title' | translate }}</h1>
28
+ <p class="mt-4 text-lg text-gray-600">{{ 'blog.subtitle' | translate }}</p>
29
+
30
+ <!-- Search -->
31
+ <div class="mt-8 mx-auto max-w-md">
32
+ <div class="relative">
33
+ <ng-icon name="heroMagnifyingGlass" size="20" class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></ng-icon>
34
+ <input
35
+ type="text"
36
+ [placeholder]="'blog.searchPlaceholder' | translate"
37
+ (input)="onSearch($event)"
38
+ class="w-full rounded-full border border-gray-300 py-3 pl-12 pr-4 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
39
+ />
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ <!-- Featured Posts -->
45
+ @if (!searchQuery() && currentPage() === 1 && blogService.featuredPosts().length > 0) {
46
+ <section class="mb-16">
47
+ <h2 class="mb-6 text-2xl font-bold text-gray-900">{{ 'blog.featured' | translate }}</h2>
48
+ <div class="grid gap-8 md:grid-cols-3">
49
+ @for (post of blogService.featuredPosts(); track post.id) {
50
+ <app-post-card [post]="post"></app-post-card>
51
+ }
52
+ </div>
53
+ </section>
54
+ }
55
+
56
+ <!-- Categories -->
57
+ <div class="mb-8 flex flex-wrap gap-2">
58
+ <button
59
+ (click)="selectedCategory.set('')"
60
+ class="rounded-full px-4 py-2 text-sm font-medium transition-colors"
61
+ [class.bg-primary-500]="!selectedCategory()"
62
+ [class.text-white]="!selectedCategory()"
63
+ [class.bg-gray-100]="selectedCategory()"
64
+ [class.text-gray-700]="selectedCategory()">
65
+ {{ 'blog.all' | translate }}
66
+ </button>
67
+ @for (cat of blogService.categories(); track cat) {
68
+ <button
69
+ (click)="selectedCategory.set(cat)"
70
+ class="rounded-full px-4 py-2 text-sm font-medium transition-colors"
71
+ [class.bg-primary-500]="selectedCategory() === cat"
72
+ [class.text-white]="selectedCategory() === cat"
73
+ [class.bg-gray-100]="selectedCategory() !== cat"
74
+ [class.text-gray-700]="selectedCategory() !== cat">
75
+ {{ cat }}
76
+ </button>
77
+ }
78
+ </div>
79
+
80
+ <!-- Posts Grid -->
81
+ <div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
82
+ @for (post of paginatedPosts(); track post.id) {
83
+ <app-post-card [post]="post"></app-post-card>
84
+ } @empty {
85
+ <div class="col-span-full py-12 text-center">
86
+ <p class="text-gray-500">{{ 'blog.noPostsFound' | translate }}</p>
87
+ </div>
88
+ }
89
+ </div>
90
+
91
+ <!-- Pagination -->
92
+ @if (totalPages() > 1) {
93
+ <div class="mt-12">
94
+ <app-pagination
95
+ [currentPage]="currentPage()"
96
+ [totalPages]="totalPages()"
97
+ (pageChange)="currentPage.set($event)">
98
+ </app-pagination>
99
+ </div>
100
+ }
101
+ </div>
102
+ \`,
103
+ })
104
+ export class PostListComponent {
105
+ blogService = inject(BlogService);
106
+
107
+ searchQuery = signal('');
108
+ selectedCategory = signal('');
109
+ currentPage = signal(1);
110
+ postsPerPage = 6;
111
+
112
+ filteredPosts = computed(() => {
113
+ let posts = this.blogService.posts();
114
+
115
+ if (this.searchQuery()) {
116
+ posts = this.blogService.searchPosts(this.searchQuery());
117
+ }
118
+
119
+ if (this.selectedCategory()) {
120
+ posts = posts.filter(p => p.category === this.selectedCategory());
121
+ }
122
+
123
+ return posts;
124
+ });
125
+
126
+ totalPages = computed(() =>
127
+ Math.ceil(this.filteredPosts().length / this.postsPerPage)
128
+ );
129
+
130
+ paginatedPosts = computed(() => {
131
+ const start = (this.currentPage() - 1) * this.postsPerPage;
132
+ return this.filteredPosts().slice(start, start + this.postsPerPage);
133
+ });
134
+
135
+ onSearch(event: Event): void {
136
+ const value = (event.target as HTMLInputElement).value;
137
+ this.searchQuery.set(value);
138
+ this.currentPage.set(1);
139
+ }
140
+ }`;
141
+
142
+ await fs.writeFile(
143
+ path.join(config.fullPath, "src/app/features/blog/post-list/post-list.component.ts"),
144
+ postList
145
+ );
146
+
147
+ // Post Detail Page
148
+ const postDetail = `import { Component, inject, signal } from '@angular/core';
149
+ import { ActivatedRoute, Router, RouterModule } from '@angular/router';
150
+ import { DatePipe } from '@angular/common';
151
+ import { FormsModule } from '@angular/forms';
152
+ import { TranslateModule } from '@ngx-translate/core';
153
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
154
+ import { heroCalendar, heroClock, heroArrowLeft } from '@ng-icons/heroicons/outline';
155
+ import { PostCardComponent } from '@shared/components/post-card/post-card.component';
156
+ import { CommentComponent } from '@shared/components/comment/comment.component';
157
+ import { TagListComponent } from '@shared/components/tag-list/tag-list.component';
158
+ import { ShareButtonsComponent } from '@shared/components/share-buttons/share-buttons.component';
159
+ import { BlogService, Post, Comment } from '@core/services/blog.service';
160
+
161
+ @Component({
162
+ selector: 'app-post-detail',
163
+ standalone: true,
164
+ imports: [
165
+ RouterModule, DatePipe, FormsModule, TranslateModule, NgIconComponent,
166
+ PostCardComponent, CommentComponent, TagListComponent, ShareButtonsComponent
167
+ ],
168
+ viewProviders: [provideIcons({ heroCalendar, heroClock, heroArrowLeft })],
169
+ template: \`
170
+ @if (post) {
171
+ <article class="mx-auto max-w-4xl px-4 py-12 sm:px-6 lg:px-8">
172
+ <!-- Back Link -->
173
+ <a routerLink="/" class="mb-8 inline-flex items-center gap-2 text-gray-600 hover:text-primary-600">
174
+ <ng-icon name="heroArrowLeft" size="20"></ng-icon>
175
+ {{ 'blog.backToBlog' | translate }}
176
+ </a>
177
+
178
+ <!-- Header -->
179
+ <header class="mb-8">
180
+ <div class="mb-4 flex items-center gap-4 text-sm text-gray-500">
181
+ <a [routerLink]="['/category', post.category]" class="text-primary-600 hover:text-primary-700 font-medium">
182
+ {{ post.category }}
183
+ </a>
184
+ <span class="flex items-center gap-1">
185
+ <ng-icon name="heroCalendar" size="16"></ng-icon>
186
+ {{ post.publishedAt | date:'mediumDate' }}
187
+ </span>
188
+ <span class="flex items-center gap-1">
189
+ <ng-icon name="heroClock" size="16"></ng-icon>
190
+ {{ post.readingTime }} min read
191
+ </span>
192
+ </div>
193
+
194
+ <h1 class="mb-6 text-4xl font-bold text-gray-900 sm:text-5xl">{{ post.title }}</h1>
195
+
196
+ <!-- Author -->
197
+ <div class="flex items-center gap-4">
198
+ <img [src]="post.author.avatar" [alt]="post.author.name" class="h-12 w-12 rounded-full object-cover" />
199
+ <div>
200
+ <a [routerLink]="['/author', post.author.id]" class="font-medium text-gray-900 hover:text-primary-600">
201
+ {{ post.author.name }}
202
+ </a>
203
+ <p class="text-sm text-gray-500">{{ post.author.bio }}</p>
204
+ </div>
205
+ </div>
206
+ </header>
207
+
208
+ <!-- Cover Image -->
209
+ <div class="mb-8 overflow-hidden rounded-2xl">
210
+ <img [src]="post.coverImage" [alt]="post.title" class="w-full" />
211
+ </div>
212
+
213
+ <!-- Content -->
214
+ <div class="prose prose-lg max-w-none mb-8" [innerHTML]="renderedContent"></div>
215
+
216
+ <!-- Tags -->
217
+ <div class="mb-8">
218
+ <app-tag-list [tags]="post.tags"></app-tag-list>
219
+ </div>
220
+
221
+ <!-- Share -->
222
+ <div class="mb-12 border-t border-b border-gray-200 py-6">
223
+ <app-share-buttons [url]="currentUrl" [title]="post.title"></app-share-buttons>
224
+ </div>
225
+
226
+ <!-- Comments Section -->
227
+ <section class="mb-12">
228
+ <h2 class="mb-6 text-2xl font-bold text-gray-900">
229
+ {{ 'blog.comments' | translate }} ({{ comments.length }})
230
+ </h2>
231
+
232
+ <!-- Comment Form -->
233
+ <div class="mb-8 rounded-xl border border-gray-200 bg-gray-50 p-6">
234
+ <h3 class="mb-4 font-semibold text-gray-900">{{ 'blog.leaveComment' | translate }}</h3>
235
+ <div class="grid gap-4 sm:grid-cols-2">
236
+ <input
237
+ type="text"
238
+ [(ngModel)]="newComment.author"
239
+ [placeholder]="'blog.yourName' | translate"
240
+ class="rounded-lg border border-gray-300 px-4 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
241
+ />
242
+ <input
243
+ type="email"
244
+ [(ngModel)]="newComment.email"
245
+ [placeholder]="'blog.yourEmail' | translate"
246
+ class="rounded-lg border border-gray-300 px-4 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
247
+ />
248
+ </div>
249
+ <textarea
250
+ [(ngModel)]="newComment.content"
251
+ [placeholder]="'blog.yourComment' | translate"
252
+ rows="4"
253
+ class="mt-4 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"
254
+ ></textarea>
255
+ <button
256
+ (click)="submitComment()"
257
+ class="mt-4 rounded-lg bg-primary-500 px-6 py-2 font-medium text-white hover:bg-primary-600">
258
+ {{ 'blog.postComment' | translate }}
259
+ </button>
260
+ </div>
261
+
262
+ <!-- Comments List -->
263
+ <div class="space-y-4">
264
+ @for (comment of comments; track comment.id) {
265
+ <app-comment [comment]="comment"></app-comment>
266
+ } @empty {
267
+ <p class="text-gray-500">{{ 'blog.noComments' | translate }}</p>
268
+ }
269
+ </div>
270
+ </section>
271
+
272
+ <!-- Related Posts -->
273
+ @if (relatedPosts.length > 0) {
274
+ <section>
275
+ <h2 class="mb-6 text-2xl font-bold text-gray-900">{{ 'blog.relatedPosts' | translate }}</h2>
276
+ <div class="grid gap-8 md:grid-cols-3">
277
+ @for (relatedPost of relatedPosts; track relatedPost.id) {
278
+ <app-post-card [post]="relatedPost"></app-post-card>
279
+ }
280
+ </div>
281
+ </section>
282
+ }
283
+ </article>
284
+ }
285
+ \`,
286
+ })
287
+ export class PostDetailComponent {
288
+ private route = inject(ActivatedRoute);
289
+ private router = inject(Router);
290
+ private blogService = inject(BlogService);
291
+
292
+ post?: Post;
293
+ comments: Comment[] = [];
294
+ relatedPosts: Post[] = [];
295
+ currentUrl = '';
296
+
297
+ newComment = {
298
+ author: '',
299
+ email: '',
300
+ content: '',
301
+ };
302
+
303
+ get renderedContent(): string {
304
+ if (!this.post) return '';
305
+ return this.post.content
306
+ .replace(/^# (.*$)/gm, '<h1 class="text-3xl font-bold mt-8 mb-4">$1</h1>')
307
+ .replace(/^## (.*$)/gm, '<h2 class="text-2xl font-bold mt-6 mb-3">$1</h2>')
308
+ .replace(/^### (.*$)/gm, '<h3 class="text-xl font-bold mt-4 mb-2">$1</h3>')
309
+ .replace(/\\\`\\\`\\\`(\\w+)?\\n([\\s\\S]*?)\\\`\\\`\\\`/g, '<pre class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto my-4"><code>$2</code></pre>')
310
+ .replace(/\\\`([^\\\`]+)\\\`/g, '<code class="bg-gray-100 px-1 rounded text-primary-600">$1</code>')
311
+ .replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')
312
+ .replace(/\\*([^*]+)\\*/g, '<em>$1</em>')
313
+ .replace(/^(\\d+)\\. (.*$)/gm, '<li class="ml-6 list-decimal">$2</li>')
314
+ .replace(/^- (.*$)/gm, '<li class="ml-6 list-disc">$1</li>')
315
+ .replace(/\\n\\n/g, '</p><p class="my-4">');
316
+ }
317
+
318
+ constructor() {
319
+ this.route.params.subscribe(params => {
320
+ const slug = params['slug'];
321
+ this.post = this.blogService.getPostBySlug(slug);
322
+
323
+ if (this.post) {
324
+ this.comments = this.blogService.getCommentsByPostId(this.post.id);
325
+ this.relatedPosts = this.blogService.getRelatedPosts(this.post);
326
+ this.currentUrl = window.location.href;
327
+ } else {
328
+ this.router.navigate(['/']);
329
+ }
330
+ });
331
+ }
332
+
333
+ submitComment(): void {
334
+ if (this.post && this.newComment.author && this.newComment.email && this.newComment.content) {
335
+ this.blogService.addComment({
336
+ postId: this.post.id,
337
+ author: this.newComment.author,
338
+ email: this.newComment.email,
339
+ content: this.newComment.content,
340
+ });
341
+ this.comments = this.blogService.getCommentsByPostId(this.post.id);
342
+ this.newComment = { author: '', email: '', content: '' };
343
+ }
344
+ }
345
+ }`;
346
+
347
+ await fs.writeFile(
348
+ path.join(config.fullPath, "src/app/features/blog/post-detail/post-detail.component.ts"),
349
+ postDetail
350
+ );
351
+
352
+ // Category Page
353
+ const category = `import { Component, inject, computed } from '@angular/core';
354
+ import { ActivatedRoute, RouterModule } from '@angular/router';
355
+ import { TranslateModule } from '@ngx-translate/core';
356
+ import { PostCardComponent } from '@shared/components/post-card/post-card.component';
357
+ import { BlogService } from '@core/services/blog.service';
358
+
359
+ @Component({
360
+ selector: 'app-category',
361
+ standalone: true,
362
+ imports: [RouterModule, TranslateModule, PostCardComponent],
363
+ template: \`
364
+ <div class="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
365
+ <div class="mb-12">
366
+ <a routerLink="/" class="text-primary-600 hover:text-primary-700 mb-4 inline-block">
367
+ ← {{ 'blog.backToBlog' | translate }}
368
+ </a>
369
+ <h1 class="text-4xl font-bold text-gray-900">{{ categoryName }}</h1>
370
+ <p class="mt-2 text-gray-600">{{ posts().length }} {{ 'blog.postsInCategory' | translate }}</p>
371
+ </div>
372
+
373
+ <div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
374
+ @for (post of posts(); track post.id) {
375
+ <app-post-card [post]="post"></app-post-card>
376
+ } @empty {
377
+ <p class="col-span-full text-gray-500">{{ 'blog.noPostsInCategory' | translate }}</p>
378
+ }
379
+ </div>
380
+ </div>
381
+ \`,
382
+ })
383
+ export class CategoryComponent {
384
+ private route = inject(ActivatedRoute);
385
+ private blogService = inject(BlogService);
386
+
387
+ categoryName = '';
388
+
389
+ posts = computed(() => this.blogService.getPostsByCategory(this.categoryName));
390
+
391
+ constructor() {
392
+ this.route.params.subscribe(params => {
393
+ this.categoryName = params['category'];
394
+ });
395
+ }
396
+ }`;
397
+
398
+ await fs.writeFile(
399
+ path.join(config.fullPath, "src/app/features/blog/category/category.component.ts"),
400
+ category
401
+ );
402
+
403
+ // Tag Page
404
+ const tag = `import { Component, inject, computed } from '@angular/core';
405
+ import { ActivatedRoute, RouterModule } from '@angular/router';
406
+ import { TranslateModule } from '@ngx-translate/core';
407
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
408
+ import { heroTag } from '@ng-icons/heroicons/outline';
409
+ import { PostCardComponent } from '@shared/components/post-card/post-card.component';
410
+ import { BlogService } from '@core/services/blog.service';
411
+
412
+ @Component({
413
+ selector: 'app-tag',
414
+ standalone: true,
415
+ imports: [RouterModule, TranslateModule, NgIconComponent, PostCardComponent],
416
+ viewProviders: [provideIcons({ heroTag })],
417
+ template: \`
418
+ <div class="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
419
+ <div class="mb-12">
420
+ <a routerLink="/" class="text-primary-600 hover:text-primary-700 mb-4 inline-block">
421
+ ← {{ 'blog.backToBlog' | translate }}
422
+ </a>
423
+ <div class="flex items-center gap-3">
424
+ <ng-icon name="heroTag" size="32" class="text-primary-500"></ng-icon>
425
+ <h1 class="text-4xl font-bold text-gray-900">{{ tagName }}</h1>
426
+ </div>
427
+ <p class="mt-2 text-gray-600">{{ posts().length }} {{ 'blog.postsWithTag' | translate }}</p>
428
+ </div>
429
+
430
+ <div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
431
+ @for (post of posts(); track post.id) {
432
+ <app-post-card [post]="post"></app-post-card>
433
+ } @empty {
434
+ <p class="col-span-full text-gray-500">{{ 'blog.noPostsWithTag' | translate }}</p>
435
+ }
436
+ </div>
437
+ </div>
438
+ \`,
439
+ })
440
+ export class TagComponent {
441
+ private route = inject(ActivatedRoute);
442
+ private blogService = inject(BlogService);
443
+
444
+ tagName = '';
445
+
446
+ posts = computed(() => this.blogService.getPostsByTag(this.tagName));
447
+
448
+ constructor() {
449
+ this.route.params.subscribe(params => {
450
+ this.tagName = params['tag'];
451
+ });
452
+ }
453
+ }`;
454
+
455
+ await fs.writeFile(
456
+ path.join(config.fullPath, "src/app/features/blog/tag/tag.component.ts"),
457
+ tag
458
+ );
459
+
460
+ // Author Page
461
+ const author = `import { Component, inject, computed, signal } from '@angular/core';
462
+ import { ActivatedRoute, RouterModule } from '@angular/router';
463
+ import { TranslateModule } from '@ngx-translate/core';
464
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
465
+ import { heroGlobeAlt, heroCodeBracket, heroBriefcase } from '@ng-icons/heroicons/outline';
466
+ import { PostCardComponent } from '@shared/components/post-card/post-card.component';
467
+ import { BlogService, Author, Post } from '@core/services/blog.service';
468
+
469
+ @Component({
470
+ selector: 'app-author',
471
+ standalone: true,
472
+ imports: [RouterModule, TranslateModule, NgIconComponent, PostCardComponent],
473
+ viewProviders: [provideIcons({ heroGlobeAlt, heroCodeBracket, heroBriefcase })],
474
+ template: \`
475
+ @if (author()) {
476
+ <div class="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
477
+ <a routerLink="/" class="text-primary-600 hover:text-primary-700 mb-8 inline-block">
478
+ ← {{ 'blog.backToBlog' | translate }}
479
+ </a>
480
+
481
+ <!-- Author Profile -->
482
+ <div class="mb-12 rounded-2xl bg-linear-to-r from-primary-500 to-purple-600 p-8 text-white">
483
+ <div class="flex flex-col items-center gap-6 sm:flex-row">
484
+ <img [src]="author()!.avatar" [alt]="author()!.name" class="h-32 w-32 rounded-full border-4 border-white object-cover shadow-lg" />
485
+ <div class="text-center sm:text-left">
486
+ <h1 class="text-3xl font-bold">{{ author()!.name }}</h1>
487
+ <p class="mt-2 text-primary-100">{{ author()!.bio }}</p>
488
+
489
+ @if (author()!.social) {
490
+ <div class="mt-4 flex justify-center gap-4 sm:justify-start">
491
+ @if (author()!.social!.twitter) {
492
+ <a [href]="'https://twitter.com/' + author()!.social!.twitter" target="_blank" class="flex h-8 w-8 items-center justify-center rounded-full bg-white/20 text-white hover:bg-white/30">
493
+ <span class="text-sm font-bold">𝕏</span>
494
+ </a>
495
+ }
496
+ @if (author()!.social!.github) {
497
+ <a [href]="'https://github.com/' + author()!.social!.github" target="_blank" class="flex h-8 w-8 items-center justify-center rounded-full bg-white/20 text-white hover:bg-white/30">
498
+ <ng-icon name="heroCodeBracket" size="18"></ng-icon>
499
+ </a>
500
+ }
501
+ @if (author()!.social!.linkedin) {
502
+ <a [href]="'https://linkedin.com/in/' + author()!.social!.linkedin" target="_blank" class="flex h-8 w-8 items-center justify-center rounded-full bg-white/20 text-white hover:bg-white/30">
503
+ <ng-icon name="heroBriefcase" size="18"></ng-icon>
504
+ </a>
505
+ }
506
+ </div>
507
+ }
508
+ </div>
509
+ </div>
510
+ </div>
511
+
512
+ <!-- Author's Posts -->
513
+ <h2 class="mb-6 text-2xl font-bold text-gray-900">
514
+ {{ 'blog.postsByAuthor' | translate }} ({{ posts().length }})
515
+ </h2>
516
+
517
+ <div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
518
+ @for (post of posts(); track post.id) {
519
+ <app-post-card [post]="post"></app-post-card>
520
+ } @empty {
521
+ <p class="col-span-full text-gray-500">{{ 'blog.noPostsByAuthor' | translate }}</p>
522
+ }
523
+ </div>
524
+ </div>
525
+ }
526
+ \`,
527
+ })
528
+ export class AuthorComponent {
529
+ private route = inject(ActivatedRoute);
530
+ private blogService = inject(BlogService);
531
+
532
+ authorId = signal(0);
533
+
534
+ author = computed(() => {
535
+ const posts = this.blogService.getPostsByAuthor(this.authorId());
536
+ return posts.length > 0 ? posts[0].author : null;
537
+ });
538
+
539
+ posts = computed(() => this.blogService.getPostsByAuthor(this.authorId()));
540
+
541
+ constructor() {
542
+ this.route.params.subscribe(params => {
543
+ this.authorId.set(Number(params['id']));
544
+ });
545
+ }
546
+ }`;
547
+
548
+ await fs.writeFile(
549
+ path.join(config.fullPath, "src/app/features/blog/author/author.component.ts"),
550
+ author
551
+ );
552
+ }
553
+
554
+ module.exports = { createPages };