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,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
|
+
© {{ 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
|
+
};
|