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,360 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+
4
+ /**
5
+ * Create Blog Components
6
+ */
7
+ async function createComponents(config) {
8
+ // Post Card Component
9
+ const postCard = `import { Component, Input } from '@angular/core';
10
+ import { RouterModule } from '@angular/router';
11
+ import { DatePipe } from '@angular/common';
12
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
13
+ import { heroCalendar, heroClock, heroTag } from '@ng-icons/heroicons/outline';
14
+ import { Post } from '@core/services/blog.service';
15
+
16
+ @Component({
17
+ selector: 'app-post-card',
18
+ standalone: true,
19
+ imports: [RouterModule, DatePipe, NgIconComponent],
20
+ viewProviders: [provideIcons({ heroCalendar, heroClock, heroTag })],
21
+ template: \`
22
+ <article class="group overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm transition-all hover:shadow-xl hover:-translate-y-1">
23
+ <a [routerLink]="['/post', post.slug]" class="block">
24
+ <div class="aspect-video overflow-hidden">
25
+ <img
26
+ [src]="post.coverImage"
27
+ [alt]="post.title"
28
+ class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
29
+ />
30
+ </div>
31
+ </a>
32
+
33
+ <div class="p-6">
34
+ <div class="mb-3 flex items-center gap-4 text-sm text-gray-500">
35
+ <a [routerLink]="['/category', post.category]" class="text-primary-600 hover:text-primary-700 font-medium">
36
+ {{ post.category }}
37
+ </a>
38
+ <span class="flex items-center gap-1">
39
+ <ng-icon name="heroClock" size="14"></ng-icon>
40
+ {{ post.readingTime }} min read
41
+ </span>
42
+ </div>
43
+
44
+ <a [routerLink]="['/post', post.slug]">
45
+ <h3 class="mb-2 text-xl font-bold text-gray-900 group-hover:text-primary-600 transition-colors line-clamp-2">
46
+ {{ post.title }}
47
+ </h3>
48
+ </a>
49
+
50
+ <p class="mb-4 text-gray-600 line-clamp-2">{{ post.excerpt }}</p>
51
+
52
+ <div class="flex items-center justify-between">
53
+ <div class="flex items-center gap-3">
54
+ <img [src]="post.author.avatar" [alt]="post.author.name" class="h-8 w-8 rounded-full object-cover" />
55
+ <div>
56
+ <a [routerLink]="['/author', post.author.id]" class="text-sm font-medium text-gray-900 hover:text-primary-600">
57
+ {{ post.author.name }}
58
+ </a>
59
+ <p class="text-xs text-gray-500">{{ post.publishedAt | date:'mediumDate' }}</p>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </article>
65
+ \`,
66
+ })
67
+ export class PostCardComponent {
68
+ @Input({ required: true }) post!: Post;
69
+ }`;
70
+
71
+ await fs.writeFile(
72
+ path.join(config.fullPath, "src/app/shared/components/post-card/post-card.component.ts"),
73
+ postCard
74
+ );
75
+
76
+ // Comment Component
77
+ const comment = `import { Component, Input, Output, EventEmitter } from '@angular/core';
78
+ import { DatePipe } from '@angular/common';
79
+ import { FormsModule } from '@angular/forms';
80
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
81
+ import { heroArrowUturnLeft, heroUser } from '@ng-icons/heroicons/outline';
82
+ import { Comment } from '@core/services/blog.service';
83
+
84
+ @Component({
85
+ selector: 'app-comment',
86
+ standalone: true,
87
+ imports: [DatePipe, FormsModule, NgIconComponent],
88
+ viewProviders: [provideIcons({ heroArrowUturnLeft, heroUser })],
89
+ template: \`
90
+ <div class="border-b border-gray-100 py-4 last:border-0" [class.ml-8]="isReply">
91
+ <div class="flex gap-4">
92
+ <div class="flex h-10 w-10 items-center justify-center rounded-full bg-gray-100">
93
+ <ng-icon name="heroUser" size="20" class="text-gray-500"></ng-icon>
94
+ </div>
95
+
96
+ <div class="flex-1">
97
+ <div class="flex items-center gap-2 mb-1">
98
+ <span class="font-semibold text-gray-900">{{ comment.author }}</span>
99
+ <span class="text-sm text-gray-500">{{ comment.createdAt | date:'mediumDate' }}</span>
100
+ </div>
101
+
102
+ <p class="text-gray-700 mb-2">{{ comment.content }}</p>
103
+
104
+ @if (!isReply) {
105
+ <button
106
+ (click)="showReplyForm = !showReplyForm"
107
+ class="flex items-center gap-1 text-sm text-gray-500 hover:text-primary-600">
108
+ <ng-icon name="heroArrowUturnLeft" size="16"></ng-icon>
109
+ Reply
110
+ </button>
111
+ }
112
+
113
+ @if (showReplyForm) {
114
+ <div class="mt-3">
115
+ <textarea
116
+ [(ngModel)]="replyText"
117
+ placeholder="Write a reply..."
118
+ rows="2"
119
+ class="w-full rounded-lg border border-gray-300 p-3 text-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
120
+ ></textarea>
121
+ <div class="mt-2 flex gap-2">
122
+ <button
123
+ (click)="submitReply()"
124
+ class="rounded-lg bg-primary-500 px-4 py-2 text-sm font-medium text-white hover:bg-primary-600">
125
+ Reply
126
+ </button>
127
+ <button
128
+ (click)="showReplyForm = false; replyText = ''"
129
+ class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50">
130
+ Cancel
131
+ </button>
132
+ </div>
133
+ </div>
134
+ }
135
+
136
+ @if (comment.replies && comment.replies.length > 0) {
137
+ <div class="mt-4">
138
+ @for (reply of comment.replies; track reply.id) {
139
+ <app-comment [comment]="reply" [isReply]="true"></app-comment>
140
+ }
141
+ </div>
142
+ }
143
+ </div>
144
+ </div>
145
+ </div>
146
+ \`,
147
+ })
148
+ export class CommentComponent {
149
+ @Input({ required: true }) comment!: Comment;
150
+ @Input() isReply = false;
151
+ @Output() reply = new EventEmitter<string>();
152
+
153
+ showReplyForm = false;
154
+ replyText = '';
155
+
156
+ submitReply(): void {
157
+ if (this.replyText.trim()) {
158
+ this.reply.emit(this.replyText);
159
+ this.replyText = '';
160
+ this.showReplyForm = false;
161
+ }
162
+ }
163
+ }`;
164
+
165
+ await fs.writeFile(
166
+ path.join(config.fullPath, "src/app/shared/components/comment/comment.component.ts"),
167
+ comment
168
+ );
169
+
170
+ // Share Buttons Component
171
+ const shareButtons = `import { Component, Input } from '@angular/core';
172
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
173
+ import { heroLink, heroShare } from '@ng-icons/heroicons/outline';
174
+
175
+ @Component({
176
+ selector: 'app-share-buttons',
177
+ standalone: true,
178
+ imports: [NgIconComponent],
179
+ viewProviders: [provideIcons({ heroLink, heroShare })],
180
+ template: \`
181
+ <div class="flex items-center gap-2">
182
+ <span class="text-sm font-medium text-gray-700">Share:</span>
183
+
184
+ <a
185
+ [href]="twitterUrl"
186
+ target="_blank"
187
+ rel="noopener noreferrer"
188
+ class="flex h-9 w-9 items-center justify-center rounded-full bg-gray-100 text-gray-600 hover:bg-blue-500 hover:text-white transition-colors"
189
+ aria-label="Share on Twitter">
190
+ <span class="text-sm font-bold">𝕏</span>
191
+ </a>
192
+
193
+ <a
194
+ [href]="facebookUrl"
195
+ target="_blank"
196
+ rel="noopener noreferrer"
197
+ class="flex h-9 w-9 items-center justify-center rounded-full bg-gray-100 text-gray-600 hover:bg-blue-600 hover:text-white transition-colors"
198
+ aria-label="Share on Facebook">
199
+ <span class="text-sm font-bold">f</span>
200
+ </a>
201
+
202
+ <a
203
+ [href]="linkedinUrl"
204
+ target="_blank"
205
+ rel="noopener noreferrer"
206
+ class="flex h-9 w-9 items-center justify-center rounded-full bg-gray-100 text-gray-600 hover:bg-blue-700 hover:text-white transition-colors"
207
+ aria-label="Share on LinkedIn">
208
+ <span class="text-sm font-bold">in</span>
209
+ </a>
210
+
211
+ <button
212
+ (click)="copyLink()"
213
+ class="flex h-9 w-9 items-center justify-center rounded-full bg-gray-100 text-gray-600 hover:bg-gray-200 transition-colors"
214
+ aria-label="Copy link">
215
+ <ng-icon name="heroLink" size="18"></ng-icon>
216
+ </button>
217
+ </div>
218
+ \`,
219
+ })
220
+ export class ShareButtonsComponent {
221
+ @Input({ required: true }) url!: string;
222
+ @Input({ required: true }) title!: string;
223
+
224
+ get twitterUrl(): string {
225
+ return \`https://twitter.com/intent/tweet?text=\${encodeURIComponent(this.title)}&url=\${encodeURIComponent(this.url)}\`;
226
+ }
227
+
228
+ get facebookUrl(): string {
229
+ return \`https://www.facebook.com/sharer/sharer.php?u=\${encodeURIComponent(this.url)}\`;
230
+ }
231
+
232
+ get linkedinUrl(): string {
233
+ return \`https://www.linkedin.com/sharing/share-offsite/?url=\${encodeURIComponent(this.url)}\`;
234
+ }
235
+
236
+ copyLink(): void {
237
+ navigator.clipboard.writeText(this.url).then(() => {
238
+ alert('Link copied to clipboard!');
239
+ });
240
+ }
241
+ }`;
242
+
243
+ await fs.writeFile(
244
+ path.join(config.fullPath, "src/app/shared/components/share-buttons/share-buttons.component.ts"),
245
+ shareButtons
246
+ );
247
+
248
+ // Pagination Component
249
+ const pagination = `import { Component, Input, Output, EventEmitter } from '@angular/core';
250
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
251
+ import { heroChevronLeft, heroChevronRight } from '@ng-icons/heroicons/outline';
252
+
253
+ @Component({
254
+ selector: 'app-pagination',
255
+ standalone: true,
256
+ imports: [NgIconComponent],
257
+ viewProviders: [provideIcons({ heroChevronLeft, heroChevronRight })],
258
+ template: \`
259
+ <nav class="flex items-center justify-center gap-2" aria-label="Pagination">
260
+ <button
261
+ (click)="goToPage(currentPage - 1)"
262
+ [disabled]="currentPage === 1"
263
+ class="flex h-10 w-10 items-center justify-center rounded-lg border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
264
+ <ng-icon name="heroChevronLeft" size="20"></ng-icon>
265
+ </button>
266
+
267
+ @for (page of visiblePages; track page) {
268
+ @if (page === '...') {
269
+ <span class="px-2 text-gray-500">...</span>
270
+ } @else {
271
+ <button
272
+ (click)="goToPage(+page)"
273
+ class="flex h-10 w-10 items-center justify-center rounded-lg text-sm font-medium transition-colors"
274
+ [class.bg-primary-500]="currentPage === page"
275
+ [class.text-white]="currentPage === page"
276
+ [class.border]="currentPage !== page"
277
+ [class.border-gray-300]="currentPage !== page"
278
+ [class.text-gray-700]="currentPage !== page"
279
+ [class.hover:bg-gray-50]="currentPage !== page">
280
+ {{ page }}
281
+ </button>
282
+ }
283
+ }
284
+
285
+ <button
286
+ (click)="goToPage(currentPage + 1)"
287
+ [disabled]="currentPage === totalPages"
288
+ class="flex h-10 w-10 items-center justify-center rounded-lg border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
289
+ <ng-icon name="heroChevronRight" size="20"></ng-icon>
290
+ </button>
291
+ </nav>
292
+ \`,
293
+ })
294
+ export class PaginationComponent {
295
+ @Input() currentPage = 1;
296
+ @Input() totalPages = 1;
297
+ @Output() pageChange = new EventEmitter<number>();
298
+
299
+ get visiblePages(): (number | string)[] {
300
+ const pages: (number | string)[] = [];
301
+ const delta = 2;
302
+
303
+ for (let i = 1; i <= this.totalPages; i++) {
304
+ if (i === 1 || i === this.totalPages || (i >= this.currentPage - delta && i <= this.currentPage + delta)) {
305
+ pages.push(i);
306
+ } else if (pages[pages.length - 1] !== '...') {
307
+ pages.push('...');
308
+ }
309
+ }
310
+
311
+ return pages;
312
+ }
313
+
314
+ goToPage(page: number): void {
315
+ if (page >= 1 && page <= this.totalPages && page !== this.currentPage) {
316
+ this.pageChange.emit(page);
317
+ }
318
+ }
319
+ }`;
320
+
321
+ await fs.writeFile(
322
+ path.join(config.fullPath, "src/app/shared/components/pagination/pagination.component.ts"),
323
+ pagination
324
+ );
325
+
326
+ // Tag List Component
327
+ const tagList = `import { Component, Input } from '@angular/core';
328
+ import { RouterModule } from '@angular/router';
329
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
330
+ import { heroTag } from '@ng-icons/heroicons/outline';
331
+
332
+ @Component({
333
+ selector: 'app-tag-list',
334
+ standalone: true,
335
+ imports: [RouterModule, NgIconComponent],
336
+ viewProviders: [provideIcons({ heroTag })],
337
+ template: \`
338
+ <div class="flex flex-wrap gap-2">
339
+ @for (tag of tags; track tag) {
340
+ <a
341
+ [routerLink]="['/tag', tag]"
342
+ class="inline-flex items-center gap-1 rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700 hover:bg-primary-100 hover:text-primary-700 transition-colors">
343
+ <ng-icon name="heroTag" size="14"></ng-icon>
344
+ {{ tag }}
345
+ </a>
346
+ }
347
+ </div>
348
+ \`,
349
+ })
350
+ export class TagListComponent {
351
+ @Input({ required: true }) tags!: string[];
352
+ }`;
353
+
354
+ await fs.writeFile(
355
+ path.join(config.fullPath, "src/app/shared/components/tag-list/tag-list.component.ts"),
356
+ tagList
357
+ );
358
+ }
359
+
360
+ module.exports = { createComponents };
@@ -0,0 +1,77 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+
4
+ /**
5
+ * Create Blog i18n Translations
6
+ */
7
+ async function createI18n(config) {
8
+ const enPath = path.join(config.fullPath, "public/assets/i18n/en.json");
9
+ const arPath = path.join(config.fullPath, "public/assets/i18n/ar.json");
10
+
11
+ let en = {};
12
+ let ar = {};
13
+
14
+ try {
15
+ en = JSON.parse(await fs.readFile(enPath, "utf-8"));
16
+ ar = JSON.parse(await fs.readFile(arPath, "utf-8"));
17
+ } catch {
18
+ // Files don't exist yet
19
+ }
20
+
21
+ const blogEn = {
22
+ blog: {
23
+ title: "Blog",
24
+ subtitle: "Insights, tutorials, and updates from our team",
25
+ searchPlaceholder: "Search articles...",
26
+ featured: "Featured Posts",
27
+ all: "All",
28
+ noPostsFound: "No posts found",
29
+ backToBlog: "Back to Blog",
30
+ comments: "Comments",
31
+ leaveComment: "Leave a Comment",
32
+ yourName: "Your Name",
33
+ yourEmail: "Your Email",
34
+ yourComment: "Your Comment",
35
+ postComment: "Post Comment",
36
+ noComments: "No comments yet. Be the first to comment!",
37
+ relatedPosts: "Related Posts",
38
+ postsInCategory: "posts in this category",
39
+ noPostsInCategory: "No posts in this category",
40
+ postsWithTag: "posts with this tag",
41
+ noPostsWithTag: "No posts with this tag",
42
+ postsByAuthor: "Posts by this author",
43
+ noPostsByAuthor: "No posts by this author",
44
+ },
45
+ };
46
+
47
+ const blogAr = {
48
+ blog: {
49
+ title: "المدونة",
50
+ subtitle: "رؤى ودروس وتحديثات من فريقنا",
51
+ searchPlaceholder: "ابحث في المقالات...",
52
+ featured: "مقالات مميزة",
53
+ all: "الكل",
54
+ noPostsFound: "لم يتم العثور على مقالات",
55
+ backToBlog: "العودة إلى المدونة",
56
+ comments: "التعليقات",
57
+ leaveComment: "اترك تعليقاً",
58
+ yourName: "اسمك",
59
+ yourEmail: "بريدك الإلكتروني",
60
+ yourComment: "تعليقك",
61
+ postComment: "نشر التعليق",
62
+ noComments: "لا توجد تعليقات بعد. كن أول من يعلق!",
63
+ relatedPosts: "مقالات ذات صلة",
64
+ postsInCategory: "مقالات في هذه الفئة",
65
+ noPostsInCategory: "لا توجد مقالات في هذه الفئة",
66
+ postsWithTag: "مقالات بهذه العلامة",
67
+ noPostsWithTag: "لا توجد مقالات بهذه العلامة",
68
+ postsByAuthor: "مقالات هذا الكاتب",
69
+ noPostsByAuthor: "لا توجد مقالات لهذا الكاتب",
70
+ },
71
+ };
72
+
73
+ await fs.writeFile(enPath, JSON.stringify({ ...en, ...blogEn }, null, 2));
74
+ await fs.writeFile(arPath, JSON.stringify({ ...ar, ...blogAr }, null, 2));
75
+ }
76
+
77
+ module.exports = { createI18n };
@@ -0,0 +1,126 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const starter = require("../starter");
4
+
5
+ // Import modular components
6
+ const { createServices } = require("./services");
7
+ const { createComponents } = require("./components");
8
+ const { createPages } = require("./pages");
9
+ const { createI18n } = require("./i18n");
10
+ const { createRouting, createAppComponent, cleanupStarterLayout } = require("./app");
11
+
12
+ /**
13
+ * Blog/CMS Template
14
+ * Extends starter template with:
15
+ * - Blog posts listing with pagination
16
+ * - Post detail with markdown rendering
17
+ * - Categories and tags
18
+ * - Comments system
19
+ * - Author profiles
20
+ * - Search functionality
21
+ * - RSS feed support
22
+ */
23
+ const blog = {
24
+ info: {
25
+ name: "Blog/CMS",
26
+ description: "Content management system with posts, categories, tags, and comments",
27
+ features: [
28
+ ...starter.info.features,
29
+ "Blog posts with markdown support",
30
+ "Categories and tags system",
31
+ "Comments with replies",
32
+ "Author profiles",
33
+ "Search functionality",
34
+ "Reading time estimation",
35
+ "Related posts",
36
+ "Social sharing",
37
+ "Newsletter subscription",
38
+ "Blog-focused header with dynamic categories",
39
+ "Blog-focused footer with newsletter signup",
40
+ ],
41
+ },
42
+
43
+ async apply(config, spinner) {
44
+ const chalk = require("chalk");
45
+
46
+ const completeStep = (message) => {
47
+ if (spinner) {
48
+ spinner.stop();
49
+ console.log(chalk.green(" ✔") + chalk.white(" " + message));
50
+ spinner.start();
51
+ }
52
+ };
53
+
54
+ // Step 1: Apply starter template (infrastructure)
55
+ if (spinner) spinner.update("Setting up starter foundation...");
56
+ await starter.apply(config, null);
57
+
58
+ // Step 2: Create blog-specific directories
59
+ if (spinner) spinner.update("Setting up blog structure...");
60
+ await this.createDirectories(config);
61
+
62
+ // Step 3: Create blog services
63
+ await createServices(config);
64
+
65
+ // Step 4: Create blog components
66
+ await createComponents(config);
67
+
68
+ // Step 5: Create blog pages
69
+ await createPages(config);
70
+
71
+ // Step 6: Create routing
72
+ await createRouting(config);
73
+
74
+ // Step 7: Create app component (blog-specific header/footer)
75
+ await createAppComponent(config);
76
+
77
+ // Step 8: Clean up starter layout (blog has its own header/footer)
78
+ await cleanupStarterLayout(config);
79
+
80
+ // Step 9: Add i18n translations
81
+ await createI18n(config);
82
+
83
+ // Step 10: Format code
84
+ const base = require("../base");
85
+ await base.formatCode(config);
86
+
87
+ if (spinner) spinner.stop();
88
+
89
+ // Show summary
90
+ console.log("");
91
+ completeStep("Blog/CMS template created");
92
+ completeStep("Posts with markdown support");
93
+ completeStep("Categories and tags system");
94
+ completeStep("Comments with replies");
95
+ completeStep("Author profiles");
96
+ completeStep("Search functionality");
97
+ completeStep("Blog-focused header with dynamic categories");
98
+ completeStep("Blog-focused footer with newsletter signup");
99
+ console.log("");
100
+ },
101
+
102
+ async createDirectories(config) {
103
+ const directories = [
104
+ "src/app/features/blog",
105
+ "src/app/features/blog/post-list",
106
+ "src/app/features/blog/post-detail",
107
+ "src/app/features/blog/category",
108
+ "src/app/features/blog/tag",
109
+ "src/app/features/blog/author",
110
+ "src/app/features/blog/search",
111
+ "src/app/shared/components/post-card",
112
+ "src/app/shared/components/comment",
113
+ "src/app/shared/components/tag-list",
114
+ "src/app/shared/components/share-buttons",
115
+ "src/app/shared/components/reading-time",
116
+ "src/app/shared/components/pagination",
117
+ "src/app/core/services",
118
+ ];
119
+
120
+ for (const dir of directories) {
121
+ await fs.ensureDir(path.join(config.fullPath, dir));
122
+ }
123
+ },
124
+ };
125
+
126
+ module.exports = blog;