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.
Files changed (47) hide show
  1. package/CHANGELOG.md +81 -344
  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 +53 -4055
  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,362 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Create API Service
6
+ */
7
+ async function createApiService(config) {
8
+ const apiService = `import { Injectable, inject } from '@angular/core';
9
+ import { HttpClient, HttpErrorResponse } from '@angular/common/http';
10
+ import { Observable, throwError } from 'rxjs';
11
+ import { catchError, retry } from 'rxjs/operators';
12
+
13
+ import { ApiResponse } from '@shared/models/api-response.interface';
14
+
15
+ @Injectable({
16
+ providedIn: 'root'
17
+ })
18
+ export class ApiService {
19
+ private http = inject(HttpClient);
20
+ private readonly baseUrl = 'https://api.example.com'; // Configure your API URL
21
+
22
+ /**
23
+ * Generic GET request
24
+ */
25
+ get<T>(endpoint: string): Observable<ApiResponse<T>> {
26
+ return this.http.get<ApiResponse<T>>(\`\${this.baseUrl}/\${endpoint}\`)
27
+ .pipe(
28
+ retry(1),
29
+ catchError(this.handleError)
30
+ );
31
+ }
32
+
33
+ /**
34
+ * Generic POST request
35
+ */
36
+ post<T, D = unknown>(endpoint: string, data: D): Observable<ApiResponse<T>> {
37
+ return this.http.post<ApiResponse<T>>(\`\${this.baseUrl}/\${endpoint}\`, data)
38
+ .pipe(
39
+ catchError(this.handleError)
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Generic PUT request
45
+ */
46
+ put<T, D = unknown>(endpoint: string, data: D): Observable<ApiResponse<T>> {
47
+ return this.http.put<ApiResponse<T>>(\`\${this.baseUrl}/\${endpoint}\`, data)
48
+ .pipe(
49
+ catchError(this.handleError)
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Generic DELETE request
55
+ */
56
+ delete<T>(endpoint: string): Observable<ApiResponse<T>> {
57
+ return this.http.delete<ApiResponse<T>>(\`\${this.baseUrl}/\${endpoint}\`)
58
+ .pipe(
59
+ catchError(this.handleError)
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Handle HTTP errors
65
+ */
66
+ private handleError(error: HttpErrorResponse) {
67
+ let errorMessage = 'An unknown error occurred';
68
+
69
+ if (error.error instanceof ErrorEvent) {
70
+ // Client-side error
71
+ errorMessage = error.error.message;
72
+ } else {
73
+ // Server-side error
74
+ errorMessage = error.error?.message || \`Server returned code: \${error.status}, error message is: \${error.message}\`;
75
+ }
76
+
77
+ console.error('API Error:', errorMessage);
78
+ return throwError(() => new Error(errorMessage));
79
+ }
80
+ }`;
81
+
82
+ await fs.writeFile(
83
+ path.join(config.fullPath, 'src/app/core/services/api.service.ts'),
84
+ apiService
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Create Auth Service
90
+ */
91
+ async function createAuthService(config) {
92
+ const authService = `import { Injectable, inject } from '@angular/core';
93
+ import { HttpClient } from '@angular/common/http';
94
+ import { Router } from '@angular/router';
95
+ import { BehaviorSubject, Observable, throwError } from 'rxjs';
96
+ import { catchError, tap } from 'rxjs/operators';
97
+ import { environment } from '@environments/environment';
98
+ import { StorageService } from './storage.service';
99
+
100
+ interface User {
101
+ id: string;
102
+ email: string;
103
+ firstName: string;
104
+ lastName: string;
105
+ }
106
+
107
+ interface AuthResponse {
108
+ token: string;
109
+ user: User;
110
+ refreshToken?: string;
111
+ }
112
+
113
+ interface LoginCredentials {
114
+ email: string;
115
+ password: string;
116
+ }
117
+
118
+ interface RegisterData {
119
+ email: string;
120
+ password: string;
121
+ firstName: string;
122
+ lastName: string;
123
+ }
124
+
125
+ @Injectable({
126
+ providedIn: 'root'
127
+ })
128
+ export class AuthService {
129
+ private http = inject(HttpClient);
130
+ private router = inject(Router);
131
+ private storage = inject(StorageService);
132
+
133
+ private readonly API_URL = environment.apiUrl;
134
+
135
+ // Reactive state
136
+ private _isAuthenticated$ = new BehaviorSubject<boolean>(this.hasValidToken());
137
+ private _currentUser$ = new BehaviorSubject<User | null>(this.getCurrentUserFromStorage());
138
+
139
+ // Public observables
140
+ public readonly isAuthenticated$ = this._isAuthenticated$.asObservable();
141
+ public readonly currentUser$ = this._currentUser$.asObservable();
142
+
143
+ constructor() {
144
+ // Initialize authentication state on service creation
145
+ this.checkAuthenticationStatus();
146
+ }
147
+
148
+ login(credentials: LoginCredentials): Observable<AuthResponse> {
149
+ // For demo purposes, simulate login
150
+ if (credentials.email === 'demo@example.com' && credentials.password === 'password') {
151
+ const mockResponse: AuthResponse = {
152
+ token: 'mock_jwt_token_12345',
153
+ user: {
154
+ id: '1',
155
+ email: 'demo@example.com',
156
+ firstName: 'Demo',
157
+ lastName: 'User'
158
+ }
159
+ };
160
+
161
+ // Store auth data
162
+ this.storage.setItem(environment.auth.tokenKey, mockResponse.token);
163
+ this.storage.setItem('current_user', JSON.stringify(mockResponse.user));
164
+
165
+ // Update reactive state
166
+ this._isAuthenticated$.next(true);
167
+ this._currentUser$.next(mockResponse.user);
168
+
169
+ return new Observable(subscriber => {
170
+ setTimeout(() => {
171
+ subscriber.next(mockResponse);
172
+ subscriber.complete();
173
+ }, 1000);
174
+ });
175
+ }
176
+
177
+ // Real API call would be:
178
+ return this.http.post<AuthResponse>(\`\${this.API_URL}/auth/login\`, credentials)
179
+ .pipe(
180
+ tap(response => {
181
+ // Store tokens
182
+ this.storage.setItem(environment.auth.tokenKey, response.token);
183
+ this.storage.setItem('current_user', JSON.stringify(response.user));
184
+ if (response.refreshToken) {
185
+ this.storage.setItem(environment.auth.refreshTokenKey, response.refreshToken);
186
+ }
187
+
188
+ // Update reactive state
189
+ this._isAuthenticated$.next(true);
190
+ this._currentUser$.next(response.user);
191
+ }),
192
+ catchError(error => throwError(() => error))
193
+ );
194
+ }
195
+
196
+ logout(): void {
197
+ this.storage.removeItem(environment.auth.tokenKey);
198
+ this.storage.removeItem(environment.auth.refreshTokenKey);
199
+ this.storage.removeItem('current_user');
200
+
201
+ // Update reactive state
202
+ this._isAuthenticated$.next(false);
203
+ this._currentUser$.next(null);
204
+
205
+ this.router.navigate(['/auth/login']);
206
+ }
207
+
208
+ register(userData: RegisterData): Observable<AuthResponse> {
209
+ return this.http.post<AuthResponse>(\`\${this.API_URL}/auth/register\`, userData)
210
+ .pipe(
211
+ tap(response => {
212
+ // Store auth data after successful registration
213
+ this.storage.setItem(environment.auth.tokenKey, response.token);
214
+ this.storage.setItem('current_user', JSON.stringify(response.user));
215
+
216
+ // Update reactive state
217
+ this._isAuthenticated$.next(true);
218
+ this._currentUser$.next(response.user);
219
+ }),
220
+ catchError(error => throwError(() => error))
221
+ );
222
+ }
223
+
224
+ // Synchronous authentication check
225
+ isAuthenticated(): boolean {
226
+ return this.hasValidToken();
227
+ }
228
+
229
+ private hasValidToken(): boolean {
230
+ const token = this.storage.getItem(environment.auth.tokenKey);
231
+ return !!token && !this.isTokenExpired(token);
232
+ }
233
+
234
+ private isTokenExpired(token: string): boolean {
235
+ // For demo token, always return false
236
+ if (token === 'mock_jwt_token_12345') {
237
+ return false;
238
+ }
239
+
240
+ try {
241
+ const payload = JSON.parse(atob(token.split('.')[1]));
242
+ const now = Date.now() / 1000;
243
+ return payload.exp < now;
244
+ } catch {
245
+ return true;
246
+ }
247
+ }
248
+
249
+ private getCurrentUserFromStorage(): User | null {
250
+ try {
251
+ const userStr = this.storage.getItem('current_user');
252
+ return userStr ? JSON.parse(userStr) : null;
253
+ } catch {
254
+ return null;
255
+ }
256
+ }
257
+
258
+ private checkAuthenticationStatus(): void {
259
+ const isAuth = this.hasValidToken();
260
+ const user = this.getCurrentUserFromStorage();
261
+
262
+ this._isAuthenticated$.next(isAuth);
263
+ this._currentUser$.next(user);
264
+ }
265
+
266
+ getToken(): string | null {
267
+ return this.storage.getItem(environment.auth.tokenKey);
268
+ }
269
+
270
+ // Get current user synchronously
271
+ getCurrentUser(): User | null {
272
+ return this._currentUser$.getValue();
273
+ }
274
+ }`;
275
+
276
+ await fs.writeFile(
277
+ path.join(config.fullPath, 'src/app/core/services/auth.service.ts'),
278
+ authService
279
+ );
280
+ }
281
+
282
+ /**
283
+ * Create Storage Service
284
+ */
285
+ async function createStorageService(config) {
286
+ const storageService = `import { Injectable, inject, PLATFORM_ID } from '@angular/core';
287
+ import { isPlatformBrowser } from '@angular/common';
288
+
289
+ @Injectable({
290
+ providedIn: 'root'
291
+ })
292
+ export class StorageService {
293
+ private platformId = inject(PLATFORM_ID);
294
+ private isBrowser = isPlatformBrowser(this.platformId);
295
+
296
+ setItem(key: string, value: string): void {
297
+ if (!this.isBrowser) {
298
+ return;
299
+ }
300
+ try {
301
+ localStorage.setItem(key, value);
302
+ } catch (error) {
303
+ console.error('Error storing to localStorage:', error);
304
+ }
305
+ }
306
+
307
+ getItem(key: string): string | null {
308
+ if (!this.isBrowser) {
309
+ return null;
310
+ }
311
+ try {
312
+ return localStorage.getItem(key);
313
+ } catch (error) {
314
+ console.error('Error retrieving from localStorage:', error);
315
+ return null;
316
+ }
317
+ }
318
+
319
+ removeItem(key: string): void {
320
+ if (!this.isBrowser) {
321
+ return;
322
+ }
323
+ try {
324
+ localStorage.removeItem(key);
325
+ } catch (error) {
326
+ console.error('Error removing from localStorage:', error);
327
+ }
328
+ }
329
+
330
+ clear(): void {
331
+ if (!this.isBrowser) {
332
+ return;
333
+ }
334
+ try {
335
+ localStorage.clear();
336
+ } catch (error) {
337
+ console.error('Error clearing localStorage:', error);
338
+ }
339
+ }
340
+ }`;
341
+
342
+ await fs.writeFile(
343
+ path.join(config.fullPath, 'src/app/core/services/storage.service.ts'),
344
+ storageService
345
+ );
346
+ }
347
+
348
+ /**
349
+ * Create all core services
350
+ */
351
+ async function createCoreServices(config) {
352
+ await createApiService(config);
353
+ await createAuthService(config);
354
+ await createStorageService(config);
355
+ }
356
+
357
+ module.exports = {
358
+ createApiService,
359
+ createAuthService,
360
+ createStorageService,
361
+ createCoreServices,
362
+ };
@@ -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
+ };