create-ng-tailwind 3.1.0 → 4.1.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.

Potentially problematic release.


This version of create-ng-tailwind might be problematic. Click here for more details.

Files changed (48) hide show
  1. package/CHANGELOG.md +96 -341
  2. package/README.md +111 -157
  3. package/lib/cli/index.js +74 -3
  4. package/lib/cli/interactive.js +26 -1
  5. package/lib/managers/ProjectManager.js +2 -5
  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/lib/utils/nodeCompat.js +85 -0
  42. package/package.json +1 -1
  43. package/lib/templates/starter/features.js +0 -867
  44. package/lib/utils/ai-config.js +0 -641
  45. /package/lib/templates/{starter → base}/advanced-features.js +0 -0
  46. /package/lib/templates/{starter → base}/seo-assets.js +0 -0
  47. /package/lib/templates/{starter → base}/seo-features.js +0 -0
  48. /package/lib/templates/{starter → base}/ui-features.js +0 -0
@@ -0,0 +1,250 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+
4
+ /**
5
+ * Create Blog Routing
6
+ */
7
+ async function createRouting(config) {
8
+ const routes = `import { Routes } from '@angular/router';
9
+
10
+ export const routes: Routes = [
11
+ // Home - Blog Post List
12
+ {
13
+ path: '',
14
+ loadComponent: () => import('./features/blog/post-list/post-list.component').then(c => c.PostListComponent)
15
+ },
16
+
17
+ // Blog routes
18
+ {
19
+ path: 'category/:category',
20
+ loadComponent: () => import('./features/blog/category/category.component').then(c => c.CategoryComponent)
21
+ },
22
+ {
23
+ path: 'tag/:tag',
24
+ loadComponent: () => import('./features/blog/tag/tag.component').then(c => c.TagComponent)
25
+ },
26
+ {
27
+ path: 'author/:id',
28
+ loadComponent: () => import('./features/blog/author/author.component').then(c => c.AuthorComponent)
29
+ },
30
+ {
31
+ path: 'post/:slug',
32
+ loadComponent: () => import('./features/blog/post-detail/post-detail.component').then(c => c.PostDetailComponent)
33
+ },
34
+
35
+ // Auth routes
36
+ {
37
+ path: 'auth',
38
+ loadComponent: () => import('./layout/auth/auth-layout.component').then(c => c.AuthLayoutComponent),
39
+ children: [
40
+ { path: '', redirectTo: 'login', pathMatch: 'full' },
41
+ { path: 'login', loadComponent: () => import('./features/auth/login/login.component').then(c => c.LoginComponent) },
42
+ { path: 'register', loadComponent: () => import('./features/auth/register/register.component').then(c => c.RegisterComponent) },
43
+ { path: 'forgot-password', loadComponent: () => import('./features/auth/forgot-password/forgot-password.component').then(c => c.ForgotPasswordComponent) }
44
+ ]
45
+ },
46
+
47
+ // Catch-all
48
+ { path: '**', redirectTo: '' }
49
+ ];`;
50
+
51
+ await fs.writeFile(path.join(config.fullPath, "src/app/app.routes.ts"), routes);
52
+ }
53
+
54
+ /**
55
+ * Create Blog App Component
56
+ */
57
+ async function createAppComponent(config) {
58
+ const appComponent = `import { Component, inject } from '@angular/core';
59
+ import { RouterOutlet, RouterModule } from '@angular/router';
60
+ import { TranslateService } from '@ngx-translate/core';
61
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
62
+ import { heroMagnifyingGlass, heroUser, heroRss, heroEnvelope } from '@ng-icons/heroicons/outline';
63
+ import { BlogService } from '@core/services/blog.service';
64
+
65
+ @Component({
66
+ selector: 'app-root',
67
+ standalone: true,
68
+ imports: [RouterOutlet, RouterModule, NgIconComponent],
69
+ viewProviders: [provideIcons({ heroMagnifyingGlass, heroUser, heroRss, heroEnvelope })],
70
+ template: \`
71
+ <div class="min-h-screen bg-gray-50">
72
+ <!-- Header -->
73
+ <header class="sticky top-0 z-50 border-b border-gray-200 bg-white/95 backdrop-blur-sm shadow-sm">
74
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
75
+ <div class="flex h-20 items-center justify-between">
76
+ <!-- Logo & Tagline -->
77
+ <div class="flex items-center gap-4">
78
+ <a routerLink="/" class="flex items-center gap-3 group">
79
+ <div class="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-primary-500 to-purple-600 text-white font-bold text-2xl shadow-lg group-hover:shadow-xl transition-shadow">
80
+ 📝
81
+ </div>
82
+ <div class="hidden sm:block">
83
+ <h1 class="text-2xl font-bold text-gray-900 group-hover:text-primary-600 transition-colors">My Blog</h1>
84
+ <p class="text-xs text-gray-500">Insights & Tutorials</p>
85
+ </div>
86
+ </a>
87
+ </div>
88
+
89
+ <!-- Desktop Navigation -->
90
+ <nav class="hidden md:flex items-center gap-8">
91
+ <a routerLink="/" routerLinkActive="text-primary-600 font-semibold" [routerLinkActiveOptions]="{exact: true}" class="text-gray-600 hover:text-primary-600 font-medium transition-colors">
92
+ Home
93
+ </a>
94
+ @for (category of blogService.categories(); track category) {
95
+ <a [routerLink]="['/category', category]" routerLinkActive="text-primary-600 font-semibold" class="text-gray-600 hover:text-primary-600 font-medium transition-colors">
96
+ {{ category }}
97
+ </a>
98
+ }
99
+ </nav>
100
+
101
+ <!-- Actions -->
102
+ <div class="flex items-center gap-3">
103
+ <button class="flex h-10 w-10 items-center justify-center rounded-full text-gray-600 hover:bg-gray-100 hover:text-primary-600 transition-colors" aria-label="Search">
104
+ <ng-icon name="heroMagnifyingGlass" size="20"></ng-icon>
105
+ </button>
106
+ <a routerLink="/auth/login" class="flex h-10 w-10 items-center justify-center rounded-full text-gray-600 hover:bg-gray-100 hover:text-primary-600 transition-colors" aria-label="Sign In">
107
+ <ng-icon name="heroUser" size="20"></ng-icon>
108
+ </a>
109
+ <a href="#" class="hidden sm:inline-flex items-center gap-2 rounded-full bg-primary-500 px-5 py-2.5 text-sm font-medium text-white hover:bg-primary-600 shadow-md hover:shadow-lg transition-all">
110
+ <ng-icon name="heroRss" size="16"></ng-icon>
111
+ Subscribe
112
+ </a>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </header>
117
+
118
+ <!-- Main Content -->
119
+ <main class="min-h-screen">
120
+ <router-outlet></router-outlet>
121
+ </main>
122
+
123
+ <!-- Footer -->
124
+ <footer class="border-t border-gray-200 bg-white">
125
+ <div class="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
126
+ <div class="grid gap-12 md:grid-cols-4">
127
+ <!-- About -->
128
+ <div class="md:col-span-2">
129
+ <div class="flex items-center gap-3 mb-4">
130
+ <div class="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-primary-500 to-purple-600 text-white font-bold text-2xl shadow-lg">
131
+ 📝
132
+ </div>
133
+ <div>
134
+ <h3 class="text-xl font-bold text-gray-900">My Blog</h3>
135
+ <p class="text-xs text-gray-500">Insights & Tutorials</p>
136
+ </div>
137
+ </div>
138
+ <p class="text-gray-600 mb-6 max-w-md">
139
+ Discover insights, tutorials, and best practices on modern web development, Angular, TypeScript, and more. Join our community of developers.
140
+ </p>
141
+ <div class="flex items-center gap-3">
142
+ <a href="#" class="flex h-10 w-10 items-center justify-center rounded-full bg-gray-100 text-gray-600 hover:bg-primary-500 hover:text-white transition-colors">
143
+ <span class="text-sm font-bold">𝕏</span>
144
+ </a>
145
+ <a href="#" class="flex h-10 w-10 items-center justify-center rounded-full bg-gray-100 text-gray-600 hover:bg-primary-500 hover:text-white transition-colors">
146
+ <span class="text-sm font-bold">in</span>
147
+ </a>
148
+ <a href="#" class="flex h-10 w-10 items-center justify-center rounded-full bg-gray-100 text-gray-600 hover:bg-primary-500 hover:text-white transition-colors">
149
+ <ng-icon name="heroRss" size="18"></ng-icon>
150
+ </a>
151
+ </div>
152
+ </div>
153
+
154
+ <!-- Categories -->
155
+ <div>
156
+ <h3 class="mb-4 font-semibold text-gray-900">Categories</h3>
157
+ <ul class="space-y-3">
158
+ @for (category of blogService.categories(); track category) {
159
+ <li>
160
+ <a [routerLink]="['/category', category]" class="text-gray-600 hover:text-primary-600 transition-colors">
161
+ {{ category }}
162
+ </a>
163
+ </li>
164
+ }
165
+ </ul>
166
+ </div>
167
+
168
+ <!-- Quick Links -->
169
+ <div>
170
+ <h3 class="mb-4 font-semibold text-gray-900">Quick Links</h3>
171
+ <ul class="space-y-3">
172
+ <li><a routerLink="/" class="text-gray-600 hover:text-primary-600 transition-colors">All Posts</a></li>
173
+ <li><a routerLink="/auth/login" class="text-gray-600 hover:text-primary-600 transition-colors">Sign In</a></li>
174
+ <li><a routerLink="/auth/register" class="text-gray-600 hover:text-primary-600 transition-colors">Register</a></li>
175
+ </ul>
176
+ </div>
177
+ </div>
178
+
179
+ <!-- Newsletter -->
180
+ <div class="mt-12 rounded-2xl bg-linear-to-r from-primary-500 to-purple-600 p-8 text-white">
181
+ <div class="mx-auto max-w-3xl text-center">
182
+ <h3 class="text-2xl font-bold mb-2">Subscribe to our Newsletter</h3>
183
+ <p class="mb-6 text-primary-100">Get the latest posts and updates delivered directly to your inbox.</p>
184
+ <div class="flex flex-col sm:flex-row gap-3 max-w-md mx-auto">
185
+ <input
186
+ type="email"
187
+ placeholder="Enter your email"
188
+ class="flex-1 rounded-full px-6 py-3 text-gray-900 placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-white/50"
189
+ />
190
+ <button class="rounded-full bg-white px-8 py-3 font-semibold text-primary-600 hover:bg-gray-100 transition-colors whitespace-nowrap">
191
+ Subscribe
192
+ </button>
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ <!-- Bottom Bar -->
198
+ <div class="mt-12 flex flex-col sm:flex-row items-center justify-between gap-4 border-t border-gray-200 pt-8">
199
+ <p class="text-sm text-gray-600">
200
+ &copy; {{ currentYear }} My Blog. All rights reserved.
201
+ </p>
202
+ <div class="flex items-center gap-6">
203
+ <a href="#" class="text-sm text-gray-600 hover:text-primary-600 transition-colors">Privacy Policy</a>
204
+ <a href="#" class="text-sm text-gray-600 hover:text-primary-600 transition-colors">Terms of Service</a>
205
+ </div>
206
+ </div>
207
+ </div>
208
+ </footer>
209
+ </div>
210
+ \`,
211
+ })
212
+ export class App {
213
+ blogService = inject(BlogService);
214
+ currentYear = new Date().getFullYear();
215
+
216
+ constructor(private translate: TranslateService) {
217
+ this.translate.setDefaultLang('en');
218
+ this.translate.use('en');
219
+ }
220
+ }`;
221
+
222
+ await fs.writeFile(path.join(config.fullPath, "src/app/app.ts"), appComponent);
223
+ }
224
+
225
+ /**
226
+ * Clean up starter layout files (blog has its own header/footer in app.component)
227
+ */
228
+ async function cleanupStarterLayout(config) {
229
+ const foldersToRemove = [
230
+ "src/app/layout/header",
231
+ "src/app/layout/footer",
232
+ "src/app/features/home",
233
+ "src/app/features/about",
234
+ "src/app/features/contact",
235
+ ];
236
+
237
+ for (const folder of foldersToRemove) {
238
+ try {
239
+ await fs.remove(path.join(config.fullPath, folder));
240
+ } catch (e) {
241
+ // Ignore errors if files don't exist
242
+ }
243
+ }
244
+ }
245
+
246
+ module.exports = {
247
+ createRouting,
248
+ createAppComponent,
249
+ cleanupStarterLayout,
250
+ };
@@ -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 };