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.
- package/CHANGELOG.md +81 -344
- 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 +53 -4055
- 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,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;
|