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.
- package/CHANGELOG.md +81 -350
- package/README.md +93 -157
- package/lib/cli/index.js +29 -3
- package/lib/cli/interactive.js +26 -1
- package/lib/managers/ProjectManager.js +0 -4
- 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/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,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 };
|