create-ng-tailwind 3.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -350
- 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 +52 -4060
- 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
|
@@ -1,81 +1,47 @@
|
|
|
1
1
|
const fs = require('fs-extra');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const
|
|
4
|
-
createContactComponent,
|
|
5
|
-
createRouting,
|
|
6
|
-
createAppConfig,
|
|
7
|
-
createAppComponent,
|
|
8
|
-
createStyles,
|
|
9
|
-
} = require('./features');
|
|
3
|
+
const base = require('../base');
|
|
10
4
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
createToastService,
|
|
17
|
-
createToastComponent,
|
|
18
|
-
createLoadingService,
|
|
19
|
-
createCacheService,
|
|
20
|
-
} = require('./advanced-features');
|
|
21
|
-
|
|
22
|
-
const {
|
|
23
|
-
createModalService,
|
|
24
|
-
createModalComponent,
|
|
25
|
-
createTruncatePipe,
|
|
26
|
-
createClickOutsideDirective,
|
|
27
|
-
createTooltipDirective,
|
|
28
|
-
} = require('./ui-features');
|
|
29
|
-
|
|
30
|
-
const { createSEOService, createStructuredDataUtil, createRobotsTxt } = require('./seo-features');
|
|
31
|
-
|
|
32
|
-
const {
|
|
33
|
-
createDefaultOGImage,
|
|
34
|
-
createFavicon,
|
|
35
|
-
createAppleTouchIcon,
|
|
36
|
-
createAndroidIcon,
|
|
37
|
-
createAndroidIconLarge,
|
|
38
|
-
createLogo,
|
|
39
|
-
} = require('./seo-assets');
|
|
5
|
+
// Import modular components
|
|
6
|
+
const { createLayout, createAuthLayout } = require('./layout');
|
|
7
|
+
const { createPages } = require('./pages');
|
|
8
|
+
const { createI18n, installI18nPackages } = require('./i18n');
|
|
9
|
+
const { createRouting, createAppConfig, createAppComponent, createStyles } = require('./app');
|
|
40
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Starter Template
|
|
13
|
+
* Extends base template with:
|
|
14
|
+
* - Professional layout (header/footer)
|
|
15
|
+
* - Authentication UI (login, register, forgot password)
|
|
16
|
+
* - Example pages (home, about, contact)
|
|
17
|
+
* - i18n support (English & Arabic with RTL)
|
|
18
|
+
*
|
|
19
|
+
* File Structure:
|
|
20
|
+
* - layout.js: Header, Footer, AuthLayout (Login, Register, Forgot Password)
|
|
21
|
+
* - pages.js: Home, About components
|
|
22
|
+
* - i18n.js: TranslationService and translation files (EN/AR)
|
|
23
|
+
* - features.js: Contact, Routes, AppConfig, AppComponent, Styles
|
|
24
|
+
*/
|
|
41
25
|
const starter = {
|
|
42
26
|
info: {
|
|
43
27
|
name: 'Starter',
|
|
44
28
|
description: 'Professional Angular foundation with essential components and best practices',
|
|
45
29
|
features: [
|
|
46
|
-
|
|
30
|
+
...base.info.features,
|
|
47
31
|
'Complete routing setup with lazy loading',
|
|
48
|
-
'HTTP Interceptors (Auth, Error, Loading, Caching)',
|
|
49
32
|
'i18n translation support (English & Arabic) with RTL',
|
|
50
33
|
'Language switcher in header (desktop & mobile)',
|
|
51
|
-
'Comprehensive SEO service (meta tags, Open Graph, Twitter Cards)',
|
|
52
|
-
'Structured data (JSON-LD) for rich search results',
|
|
53
|
-
'Multi-language SEO with hreflang tags',
|
|
54
|
-
'robots.txt and favicon set included',
|
|
55
|
-
'Toast/Notification system with auto-dismiss',
|
|
56
|
-
'Modal/Dialog system with confirm & alert',
|
|
57
|
-
'Essential UI components (Button, Card, Spinner, Toast, Modal)',
|
|
58
|
-
'Truncate pipe for text truncation',
|
|
59
|
-
'Useful Directives (ClickOutside, Tooltip)',
|
|
60
34
|
'Professional layout with header/footer',
|
|
61
35
|
'Authentication UI (Login, Register, Forgot Password)',
|
|
62
36
|
'3 example pages (Home, About, Contact)',
|
|
63
37
|
'Reactive forms with validation',
|
|
64
|
-
'API, Auth, Storage, Loading, Cache, SEO services',
|
|
65
|
-
'TypeScript interfaces and models',
|
|
66
|
-
'Responsive Tailwind design system',
|
|
67
|
-
'ESLint + Prettier + simple-git-hooks (pre-commit)',
|
|
68
|
-
'PWA-ready with service worker configuration',
|
|
69
|
-
'Icons library (@ng-icons/heroicons)',
|
|
70
38
|
],
|
|
71
39
|
},
|
|
72
40
|
|
|
73
41
|
async apply(config, spinner) {
|
|
74
|
-
// Store spinner reference for progress updates
|
|
75
42
|
this.spinner = spinner;
|
|
76
43
|
const chalk = require('chalk');
|
|
77
44
|
|
|
78
|
-
// Helper function to show completed step with green checkmark
|
|
79
45
|
const completeStep = (message) => {
|
|
80
46
|
if (spinner) {
|
|
81
47
|
spinner.stop();
|
|
@@ -84,40 +50,40 @@ const starter = {
|
|
|
84
50
|
}
|
|
85
51
|
};
|
|
86
52
|
|
|
87
|
-
//
|
|
88
|
-
if (spinner) spinner.update('Setting up
|
|
89
|
-
await
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
await this.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
await
|
|
97
|
-
await
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
await
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
await
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
await
|
|
53
|
+
// Step 1: Apply base template (infrastructure: services, guards, interceptors, etc.)
|
|
54
|
+
if (spinner) spinner.update('Setting up base infrastructure...');
|
|
55
|
+
await base.apply(config, null);
|
|
56
|
+
|
|
57
|
+
// Step 2: Create UI-specific directories
|
|
58
|
+
if (spinner) spinner.update('Setting up starter UI...');
|
|
59
|
+
await this.createUIDirectories(config);
|
|
60
|
+
|
|
61
|
+
// Step 3: Add i18n translations
|
|
62
|
+
await createI18n(config);
|
|
63
|
+
await installI18nPackages(config);
|
|
64
|
+
|
|
65
|
+
// Step 4: Create layout components (header, footer)
|
|
66
|
+
await createLayout(config);
|
|
67
|
+
|
|
68
|
+
// Step 5: Create auth layout (login, register, forgot password)
|
|
69
|
+
await createAuthLayout(config);
|
|
70
|
+
|
|
71
|
+
// Step 6: Create feature pages (home, about, contact)
|
|
72
|
+
await createPages(config);
|
|
73
|
+
|
|
74
|
+
// Step 7: Create app routing, config, component, styles
|
|
75
|
+
await createRouting(config);
|
|
76
|
+
await createAppConfig(config);
|
|
77
|
+
await createAppComponent(config);
|
|
78
|
+
await createStyles(config);
|
|
110
79
|
await this.createAssets(config);
|
|
111
|
-
await this.createVSCodeSettings(config);
|
|
112
|
-
await this.updateTsConfig(config);
|
|
113
|
-
await this.setupLinting(config);
|
|
114
|
-
await this.setupPWA(config);
|
|
115
|
-
await this.formatCode(config);
|
|
116
80
|
|
|
117
|
-
//
|
|
81
|
+
// Step 8: Format code
|
|
82
|
+
await base.formatCode(config);
|
|
83
|
+
|
|
118
84
|
if (spinner) spinner.stop();
|
|
119
85
|
|
|
120
|
-
// Show
|
|
86
|
+
// Show summary
|
|
121
87
|
console.log('');
|
|
122
88
|
completeStep('Angular 20+ project created');
|
|
123
89
|
completeStep('Tailwind CSS v4 configured');
|
|
@@ -133,22 +99,9 @@ const starter = {
|
|
|
133
99
|
console.log('');
|
|
134
100
|
},
|
|
135
101
|
|
|
136
|
-
async
|
|
102
|
+
async createUIDirectories(config) {
|
|
137
103
|
const directories = [
|
|
138
|
-
'src/environments',
|
|
139
|
-
'src/app/core/services',
|
|
140
|
-
'src/app/core/guards',
|
|
141
104
|
'src/app/core/i18n',
|
|
142
|
-
'src/app/core/interceptors',
|
|
143
|
-
'src/app/core/utils',
|
|
144
|
-
'src/app/shared/components/button',
|
|
145
|
-
'src/app/shared/components/card',
|
|
146
|
-
'src/app/shared/components/loading-spinner',
|
|
147
|
-
'src/app/shared/components/toast',
|
|
148
|
-
'src/app/shared/components/modal',
|
|
149
|
-
'src/app/shared/pipes',
|
|
150
|
-
'src/app/shared/directives',
|
|
151
|
-
'src/app/shared/models',
|
|
152
105
|
'src/app/features/home',
|
|
153
106
|
'src/app/features/about',
|
|
154
107
|
'src/app/features/contact',
|
|
@@ -158,9 +111,7 @@ const starter = {
|
|
|
158
111
|
'src/app/layout/header',
|
|
159
112
|
'src/app/layout/footer',
|
|
160
113
|
'src/app/layout/auth',
|
|
161
|
-
'public/assets/images',
|
|
162
114
|
'public/assets/i18n',
|
|
163
|
-
'public/assets/icons',
|
|
164
115
|
];
|
|
165
116
|
|
|
166
117
|
for (const dir of directories) {
|
|
@@ -168,2565 +119,7 @@ const starter = {
|
|
|
168
119
|
}
|
|
169
120
|
},
|
|
170
121
|
|
|
171
|
-
async createEnvironments(config) {
|
|
172
|
-
// Development environment
|
|
173
|
-
const devEnvironment = `export const environment = {
|
|
174
|
-
production: false,
|
|
175
|
-
apiUrl: 'http://localhost:3000/api',
|
|
176
|
-
appName: '${config.projectName}',
|
|
177
|
-
version: '1.0.0',
|
|
178
|
-
enableDevTools: true,
|
|
179
|
-
logLevel: 'debug' as 'debug' | 'info' | 'warn' | 'error',
|
|
180
|
-
features: {
|
|
181
|
-
enableAnalytics: false,
|
|
182
|
-
enableLogging: true,
|
|
183
|
-
enableMocking: true
|
|
184
|
-
},
|
|
185
|
-
auth: {
|
|
186
|
-
tokenKey: 'auth_token',
|
|
187
|
-
refreshTokenKey: 'refresh_token',
|
|
188
|
-
tokenExpirationBuffer: 300000 // 5 minutes
|
|
189
|
-
},
|
|
190
|
-
api: {
|
|
191
|
-
timeout: 30000,
|
|
192
|
-
retryAttempts: 3
|
|
193
|
-
}
|
|
194
|
-
};`;
|
|
195
|
-
|
|
196
|
-
await fs.writeFile(
|
|
197
|
-
path.join(config.fullPath, 'src/environments/environment.ts'),
|
|
198
|
-
devEnvironment
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
// Production environment
|
|
202
|
-
const prodEnvironment = `export const environment = {
|
|
203
|
-
production: true,
|
|
204
|
-
apiUrl: 'https://api.${config.projectName}.com/api',
|
|
205
|
-
appName: '${config.projectName}',
|
|
206
|
-
version: '1.0.0',
|
|
207
|
-
enableDevTools: false,
|
|
208
|
-
logLevel: 'error' as 'debug' | 'info' | 'warn' | 'error',
|
|
209
|
-
features: {
|
|
210
|
-
enableAnalytics: true,
|
|
211
|
-
enableLogging: false,
|
|
212
|
-
enableMocking: false
|
|
213
|
-
},
|
|
214
|
-
auth: {
|
|
215
|
-
tokenKey: 'auth_token',
|
|
216
|
-
refreshTokenKey: 'refresh_token',
|
|
217
|
-
tokenExpirationBuffer: 300000 // 5 minutes
|
|
218
|
-
},
|
|
219
|
-
api: {
|
|
220
|
-
timeout: 30000,
|
|
221
|
-
retryAttempts: 1
|
|
222
|
-
}
|
|
223
|
-
};`;
|
|
224
|
-
|
|
225
|
-
await fs.writeFile(
|
|
226
|
-
path.join(config.fullPath, 'src/environments/environment.prod.ts'),
|
|
227
|
-
prodEnvironment
|
|
228
|
-
);
|
|
229
|
-
},
|
|
230
|
-
|
|
231
|
-
async createCoreServices(config) {
|
|
232
|
-
// API Service
|
|
233
|
-
const apiService = `import { Injectable, inject } from '@angular/core';
|
|
234
|
-
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
|
235
|
-
import { Observable, throwError } from 'rxjs';
|
|
236
|
-
import { catchError, retry } from 'rxjs/operators';
|
|
237
|
-
|
|
238
|
-
import { ApiResponse } from '@shared/models/api-response.interface';
|
|
239
|
-
|
|
240
|
-
@Injectable({
|
|
241
|
-
providedIn: 'root'
|
|
242
|
-
})
|
|
243
|
-
export class ApiService {
|
|
244
|
-
private http = inject(HttpClient);
|
|
245
|
-
private readonly baseUrl = 'https://api.example.com'; // Configure your API URL
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Generic GET request
|
|
249
|
-
*/
|
|
250
|
-
get<T>(endpoint: string): Observable<ApiResponse<T>> {
|
|
251
|
-
return this.http.get<ApiResponse<T>>(\`\${this.baseUrl}/\${endpoint}\`)
|
|
252
|
-
.pipe(
|
|
253
|
-
retry(1),
|
|
254
|
-
catchError(this.handleError)
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Generic POST request
|
|
260
|
-
*/
|
|
261
|
-
post<T, D = unknown>(endpoint: string, data: D): Observable<ApiResponse<T>> {
|
|
262
|
-
return this.http.post<ApiResponse<T>>(\`\${this.baseUrl}/\${endpoint}\`, data)
|
|
263
|
-
.pipe(
|
|
264
|
-
catchError(this.handleError)
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Generic PUT request
|
|
270
|
-
*/
|
|
271
|
-
put<T, D = unknown>(endpoint: string, data: D): Observable<ApiResponse<T>> {
|
|
272
|
-
return this.http.put<ApiResponse<T>>(\`\${this.baseUrl}/\${endpoint}\`, data)
|
|
273
|
-
.pipe(
|
|
274
|
-
catchError(this.handleError)
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Generic DELETE request
|
|
280
|
-
*/
|
|
281
|
-
delete<T>(endpoint: string): Observable<ApiResponse<T>> {
|
|
282
|
-
return this.http.delete<ApiResponse<T>>(\`\${this.baseUrl}/\${endpoint}\`)
|
|
283
|
-
.pipe(
|
|
284
|
-
catchError(this.handleError)
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Handle HTTP errors
|
|
290
|
-
*/
|
|
291
|
-
private handleError(error: HttpErrorResponse) {
|
|
292
|
-
let errorMessage = 'An unknown error occurred';
|
|
293
|
-
|
|
294
|
-
if (error.error instanceof ErrorEvent) {
|
|
295
|
-
// Client-side error
|
|
296
|
-
errorMessage = error.error.message;
|
|
297
|
-
} else {
|
|
298
|
-
// Server-side error
|
|
299
|
-
errorMessage = error.error?.message || \`Server returned code: \${error.status}, error message is: \${error.message}\`;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
console.error('API Error:', errorMessage);
|
|
303
|
-
return throwError(() => new Error(errorMessage));
|
|
304
|
-
}
|
|
305
|
-
}`;
|
|
306
|
-
|
|
307
|
-
await fs.writeFile(
|
|
308
|
-
path.join(config.fullPath, 'src/app/core/services/api.service.ts'),
|
|
309
|
-
apiService
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
// Auth Service with Reactive State
|
|
313
|
-
const authService = `import { Injectable, inject } from '@angular/core';
|
|
314
|
-
import { HttpClient } from '@angular/common/http';
|
|
315
|
-
import { Router } from '@angular/router';
|
|
316
|
-
import { BehaviorSubject, Observable, throwError } from 'rxjs';
|
|
317
|
-
import { catchError, tap } from 'rxjs/operators';
|
|
318
|
-
import { environment } from '@environments/environment';
|
|
319
|
-
import { StorageService } from './storage.service';
|
|
320
|
-
|
|
321
|
-
interface User {
|
|
322
|
-
id: string;
|
|
323
|
-
email: string;
|
|
324
|
-
firstName: string;
|
|
325
|
-
lastName: string;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
interface AuthResponse {
|
|
329
|
-
token: string;
|
|
330
|
-
user: User;
|
|
331
|
-
refreshToken?: string;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
interface LoginCredentials {
|
|
335
|
-
email: string;
|
|
336
|
-
password: string;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
interface RegisterData {
|
|
340
|
-
email: string;
|
|
341
|
-
password: string;
|
|
342
|
-
firstName: string;
|
|
343
|
-
lastName: string;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
@Injectable({
|
|
347
|
-
providedIn: 'root'
|
|
348
|
-
})
|
|
349
|
-
export class AuthService {
|
|
350
|
-
private http = inject(HttpClient);
|
|
351
|
-
private router = inject(Router);
|
|
352
|
-
private storage = inject(StorageService);
|
|
353
|
-
|
|
354
|
-
private readonly API_URL = environment.apiUrl;
|
|
355
|
-
|
|
356
|
-
// Reactive state
|
|
357
|
-
private _isAuthenticated$ = new BehaviorSubject<boolean>(this.hasValidToken());
|
|
358
|
-
private _currentUser$ = new BehaviorSubject<User | null>(this.getCurrentUserFromStorage());
|
|
359
|
-
|
|
360
|
-
// Public observables
|
|
361
|
-
public readonly isAuthenticated$ = this._isAuthenticated$.asObservable();
|
|
362
|
-
public readonly currentUser$ = this._currentUser$.asObservable();
|
|
363
|
-
|
|
364
|
-
constructor() {
|
|
365
|
-
// Initialize authentication state on service creation
|
|
366
|
-
this.checkAuthenticationStatus();
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
login(credentials: LoginCredentials): Observable<AuthResponse> {
|
|
370
|
-
// For demo purposes, simulate login
|
|
371
|
-
if (credentials.email === 'demo@example.com' && credentials.password === 'password') {
|
|
372
|
-
const mockResponse: AuthResponse = {
|
|
373
|
-
token: 'mock_jwt_token_12345',
|
|
374
|
-
user: {
|
|
375
|
-
id: '1',
|
|
376
|
-
email: 'demo@example.com',
|
|
377
|
-
firstName: 'Demo',
|
|
378
|
-
lastName: 'User'
|
|
379
|
-
}
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
// Store auth data
|
|
383
|
-
this.storage.setItem(environment.auth.tokenKey, mockResponse.token);
|
|
384
|
-
this.storage.setItem('current_user', JSON.stringify(mockResponse.user));
|
|
385
|
-
|
|
386
|
-
// Update reactive state
|
|
387
|
-
this._isAuthenticated$.next(true);
|
|
388
|
-
this._currentUser$.next(mockResponse.user);
|
|
389
|
-
|
|
390
|
-
return new Observable(subscriber => {
|
|
391
|
-
setTimeout(() => {
|
|
392
|
-
subscriber.next(mockResponse);
|
|
393
|
-
subscriber.complete();
|
|
394
|
-
}, 1000);
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Real API call would be:
|
|
399
|
-
return this.http.post<AuthResponse>(\`\${this.API_URL}/auth/login\`, credentials)
|
|
400
|
-
.pipe(
|
|
401
|
-
tap(response => {
|
|
402
|
-
// Store tokens
|
|
403
|
-
this.storage.setItem(environment.auth.tokenKey, response.token);
|
|
404
|
-
this.storage.setItem('current_user', JSON.stringify(response.user));
|
|
405
|
-
if (response.refreshToken) {
|
|
406
|
-
this.storage.setItem(environment.auth.refreshTokenKey, response.refreshToken);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Update reactive state
|
|
410
|
-
this._isAuthenticated$.next(true);
|
|
411
|
-
this._currentUser$.next(response.user);
|
|
412
|
-
}),
|
|
413
|
-
catchError(error => throwError(() => error))
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
logout(): void {
|
|
418
|
-
this.storage.removeItem(environment.auth.tokenKey);
|
|
419
|
-
this.storage.removeItem(environment.auth.refreshTokenKey);
|
|
420
|
-
this.storage.removeItem('current_user');
|
|
421
|
-
|
|
422
|
-
// Update reactive state
|
|
423
|
-
this._isAuthenticated$.next(false);
|
|
424
|
-
this._currentUser$.next(null);
|
|
425
|
-
|
|
426
|
-
this.router.navigate(['/auth/login']);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
register(userData: RegisterData): Observable<AuthResponse> {
|
|
430
|
-
return this.http.post<AuthResponse>(\`\${this.API_URL}/auth/register\`, userData)
|
|
431
|
-
.pipe(
|
|
432
|
-
tap(response => {
|
|
433
|
-
// Store auth data after successful registration
|
|
434
|
-
this.storage.setItem(environment.auth.tokenKey, response.token);
|
|
435
|
-
this.storage.setItem('current_user', JSON.stringify(response.user));
|
|
436
|
-
|
|
437
|
-
// Update reactive state
|
|
438
|
-
this._isAuthenticated$.next(true);
|
|
439
|
-
this._currentUser$.next(response.user);
|
|
440
|
-
}),
|
|
441
|
-
catchError(error => throwError(() => error))
|
|
442
|
-
);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Synchronous authentication check
|
|
446
|
-
isAuthenticated(): boolean {
|
|
447
|
-
return this.hasValidToken();
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
private hasValidToken(): boolean {
|
|
451
|
-
const token = this.storage.getItem(environment.auth.tokenKey);
|
|
452
|
-
return !!token && !this.isTokenExpired(token);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
private isTokenExpired(token: string): boolean {
|
|
456
|
-
// For demo token, always return false
|
|
457
|
-
if (token === 'mock_jwt_token_12345') {
|
|
458
|
-
return false;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
try {
|
|
462
|
-
const payload = JSON.parse(atob(token.split('.')[1]));
|
|
463
|
-
const now = Date.now() / 1000;
|
|
464
|
-
return payload.exp < now;
|
|
465
|
-
} catch {
|
|
466
|
-
return true;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
private getCurrentUserFromStorage(): User | null {
|
|
471
|
-
try {
|
|
472
|
-
const userStr = this.storage.getItem('current_user');
|
|
473
|
-
return userStr ? JSON.parse(userStr) : null;
|
|
474
|
-
} catch {
|
|
475
|
-
return null;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
private checkAuthenticationStatus(): void {
|
|
480
|
-
const isAuth = this.hasValidToken();
|
|
481
|
-
const user = this.getCurrentUserFromStorage();
|
|
482
|
-
|
|
483
|
-
this._isAuthenticated$.next(isAuth);
|
|
484
|
-
this._currentUser$.next(user);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
getToken(): string | null {
|
|
488
|
-
return this.storage.getItem(environment.auth.tokenKey);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Get current user synchronously
|
|
492
|
-
getCurrentUser(): User | null {
|
|
493
|
-
return this._currentUser$.getValue();
|
|
494
|
-
}
|
|
495
|
-
}`;
|
|
496
|
-
|
|
497
|
-
await fs.writeFile(
|
|
498
|
-
path.join(config.fullPath, 'src/app/core/services/auth.service.ts'),
|
|
499
|
-
authService
|
|
500
|
-
);
|
|
501
|
-
|
|
502
|
-
// Storage Service
|
|
503
|
-
const storageService = `import { Injectable, inject, PLATFORM_ID } from '@angular/core';
|
|
504
|
-
import { isPlatformBrowser } from '@angular/common';
|
|
505
|
-
|
|
506
|
-
@Injectable({
|
|
507
|
-
providedIn: 'root'
|
|
508
|
-
})
|
|
509
|
-
export class StorageService {
|
|
510
|
-
private platformId = inject(PLATFORM_ID);
|
|
511
|
-
private isBrowser = isPlatformBrowser(this.platformId);
|
|
512
|
-
|
|
513
|
-
setItem(key: string, value: string): void {
|
|
514
|
-
if (!this.isBrowser) {
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
try {
|
|
518
|
-
localStorage.setItem(key, value);
|
|
519
|
-
} catch (error) {
|
|
520
|
-
console.error('Error storing to localStorage:', error);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
getItem(key: string): string | null {
|
|
525
|
-
if (!this.isBrowser) {
|
|
526
|
-
return null;
|
|
527
|
-
}
|
|
528
|
-
try {
|
|
529
|
-
return localStorage.getItem(key);
|
|
530
|
-
} catch (error) {
|
|
531
|
-
console.error('Error retrieving from localStorage:', error);
|
|
532
|
-
return null;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
removeItem(key: string): void {
|
|
537
|
-
if (!this.isBrowser) {
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
try {
|
|
541
|
-
localStorage.removeItem(key);
|
|
542
|
-
} catch (error) {
|
|
543
|
-
console.error('Error removing from localStorage:', error);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
clear(): void {
|
|
548
|
-
if (!this.isBrowser) {
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
try {
|
|
552
|
-
localStorage.clear();
|
|
553
|
-
} catch (error) {
|
|
554
|
-
console.error('Error clearing localStorage:', error);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}`;
|
|
558
|
-
|
|
559
|
-
await fs.writeFile(
|
|
560
|
-
path.join(config.fullPath, 'src/app/core/services/storage.service.ts'),
|
|
561
|
-
storageService
|
|
562
|
-
);
|
|
563
|
-
},
|
|
564
|
-
|
|
565
|
-
async createSEOFeatures(config) {
|
|
566
|
-
// Create SEO Service
|
|
567
|
-
const seoService = createSEOService();
|
|
568
|
-
await fs.writeFile(
|
|
569
|
-
path.join(config.fullPath, 'src/app/core/services/seo.service.ts'),
|
|
570
|
-
seoService
|
|
571
|
-
);
|
|
572
|
-
|
|
573
|
-
// Create Structured Data Utility
|
|
574
|
-
const structuredDataUtil = createStructuredDataUtil();
|
|
575
|
-
await fs.writeFile(
|
|
576
|
-
path.join(config.fullPath, 'src/app/core/utils/structured-data.ts'),
|
|
577
|
-
structuredDataUtil
|
|
578
|
-
);
|
|
579
|
-
|
|
580
|
-
// Create robots.txt in public folder
|
|
581
|
-
const robotsTxt = createRobotsTxt();
|
|
582
|
-
await fs.writeFile(path.join(config.fullPath, 'public/robots.txt'), robotsTxt);
|
|
583
|
-
|
|
584
|
-
// Create default OG image (SVG as placeholder)
|
|
585
|
-
const ogImage = createDefaultOGImage(config.projectName);
|
|
586
|
-
await fs.writeFile(path.join(config.fullPath, 'public/assets/images/og-default.svg'), ogImage);
|
|
587
|
-
|
|
588
|
-
// Create favicon
|
|
589
|
-
const favicon = createFavicon(config.projectName);
|
|
590
|
-
await fs.writeFile(path.join(config.fullPath, 'public/favicon.svg'), favicon);
|
|
591
|
-
|
|
592
|
-
// Create Apple Touch Icon (SVG as placeholder)
|
|
593
|
-
const appleTouchIcon = createAppleTouchIcon(config.projectName);
|
|
594
|
-
await fs.writeFile(
|
|
595
|
-
path.join(config.fullPath, 'public/assets/icons/apple-touch-icon.svg'),
|
|
596
|
-
appleTouchIcon
|
|
597
|
-
);
|
|
598
|
-
|
|
599
|
-
// Create Android Chrome icons (SVG as placeholder)
|
|
600
|
-
const androidIcon = createAndroidIcon(config.projectName);
|
|
601
|
-
await fs.writeFile(
|
|
602
|
-
path.join(config.fullPath, 'public/assets/icons/android-chrome-192x192.svg'),
|
|
603
|
-
androidIcon
|
|
604
|
-
);
|
|
605
|
-
|
|
606
|
-
const androidIconLarge = createAndroidIconLarge(config.projectName);
|
|
607
|
-
await fs.writeFile(
|
|
608
|
-
path.join(config.fullPath, 'public/assets/icons/android-chrome-512x512.svg'),
|
|
609
|
-
androidIconLarge
|
|
610
|
-
);
|
|
611
|
-
|
|
612
|
-
// Create logo
|
|
613
|
-
const logo = createLogo(config.projectName);
|
|
614
|
-
await fs.writeFile(path.join(config.fullPath, 'public/assets/images/logo.svg'), logo);
|
|
615
|
-
},
|
|
616
|
-
|
|
617
|
-
async createHttpInterceptors(config) {
|
|
618
|
-
// Create all HTTP interceptors
|
|
619
|
-
await createAuthInterceptor(config);
|
|
620
|
-
await createErrorInterceptor(config);
|
|
621
|
-
await createLoadingInterceptor(config);
|
|
622
|
-
await createCachingInterceptor(config);
|
|
623
|
-
|
|
624
|
-
// Create supporting services
|
|
625
|
-
await createLoadingService(config);
|
|
626
|
-
await createCacheService(config);
|
|
627
|
-
},
|
|
628
|
-
|
|
629
|
-
async createToastSystem(config) {
|
|
630
|
-
await createToastService(config);
|
|
631
|
-
await createToastComponent(config);
|
|
632
|
-
},
|
|
633
|
-
|
|
634
|
-
async createModalSystem(config) {
|
|
635
|
-
await createModalService(config);
|
|
636
|
-
await createModalComponent(config);
|
|
637
|
-
},
|
|
638
|
-
|
|
639
|
-
async createAdditionalPipes(config) {
|
|
640
|
-
await createTruncatePipe(config);
|
|
641
|
-
},
|
|
642
|
-
|
|
643
|
-
async createDirectives(config) {
|
|
644
|
-
await createClickOutsideDirective(config);
|
|
645
|
-
await createTooltipDirective(config);
|
|
646
|
-
},
|
|
647
|
-
|
|
648
|
-
async createI18n(config) {
|
|
649
|
-
// Create Translation Service
|
|
650
|
-
const translationService = `import { Injectable, inject, signal, effect, PLATFORM_ID } from '@angular/core';
|
|
651
|
-
import { isPlatformBrowser } from '@angular/common';
|
|
652
|
-
import { TranslateService } from '@ngx-translate/core';
|
|
653
|
-
import { StorageService } from '@core/services/storage.service';
|
|
654
|
-
|
|
655
|
-
export type SupportedLanguage = 'en' | 'ar';
|
|
656
|
-
|
|
657
|
-
export interface LanguageOption {
|
|
658
|
-
code: SupportedLanguage;
|
|
659
|
-
name: string;
|
|
660
|
-
nativeName: string;
|
|
661
|
-
dir: 'ltr' | 'rtl';
|
|
662
|
-
flag: string;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
@Injectable({
|
|
666
|
-
providedIn: 'root'
|
|
667
|
-
})
|
|
668
|
-
export class TranslationService {
|
|
669
|
-
private translateService = inject(TranslateService);
|
|
670
|
-
private storage = inject(StorageService);
|
|
671
|
-
private platformId = inject(PLATFORM_ID);
|
|
672
|
-
private isBrowser = isPlatformBrowser(this.platformId);
|
|
673
|
-
|
|
674
|
-
// Reactive current language
|
|
675
|
-
public currentLang = signal<SupportedLanguage>('en');
|
|
676
|
-
|
|
677
|
-
// Reactive text direction
|
|
678
|
-
public textDirection = signal<'ltr' | 'rtl'>('ltr');
|
|
679
|
-
|
|
680
|
-
// Available languages
|
|
681
|
-
public readonly languages: LanguageOption[] = [
|
|
682
|
-
{
|
|
683
|
-
code: 'en',
|
|
684
|
-
name: 'English',
|
|
685
|
-
nativeName: 'English',
|
|
686
|
-
dir: 'ltr',
|
|
687
|
-
flag: '🇺🇸'
|
|
688
|
-
},
|
|
689
|
-
{
|
|
690
|
-
code: 'ar',
|
|
691
|
-
name: 'Arabic',
|
|
692
|
-
nativeName: 'العربية',
|
|
693
|
-
dir: 'rtl',
|
|
694
|
-
flag: '🇸🇦'
|
|
695
|
-
}
|
|
696
|
-
];
|
|
697
|
-
|
|
698
|
-
constructor() {
|
|
699
|
-
// Initialize translation service
|
|
700
|
-
this.translateService.setDefaultLang('en');
|
|
701
|
-
|
|
702
|
-
// Load saved language or detect browser language
|
|
703
|
-
const savedLang = this.getSavedLanguage();
|
|
704
|
-
const browserLang = this.translateService.getBrowserLang() as SupportedLanguage;
|
|
705
|
-
const initialLang = savedLang || (this.isSupportedLanguage(browserLang) ? browserLang : 'en');
|
|
706
|
-
|
|
707
|
-
this.setLanguage(initialLang);
|
|
708
|
-
|
|
709
|
-
// Effect to update document direction when language changes (browser only)
|
|
710
|
-
if (this.isBrowser) {
|
|
711
|
-
effect(() => {
|
|
712
|
-
const dir = this.textDirection();
|
|
713
|
-
document.documentElement.setAttribute('dir', dir);
|
|
714
|
-
document.documentElement.setAttribute('lang', this.currentLang());
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* Change the current language
|
|
721
|
-
*/
|
|
722
|
-
setLanguage(lang: SupportedLanguage): void {
|
|
723
|
-
if (!this.isSupportedLanguage(lang)) {
|
|
724
|
-
console.warn(\`Language '\${lang}' is not supported. Falling back to 'en'.\`);
|
|
725
|
-
lang = 'en';
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
this.translateService.use(lang);
|
|
729
|
-
this.currentLang.set(lang);
|
|
730
|
-
|
|
731
|
-
const languageOption = this.languages.find(l => l.code === lang);
|
|
732
|
-
if (languageOption) {
|
|
733
|
-
this.textDirection.set(languageOption.dir);
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
this.saveLanguage(lang);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
/**
|
|
740
|
-
* Get current language
|
|
741
|
-
*/
|
|
742
|
-
getCurrentLanguage(): SupportedLanguage {
|
|
743
|
-
return this.currentLang();
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
/**
|
|
747
|
-
* Get language option by code
|
|
748
|
-
*/
|
|
749
|
-
getLanguageOption(code: SupportedLanguage): LanguageOption | undefined {
|
|
750
|
-
return this.languages.find(lang => lang.code === code);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
/**
|
|
754
|
-
* Check if language is supported
|
|
755
|
-
*/
|
|
756
|
-
isSupportedLanguage(lang: string): lang is SupportedLanguage {
|
|
757
|
-
return ['en', 'ar'].includes(lang);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
* Toggle between English and Arabic
|
|
762
|
-
*/
|
|
763
|
-
toggleLanguage(): void {
|
|
764
|
-
const newLang: SupportedLanguage = this.currentLang() === 'en' ? 'ar' : 'en';
|
|
765
|
-
this.setLanguage(newLang);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
/**
|
|
769
|
-
* Get instant translation
|
|
770
|
-
*/
|
|
771
|
-
instant(key: string, params?: object): string {
|
|
772
|
-
return this.translateService.instant(key, params);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
/**
|
|
776
|
-
* Get translation as observable
|
|
777
|
-
*/
|
|
778
|
-
get(key: string | string[], params?: object) {
|
|
779
|
-
return this.translateService.get(key, params);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
* Save language preference to storage
|
|
784
|
-
*/
|
|
785
|
-
private saveLanguage(lang: SupportedLanguage): void {
|
|
786
|
-
this.storage.setItem('preferred_language', lang);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
/**
|
|
790
|
-
* Get saved language from storage
|
|
791
|
-
*/
|
|
792
|
-
private getSavedLanguage(): SupportedLanguage | null {
|
|
793
|
-
const saved = this.storage.getItem('preferred_language');
|
|
794
|
-
return saved && this.isSupportedLanguage(saved) ? saved : null;
|
|
795
|
-
}
|
|
796
|
-
}`;
|
|
797
|
-
|
|
798
|
-
await fs.writeFile(
|
|
799
|
-
path.join(config.fullPath, 'src/app/core/i18n/translation.service.ts'),
|
|
800
|
-
translationService
|
|
801
|
-
);
|
|
802
|
-
|
|
803
|
-
// Create English translations
|
|
804
|
-
const enTranslations = {
|
|
805
|
-
app: {
|
|
806
|
-
title: config.projectName,
|
|
807
|
-
welcome: 'Welcome to {{name}}',
|
|
808
|
-
description: 'A professional Angular starter template with Tailwind CSS',
|
|
809
|
-
},
|
|
810
|
-
nav: {
|
|
811
|
-
home: 'Home',
|
|
812
|
-
about: 'About',
|
|
813
|
-
contact: 'Contact',
|
|
814
|
-
login: 'Sign In',
|
|
815
|
-
register: 'Get Started',
|
|
816
|
-
logout: 'Sign Out',
|
|
817
|
-
profile: 'Profile Settings',
|
|
818
|
-
account: 'Account Settings',
|
|
819
|
-
},
|
|
820
|
-
home: {
|
|
821
|
-
hero: {
|
|
822
|
-
title: 'Welcome to {{projectName}}',
|
|
823
|
-
subtitle:
|
|
824
|
-
'A professional Angular starter template with Tailwind CSS. Built with modern best practices and ready to scale.',
|
|
825
|
-
button: 'Get Started',
|
|
826
|
-
learnMore: 'Learn More',
|
|
827
|
-
},
|
|
828
|
-
features: {
|
|
829
|
-
title: 'Modern Angular Features',
|
|
830
|
-
subtitle: 'Everything you need to build professional web applications.',
|
|
831
|
-
standalone: {
|
|
832
|
-
title: 'Standalone Components',
|
|
833
|
-
description: 'Modern Angular architecture without NgModules',
|
|
834
|
-
},
|
|
835
|
-
tailwind: {
|
|
836
|
-
title: 'Tailwind CSS',
|
|
837
|
-
description: 'Utility-first CSS framework for rapid UI development',
|
|
838
|
-
},
|
|
839
|
-
typescript: {
|
|
840
|
-
title: 'TypeScript',
|
|
841
|
-
description: 'Full type safety and enhanced developer experience',
|
|
842
|
-
},
|
|
843
|
-
},
|
|
844
|
-
techStack: {
|
|
845
|
-
title: 'Leveraging the latest tools and frameworks for optimal performance',
|
|
846
|
-
angular: {
|
|
847
|
-
title: 'Angular 20+',
|
|
848
|
-
description: 'Modern standalone components with signals for reactive state management',
|
|
849
|
-
},
|
|
850
|
-
tailwind: {
|
|
851
|
-
title: 'Tailwind CSS v4',
|
|
852
|
-
description: 'Utility-first CSS framework with modern PostCSS configuration',
|
|
853
|
-
},
|
|
854
|
-
typescript: {
|
|
855
|
-
title: 'TypeScript',
|
|
856
|
-
description: 'Strongly-typed interfaces and models for enhanced developer experience',
|
|
857
|
-
},
|
|
858
|
-
},
|
|
859
|
-
readyToStart: {
|
|
860
|
-
title: 'Ready to Start Building?',
|
|
861
|
-
subtitle: 'Explore other pages or get in touch with us to learn more',
|
|
862
|
-
viewHome: 'View Home',
|
|
863
|
-
contactUs: 'Contact Us',
|
|
864
|
-
},
|
|
865
|
-
productionReady: {
|
|
866
|
-
sectionTitle: "What's Included in This Starter Template",
|
|
867
|
-
title:
|
|
868
|
-
'A production-ready Angular application with 20+ pre-configured features, services, and components',
|
|
869
|
-
modernArchitecture: {
|
|
870
|
-
title: 'Modern Architecture',
|
|
871
|
-
description: 'Standalone components, Signals, Zoneless support, Angular 20+',
|
|
872
|
-
},
|
|
873
|
-
i18n: {
|
|
874
|
-
title: 'i18n Translation',
|
|
875
|
-
description: 'English & Arabic with RTL support, language switcher in header',
|
|
876
|
-
},
|
|
877
|
-
interceptors: {
|
|
878
|
-
title: 'HTTP Interceptors',
|
|
879
|
-
description: 'Auth, Error handling, Loading state, Response caching (5min TTL)',
|
|
880
|
-
},
|
|
881
|
-
tailwind: {
|
|
882
|
-
title: 'Tailwind CSS v4',
|
|
883
|
-
description: 'Modern PostCSS setup, responsive utilities, dark mode ready',
|
|
884
|
-
},
|
|
885
|
-
uiComponents: {
|
|
886
|
-
title: 'UI Components',
|
|
887
|
-
description: 'Button, Card, Spinner, Toast, Modal with full customization',
|
|
888
|
-
},
|
|
889
|
-
typeSafe: {
|
|
890
|
-
title: 'Type-Safe',
|
|
891
|
-
description: 'TypeScript interfaces, models, strongly-typed services',
|
|
892
|
-
},
|
|
893
|
-
},
|
|
894
|
-
coreServices: {
|
|
895
|
-
title: 'Core Services (src/app/core/)',
|
|
896
|
-
authService: 'AuthService - Authentication & user management',
|
|
897
|
-
apiService: 'ApiService - Centralized HTTP request handling',
|
|
898
|
-
toastService: 'ToastService - Notification system (success, error, warning, info)',
|
|
899
|
-
modalService: 'ModalService - Dialog system with confirm & alert',
|
|
900
|
-
loadingService: 'LoadingService - Global loading state with signals',
|
|
901
|
-
cacheService: 'CacheService - Response caching with TTL',
|
|
902
|
-
storageService: 'StorageService - LocalStorage wrapper with type safety',
|
|
903
|
-
i18nService: 'i18nService - Internationalization management',
|
|
904
|
-
},
|
|
905
|
-
sharedComponents: {
|
|
906
|
-
title: 'Shared Components (src/app/shared/)',
|
|
907
|
-
button: 'ButtonComponent - Variants: primary, secondary, danger',
|
|
908
|
-
card: 'CardComponent - Flexible container with title & shadow',
|
|
909
|
-
spinner: 'LoadingSpinnerComponent - Animated loading indicator',
|
|
910
|
-
toast: 'ToastComponent - Auto-dismiss notifications',
|
|
911
|
-
modal: 'ModalComponent - Accessible dialog with sizes',
|
|
912
|
-
pipes: 'Pipes - Truncate, TimeAgo',
|
|
913
|
-
directives: 'Directives - ClickOutside, Tooltip',
|
|
914
|
-
},
|
|
915
|
-
preConfigured: {
|
|
916
|
-
title: 'Pre-Configured Packages & Tools',
|
|
917
|
-
subtitle: 'Production-ready dependencies and development tools included',
|
|
918
|
-
coreDependencies: 'Core Dependencies',
|
|
919
|
-
developmentTools: 'Development Tools',
|
|
920
|
-
latest: 'Latest',
|
|
921
|
-
i18n: 'i18n',
|
|
922
|
-
iconLibrary: 'Icon library',
|
|
923
|
-
linting: 'Linting',
|
|
924
|
-
formatting: 'Formatting',
|
|
925
|
-
gitHooks: 'Git hooks',
|
|
926
|
-
typeSafety: 'Type safety',
|
|
927
|
-
serviceWorker: 'Service worker',
|
|
928
|
-
},
|
|
929
|
-
pathAliases: {
|
|
930
|
-
title: 'TypeScript Path Aliases (tsconfig.json)',
|
|
931
|
-
core: '@core/* → src/app/core/*',
|
|
932
|
-
shared: '@shared/* → src/app/shared/*',
|
|
933
|
-
features: '@features/* → src/app/features/*',
|
|
934
|
-
environments: '@environments/* → src/environments/*',
|
|
935
|
-
},
|
|
936
|
-
projectStructure: {
|
|
937
|
-
title: 'Well-Organized Project Structure',
|
|
938
|
-
subtitle: 'Clean architecture following Angular best practices',
|
|
939
|
-
coreServices: 'services/ (8 services)',
|
|
940
|
-
guards: 'guards/ (auth guard)',
|
|
941
|
-
interceptors: 'interceptors/ (4 types)',
|
|
942
|
-
i18n: 'i18n/ (translation system)',
|
|
943
|
-
components: 'components/ (5 components)',
|
|
944
|
-
pipes: 'pipes/ (2 pipes)',
|
|
945
|
-
directives: 'directives/ (2 directives)',
|
|
946
|
-
models: 'models/ (TypeScript interfaces)',
|
|
947
|
-
home: 'home/',
|
|
948
|
-
about: 'about/',
|
|
949
|
-
contact: 'contact/',
|
|
950
|
-
auth: 'auth/ (login, register, forgot)',
|
|
951
|
-
mainLayout: 'main-layout/ (header + footer)',
|
|
952
|
-
authLayout: 'auth-layout/ (auth pages)',
|
|
953
|
-
},
|
|
954
|
-
interactiveExamples: {
|
|
955
|
-
title: 'Try Interactive Examples',
|
|
956
|
-
subtitle: 'Test the Toast and Modal services included in this starter template',
|
|
957
|
-
toastNotifications: {
|
|
958
|
-
title: 'Toast Notifications',
|
|
959
|
-
subtitle: 'Auto-dismiss alerts with 4 types',
|
|
960
|
-
description: 'Click any button to see different toast notification types:',
|
|
961
|
-
success: 'Success',
|
|
962
|
-
error: 'Error',
|
|
963
|
-
warning: 'Warning',
|
|
964
|
-
info: 'Info',
|
|
965
|
-
},
|
|
966
|
-
modalDialogs: {
|
|
967
|
-
title: 'Modal Dialogs',
|
|
968
|
-
subtitle: 'Accessible & responsive dialogs',
|
|
969
|
-
description: 'Test confirmation and alert modals with transitions:',
|
|
970
|
-
showConfirm: 'Show Confirm Dialog',
|
|
971
|
-
showAlert: 'Show Alert Dialog',
|
|
972
|
-
},
|
|
973
|
-
proTip: {
|
|
974
|
-
title: 'Pro Tip',
|
|
975
|
-
description:
|
|
976
|
-
'These services are fully typed and can be injected anywhere in your app. Check src/app/core/services/ to see the implementation.',
|
|
977
|
-
},
|
|
978
|
-
},
|
|
979
|
-
readyToBuild: {
|
|
980
|
-
title: 'Ready to Build Something Amazing?',
|
|
981
|
-
subtitle:
|
|
982
|
-
'Explore the other pages to see forms, routing, and authentication UI in action',
|
|
983
|
-
learnMore: 'Learn More',
|
|
984
|
-
},
|
|
985
|
-
},
|
|
986
|
-
about: {
|
|
987
|
-
title: 'About {{projectName}}',
|
|
988
|
-
subtitle:
|
|
989
|
-
'A modern Angular starter template built with best practices and developer experience in mind.',
|
|
990
|
-
overview: {
|
|
991
|
-
title: 'Project Overview',
|
|
992
|
-
paragraph1:
|
|
993
|
-
'This starter template provides a solid foundation for building modern Angular applications with Tailwind CSS, including essential components, services, and best practices.',
|
|
994
|
-
paragraph2:
|
|
995
|
-
'Built with the latest Angular features including standalone components, signals, and optimized for performance and developer experience.',
|
|
996
|
-
},
|
|
997
|
-
features: {
|
|
998
|
-
title: 'Features Included',
|
|
999
|
-
standalone: 'Standalone Angular components',
|
|
1000
|
-
tailwind: 'Tailwind CSS integration',
|
|
1001
|
-
responsive: 'Responsive design',
|
|
1002
|
-
typescript: 'TypeScript ready',
|
|
1003
|
-
production: 'Production optimized',
|
|
1004
|
-
},
|
|
1005
|
-
techStack: {
|
|
1006
|
-
title: 'Built With Modern Technologies',
|
|
1007
|
-
},
|
|
1008
|
-
},
|
|
1009
|
-
contact: {
|
|
1010
|
-
title: 'Get in Touch',
|
|
1011
|
-
subtitle:
|
|
1012
|
-
"Have questions? We'd love to hear from you. Send us a message and we'll respond as soon as possible.",
|
|
1013
|
-
form: {
|
|
1014
|
-
title: 'Send us a message',
|
|
1015
|
-
description: "Fill out the form below and we'll get back to you soon",
|
|
1016
|
-
name: 'Full Name',
|
|
1017
|
-
email: 'Email Address',
|
|
1018
|
-
subject: 'Subject',
|
|
1019
|
-
message: 'Message',
|
|
1020
|
-
submit: 'Send Message',
|
|
1021
|
-
sending: 'Sending...',
|
|
1022
|
-
success: "Thank you for your message! We'll get back to you soon.",
|
|
1023
|
-
errors: {
|
|
1024
|
-
nameRequired: 'Name is required',
|
|
1025
|
-
nameMinLength: 'Name must be at least 2 characters',
|
|
1026
|
-
emailRequired: 'Email is required',
|
|
1027
|
-
emailInvalid: 'Please enter a valid email',
|
|
1028
|
-
subjectRequired: 'Subject is required',
|
|
1029
|
-
messageRequired: 'Message is required',
|
|
1030
|
-
messageMinLength: 'Message must be at least 10 characters',
|
|
1031
|
-
},
|
|
1032
|
-
},
|
|
1033
|
-
info: {
|
|
1034
|
-
title: 'Contact Information',
|
|
1035
|
-
description: "We're here to help and answer any question you might have",
|
|
1036
|
-
email: {
|
|
1037
|
-
label: 'Email',
|
|
1038
|
-
description: "We'll respond within 24 hours",
|
|
1039
|
-
},
|
|
1040
|
-
location: {
|
|
1041
|
-
label: 'Location',
|
|
1042
|
-
value: 'Remote & Global',
|
|
1043
|
-
description: 'Working across all time zones',
|
|
1044
|
-
},
|
|
1045
|
-
responseTime: {
|
|
1046
|
-
label: 'Response Time',
|
|
1047
|
-
value: '24 hours',
|
|
1048
|
-
description: 'Usually much faster',
|
|
1049
|
-
},
|
|
1050
|
-
},
|
|
1051
|
-
help: {
|
|
1052
|
-
title: 'Helpful Resources',
|
|
1053
|
-
subtitle: 'Have questions about the starter template? Check out these resources:',
|
|
1054
|
-
links: {
|
|
1055
|
-
angular: 'Angular Documentation',
|
|
1056
|
-
tailwind: 'Tailwind CSS Guide',
|
|
1057
|
-
github: 'GitHub Repository',
|
|
1058
|
-
},
|
|
1059
|
-
},
|
|
1060
|
-
},
|
|
1061
|
-
auth: {
|
|
1062
|
-
layout: {
|
|
1063
|
-
welcome: 'Welcome back! Please sign in to continue.',
|
|
1064
|
-
copyright: '© {{year}} {{name}}. All rights reserved.',
|
|
1065
|
-
},
|
|
1066
|
-
login: {
|
|
1067
|
-
title: 'Sign In',
|
|
1068
|
-
subtitle: 'Enter your credentials to access your account',
|
|
1069
|
-
email: 'Email Address',
|
|
1070
|
-
password: 'Password',
|
|
1071
|
-
rememberMe: 'Remember me',
|
|
1072
|
-
forgotPassword: 'Forgot password?',
|
|
1073
|
-
submit: 'Sign In',
|
|
1074
|
-
signing: 'Signing in...',
|
|
1075
|
-
noAccount: "Don't have an account?",
|
|
1076
|
-
createAccount: 'Create one here',
|
|
1077
|
-
showPassword: 'Show password',
|
|
1078
|
-
hidePassword: 'Hide password',
|
|
1079
|
-
},
|
|
1080
|
-
register: {
|
|
1081
|
-
title: 'Create Account',
|
|
1082
|
-
subtitle: 'Sign up to get started',
|
|
1083
|
-
firstName: 'First Name',
|
|
1084
|
-
lastName: 'Last Name',
|
|
1085
|
-
email: 'Email',
|
|
1086
|
-
password: 'Password',
|
|
1087
|
-
submit: 'Create Account',
|
|
1088
|
-
hasAccount: 'Already have an account?',
|
|
1089
|
-
},
|
|
1090
|
-
forgot: {
|
|
1091
|
-
title: 'Forgot Password',
|
|
1092
|
-
subtitle: 'Enter your email to reset password',
|
|
1093
|
-
email: 'Email Address',
|
|
1094
|
-
submit: 'Send Reset Link',
|
|
1095
|
-
backToLogin: 'Back to Sign In',
|
|
1096
|
-
},
|
|
1097
|
-
validation: {
|
|
1098
|
-
emailRequired: 'Email is required',
|
|
1099
|
-
emailInvalid: 'Please enter a valid email',
|
|
1100
|
-
passwordRequired: 'Password is required',
|
|
1101
|
-
passwordMinLength: 'Password must be at least 6 characters',
|
|
1102
|
-
},
|
|
1103
|
-
},
|
|
1104
|
-
footer: {
|
|
1105
|
-
description:
|
|
1106
|
-
'Built with Angular and Tailwind CSS. A modern, fast, and responsive web application starter template.',
|
|
1107
|
-
quickLinks: 'Quick Links',
|
|
1108
|
-
builtWith: 'Built With',
|
|
1109
|
-
copyright: '© {{year}} {{name}}. All rights reserved.',
|
|
1110
|
-
},
|
|
1111
|
-
language: {
|
|
1112
|
-
select: 'Select Language',
|
|
1113
|
-
english: 'English',
|
|
1114
|
-
arabic: 'العربية',
|
|
1115
|
-
},
|
|
1116
|
-
};
|
|
1117
|
-
|
|
1118
|
-
await fs.writeFile(
|
|
1119
|
-
path.join(config.fullPath, 'public/assets/i18n/en.json'),
|
|
1120
|
-
JSON.stringify(enTranslations, null, 2)
|
|
1121
|
-
);
|
|
1122
|
-
|
|
1123
|
-
// Create Arabic translations
|
|
1124
|
-
const arTranslations = {
|
|
1125
|
-
app: {
|
|
1126
|
-
title: config.projectName,
|
|
1127
|
-
welcome: 'مرحباً بك في {{name}}',
|
|
1128
|
-
description: 'قالب احترافي لتطبيقات Angular مع Tailwind CSS',
|
|
1129
|
-
},
|
|
1130
|
-
nav: {
|
|
1131
|
-
home: 'الرئيسية',
|
|
1132
|
-
about: 'حول',
|
|
1133
|
-
contact: 'اتصل بنا',
|
|
1134
|
-
login: 'تسجيل الدخول',
|
|
1135
|
-
register: 'ابدأ الآن',
|
|
1136
|
-
logout: 'تسجيل الخروج',
|
|
1137
|
-
profile: 'إعدادات الملف الشخصي',
|
|
1138
|
-
account: 'إعدادات الحساب',
|
|
1139
|
-
},
|
|
1140
|
-
home: {
|
|
1141
|
-
hero: {
|
|
1142
|
-
title: 'مرحباً بك في {{projectName}}',
|
|
1143
|
-
subtitle:
|
|
1144
|
-
'قالب احترافي لتطبيقات Angular مع Tailwind CSS. مبني بأحدث الممارسات وجاهز للتوسع.',
|
|
1145
|
-
button: 'ابدأ الآن',
|
|
1146
|
-
learnMore: 'معرفة المزيد',
|
|
1147
|
-
},
|
|
1148
|
-
features: {
|
|
1149
|
-
title: 'ميزات Angular الحديثة',
|
|
1150
|
-
subtitle: 'كل ما تحتاجه لبناء تطبيقات ويب احترافية.',
|
|
1151
|
-
standalone: {
|
|
1152
|
-
title: 'مكونات مستقلة',
|
|
1153
|
-
description: 'بنية Angular الحديثة بدون NgModules',
|
|
1154
|
-
},
|
|
1155
|
-
tailwind: {
|
|
1156
|
-
title: 'Tailwind CSS',
|
|
1157
|
-
description: 'إطار عمل CSS سريع للتطوير',
|
|
1158
|
-
},
|
|
1159
|
-
typescript: {
|
|
1160
|
-
title: 'TypeScript',
|
|
1161
|
-
description: 'الأمان الكامل للأنواع وتجربة تطوير محسّنة',
|
|
1162
|
-
},
|
|
1163
|
-
},
|
|
1164
|
-
techStack: {
|
|
1165
|
-
title: 'الاستفادة من أحدث الأدوات والأطر لتحسين الأداء',
|
|
1166
|
-
angular: {
|
|
1167
|
-
title: 'Angular 20+',
|
|
1168
|
-
description: 'مكونات مستقلة حديثة مع الإشارات لإدارة الحالة التفاعلية',
|
|
1169
|
-
},
|
|
1170
|
-
tailwind: {
|
|
1171
|
-
title: 'Tailwind CSS v4',
|
|
1172
|
-
description: 'إطار عمل CSS مبني على المرافق مع إعداد PostCSS حديث',
|
|
1173
|
-
},
|
|
1174
|
-
typescript: {
|
|
1175
|
-
title: 'TypeScript',
|
|
1176
|
-
description: 'واجهات ونماذج مُكتوبة بقوة لتحسين تجربة المطور',
|
|
1177
|
-
},
|
|
1178
|
-
},
|
|
1179
|
-
readyToStart: {
|
|
1180
|
-
title: 'جاهز لبدء البناء؟',
|
|
1181
|
-
subtitle: 'استكشف الصفحات الأخرى أو تواصل معنا لمعرفة المزيد',
|
|
1182
|
-
viewHome: 'عرض الرئيسية',
|
|
1183
|
-
contactUs: 'تواصل معنا',
|
|
1184
|
-
},
|
|
1185
|
-
productionReady: {
|
|
1186
|
-
sectionTitle: 'ما هو مُضمن في هذا القالب',
|
|
1187
|
-
title: 'تطبيق Angular جاهز للإنتاج مع 20+ ميزة وخدمة ومكون مُعد مسبقاً',
|
|
1188
|
-
modernArchitecture: {
|
|
1189
|
-
title: 'معمارية حديثة',
|
|
1190
|
-
description: 'مكونات مستقلة، إشارات، دعم بدون منطقة، Angular 20+',
|
|
1191
|
-
},
|
|
1192
|
-
i18n: {
|
|
1193
|
-
title: 'ترجمة i18n',
|
|
1194
|
-
description: 'الإنجليزية والعربية مع دعم RTL، مبدل اللغة في الرأس',
|
|
1195
|
-
},
|
|
1196
|
-
interceptors: {
|
|
1197
|
-
title: 'مُعترِضات HTTP',
|
|
1198
|
-
description:
|
|
1199
|
-
'المصادقة، معالجة الأخطاء، حالة التحميل، تخزين الاستجابة مؤقتاً (5 دقائق TTL)',
|
|
1200
|
-
},
|
|
1201
|
-
tailwind: {
|
|
1202
|
-
title: 'Tailwind CSS v4',
|
|
1203
|
-
description: 'إعداد PostCSS حديث، مرافق متجاوبة، جاهز للوضع المظلم',
|
|
1204
|
-
},
|
|
1205
|
-
uiComponents: {
|
|
1206
|
-
title: 'مكونات واجهة المستخدم',
|
|
1207
|
-
description: 'زر، بطاقة، دوار، تنبيه، نافذة منبثقة مع تخصيص كامل',
|
|
1208
|
-
},
|
|
1209
|
-
typeSafe: {
|
|
1210
|
-
title: 'آمن النوع',
|
|
1211
|
-
description: 'واجهات TypeScript، نماذج، خدمات مُكتوبة بقوة',
|
|
1212
|
-
},
|
|
1213
|
-
},
|
|
1214
|
-
coreServices: {
|
|
1215
|
-
title: 'الخدمات الأساسية (src/app/core/)',
|
|
1216
|
-
authService: 'AuthService - المصادقة وإدارة المستخدمين',
|
|
1217
|
-
apiService: 'ApiService - معالجة طلبات HTTP مركزية',
|
|
1218
|
-
toastService: 'ToastService - نظام الإشعارات (نجاح، خطأ، تحذير، معلومات)',
|
|
1219
|
-
modalService: 'ModalService - نظام الحوار مع التأكيد والتنبيه',
|
|
1220
|
-
loadingService: 'LoadingService - حالة التحميل العامة مع الإشارات',
|
|
1221
|
-
cacheService: 'CacheService - تخزين الاستجابة مؤقتاً مع TTL',
|
|
1222
|
-
storageService: 'StorageService - غلاف LocalStorage مع أمان النوع',
|
|
1223
|
-
i18nService: 'i18nService - إدارة الترجمة',
|
|
1224
|
-
},
|
|
1225
|
-
sharedComponents: {
|
|
1226
|
-
title: 'المكونات المشتركة (src/app/shared/)',
|
|
1227
|
-
button: 'ButtonComponent - متغيرات: أساسي، ثانوي، خطر',
|
|
1228
|
-
card: 'CardComponent - حاوية مرنة مع عنوان وظل',
|
|
1229
|
-
spinner: 'LoadingSpinnerComponent - مؤشر تحميل متحرك',
|
|
1230
|
-
toast: 'ToastComponent - إشعارات إغلاق تلقائي',
|
|
1231
|
-
modal: 'ModalComponent - حوار قابل للوصول بأحجام',
|
|
1232
|
-
pipes: 'الأنابيب - قطع، منذ',
|
|
1233
|
-
directives: 'التوجيهات - خارج النقر، تلميح',
|
|
1234
|
-
},
|
|
1235
|
-
preConfigured: {
|
|
1236
|
-
title: 'الحزم والأدوات المُعدة مسبقاً',
|
|
1237
|
-
subtitle: 'التبعيات وأدوات التطوير الجاهزة للإنتاج مُضمنة',
|
|
1238
|
-
coreDependencies: 'التبعيات الأساسية',
|
|
1239
|
-
developmentTools: 'أدوات التطوير',
|
|
1240
|
-
latest: 'الأحدث',
|
|
1241
|
-
i18n: 'i18n',
|
|
1242
|
-
iconLibrary: 'مكتبة الأيقونات',
|
|
1243
|
-
linting: 'الفحص',
|
|
1244
|
-
formatting: 'التنسيق',
|
|
1245
|
-
gitHooks: 'خطافات Git',
|
|
1246
|
-
typeSafety: 'أمان النوع',
|
|
1247
|
-
serviceWorker: 'عامل الخدمة',
|
|
1248
|
-
},
|
|
1249
|
-
pathAliases: {
|
|
1250
|
-
title: 'أسماء مسارات TypeScript (tsconfig.json)',
|
|
1251
|
-
core: '@core/* → src/app/core/*',
|
|
1252
|
-
shared: '@shared/* → src/app/shared/*',
|
|
1253
|
-
features: '@features/* → src/app/features/*',
|
|
1254
|
-
environments: '@environments/* → src/environments/*',
|
|
1255
|
-
},
|
|
1256
|
-
projectStructure: {
|
|
1257
|
-
title: 'هيكل المشروع منظم جيداً',
|
|
1258
|
-
subtitle: 'معمارية نظيفة تتبع أفضل ممارسات Angular',
|
|
1259
|
-
coreServices: 'services/ (8 خدمات)',
|
|
1260
|
-
guards: 'guards/ (حارس المصادقة)',
|
|
1261
|
-
interceptors: 'interceptors/ (4 أنواع)',
|
|
1262
|
-
i18n: 'i18n/ (نظام الترجمة)',
|
|
1263
|
-
components: 'components/ (5 مكونات)',
|
|
1264
|
-
pipes: 'pipes/ (2 أنابيب)',
|
|
1265
|
-
directives: 'directives/ (2 توجيهات)',
|
|
1266
|
-
models: 'models/ (واجهات TypeScript)',
|
|
1267
|
-
home: 'home/',
|
|
1268
|
-
about: 'about/',
|
|
1269
|
-
contact: 'contact/',
|
|
1270
|
-
auth: 'auth/ (تسجيل دخول، تسجيل، نسيان)',
|
|
1271
|
-
mainLayout: 'main-layout/ (رأس + تذييل)',
|
|
1272
|
-
authLayout: 'auth-layout/ (صفحات المصادقة)',
|
|
1273
|
-
},
|
|
1274
|
-
interactiveExamples: {
|
|
1275
|
-
title: 'جرب الأمثلة التفاعلية',
|
|
1276
|
-
subtitle: 'اختبر خدمات Toast و Modal المُضمنة في هذا القالب',
|
|
1277
|
-
toastNotifications: {
|
|
1278
|
-
title: 'إشعارات Toast',
|
|
1279
|
-
subtitle: 'تنبيهات إغلاق تلقائي مع 4 أنواع',
|
|
1280
|
-
description: 'انقر على أي زر لرؤية أنواع مختلفة من إشعارات Toast:',
|
|
1281
|
-
success: 'نجح',
|
|
1282
|
-
error: 'خطأ',
|
|
1283
|
-
warning: 'تحذير',
|
|
1284
|
-
info: 'معلومات',
|
|
1285
|
-
},
|
|
1286
|
-
modalDialogs: {
|
|
1287
|
-
title: 'حوارات النافذة المنبثقة',
|
|
1288
|
-
subtitle: 'حوارات قابلة للوصول ومتجاوبة',
|
|
1289
|
-
description: 'اختبر حوارات التأكيد والتنبيه مع الانتقالات:',
|
|
1290
|
-
showConfirm: 'إظهار حوار التأكيد',
|
|
1291
|
-
showAlert: 'إظهار حوار التنبيه',
|
|
1292
|
-
},
|
|
1293
|
-
proTip: {
|
|
1294
|
-
title: 'نصيحة احترافية',
|
|
1295
|
-
description:
|
|
1296
|
-
'هذه الخدمات مُكتوبة بالكامل ويمكن حقنها في أي مكان في تطبيقك. تحقق من src/app/core/services/ لرؤية التنفيذ.',
|
|
1297
|
-
},
|
|
1298
|
-
},
|
|
1299
|
-
readyToBuild: {
|
|
1300
|
-
title: 'جاهز لبناء شيء مذهل؟',
|
|
1301
|
-
subtitle: 'استكشف الصفحات الأخرى لرؤية النماذج والتوجيه وواجهة المصادقة في العمل',
|
|
1302
|
-
learnMore: 'معرفة المزيد',
|
|
1303
|
-
},
|
|
1304
|
-
},
|
|
1305
|
-
about: {
|
|
1306
|
-
title: 'حول {{projectName}}',
|
|
1307
|
-
subtitle: 'قالب Angular حديث مبني بأفضل الممارسات وتجربة المطور في الاعتبار.',
|
|
1308
|
-
overview: {
|
|
1309
|
-
title: 'نظرة عامة على المشروع',
|
|
1310
|
-
paragraph1:
|
|
1311
|
-
'يوفر هذا القالب أساساً قوياً لبناء تطبيقات Angular الحديثة مع Tailwind CSS، بما في ذلك المكونات الأساسية والخدمات وأفضل الممارسات.',
|
|
1312
|
-
paragraph2:
|
|
1313
|
-
'مبني بأحدث ميزات Angular بما في ذلك المكونات المستقلة والإشارات، ومُحسّن للأداء وتجربة المطور.',
|
|
1314
|
-
},
|
|
1315
|
-
features: {
|
|
1316
|
-
title: 'الميزات المتضمنة',
|
|
1317
|
-
standalone: 'مكونات Angular المستقلة',
|
|
1318
|
-
tailwind: 'تكامل Tailwind CSS',
|
|
1319
|
-
responsive: 'تصميم متجاوب',
|
|
1320
|
-
typescript: 'جاهز لـ TypeScript',
|
|
1321
|
-
production: 'مُحسّن للإنتاج',
|
|
1322
|
-
},
|
|
1323
|
-
techStack: {
|
|
1324
|
-
title: 'مبني بأحدث التقنيات',
|
|
1325
|
-
},
|
|
1326
|
-
},
|
|
1327
|
-
contact: {
|
|
1328
|
-
title: 'تواصل معنا',
|
|
1329
|
-
subtitle: 'هل لديك أسئلة؟ نحب أن نسمع منك. أرسل لنا رسالة وسنرد في أقرب وقت ممكن.',
|
|
1330
|
-
form: {
|
|
1331
|
-
title: 'أرسل لنا رسالة',
|
|
1332
|
-
description: 'املأ النموذج أدناه وسنعاود الاتصال بك قريباً',
|
|
1333
|
-
name: 'الاسم الكامل',
|
|
1334
|
-
email: 'عنوان البريد الإلكتروني',
|
|
1335
|
-
subject: 'الموضوع',
|
|
1336
|
-
message: 'الرسالة',
|
|
1337
|
-
submit: 'إرسال الرسالة',
|
|
1338
|
-
sending: 'جاري الإرسال...',
|
|
1339
|
-
success: 'شكراً على رسالتك! سنعاود الاتصال بك قريباً.',
|
|
1340
|
-
errors: {
|
|
1341
|
-
nameRequired: 'الاسم مطلوب',
|
|
1342
|
-
nameMinLength: 'يجب أن يكون الاسم على الأقل حرفين',
|
|
1343
|
-
emailRequired: 'البريد الإلكتروني مطلوب',
|
|
1344
|
-
emailInvalid: 'يرجى إدخال بريد إلكتروني صحيح',
|
|
1345
|
-
subjectRequired: 'الموضوع مطلوب',
|
|
1346
|
-
messageRequired: 'الرسالة مطلوبة',
|
|
1347
|
-
messageMinLength: 'يجب أن تكون الرسالة على الأقل 10 أحرف',
|
|
1348
|
-
},
|
|
1349
|
-
},
|
|
1350
|
-
info: {
|
|
1351
|
-
title: 'معلومات الاتصال',
|
|
1352
|
-
description: 'نحن هنا للمساعدة والإجابة على أي سؤال قد يكون لديك',
|
|
1353
|
-
email: {
|
|
1354
|
-
label: 'البريد الإلكتروني',
|
|
1355
|
-
description: 'سنرد خلال 24 ساعة',
|
|
1356
|
-
},
|
|
1357
|
-
location: {
|
|
1358
|
-
label: 'الموقع',
|
|
1359
|
-
value: 'عن بُعد وعالمي',
|
|
1360
|
-
description: 'نعمل عبر جميع المناطق الزمنية',
|
|
1361
|
-
},
|
|
1362
|
-
responseTime: {
|
|
1363
|
-
label: 'وقت الاستجابة',
|
|
1364
|
-
value: '24 ساعة',
|
|
1365
|
-
description: 'عادة أسرع بكثير',
|
|
1366
|
-
},
|
|
1367
|
-
},
|
|
1368
|
-
help: {
|
|
1369
|
-
title: 'موارد مفيدة',
|
|
1370
|
-
subtitle: 'هل لديك أسئلة حول القالب؟ تحقق من هذه الموارد:',
|
|
1371
|
-
links: {
|
|
1372
|
-
angular: 'وثائق Angular',
|
|
1373
|
-
tailwind: 'دليل Tailwind CSS',
|
|
1374
|
-
github: 'مستودع GitHub',
|
|
1375
|
-
},
|
|
1376
|
-
},
|
|
1377
|
-
},
|
|
1378
|
-
auth: {
|
|
1379
|
-
layout: {
|
|
1380
|
-
welcome: 'مرحبًا بعودتك! يرجى تسجيل الدخول للمتابعة.',
|
|
1381
|
-
copyright: '© {{year}} {{name}}. جميع الحقوق محفوظة.',
|
|
1382
|
-
},
|
|
1383
|
-
login: {
|
|
1384
|
-
title: 'تسجيل الدخول',
|
|
1385
|
-
subtitle: 'أدخل بيانات الاعتماد للوصول إلى حسابك',
|
|
1386
|
-
email: 'عنوان البريد الإلكتروني',
|
|
1387
|
-
password: 'كلمة المرور',
|
|
1388
|
-
rememberMe: 'تذكرني',
|
|
1389
|
-
forgotPassword: 'نسيت كلمة المرور؟',
|
|
1390
|
-
submit: 'تسجيل الدخول',
|
|
1391
|
-
signing: 'جاري تسجيل الدخول...',
|
|
1392
|
-
noAccount: 'ليس لديك حساب؟',
|
|
1393
|
-
createAccount: 'أنشئ حساباً هنا',
|
|
1394
|
-
showPassword: 'إظهار كلمة المرور',
|
|
1395
|
-
hidePassword: 'إخفاء كلمة المرور',
|
|
1396
|
-
},
|
|
1397
|
-
register: {
|
|
1398
|
-
title: 'إنشاء حساب',
|
|
1399
|
-
subtitle: 'سجل للبدء',
|
|
1400
|
-
firstName: 'الاسم الأول',
|
|
1401
|
-
lastName: 'اسم العائلة',
|
|
1402
|
-
email: 'البريد الإلكتروني',
|
|
1403
|
-
password: 'كلمة المرور',
|
|
1404
|
-
submit: 'إنشاء حساب',
|
|
1405
|
-
hasAccount: 'لديك حساب بالفعل؟',
|
|
1406
|
-
},
|
|
1407
|
-
forgot: {
|
|
1408
|
-
title: 'نسيت كلمة المرور',
|
|
1409
|
-
subtitle: 'أدخل بريدك الإلكتروني لإعادة تعيين كلمة المرور',
|
|
1410
|
-
email: 'عنوان البريد الإلكتروني',
|
|
1411
|
-
submit: 'إرسال رابط إعادة التعيين',
|
|
1412
|
-
backToLogin: 'العودة إلى تسجيل الدخول',
|
|
1413
|
-
},
|
|
1414
|
-
validation: {
|
|
1415
|
-
emailRequired: 'البريد الإلكتروني مطلوب',
|
|
1416
|
-
emailInvalid: 'يرجى إدخال بريد إلكتروني صحيح',
|
|
1417
|
-
passwordRequired: 'كلمة المرور مطلوبة',
|
|
1418
|
-
passwordMinLength: 'يجب أن تكون كلمة المرور 6 أحرف على الأقل',
|
|
1419
|
-
},
|
|
1420
|
-
},
|
|
1421
|
-
footer: {
|
|
1422
|
-
description: 'مبني بـ Angular و Tailwind CSS. قالب تطبيق ويب حديث وسريع ومتجاوب.',
|
|
1423
|
-
quickLinks: 'روابط سريعة',
|
|
1424
|
-
builtWith: 'مبني بـ',
|
|
1425
|
-
copyright: '© {{year}} {{name}}. جميع الحقوق محفوظة.',
|
|
1426
|
-
},
|
|
1427
|
-
language: {
|
|
1428
|
-
select: 'اختر اللغة',
|
|
1429
|
-
english: 'English',
|
|
1430
|
-
arabic: 'العربية',
|
|
1431
|
-
},
|
|
1432
|
-
};
|
|
1433
|
-
|
|
1434
|
-
await fs.writeFile(
|
|
1435
|
-
path.join(config.fullPath, 'public/assets/i18n/ar.json'),
|
|
1436
|
-
JSON.stringify(arTranslations, null, 2)
|
|
1437
|
-
);
|
|
1438
|
-
|
|
1439
|
-
// Update package.json to include translation dependencies
|
|
1440
|
-
const packageJsonPath = path.join(config.fullPath, 'package.json');
|
|
1441
|
-
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
1442
|
-
|
|
1443
|
-
const translationDependencies = {
|
|
1444
|
-
'@ngx-translate/core': '^15.0.0',
|
|
1445
|
-
'@ngx-translate/http-loader': '^8.0.0',
|
|
1446
|
-
};
|
|
1447
|
-
|
|
1448
|
-
packageJson.dependencies = {
|
|
1449
|
-
...packageJson.dependencies,
|
|
1450
|
-
...translationDependencies,
|
|
1451
|
-
};
|
|
1452
|
-
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
1453
|
-
},
|
|
1454
|
-
|
|
1455
|
-
async createGuards(config) {
|
|
1456
|
-
const authGuard = `import { CanActivateFn, Router } from '@angular/router';
|
|
1457
|
-
import { inject } from '@angular/core';
|
|
1458
|
-
|
|
1459
|
-
/**
|
|
1460
|
-
* Simple authentication guard
|
|
1461
|
-
* Replace with your actual authentication logic
|
|
1462
|
-
*/
|
|
1463
|
-
export const authGuard: CanActivateFn = (_route, _state) => {
|
|
1464
|
-
const router = inject(Router);
|
|
1465
|
-
|
|
1466
|
-
// Replace with your actual authentication check
|
|
1467
|
-
const isAuthenticated = localStorage.getItem('auth-token') !== null;
|
|
1468
|
-
|
|
1469
|
-
if (!isAuthenticated) {
|
|
1470
|
-
// Redirect to login page
|
|
1471
|
-
router.navigate(['/auth/login']);
|
|
1472
|
-
return false;
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
return true;
|
|
1476
|
-
};
|
|
1477
|
-
|
|
1478
|
-
/**
|
|
1479
|
-
* Example: Check if user has specific role
|
|
1480
|
-
*/
|
|
1481
|
-
export const roleGuard = (requiredRole: string): CanActivateFn => {
|
|
1482
|
-
return (_route, _state) => {
|
|
1483
|
-
const router = inject(Router);
|
|
1484
|
-
|
|
1485
|
-
// Replace with your actual role checking logic
|
|
1486
|
-
const userRole = localStorage.getItem('user-role');
|
|
1487
|
-
|
|
1488
|
-
if (userRole !== requiredRole) {
|
|
1489
|
-
router.navigate(['/unauthorized']);
|
|
1490
|
-
return false;
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
return true;
|
|
1494
|
-
};
|
|
1495
|
-
};`;
|
|
1496
|
-
|
|
1497
|
-
await fs.writeFile(path.join(config.fullPath, 'src/app/core/guards/auth.guard.ts'), authGuard);
|
|
1498
|
-
},
|
|
1499
|
-
|
|
1500
|
-
async createSharedComponents(config) {
|
|
1501
|
-
// Button Component
|
|
1502
|
-
const buttonComponent = `import { Component, Input, Output, EventEmitter } from '@angular/core';
|
|
1503
|
-
|
|
1504
|
-
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
|
|
1505
|
-
export type ButtonSize = 'sm' | 'md' | 'lg';
|
|
1506
|
-
|
|
1507
|
-
@Component({
|
|
1508
|
-
selector: 'app-button',
|
|
1509
|
-
standalone: true,
|
|
1510
|
-
imports: [],
|
|
1511
|
-
template: \`
|
|
1512
|
-
<button
|
|
1513
|
-
[class]="buttonClasses"
|
|
1514
|
-
[disabled]="disabled || loading"
|
|
1515
|
-
(click)="handleClick($event)"
|
|
1516
|
-
[type]="type">
|
|
1517
|
-
|
|
1518
|
-
@if (loading) {
|
|
1519
|
-
<svg class="animate-spin -ml-1 mr-3 h-4 w-4" fill="none" viewBox="0 0 24 24">
|
|
1520
|
-
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" class="opacity-25"></circle>
|
|
1521
|
-
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
1522
|
-
</svg>
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
<ng-content></ng-content>
|
|
1526
|
-
</button>
|
|
1527
|
-
\`
|
|
1528
|
-
})
|
|
1529
|
-
export class ButtonComponent {
|
|
1530
|
-
@Input() variant: ButtonVariant = 'primary';
|
|
1531
|
-
@Input() size: ButtonSize = 'md';
|
|
1532
|
-
@Input() disabled = false;
|
|
1533
|
-
@Input() loading = false;
|
|
1534
|
-
@Input() type: 'button' | 'submit' | 'reset' = 'button';
|
|
1535
|
-
@Input() fullWidth = false;
|
|
1536
|
-
|
|
1537
|
-
@Output() clicked = new EventEmitter<Event>();
|
|
1538
|
-
|
|
1539
|
-
get buttonClasses(): string {
|
|
1540
|
-
const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50';
|
|
1541
|
-
|
|
1542
|
-
const sizeClasses = {
|
|
1543
|
-
sm: 'text-sm px-3 py-2 h-9',
|
|
1544
|
-
md: 'text-sm px-4 py-2 h-10',
|
|
1545
|
-
lg: 'text-base px-8 py-3 h-11'
|
|
1546
|
-
};
|
|
1547
|
-
|
|
1548
|
-
const variantClasses = {
|
|
1549
|
-
primary: 'bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500',
|
|
1550
|
-
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-400',
|
|
1551
|
-
outline: 'border-2 border-primary-600 text-primary-600 hover:bg-primary-600 hover:text-white focus:ring-primary-500',
|
|
1552
|
-
ghost: 'hover:bg-gray-100 hover:text-gray-900 focus:ring-gray-400',
|
|
1553
|
-
danger: 'bg-danger-600 text-white hover:bg-danger-700 focus:ring-danger-500'
|
|
1554
|
-
};
|
|
1555
|
-
|
|
1556
|
-
const widthClass = this.fullWidth ? 'w-full' : '';
|
|
1557
|
-
|
|
1558
|
-
return \`\${baseClasses} \${sizeClasses[this.size]} \${variantClasses[this.variant]} \${widthClass}\`.trim();
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
handleClick(event: Event): void {
|
|
1562
|
-
if (!this.disabled && !this.loading) {
|
|
1563
|
-
this.clicked.emit(event);
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
}`;
|
|
1567
|
-
|
|
1568
|
-
await fs.writeFile(
|
|
1569
|
-
path.join(config.fullPath, 'src/app/shared/components/button/button.component.ts'),
|
|
1570
|
-
buttonComponent
|
|
1571
|
-
);
|
|
1572
|
-
|
|
1573
|
-
// Card Component
|
|
1574
|
-
const cardComponent = `import { Component, Input } from '@angular/core';
|
|
1575
|
-
|
|
1576
|
-
@Component({
|
|
1577
|
-
selector: 'app-card',
|
|
1578
|
-
standalone: true,
|
|
1579
|
-
imports: [],
|
|
1580
|
-
template: \`
|
|
1581
|
-
<div [class]="cardClasses">
|
|
1582
|
-
@if (title || subtitle) {
|
|
1583
|
-
<div class="px-6 py-4 border-b border-gray-200">
|
|
1584
|
-
@if (title) {
|
|
1585
|
-
<h3 class="text-lg font-semibold text-gray-900">
|
|
1586
|
-
{{ title }}
|
|
1587
|
-
</h3>
|
|
1588
|
-
}
|
|
1589
|
-
@if (subtitle) {
|
|
1590
|
-
<p class="text-sm text-gray-600 mt-1">
|
|
1591
|
-
{{ subtitle }}
|
|
1592
|
-
</p>
|
|
1593
|
-
}
|
|
1594
|
-
</div>
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
<div [class]="contentClasses">
|
|
1598
|
-
<ng-content></ng-content>
|
|
1599
|
-
</div>
|
|
1600
|
-
|
|
1601
|
-
@if (hasFooter) {
|
|
1602
|
-
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50">
|
|
1603
|
-
<ng-content select="[slot=footer]"></ng-content>
|
|
1604
|
-
</div>
|
|
1605
|
-
}
|
|
1606
|
-
</div>
|
|
1607
|
-
\`
|
|
1608
|
-
})
|
|
1609
|
-
export class CardComponent {
|
|
1610
|
-
@Input() title?: string;
|
|
1611
|
-
@Input() subtitle?: string;
|
|
1612
|
-
@Input() padding = true;
|
|
1613
|
-
@Input() shadow = true;
|
|
1614
|
-
@Input() hover = false;
|
|
1615
|
-
@Input() hasFooter = false;
|
|
1616
|
-
|
|
1617
|
-
get cardClasses(): string {
|
|
1618
|
-
const baseClasses = 'bg-white border border-gray-200 rounded-lg overflow-hidden';
|
|
1619
|
-
const shadowClasses = this.shadow ? 'shadow-sm' : '';
|
|
1620
|
-
const hoverClasses = this.hover ? 'hover:shadow-md transition-shadow duration-200' : '';
|
|
1621
|
-
|
|
1622
|
-
return \`\${baseClasses} \${shadowClasses} \${hoverClasses}\`.trim();
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
get contentClasses(): string {
|
|
1626
|
-
return this.padding ? 'p-6' : '';
|
|
1627
|
-
}
|
|
1628
|
-
}`;
|
|
1629
|
-
|
|
1630
|
-
await fs.writeFile(
|
|
1631
|
-
path.join(config.fullPath, 'src/app/shared/components/card/card.component.ts'),
|
|
1632
|
-
cardComponent
|
|
1633
|
-
);
|
|
1634
|
-
|
|
1635
|
-
// Loading Spinner Component
|
|
1636
|
-
const spinnerComponent = `import { Component, Input } from '@angular/core';
|
|
1637
|
-
|
|
1638
|
-
export type SpinnerSize = 'sm' | 'md' | 'lg' | 'xl';
|
|
1639
|
-
|
|
1640
|
-
@Component({
|
|
1641
|
-
selector: 'app-loading-spinner',
|
|
1642
|
-
standalone: true,
|
|
1643
|
-
imports: [],
|
|
1644
|
-
template: \`
|
|
1645
|
-
<div [class]="containerClasses">
|
|
1646
|
-
<svg [class]="spinnerClasses" fill="none" viewBox="0 0 24 24">
|
|
1647
|
-
<circle
|
|
1648
|
-
cx="12" cy="12" r="10"
|
|
1649
|
-
stroke="currentColor"
|
|
1650
|
-
stroke-width="4"
|
|
1651
|
-
class="opacity-25">
|
|
1652
|
-
</circle>
|
|
1653
|
-
<path
|
|
1654
|
-
class="opacity-75"
|
|
1655
|
-
fill="currentColor"
|
|
1656
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
|
1657
|
-
</path>
|
|
1658
|
-
</svg>
|
|
1659
|
-
@if (text) {
|
|
1660
|
-
<span [class]="textClasses">{{ text }}</span>
|
|
1661
|
-
}
|
|
1662
|
-
</div>
|
|
1663
|
-
\`
|
|
1664
|
-
})
|
|
1665
|
-
export class LoadingSpinnerComponent {
|
|
1666
|
-
@Input() size: SpinnerSize = 'md';
|
|
1667
|
-
@Input() text?: string;
|
|
1668
|
-
@Input() centered = false;
|
|
1669
|
-
@Input() color = 'primary';
|
|
1670
|
-
|
|
1671
|
-
private readonly sizeClasses = {
|
|
1672
|
-
sm: 'h-4 w-4',
|
|
1673
|
-
md: 'h-8 w-8',
|
|
1674
|
-
lg: 'h-12 w-12',
|
|
1675
|
-
xl: 'h-16 w-16'
|
|
1676
|
-
} as const;
|
|
1677
|
-
|
|
1678
|
-
private readonly colorClasses = {
|
|
1679
|
-
primary: 'text-primary-600',
|
|
1680
|
-
secondary: 'text-secondary-600',
|
|
1681
|
-
accent: 'text-accent-600',
|
|
1682
|
-
success: 'text-success-600',
|
|
1683
|
-
danger: 'text-danger-600',
|
|
1684
|
-
warning: 'text-warning-600',
|
|
1685
|
-
info: 'text-info-600',
|
|
1686
|
-
gray: 'text-gray-600',
|
|
1687
|
-
white: 'text-white'
|
|
1688
|
-
} as const;
|
|
1689
|
-
|
|
1690
|
-
protected readonly textClasses = 'text-sm text-gray-600';
|
|
1691
|
-
|
|
1692
|
-
get containerClasses(): string {
|
|
1693
|
-
const baseClasses = 'flex items-center';
|
|
1694
|
-
const centerClasses = this.centered ? 'justify-center' : '';
|
|
1695
|
-
const directionClasses = this.text ? 'flex-col space-y-2' : '';
|
|
1696
|
-
|
|
1697
|
-
return \`\${baseClasses} \${centerClasses} \${directionClasses}\`.trim();
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
get spinnerClasses(): string {
|
|
1701
|
-
const baseClasses = 'animate-spin';
|
|
1702
|
-
return \`\${baseClasses} \${this.sizeClasses[this.size]} \${this.colorClasses[this.color as keyof typeof this.colorClasses] || this.colorClasses.primary}\`.trim();
|
|
1703
|
-
}
|
|
1704
|
-
}`;
|
|
1705
|
-
|
|
1706
|
-
await fs.writeFile(
|
|
1707
|
-
path.join(
|
|
1708
|
-
config.fullPath,
|
|
1709
|
-
'src/app/shared/components/loading-spinner/loading-spinner.component.ts'
|
|
1710
|
-
),
|
|
1711
|
-
spinnerComponent
|
|
1712
|
-
);
|
|
1713
|
-
},
|
|
1714
|
-
|
|
1715
|
-
async createPipes(config) {
|
|
1716
|
-
// Truncate Pipe
|
|
1717
|
-
const truncatePipe = `import { Pipe, PipeTransform } from '@angular/core';
|
|
1718
|
-
|
|
1719
|
-
@Pipe({
|
|
1720
|
-
name: 'truncate',
|
|
1721
|
-
standalone: true
|
|
1722
|
-
})
|
|
1723
|
-
export class TruncatePipe implements PipeTransform {
|
|
1724
|
-
transform(value: string | null | undefined, limit = 50, suffix = '...'): string {
|
|
1725
|
-
if (!value) return '';
|
|
1726
|
-
|
|
1727
|
-
if (value.length <= limit) {
|
|
1728
|
-
return value;
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
return value.substring(0, limit).trim() + suffix;
|
|
1732
|
-
}
|
|
1733
|
-
}`;
|
|
1734
|
-
|
|
1735
|
-
await fs.writeFile(
|
|
1736
|
-
path.join(config.fullPath, 'src/app/shared/pipes/truncate.pipe.ts'),
|
|
1737
|
-
truncatePipe
|
|
1738
|
-
);
|
|
1739
|
-
|
|
1740
|
-
// Time Ago Pipe
|
|
1741
|
-
const timeAgoPipe = `import { Pipe, PipeTransform } from '@angular/core';
|
|
1742
|
-
|
|
1743
|
-
@Pipe({
|
|
1744
|
-
name: 'timeAgo',
|
|
1745
|
-
standalone: true
|
|
1746
|
-
})
|
|
1747
|
-
export class TimeAgoPipe implements PipeTransform {
|
|
1748
|
-
transform(value: string | number | Date | null | undefined): string {
|
|
1749
|
-
if (!value) return '';
|
|
1750
|
-
|
|
1751
|
-
const date = new Date(value);
|
|
1752
|
-
const now = new Date();
|
|
1753
|
-
const secondsAgo = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
1754
|
-
|
|
1755
|
-
if (secondsAgo < 60) {
|
|
1756
|
-
return 'just now';
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
const minutesAgo = Math.floor(secondsAgo / 60);
|
|
1760
|
-
if (minutesAgo < 60) {
|
|
1761
|
-
return \`\${minutesAgo} minute\${minutesAgo === 1 ? '' : 's'} ago\`;
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
const hoursAgo = Math.floor(minutesAgo / 60);
|
|
1765
|
-
if (hoursAgo < 24) {
|
|
1766
|
-
return \`\${hoursAgo} hour\${hoursAgo === 1 ? '' : 's'} ago\`;
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
const daysAgo = Math.floor(hoursAgo / 24);
|
|
1770
|
-
if (daysAgo < 7) {
|
|
1771
|
-
return \`\${daysAgo} day\${daysAgo === 1 ? '' : 's'} ago\`;
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
const weeksAgo = Math.floor(daysAgo / 7);
|
|
1775
|
-
if (weeksAgo < 4) {
|
|
1776
|
-
return \`\${weeksAgo} week\${weeksAgo === 1 ? '' : 's'} ago\`;
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
const monthsAgo = Math.floor(daysAgo / 30);
|
|
1780
|
-
if (monthsAgo < 12) {
|
|
1781
|
-
return \`\${monthsAgo} month\${monthsAgo === 1 ? '' : 's'} ago\`;
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
const yearsAgo = Math.floor(daysAgo / 365);
|
|
1785
|
-
return \`\${yearsAgo} year\${yearsAgo === 1 ? '' : 's'} ago\`;
|
|
1786
|
-
}
|
|
1787
|
-
}`;
|
|
1788
|
-
|
|
1789
|
-
await fs.writeFile(
|
|
1790
|
-
path.join(config.fullPath, 'src/app/shared/pipes/time-ago.pipe.ts'),
|
|
1791
|
-
timeAgoPipe
|
|
1792
|
-
);
|
|
1793
|
-
},
|
|
1794
|
-
|
|
1795
|
-
async createModels(config) {
|
|
1796
|
-
// User Interface
|
|
1797
|
-
const userInterface = `export interface User {
|
|
1798
|
-
id: string;
|
|
1799
|
-
email: string;
|
|
1800
|
-
firstName: string;
|
|
1801
|
-
lastName: string;
|
|
1802
|
-
avatar?: string;
|
|
1803
|
-
role: 'user' | 'admin' | 'moderator';
|
|
1804
|
-
createdAt: Date;
|
|
1805
|
-
updatedAt: Date;
|
|
1806
|
-
isActive: boolean;
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
export interface UserProfile {
|
|
1810
|
-
user: User;
|
|
1811
|
-
preferences: {
|
|
1812
|
-
notifications: boolean;
|
|
1813
|
-
language: string;
|
|
1814
|
-
};
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
export interface CreateUserRequest {
|
|
1818
|
-
email: string;
|
|
1819
|
-
password: string;
|
|
1820
|
-
firstName: string;
|
|
1821
|
-
lastName: string;
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
export interface UpdateUserRequest {
|
|
1825
|
-
firstName?: string;
|
|
1826
|
-
lastName?: string;
|
|
1827
|
-
avatar?: string;
|
|
1828
|
-
}`;
|
|
1829
|
-
|
|
1830
|
-
await fs.writeFile(
|
|
1831
|
-
path.join(config.fullPath, 'src/app/shared/models/user.interface.ts'),
|
|
1832
|
-
userInterface
|
|
1833
|
-
);
|
|
1834
|
-
|
|
1835
|
-
// API Response Interface
|
|
1836
|
-
const apiResponseInterface = `export interface ApiResponse<T = unknown> {
|
|
1837
|
-
data: T;
|
|
1838
|
-
message: string;
|
|
1839
|
-
success: boolean;
|
|
1840
|
-
errors?: string[];
|
|
1841
|
-
meta?: {
|
|
1842
|
-
total: number;
|
|
1843
|
-
page: number;
|
|
1844
|
-
perPage: number;
|
|
1845
|
-
totalPages: number;
|
|
1846
|
-
};
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
export interface ApiError {
|
|
1850
|
-
message: string;
|
|
1851
|
-
code: string;
|
|
1852
|
-
field?: string;
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
export interface PaginationParams {
|
|
1856
|
-
page: number;
|
|
1857
|
-
perPage: number;
|
|
1858
|
-
sortBy?: string;
|
|
1859
|
-
sortOrder?: 'asc' | 'desc';
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
export interface ContactFormData {
|
|
1863
|
-
name: string;
|
|
1864
|
-
email: string;
|
|
1865
|
-
subject: string;
|
|
1866
|
-
message: string;
|
|
1867
|
-
}`;
|
|
1868
|
-
|
|
1869
|
-
await fs.writeFile(
|
|
1870
|
-
path.join(config.fullPath, 'src/app/shared/models/api-response.interface.ts'),
|
|
1871
|
-
apiResponseInterface
|
|
1872
|
-
);
|
|
1873
|
-
},
|
|
1874
|
-
|
|
1875
|
-
async createLayout(config) {
|
|
1876
|
-
// Header Component with Authentication and Language Switcher
|
|
1877
|
-
const headerComponentTs = `import { Component, inject, OnInit } from '@angular/core';
|
|
1878
|
-
import { RouterModule, Router } from '@angular/router';
|
|
1879
|
-
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
1880
|
-
import { heroHome, heroInformationCircle, heroEnvelope, heroUser, heroBars3, heroXMark, heroChevronDown, heroLanguage } from '@ng-icons/heroicons/outline';
|
|
1881
|
-
import { TranslateModule } from '@ngx-translate/core';
|
|
1882
|
-
import { AuthService } from '@core/services/auth.service';
|
|
1883
|
-
import { TranslationService, type SupportedLanguage, type LanguageOption } from '@core/i18n/translation.service';
|
|
1884
|
-
|
|
1885
|
-
interface User {
|
|
1886
|
-
id: string;
|
|
1887
|
-
email: string;
|
|
1888
|
-
firstName: string;
|
|
1889
|
-
lastName: string;
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
@Component({
|
|
1893
|
-
selector: 'app-header',
|
|
1894
|
-
standalone: true,
|
|
1895
|
-
imports: [RouterModule, NgIconComponent, TranslateModule],
|
|
1896
|
-
viewProviders: [provideIcons({ heroHome, heroInformationCircle, heroEnvelope, heroUser, heroBars3, heroXMark, heroChevronDown, heroLanguage })],
|
|
1897
|
-
templateUrl: './header.component.html'
|
|
1898
|
-
})
|
|
1899
|
-
export class HeaderComponent implements OnInit {
|
|
1900
|
-
private authService = inject(AuthService);
|
|
1901
|
-
private router = inject(Router);
|
|
1902
|
-
public translationService = inject(TranslationService);
|
|
1903
|
-
|
|
1904
|
-
protected readonly projectName = '${config.projectName}';
|
|
1905
|
-
|
|
1906
|
-
mobileMenuOpen = false;
|
|
1907
|
-
userMenuOpen = false;
|
|
1908
|
-
langMenuOpen = false;
|
|
1909
|
-
isAuthenticated = false;
|
|
1910
|
-
currentUser: User | null = null;
|
|
1911
|
-
currentLang: SupportedLanguage = 'en';
|
|
1912
|
-
currentLanguage: LanguageOption = this.translationService.languages[0];
|
|
1913
|
-
|
|
1914
|
-
ngOnInit() {
|
|
1915
|
-
// Subscribe to authentication state
|
|
1916
|
-
this.authService.isAuthenticated$.subscribe(
|
|
1917
|
-
isAuth => this.isAuthenticated = isAuth
|
|
1918
|
-
);
|
|
1919
|
-
|
|
1920
|
-
this.authService.currentUser$.subscribe(
|
|
1921
|
-
user => this.currentUser = user
|
|
1922
|
-
);
|
|
1923
|
-
|
|
1924
|
-
// Subscribe to language changes
|
|
1925
|
-
this.currentLang = this.translationService.getCurrentLanguage();
|
|
1926
|
-
const lang = this.translationService.getLanguageOption(this.currentLang);
|
|
1927
|
-
if (lang) {
|
|
1928
|
-
this.currentLanguage = lang;
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
changeLanguage(lang: SupportedLanguage) {
|
|
1933
|
-
this.translationService.setLanguage(lang);
|
|
1934
|
-
this.currentLang = lang;
|
|
1935
|
-
const langOption = this.translationService.getLanguageOption(lang);
|
|
1936
|
-
if (langOption) {
|
|
1937
|
-
this.currentLanguage = langOption;
|
|
1938
|
-
}
|
|
1939
|
-
this.langMenuOpen = false;
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
onLogout() {
|
|
1943
|
-
this.authService.logout();
|
|
1944
|
-
this.userMenuOpen = false;
|
|
1945
|
-
this.router.navigate(['/']);
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
closeAllMenus() {
|
|
1949
|
-
this.mobileMenuOpen = false;
|
|
1950
|
-
this.userMenuOpen = false;
|
|
1951
|
-
this.langMenuOpen = false;
|
|
1952
|
-
}
|
|
1953
|
-
}`;
|
|
1954
|
-
|
|
1955
|
-
const headerComponentHtml = `<header class="sticky top-0 z-50 bg-white/80 backdrop-blur-lg border-b border-gray-200/80">
|
|
1956
|
-
<nav class="container mx-auto px-4 sm:px-6 lg:px-8">
|
|
1957
|
-
<div class="flex items-center justify-between h-16">
|
|
1958
|
-
|
|
1959
|
-
<!-- Logo -->
|
|
1960
|
-
<div class="flex items-center space-x-4">
|
|
1961
|
-
<a routerLink="/" class="flex items-center space-x-3">
|
|
1962
|
-
<div class="flex rounded-lg">
|
|
1963
|
-
<img src="assets/images/logo.svg" alt="Logo" class="h-10 w-10" />
|
|
1964
|
-
<span class="text-white font-bold text-sm">{{ projectName.charAt(0).toUpperCase() }}</span>
|
|
1965
|
-
</div>
|
|
1966
|
-
<span class="text-xl font-semibold text-gray-900">
|
|
1967
|
-
{{ projectName }}
|
|
1968
|
-
</span>
|
|
1969
|
-
</a>
|
|
1970
|
-
</div>
|
|
1971
|
-
|
|
1972
|
-
<!-- Desktop Navigation -->
|
|
1973
|
-
<div class="hidden md:block">
|
|
1974
|
-
<div class="ml-10 flex items-center space-x-8">
|
|
1975
|
-
<a
|
|
1976
|
-
routerLink="/"
|
|
1977
|
-
routerLinkActive="text-primary-600"
|
|
1978
|
-
[routerLinkActiveOptions]="{exact: true}"
|
|
1979
|
-
class="flex items-center gap-1.5 text-gray-700 hover:text-primary-600 transition-colors font-medium">
|
|
1980
|
-
<ng-icon name="heroHome" size="18"></ng-icon>
|
|
1981
|
-
{{ 'nav.home' | translate }}
|
|
1982
|
-
</a>
|
|
1983
|
-
<a
|
|
1984
|
-
routerLink="/about"
|
|
1985
|
-
routerLinkActive="text-primary-600"
|
|
1986
|
-
class="flex items-center gap-1.5 text-gray-700 hover:text-primary-600 transition-colors font-medium">
|
|
1987
|
-
<ng-icon name="heroInformationCircle" size="18"></ng-icon>
|
|
1988
|
-
{{ 'nav.about' | translate }}
|
|
1989
|
-
</a>
|
|
1990
|
-
<a
|
|
1991
|
-
routerLink="/contact"
|
|
1992
|
-
routerLinkActive="text-primary-600"
|
|
1993
|
-
class="flex items-center gap-1.5 text-gray-700 hover:text-primary-600 transition-colors font-medium">
|
|
1994
|
-
<ng-icon name="heroEnvelope" size="18"></ng-icon>
|
|
1995
|
-
{{ 'nav.contact' | translate }}
|
|
1996
|
-
</a>
|
|
1997
|
-
</div>
|
|
1998
|
-
</div>
|
|
1999
|
-
|
|
2000
|
-
<!-- Right side actions -->
|
|
2001
|
-
<div class="flex items-center space-x-4">
|
|
2002
|
-
|
|
2003
|
-
<!-- Language Switcher -->
|
|
2004
|
-
<div class="hidden md:block relative">
|
|
2005
|
-
<button
|
|
2006
|
-
(click)="langMenuOpen = !langMenuOpen"
|
|
2007
|
-
class="flex items-center space-x-2 text-gray-700 hover:text-primary-600 transition-colors px-3 py-2 rounded-lg hover:bg-gray-100"
|
|
2008
|
-
[class.text-primary-600]="langMenuOpen"
|
|
2009
|
-
[class.bg-gray-100]="langMenuOpen">
|
|
2010
|
-
<ng-icon name="heroLanguage" size="20"></ng-icon>
|
|
2011
|
-
<span class="font-medium text-sm">{{ currentLanguage.nativeName }}</span>
|
|
2012
|
-
<ng-icon name="heroChevronDown" size="16"></ng-icon>
|
|
2013
|
-
</button>
|
|
2014
|
-
|
|
2015
|
-
<!-- Language dropdown menu -->
|
|
2016
|
-
@if (langMenuOpen) {
|
|
2017
|
-
<div class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 z-50">
|
|
2018
|
-
<div class="py-1">
|
|
2019
|
-
@for (lang of translationService.languages; track lang.code) {
|
|
2020
|
-
<button
|
|
2021
|
-
(click)="changeLanguage(lang.code)"
|
|
2022
|
-
class="w-full text-left px-4 py-2 text-sm transition-colors flex items-center justify-between"
|
|
2023
|
-
[class.bg-primary-50]="currentLang === lang.code"
|
|
2024
|
-
[class.text-primary-600]="currentLang === lang.code"
|
|
2025
|
-
[class.text-gray-700]="currentLang !== lang.code"
|
|
2026
|
-
[class.hover:bg-gray-100]="currentLang !== lang.code">
|
|
2027
|
-
<span class="flex items-center space-x-2">
|
|
2028
|
-
<span class="text-lg">{{ lang.flag }}</span>
|
|
2029
|
-
<span>{{ lang.nativeName }}</span>
|
|
2030
|
-
</span>
|
|
2031
|
-
@if (currentLang === lang.code) {
|
|
2032
|
-
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
2033
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
2034
|
-
</svg>
|
|
2035
|
-
}
|
|
2036
|
-
</button>
|
|
2037
|
-
}
|
|
2038
|
-
</div>
|
|
2039
|
-
</div>
|
|
2040
|
-
}
|
|
2041
|
-
</div>
|
|
2042
|
-
|
|
2043
|
-
<!-- Authentication Section -->
|
|
2044
|
-
<div class="hidden md:flex items-center space-x-3">
|
|
2045
|
-
<!-- Logged out state -->
|
|
2046
|
-
@if (!isAuthenticated) {
|
|
2047
|
-
<a
|
|
2048
|
-
routerLink="/auth/login"
|
|
2049
|
-
class="text-gray-700 hover:text-primary-600 transition-colors font-medium">
|
|
2050
|
-
{{ 'nav.login' | translate }}
|
|
2051
|
-
</a>
|
|
2052
|
-
<a
|
|
2053
|
-
routerLink="/auth/register"
|
|
2054
|
-
class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg transition-colors font-medium">
|
|
2055
|
-
{{ 'nav.register' | translate }}
|
|
2056
|
-
</a>
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
<!-- Logged in state -->
|
|
2060
|
-
@if (isAuthenticated) {
|
|
2061
|
-
<div class="relative">
|
|
2062
|
-
<button
|
|
2063
|
-
(click)="userMenuOpen = !userMenuOpen"
|
|
2064
|
-
class="flex items-center space-x-2 text-gray-700 hover:text-primary-600 transition-colors"
|
|
2065
|
-
[class.text-primary-600]="userMenuOpen">
|
|
2066
|
-
<div class="h-8 w-8 bg-primary-600 rounded-full flex items-center justify-center">
|
|
2067
|
-
<ng-icon name="heroUser" size="16" style="color: white;"></ng-icon>
|
|
2068
|
-
</div>
|
|
2069
|
-
<span class="font-medium">{{ currentUser?.firstName || 'User' }}</span>
|
|
2070
|
-
<ng-icon name="heroChevronDown" size="16"></ng-icon>
|
|
2071
|
-
</button>
|
|
2072
|
-
|
|
2073
|
-
<!-- User dropdown menu -->
|
|
2074
|
-
@if (userMenuOpen) {
|
|
2075
|
-
<div class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 z-50">
|
|
2076
|
-
<div class="py-1">
|
|
2077
|
-
<div class="px-4 py-2 text-sm text-gray-500 border-b border-gray-100">
|
|
2078
|
-
{{ currentUser?.email }}
|
|
2079
|
-
</div>
|
|
2080
|
-
<button
|
|
2081
|
-
class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors">
|
|
2082
|
-
{{ 'nav.profile' | translate }}
|
|
2083
|
-
</button>
|
|
2084
|
-
<button
|
|
2085
|
-
class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors">
|
|
2086
|
-
{{ 'nav.account' | translate }}
|
|
2087
|
-
</button>
|
|
2088
|
-
<hr class="my-1">
|
|
2089
|
-
<button
|
|
2090
|
-
(click)="onLogout()"
|
|
2091
|
-
class="w-full text-left px-4 py-2 text-sm text-danger-600 hover:bg-danger-50 transition-colors">
|
|
2092
|
-
{{ 'nav.logout' | translate }}
|
|
2093
|
-
</button>
|
|
2094
|
-
</div>
|
|
2095
|
-
</div>
|
|
2096
|
-
}
|
|
2097
|
-
</div>
|
|
2098
|
-
}
|
|
2099
|
-
</div>
|
|
2100
|
-
|
|
2101
|
-
<!-- Mobile menu button -->
|
|
2102
|
-
<button
|
|
2103
|
-
(click)="mobileMenuOpen = !mobileMenuOpen"
|
|
2104
|
-
class="md:hidden p-2 rounded-lg hover:bg-gray-100 transition-colors">
|
|
2105
|
-
@if (!mobileMenuOpen) {
|
|
2106
|
-
<ng-icon name="heroBars3" size="24"></ng-icon>
|
|
2107
|
-
}
|
|
2108
|
-
@if (mobileMenuOpen) {
|
|
2109
|
-
<ng-icon name="heroXMark" size="24"></ng-icon>
|
|
2110
|
-
}
|
|
2111
|
-
</button>
|
|
2112
|
-
</div>
|
|
2113
|
-
</div>
|
|
2114
|
-
|
|
2115
|
-
<!-- Mobile Navigation -->
|
|
2116
|
-
@if (mobileMenuOpen) {
|
|
2117
|
-
<div class="md:hidden pb-4">
|
|
2118
|
-
<div class="flex flex-col space-y-2 mt-4">
|
|
2119
|
-
<!-- Navigation Links -->
|
|
2120
|
-
<a
|
|
2121
|
-
routerLink="/"
|
|
2122
|
-
routerLinkActive="text-primary-600 bg-primary-50"
|
|
2123
|
-
[routerLinkActiveOptions]="{exact: true}"
|
|
2124
|
-
(click)="mobileMenuOpen = false"
|
|
2125
|
-
class="px-3 py-2 rounded-lg text-gray-700 hover:text-primary-600 hover:bg-gray-100 transition-colors font-medium">
|
|
2126
|
-
{{ 'nav.home' | translate }}
|
|
2127
|
-
</a>
|
|
2128
|
-
<a
|
|
2129
|
-
routerLink="/about"
|
|
2130
|
-
routerLinkActive="text-primary-600 bg-primary-50"
|
|
2131
|
-
(click)="mobileMenuOpen = false"
|
|
2132
|
-
class="px-3 py-2 rounded-lg text-gray-700 hover:text-primary-600 hover:bg-gray-100 transition-colors font-medium">
|
|
2133
|
-
{{ 'nav.about' | translate }}
|
|
2134
|
-
</a>
|
|
2135
|
-
<a
|
|
2136
|
-
routerLink="/contact"
|
|
2137
|
-
routerLinkActive="text-primary-600 bg-primary-50"
|
|
2138
|
-
(click)="mobileMenuOpen = false"
|
|
2139
|
-
class="px-3 py-2 rounded-lg text-gray-700 hover:text-primary-600 hover:bg-gray-100 transition-colors font-medium">
|
|
2140
|
-
{{ 'nav.contact' | translate }}
|
|
2141
|
-
</a>
|
|
2142
|
-
|
|
2143
|
-
<!-- Mobile Language Switcher -->
|
|
2144
|
-
<hr class="my-2">
|
|
2145
|
-
<div class="px-3 py-2">
|
|
2146
|
-
<p class="text-xs font-semibold text-gray-500 uppercase mb-2">{{ 'language.select' | translate }}</p>
|
|
2147
|
-
<div class="flex flex-col space-y-1">
|
|
2148
|
-
@for (lang of translationService.languages; track lang.code) {
|
|
2149
|
-
<button
|
|
2150
|
-
(click)="changeLanguage(lang.code)"
|
|
2151
|
-
class="w-full text-left px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between"
|
|
2152
|
-
[class.bg-primary-50]="currentLang === lang.code"
|
|
2153
|
-
[class.text-primary-600]="currentLang === lang.code"
|
|
2154
|
-
[class.text-gray-700]="currentLang !== lang.code"
|
|
2155
|
-
[class.hover:bg-gray-100]="currentLang !== lang.code">
|
|
2156
|
-
<span class="flex items-center space-x-2">
|
|
2157
|
-
<span class="text-lg">{{ lang.flag }}</span>
|
|
2158
|
-
<span>{{ lang.nativeName }}</span>
|
|
2159
|
-
</span>
|
|
2160
|
-
@if (currentLang === lang.code) {
|
|
2161
|
-
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
2162
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
2163
|
-
</svg>
|
|
2164
|
-
}
|
|
2165
|
-
</button>
|
|
2166
|
-
}
|
|
2167
|
-
</div>
|
|
2168
|
-
</div>
|
|
2169
|
-
|
|
2170
|
-
<!-- Mobile Auth Section -->
|
|
2171
|
-
<hr class="my-2">
|
|
2172
|
-
@if (!isAuthenticated) {
|
|
2173
|
-
<a
|
|
2174
|
-
routerLink="/auth/login"
|
|
2175
|
-
(click)="mobileMenuOpen = false"
|
|
2176
|
-
class="px-3 py-2 rounded-lg text-gray-700 hover:text-primary-600 hover:bg-gray-100 transition-colors font-medium">
|
|
2177
|
-
{{ 'nav.login' | translate }}
|
|
2178
|
-
</a>
|
|
2179
|
-
<a
|
|
2180
|
-
routerLink="/auth/register"
|
|
2181
|
-
(click)="mobileMenuOpen = false"
|
|
2182
|
-
class="px-3 py-2 rounded-lg bg-primary-600 text-white hover:bg-primary-700 transition-colors font-medium text-center">
|
|
2183
|
-
{{ 'nav.register' | translate }}
|
|
2184
|
-
</a>
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
@if (isAuthenticated) {
|
|
2188
|
-
<div class="px-3 py-2 text-sm text-gray-500">
|
|
2189
|
-
Welcome, {{ currentUser?.firstName || 'User' }}!
|
|
2190
|
-
</div>
|
|
2191
|
-
<button
|
|
2192
|
-
class="w-full text-left px-3 py-2 rounded-lg text-gray-700 hover:bg-gray-100 transition-colors">
|
|
2193
|
-
{{ 'nav.profile' | translate }}
|
|
2194
|
-
</button>
|
|
2195
|
-
<button
|
|
2196
|
-
(click)="onLogout(); mobileMenuOpen = false"
|
|
2197
|
-
class="w-full text-left px-3 py-2 rounded-lg text-danger-600 hover:bg-danger-50 transition-colors">
|
|
2198
|
-
{{ 'nav.logout' | translate }}
|
|
2199
|
-
</button>
|
|
2200
|
-
}
|
|
2201
|
-
</div>
|
|
2202
|
-
</div>
|
|
2203
|
-
}
|
|
2204
|
-
</nav>
|
|
2205
|
-
</header>
|
|
2206
|
-
|
|
2207
|
-
<!-- Backdrop for mobile menu and user dropdown -->
|
|
2208
|
-
@if (mobileMenuOpen || userMenuOpen || langMenuOpen) {
|
|
2209
|
-
<button
|
|
2210
|
-
(click)="closeAllMenus()"
|
|
2211
|
-
(keydown.escape)="closeAllMenus()"
|
|
2212
|
-
type="button"
|
|
2213
|
-
aria-label="Close menu"
|
|
2214
|
-
class="fixed inset-0 z-40 bg-black/20 backdrop-blur-sm cursor-default">
|
|
2215
|
-
</button>
|
|
2216
|
-
}
|
|
2217
|
-
`;
|
|
2218
|
-
|
|
2219
|
-
await fs.writeFile(
|
|
2220
|
-
path.join(config.fullPath, 'src/app/layout/header/header.component.ts'),
|
|
2221
|
-
headerComponentTs
|
|
2222
|
-
);
|
|
2223
|
-
|
|
2224
|
-
await fs.writeFile(
|
|
2225
|
-
path.join(config.fullPath, 'src/app/layout/header/header.component.html'),
|
|
2226
|
-
headerComponentHtml
|
|
2227
|
-
);
|
|
2228
|
-
|
|
2229
|
-
// Footer Component
|
|
2230
|
-
const footerComponentTs = `import { Component } from '@angular/core';
|
|
2231
|
-
import { TranslateModule } from '@ngx-translate/core';
|
|
2232
|
-
|
|
2233
|
-
@Component({
|
|
2234
|
-
selector: 'app-footer',
|
|
2235
|
-
standalone: true,
|
|
2236
|
-
imports: [TranslateModule],
|
|
2237
|
-
templateUrl: './footer.component.html'
|
|
2238
|
-
})
|
|
2239
|
-
export class FooterComponent {
|
|
2240
|
-
protected readonly projectName = '${config.projectName}';
|
|
2241
|
-
protected readonly currentYear = new Date().getFullYear();
|
|
2242
|
-
}`;
|
|
2243
|
-
|
|
2244
|
-
const footerComponentHtml = `<footer class="bg-white border-t border-gray-200">
|
|
2245
|
-
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
2246
|
-
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
2247
|
-
|
|
2248
|
-
<!-- Company Info -->
|
|
2249
|
-
<div>
|
|
2250
|
-
<div class="flex items-center space-x-3 mb-4">
|
|
2251
|
-
<img src="assets/images/logo.svg" alt="Logo" class="h-10 w-10" />
|
|
2252
|
-
<span class="text-xl font-semibold text-gray-900">
|
|
2253
|
-
{{ projectName }}
|
|
2254
|
-
</span>
|
|
2255
|
-
</div>
|
|
2256
|
-
<p class="text-gray-600 text-sm max-w-md">
|
|
2257
|
-
{{ 'footer.description' | translate }}
|
|
2258
|
-
</p>
|
|
2259
|
-
</div>
|
|
2260
|
-
|
|
2261
|
-
<!-- Quick Links -->
|
|
2262
|
-
<div>
|
|
2263
|
-
<h3 class="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">
|
|
2264
|
-
{{ 'footer.quickLinks' | translate }}
|
|
2265
|
-
</h3>
|
|
2266
|
-
<ul class="space-y-2">
|
|
2267
|
-
<li>
|
|
2268
|
-
<a href="/" class="text-gray-600 hover:text-primary-600 transition-colors text-sm">
|
|
2269
|
-
{{ 'nav.home' | translate }}
|
|
2270
|
-
</a>
|
|
2271
|
-
</li>
|
|
2272
|
-
<li>
|
|
2273
|
-
<a href="/about" class="text-gray-600 hover:text-primary-600 transition-colors text-sm">
|
|
2274
|
-
{{ 'nav.about' | translate }}
|
|
2275
|
-
</a>
|
|
2276
|
-
</li>
|
|
2277
|
-
<li>
|
|
2278
|
-
<a href="/contact" class="text-gray-600 hover:text-primary-600 transition-colors text-sm">
|
|
2279
|
-
{{ 'nav.contact' | translate }}
|
|
2280
|
-
</a>
|
|
2281
|
-
</li>
|
|
2282
|
-
</ul>
|
|
2283
|
-
</div>
|
|
2284
|
-
|
|
2285
|
-
<!-- Tech Stack -->
|
|
2286
|
-
<div>
|
|
2287
|
-
<h3 class="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">
|
|
2288
|
-
{{ 'footer.builtWith' | translate }}
|
|
2289
|
-
</h3>
|
|
2290
|
-
<ul class="space-y-2">
|
|
2291
|
-
<li>
|
|
2292
|
-
<a href="https://angular.dev" target="_blank" class="text-gray-600 hover:text-primary-600 transition-colors text-sm">
|
|
2293
|
-
Angular
|
|
2294
|
-
</a>
|
|
2295
|
-
</li>
|
|
2296
|
-
<li>
|
|
2297
|
-
<a href="https://tailwindcss.com" target="_blank" class="text-gray-600 hover:text-primary-600 transition-colors text-sm">
|
|
2298
|
-
Tailwind CSS
|
|
2299
|
-
</a>
|
|
2300
|
-
</li>
|
|
2301
|
-
<li>
|
|
2302
|
-
<a href="https://typescript.org" target="_blank" class="text-gray-600 hover:text-primary-600 transition-colors text-sm">
|
|
2303
|
-
TypeScript
|
|
2304
|
-
</a>
|
|
2305
|
-
</li>
|
|
2306
|
-
</ul>
|
|
2307
|
-
</div>
|
|
2308
|
-
</div>
|
|
2309
|
-
|
|
2310
|
-
<!-- Bottom Bar -->
|
|
2311
|
-
<div class="border-t border-gray-200 mt-8 pt-8">
|
|
2312
|
-
<p class="text-gray-500 text-sm text-center">
|
|
2313
|
-
{{ 'footer.copyright' | translate:{ year: currentYear, name: projectName } }}
|
|
2314
|
-
</p>
|
|
2315
|
-
</div>
|
|
2316
|
-
</div>
|
|
2317
|
-
</footer>
|
|
2318
|
-
`;
|
|
2319
|
-
|
|
2320
|
-
await fs.writeFile(
|
|
2321
|
-
path.join(config.fullPath, 'src/app/layout/footer/footer.component.ts'),
|
|
2322
|
-
footerComponentTs
|
|
2323
|
-
);
|
|
2324
|
-
|
|
2325
|
-
await fs.writeFile(
|
|
2326
|
-
path.join(config.fullPath, 'src/app/layout/footer/footer.component.html'),
|
|
2327
|
-
footerComponentHtml
|
|
2328
|
-
);
|
|
2329
|
-
},
|
|
2330
|
-
|
|
2331
|
-
async createAuthLayout(config) {
|
|
2332
|
-
// Auth Layout Component
|
|
2333
|
-
const authLayoutComponent = `import { Component } from '@angular/core';
|
|
2334
|
-
import { RouterOutlet } from '@angular/router';
|
|
2335
|
-
import { TranslateModule } from '@ngx-translate/core';
|
|
2336
|
-
|
|
2337
|
-
@Component({
|
|
2338
|
-
selector: 'app-auth-layout',
|
|
2339
|
-
standalone: true,
|
|
2340
|
-
imports: [RouterOutlet, TranslateModule],
|
|
2341
|
-
template: \`
|
|
2342
|
-
<div class="min-h-screen bg-linear-to-br from-primary-500 via-accent-600 to-secondary-700 flex items-center justify-center p-4">
|
|
2343
|
-
<div class="w-full max-w-md">
|
|
2344
|
-
|
|
2345
|
-
<!-- Logo/Brand -->
|
|
2346
|
-
<div class="text-center mb-8">
|
|
2347
|
-
<div class="inline-flex items-center justify-center h-16 w-16 bg-white/20 backdrop-blur-lg rounded-2xl mb-4">
|
|
2348
|
-
<span class="text-2xl font-bold text-white">{{ firstLetter }}</span>
|
|
2349
|
-
</div>
|
|
2350
|
-
<h1 class="text-2xl font-bold text-white">{{ projectName }}</h1>
|
|
2351
|
-
<p class="text-primary-100 mt-2">{{ 'auth.layout.welcome' | translate }}</p>
|
|
2352
|
-
</div>
|
|
2353
|
-
|
|
2354
|
-
<!-- Auth Form Container -->
|
|
2355
|
-
<div class="bg-white/95 backdrop-blur-sm rounded-2xl shadow-xl p-8">
|
|
2356
|
-
<router-outlet />
|
|
2357
|
-
</div>
|
|
2358
|
-
|
|
2359
|
-
<!-- Footer -->
|
|
2360
|
-
<div class="text-center mt-8 text-primary-100 text-sm">
|
|
2361
|
-
<p>{{ 'auth.layout.copyright' | translate:{ year: currentYear, name: projectName } }}</p>
|
|
2362
|
-
</div>
|
|
2363
|
-
</div>
|
|
2364
|
-
</div>
|
|
2365
|
-
\`
|
|
2366
|
-
})
|
|
2367
|
-
export class AuthLayoutComponent {
|
|
2368
|
-
protected readonly projectName = '${config.projectName}';
|
|
2369
|
-
protected readonly firstLetter = '${config.projectName}'.charAt(0).toUpperCase();
|
|
2370
|
-
protected readonly currentYear = new Date().getFullYear();
|
|
2371
|
-
}`;
|
|
2372
|
-
|
|
2373
|
-
await fs.writeFile(
|
|
2374
|
-
path.join(config.fullPath, 'src/app/layout/auth/auth-layout.component.ts'),
|
|
2375
|
-
authLayoutComponent
|
|
2376
|
-
);
|
|
2377
|
-
|
|
2378
|
-
// Login Component
|
|
2379
|
-
const loginComponentTs = `import { Component, inject } from '@angular/core';
|
|
2380
|
-
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
|
|
2381
|
-
import { Router, RouterModule } from '@angular/router';
|
|
2382
|
-
import { TranslateModule } from '@ngx-translate/core';
|
|
2383
|
-
import { ButtonComponent } from '@shared/components/button/button.component';
|
|
2384
|
-
|
|
2385
|
-
@Component({
|
|
2386
|
-
selector: 'app-login',
|
|
2387
|
-
standalone: true,
|
|
2388
|
-
imports: [ReactiveFormsModule, RouterModule, TranslateModule, ButtonComponent],
|
|
2389
|
-
templateUrl: './login.component.html'
|
|
2390
|
-
})
|
|
2391
|
-
export class LoginComponent {
|
|
2392
|
-
private fb = inject(FormBuilder);
|
|
2393
|
-
private router = inject(Router);
|
|
2394
|
-
|
|
2395
|
-
showPassword = false;
|
|
2396
|
-
isLoading = false;
|
|
2397
|
-
errorMessage = '';
|
|
2398
|
-
|
|
2399
|
-
loginForm = this.fb.group({
|
|
2400
|
-
email: ['', [Validators.required, Validators.email]],
|
|
2401
|
-
password: ['', [Validators.required, Validators.minLength(6)]],
|
|
2402
|
-
rememberMe: [false]
|
|
2403
|
-
});
|
|
2404
|
-
|
|
2405
|
-
togglePassword(): void {
|
|
2406
|
-
this.showPassword = !this.showPassword;
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
|
-
onSubmit(): void {
|
|
2410
|
-
if (this.loginForm.valid) {
|
|
2411
|
-
this.isLoading = true;
|
|
2412
|
-
this.errorMessage = '';
|
|
2413
|
-
|
|
2414
|
-
// Simulate login API call
|
|
2415
|
-
setTimeout(() => {
|
|
2416
|
-
this.isLoading = false;
|
|
2417
|
-
|
|
2418
|
-
// Mock login logic - replace with actual authentication service
|
|
2419
|
-
const { email, password } = this.loginForm.value;
|
|
2420
|
-
|
|
2421
|
-
if (email === 'demo@example.com' && password === 'password') {
|
|
2422
|
-
// Store token (mock)
|
|
2423
|
-
localStorage.setItem('auth_token', 'mock_token_12345');
|
|
2424
|
-
this.router.navigate(['/']);
|
|
2425
|
-
} else {
|
|
2426
|
-
this.errorMessage = 'Invalid email or password. Try demo@example.com / password';
|
|
2427
|
-
}
|
|
2428
|
-
}, 1500);
|
|
2429
|
-
} else {
|
|
2430
|
-
// Mark all fields as touched to show validation errors
|
|
2431
|
-
Object.keys(this.loginForm.controls).forEach(key => {
|
|
2432
|
-
this.loginForm.get(key)?.markAsTouched();
|
|
2433
|
-
});
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
}`;
|
|
2437
|
-
|
|
2438
|
-
const loginComponentHtml = `<div class="space-y-6">
|
|
2439
|
-
<div class="text-center">
|
|
2440
|
-
<h2 class="text-2xl font-bold text-gray-900">{{ 'auth.login.title' | translate }}</h2>
|
|
2441
|
-
<p class="text-gray-600 mt-2">{{ 'auth.login.subtitle' | translate }}</p>
|
|
2442
|
-
</div>
|
|
2443
|
-
|
|
2444
|
-
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()" class="space-y-4">
|
|
2445
|
-
|
|
2446
|
-
<!-- Email Field -->
|
|
2447
|
-
<div>
|
|
2448
|
-
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
|
|
2449
|
-
{{ 'auth.login.email' | translate }}
|
|
2450
|
-
</label>
|
|
2451
|
-
<input
|
|
2452
|
-
id="email"
|
|
2453
|
-
type="email"
|
|
2454
|
-
formControlName="email"
|
|
2455
|
-
autocomplete="email"
|
|
2456
|
-
[placeholder]="'auth.login.email' | translate"
|
|
2457
|
-
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-colors"
|
|
2458
|
-
[class.border-danger-500]="loginForm.get('email')?.invalid && loginForm.get('email')?.touched">
|
|
2459
|
-
@if (loginForm.get('email')?.invalid && loginForm.get('email')?.touched) {
|
|
2460
|
-
<div class="mt-1 text-sm text-danger-600">
|
|
2461
|
-
@if (loginForm.get('email')?.errors?.['required']) {
|
|
2462
|
-
<span>{{ 'auth.validation.emailRequired' | translate }}</span>
|
|
2463
|
-
}
|
|
2464
|
-
@if (loginForm.get('email')?.errors?.['email']) {
|
|
2465
|
-
<span>{{ 'auth.validation.emailInvalid' | translate }}</span>
|
|
2466
|
-
}
|
|
2467
|
-
</div>
|
|
2468
|
-
}
|
|
2469
|
-
</div>
|
|
2470
|
-
|
|
2471
|
-
<!-- Password Field -->
|
|
2472
|
-
<div>
|
|
2473
|
-
<label for="password" class="block text-sm font-medium text-gray-700 mb-2">
|
|
2474
|
-
{{ 'auth.login.password' | translate }}
|
|
2475
|
-
</label>
|
|
2476
|
-
<div class="relative">
|
|
2477
|
-
<input
|
|
2478
|
-
id="password"
|
|
2479
|
-
[type]="showPassword ? 'text' : 'password'"
|
|
2480
|
-
formControlName="password"
|
|
2481
|
-
autocomplete="current-password"
|
|
2482
|
-
[placeholder]="'auth.login.password' | translate"
|
|
2483
|
-
class="w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-colors"
|
|
2484
|
-
[class.border-danger-500]="loginForm.get('password')?.invalid && loginForm.get('password')?.touched">
|
|
2485
|
-
<button
|
|
2486
|
-
type="button"
|
|
2487
|
-
(click)="togglePassword()"
|
|
2488
|
-
[attr.aria-label]="showPassword ? ('auth.login.hidePassword' | translate) : ('auth.login.showPassword' | translate)"
|
|
2489
|
-
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500 hover:text-gray-700">
|
|
2490
|
-
@if (!showPassword) {
|
|
2491
|
-
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
2492
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
2493
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
|
2494
|
-
</svg>
|
|
2495
|
-
}
|
|
2496
|
-
@if (showPassword) {
|
|
2497
|
-
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
2498
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"></path>
|
|
2499
|
-
</svg>
|
|
2500
|
-
}
|
|
2501
|
-
</button>
|
|
2502
|
-
</div>
|
|
2503
|
-
@if (loginForm.get('password')?.invalid && loginForm.get('password')?.touched) {
|
|
2504
|
-
<div class="mt-1 text-sm text-danger-600">
|
|
2505
|
-
@if (loginForm.get('password')?.errors?.['required']) {
|
|
2506
|
-
<span>{{ 'auth.validation.passwordRequired' | translate }}</span>
|
|
2507
|
-
}
|
|
2508
|
-
@if (loginForm.get('password')?.errors?.['minlength']) {
|
|
2509
|
-
<span>{{ 'auth.validation.passwordMinLength' | translate }}</span>
|
|
2510
|
-
}
|
|
2511
|
-
</div>
|
|
2512
|
-
}
|
|
2513
|
-
</div>
|
|
2514
|
-
|
|
2515
|
-
<!-- Remember Me & Forgot Password -->
|
|
2516
|
-
<div class="flex items-center justify-between">
|
|
2517
|
-
<label class="flex items-center">
|
|
2518
|
-
<input
|
|
2519
|
-
type="checkbox"
|
|
2520
|
-
formControlName="rememberMe"
|
|
2521
|
-
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded">
|
|
2522
|
-
<span class="ml-2 rtl:mr-2 text-sm text-gray-600">{{ 'auth.login.rememberMe' | translate }}</span>
|
|
2523
|
-
</label>
|
|
2524
|
-
<a routerLink="/auth/forgot-password" class="text-sm text-primary-600 hover:text-primary-700">
|
|
2525
|
-
{{ 'auth.login.forgotPassword' | translate }}
|
|
2526
|
-
</a>
|
|
2527
|
-
</div>
|
|
2528
|
-
|
|
2529
|
-
<!-- Submit Button -->
|
|
2530
|
-
<app-button
|
|
2531
|
-
type="submit"
|
|
2532
|
-
[fullWidth]="true"
|
|
2533
|
-
[loading]="isLoading"
|
|
2534
|
-
[disabled]="loginForm.invalid">
|
|
2535
|
-
{{ (isLoading ? 'auth.login.signing' : 'auth.login.submit') | translate }}
|
|
2536
|
-
</app-button>
|
|
2537
|
-
|
|
2538
|
-
<!-- Error Message -->
|
|
2539
|
-
@if (errorMessage) {
|
|
2540
|
-
<div class="p-3 bg-danger-100 border border-danger-300 text-danger-700 rounded-lg text-sm">
|
|
2541
|
-
{{ errorMessage }}
|
|
2542
|
-
</div>
|
|
2543
|
-
}
|
|
2544
|
-
</form>
|
|
2545
|
-
|
|
2546
|
-
<!-- Sign Up Link -->
|
|
2547
|
-
<div class="text-center pt-4 border-t border-gray-200">
|
|
2548
|
-
<p class="text-sm text-gray-600">
|
|
2549
|
-
{{ 'auth.login.noAccount' | translate }}
|
|
2550
|
-
<a routerLink="/auth/register" class="text-primary-600 hover:text-primary-700 font-medium">
|
|
2551
|
-
{{ 'auth.login.createAccount' | translate }}
|
|
2552
|
-
</a>
|
|
2553
|
-
</p>
|
|
2554
|
-
</div>
|
|
2555
|
-
</div>
|
|
2556
|
-
`;
|
|
2557
|
-
|
|
2558
|
-
await fs.writeFile(
|
|
2559
|
-
path.join(config.fullPath, 'src/app/features/auth/login/login.component.ts'),
|
|
2560
|
-
loginComponentTs
|
|
2561
|
-
);
|
|
2562
|
-
|
|
2563
|
-
await fs.writeFile(
|
|
2564
|
-
path.join(config.fullPath, 'src/app/features/auth/login/login.component.html'),
|
|
2565
|
-
loginComponentHtml
|
|
2566
|
-
);
|
|
2567
|
-
|
|
2568
|
-
// Register Component
|
|
2569
|
-
const registerComponentTs = `import { Component, inject } from '@angular/core';
|
|
2570
|
-
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
|
|
2571
|
-
import { RouterModule } from '@angular/router';
|
|
2572
|
-
import { TranslateModule } from '@ngx-translate/core';
|
|
2573
|
-
import { ButtonComponent } from '@shared/components/button/button.component';
|
|
2574
|
-
|
|
2575
|
-
@Component({
|
|
2576
|
-
selector: 'app-register',
|
|
2577
|
-
standalone: true,
|
|
2578
|
-
imports: [ReactiveFormsModule, RouterModule, TranslateModule, ButtonComponent],
|
|
2579
|
-
templateUrl: './register.component.html'
|
|
2580
|
-
})
|
|
2581
|
-
export class RegisterComponent {
|
|
2582
|
-
private fb = inject(FormBuilder);
|
|
2583
|
-
registerForm = this.fb.group({
|
|
2584
|
-
firstName: ['', Validators.required],
|
|
2585
|
-
lastName: ['', Validators.required],
|
|
2586
|
-
email: ['', [Validators.required, Validators.email]],
|
|
2587
|
-
password: ['', [Validators.required, Validators.minLength(6)]]
|
|
2588
|
-
});
|
|
2589
|
-
onSubmit() { /* Implementation */ }
|
|
2590
|
-
}`;
|
|
2591
|
-
|
|
2592
|
-
const registerComponentHtml = `<div class="space-y-6">
|
|
2593
|
-
<div class="text-center">
|
|
2594
|
-
<h2 class="text-2xl font-bold text-gray-900">{{ 'auth.register.title' | translate }}</h2>
|
|
2595
|
-
<p class="text-gray-600 mt-2">{{ 'auth.register.subtitle' | translate }}</p>
|
|
2596
|
-
</div>
|
|
2597
|
-
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()" class="space-y-4">
|
|
2598
|
-
<div class="grid grid-cols-2 gap-4">
|
|
2599
|
-
<input
|
|
2600
|
-
type="text"
|
|
2601
|
-
formControlName="firstName"
|
|
2602
|
-
autocomplete="given-name"
|
|
2603
|
-
[placeholder]="'auth.register.firstName' | translate"
|
|
2604
|
-
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
|
|
2605
|
-
<input
|
|
2606
|
-
type="text"
|
|
2607
|
-
formControlName="lastName"
|
|
2608
|
-
autocomplete="family-name"
|
|
2609
|
-
[placeholder]="'auth.register.lastName' | translate"
|
|
2610
|
-
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
|
|
2611
|
-
</div>
|
|
2612
|
-
<input
|
|
2613
|
-
type="email"
|
|
2614
|
-
formControlName="email"
|
|
2615
|
-
autocomplete="email"
|
|
2616
|
-
[placeholder]="'auth.register.email' | translate"
|
|
2617
|
-
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
|
|
2618
|
-
<input
|
|
2619
|
-
type="password"
|
|
2620
|
-
formControlName="password"
|
|
2621
|
-
autocomplete="new-password"
|
|
2622
|
-
[placeholder]="'auth.register.password' | translate"
|
|
2623
|
-
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
|
|
2624
|
-
<app-button type="submit" [fullWidth]="true" [disabled]="registerForm.invalid">
|
|
2625
|
-
{{ 'auth.register.submit' | translate }}
|
|
2626
|
-
</app-button>
|
|
2627
|
-
<div class="text-center">
|
|
2628
|
-
<a routerLink="/auth/login" class="text-primary-600 hover:text-primary-700">
|
|
2629
|
-
{{ 'auth.register.hasAccount' | translate }}
|
|
2630
|
-
</a>
|
|
2631
|
-
</div>
|
|
2632
|
-
</form>
|
|
2633
|
-
</div>
|
|
2634
|
-
`;
|
|
2635
|
-
|
|
2636
|
-
await fs.writeFile(
|
|
2637
|
-
path.join(config.fullPath, 'src/app/features/auth/register/register.component.ts'),
|
|
2638
|
-
registerComponentTs
|
|
2639
|
-
);
|
|
2640
|
-
|
|
2641
|
-
await fs.writeFile(
|
|
2642
|
-
path.join(config.fullPath, 'src/app/features/auth/register/register.component.html'),
|
|
2643
|
-
registerComponentHtml
|
|
2644
|
-
);
|
|
2645
|
-
|
|
2646
|
-
// Forgot Password Component
|
|
2647
|
-
const forgotPasswordComponentTs = `import { Component, inject } from '@angular/core';
|
|
2648
|
-
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
|
|
2649
|
-
import { RouterModule } from '@angular/router';
|
|
2650
|
-
import { TranslateModule } from '@ngx-translate/core';
|
|
2651
|
-
import { ButtonComponent } from '@shared/components/button/button.component';
|
|
2652
|
-
|
|
2653
|
-
@Component({
|
|
2654
|
-
selector: 'app-forgot-password',
|
|
2655
|
-
standalone: true,
|
|
2656
|
-
imports: [ReactiveFormsModule, RouterModule, TranslateModule, ButtonComponent],
|
|
2657
|
-
templateUrl: './forgot-password.component.html'
|
|
2658
|
-
})
|
|
2659
|
-
export class ForgotPasswordComponent {
|
|
2660
|
-
private fb = inject(FormBuilder);
|
|
2661
|
-
forgotForm = this.fb.group({ email: ['', [Validators.required, Validators.email]] });
|
|
2662
|
-
onSubmit() { /* Implementation */ }
|
|
2663
|
-
}`;
|
|
2664
|
-
|
|
2665
|
-
const forgotPasswordComponentHtml = `<div class="space-y-6">
|
|
2666
|
-
<div class="text-center">
|
|
2667
|
-
<h2 class="text-2xl font-bold text-gray-900">{{ 'auth.forgot.title' | translate }}</h2>
|
|
2668
|
-
<p class="text-gray-600 mt-2">{{ 'auth.forgot.subtitle' | translate }}</p>
|
|
2669
|
-
</div>
|
|
2670
|
-
<form [formGroup]="forgotForm" (ngSubmit)="onSubmit()" class="space-y-4">
|
|
2671
|
-
<input
|
|
2672
|
-
type="email"
|
|
2673
|
-
formControlName="email"
|
|
2674
|
-
autocomplete="email"
|
|
2675
|
-
[placeholder]="'auth.forgot.email' | translate"
|
|
2676
|
-
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent">
|
|
2677
|
-
<app-button type="submit" [fullWidth]="true" [disabled]="forgotForm.invalid">
|
|
2678
|
-
{{ 'auth.forgot.submit' | translate }}
|
|
2679
|
-
</app-button>
|
|
2680
|
-
<div class="text-center">
|
|
2681
|
-
<a routerLink="/auth/login" class="text-primary-600 hover:text-primary-700">
|
|
2682
|
-
{{ 'auth.forgot.backToLogin' | translate }}
|
|
2683
|
-
</a>
|
|
2684
|
-
</div>
|
|
2685
|
-
</form>
|
|
2686
|
-
</div>
|
|
2687
|
-
`;
|
|
2688
|
-
|
|
2689
|
-
await fs.writeFile(
|
|
2690
|
-
path.join(
|
|
2691
|
-
config.fullPath,
|
|
2692
|
-
'src/app/features/auth/forgot-password/forgot-password.component.ts'
|
|
2693
|
-
),
|
|
2694
|
-
forgotPasswordComponentTs
|
|
2695
|
-
);
|
|
2696
|
-
|
|
2697
|
-
await fs.writeFile(
|
|
2698
|
-
path.join(
|
|
2699
|
-
config.fullPath,
|
|
2700
|
-
'src/app/features/auth/forgot-password/forgot-password.component.html'
|
|
2701
|
-
),
|
|
2702
|
-
forgotPasswordComponentHtml
|
|
2703
|
-
);
|
|
2704
|
-
},
|
|
2705
|
-
|
|
2706
|
-
async createFeatures(config) {
|
|
2707
|
-
await this.createHomeComponent(config);
|
|
2708
|
-
await this.createAboutComponent(config);
|
|
2709
|
-
await createContactComponent(config);
|
|
2710
|
-
},
|
|
2711
|
-
|
|
2712
|
-
async createRouting(config) {
|
|
2713
|
-
await createRouting(config);
|
|
2714
|
-
},
|
|
2715
|
-
|
|
2716
|
-
async createAppConfig(config) {
|
|
2717
|
-
await createAppConfig(config);
|
|
2718
|
-
},
|
|
2719
|
-
|
|
2720
|
-
async createAppComponent(config) {
|
|
2721
|
-
await createAppComponent(config);
|
|
2722
|
-
},
|
|
2723
|
-
|
|
2724
|
-
async createStyles(config) {
|
|
2725
|
-
await createStyles(config);
|
|
2726
|
-
},
|
|
2727
|
-
|
|
2728
122
|
async createAssets(config) {
|
|
2729
|
-
// Create a simple logo placeholder
|
|
2730
123
|
const logoSvg = `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
|
2731
124
|
<rect width="100" height="100" fill="#3B82F6" rx="20"/>
|
|
2732
125
|
<text x="50" y="60" text-anchor="middle" fill="white" font-size="24" font-family="Arial, sans-serif" font-weight="bold">
|
|
@@ -2736,1407 +129,6 @@ export class ForgotPasswordComponent {
|
|
|
2736
129
|
|
|
2737
130
|
await fs.writeFile(path.join(config.fullPath, 'public/assets/images/logo.svg'), logoSvg);
|
|
2738
131
|
},
|
|
2739
|
-
|
|
2740
|
-
async createVSCodeSettings(config) {
|
|
2741
|
-
// Create .vscode directory
|
|
2742
|
-
await fs.ensureDir(path.join(config.fullPath, '.vscode'));
|
|
2743
|
-
|
|
2744
|
-
// VSCode settings for format on save
|
|
2745
|
-
const vscodeSettings = {
|
|
2746
|
-
'editor.formatOnSave': true,
|
|
2747
|
-
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
2748
|
-
'editor.codeActionsOnSave': {
|
|
2749
|
-
'source.fixAll.eslint': 'explicit',
|
|
2750
|
-
},
|
|
2751
|
-
'[typescript]': {
|
|
2752
|
-
'editor.formatOnSave': true,
|
|
2753
|
-
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
2754
|
-
},
|
|
2755
|
-
'[html]': {
|
|
2756
|
-
'editor.formatOnSave': true,
|
|
2757
|
-
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
2758
|
-
},
|
|
2759
|
-
'[css]': {
|
|
2760
|
-
'editor.formatOnSave': true,
|
|
2761
|
-
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
2762
|
-
},
|
|
2763
|
-
'[json]': {
|
|
2764
|
-
'editor.formatOnSave': true,
|
|
2765
|
-
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
2766
|
-
},
|
|
2767
|
-
'[jsonc]': {
|
|
2768
|
-
'editor.formatOnSave': true,
|
|
2769
|
-
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
2770
|
-
},
|
|
2771
|
-
'files.eol': '\n',
|
|
2772
|
-
'files.trimTrailingWhitespace': true,
|
|
2773
|
-
'files.insertFinalNewline': true,
|
|
2774
|
-
'editor.tabSize': 2,
|
|
2775
|
-
'prettier.requireConfig': false,
|
|
2776
|
-
'eslint.validate': ['javascript', 'typescript', 'html'],
|
|
2777
|
-
// Suppress CSS linting warnings for Tailwind CSS directives
|
|
2778
|
-
'css.lint.unknownAtRules': 'ignore',
|
|
2779
|
-
};
|
|
2780
|
-
|
|
2781
|
-
// VSCode extensions recommendations
|
|
2782
|
-
const vscodeExtensions = {
|
|
2783
|
-
recommendations: [
|
|
2784
|
-
'esbenp.prettier-vscode',
|
|
2785
|
-
'dbaeumer.vscode-eslint',
|
|
2786
|
-
'angular.ng-template',
|
|
2787
|
-
'bradlc.vscode-tailwindcss',
|
|
2788
|
-
],
|
|
2789
|
-
};
|
|
2790
|
-
|
|
2791
|
-
// Write settings.json
|
|
2792
|
-
await fs.writeFile(
|
|
2793
|
-
path.join(config.fullPath, '.vscode/settings.json'),
|
|
2794
|
-
JSON.stringify(vscodeSettings, null, 2)
|
|
2795
|
-
);
|
|
2796
|
-
|
|
2797
|
-
// Write extensions.json
|
|
2798
|
-
await fs.writeFile(
|
|
2799
|
-
path.join(config.fullPath, '.vscode/extensions.json'),
|
|
2800
|
-
JSON.stringify(vscodeExtensions, null, 2)
|
|
2801
|
-
);
|
|
2802
|
-
},
|
|
2803
|
-
|
|
2804
|
-
async createHomeComponent(config) {
|
|
2805
|
-
const homeComponentTs = `import { Component, OnInit, inject } from '@angular/core';
|
|
2806
|
-
import { RouterModule } from '@angular/router';
|
|
2807
|
-
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
2808
|
-
import {
|
|
2809
|
-
heroRocketLaunch,
|
|
2810
|
-
heroPaintBrush,
|
|
2811
|
-
heroShieldCheck,
|
|
2812
|
-
heroBolt,
|
|
2813
|
-
heroCodeBracket,
|
|
2814
|
-
heroUsers,
|
|
2815
|
-
heroGlobeAlt,
|
|
2816
|
-
heroCog,
|
|
2817
|
-
heroCheckCircle,
|
|
2818
|
-
heroServer,
|
|
2819
|
-
heroFolder,
|
|
2820
|
-
heroCube
|
|
2821
|
-
} from '@ng-icons/heroicons/outline';
|
|
2822
|
-
import { TranslateModule } from '@ngx-translate/core';
|
|
2823
|
-
import { ButtonComponent } from '@shared/components/button/button.component';
|
|
2824
|
-
import { CardComponent } from '@shared/components/card/card.component';
|
|
2825
|
-
import { ToastService } from '@core/services/toast.service';
|
|
2826
|
-
import { ModalService } from '@core/services/modal.service';
|
|
2827
|
-
import { SeoService } from '@core/services/seo.service';
|
|
2828
|
-
|
|
2829
|
-
@Component({
|
|
2830
|
-
selector: 'app-home',
|
|
2831
|
-
standalone: true,
|
|
2832
|
-
imports: [RouterModule, NgIconComponent, TranslateModule, ButtonComponent, CardComponent],
|
|
2833
|
-
viewProviders: [provideIcons({
|
|
2834
|
-
heroRocketLaunch,
|
|
2835
|
-
heroPaintBrush,
|
|
2836
|
-
heroShieldCheck,
|
|
2837
|
-
heroBolt,
|
|
2838
|
-
heroCodeBracket,
|
|
2839
|
-
heroUsers,
|
|
2840
|
-
heroGlobeAlt,
|
|
2841
|
-
heroCog,
|
|
2842
|
-
heroCheckCircle,
|
|
2843
|
-
heroServer,
|
|
2844
|
-
heroFolder,
|
|
2845
|
-
heroCube
|
|
2846
|
-
})],
|
|
2847
|
-
templateUrl: './home.component.html'
|
|
2848
|
-
})
|
|
2849
|
-
export class HomeComponent implements OnInit {
|
|
2850
|
-
protected readonly projectName = '${config.projectName}';
|
|
2851
|
-
|
|
2852
|
-
private toast = inject(ToastService);
|
|
2853
|
-
private modal = inject(ModalService);
|
|
2854
|
-
private seo = inject(SeoService);
|
|
2855
|
-
|
|
2856
|
-
ngOnInit(): void {
|
|
2857
|
-
// Set SEO meta tags for Home page
|
|
2858
|
-
this.seo.updateMeta({
|
|
2859
|
-
title: 'Home',
|
|
2860
|
-
description: 'Welcome to ${config.projectName} - A modern Angular application with Tailwind CSS, SEO optimization, and i18n support.',
|
|
2861
|
-
keywords: 'angular, tailwind, seo, i18n, typescript',
|
|
2862
|
-
ogType: 'website'
|
|
2863
|
-
});
|
|
2864
|
-
}
|
|
2865
|
-
|
|
2866
|
-
// Toast Examples
|
|
2867
|
-
showSuccessToast(): void {
|
|
2868
|
-
this.toast.success('Operation completed successfully!');
|
|
2869
|
-
}
|
|
2870
|
-
|
|
2871
|
-
showErrorToast(): void {
|
|
2872
|
-
this.toast.error('Something went wrong. Please try again.');
|
|
2873
|
-
}
|
|
2874
|
-
|
|
2875
|
-
showWarningToast(): void {
|
|
2876
|
-
this.toast.warning('This action requires your attention.');
|
|
2877
|
-
}
|
|
2878
|
-
|
|
2879
|
-
showInfoToast(): void {
|
|
2880
|
-
this.toast.info('Here is some useful information.');
|
|
2881
|
-
}
|
|
2882
|
-
|
|
2883
|
-
// Modal Examples
|
|
2884
|
-
async showConfirmModal(): Promise<void> {
|
|
2885
|
-
const confirmed = await this.modal.confirm(
|
|
2886
|
-
'Are you sure you want to proceed with this action?',
|
|
2887
|
-
'Confirm Action'
|
|
2888
|
-
);
|
|
2889
|
-
|
|
2890
|
-
if (confirmed) {
|
|
2891
|
-
this.toast.success('Action confirmed!');
|
|
2892
|
-
} else {
|
|
2893
|
-
this.toast.info('Action cancelled');
|
|
2894
|
-
}
|
|
2895
|
-
}
|
|
2896
|
-
|
|
2897
|
-
async showAlertModal(): Promise<void> {
|
|
2898
|
-
await this.modal.alert(
|
|
2899
|
-
'This is an alert message. Click OK to close.',
|
|
2900
|
-
'Alert'
|
|
2901
|
-
);
|
|
2902
|
-
this.toast.info('Alert closed');
|
|
2903
|
-
}
|
|
2904
|
-
}`;
|
|
2905
|
-
|
|
2906
|
-
const homeComponentHtml = `<!-- Hero Section -->
|
|
2907
|
-
<section class="relative py-20 bg-linear-to-br from-primary-600 via-secondary-600 to-accent-700 overflow-hidden">
|
|
2908
|
-
<!-- Animated Background Elements -->
|
|
2909
|
-
<div class="absolute inset-0 opacity-20">
|
|
2910
|
-
<div class="absolute top-0 -left-4 w-72 h-72 bg-accent-300 rounded-full mix-blend-multiply filter blur-xl animate-blob"></div>
|
|
2911
|
-
<div class="absolute top-0 -right-4 w-72 h-72 bg-warning-300 rounded-full mix-blend-multiply filter blur-xl animate-blob animation-delay-2000"></div>
|
|
2912
|
-
<div class="absolute -bottom-8 left-20 w-72 h-72 bg-accent-400 rounded-full mix-blend-multiply filter blur-xl animate-blob animation-delay-4000"></div>
|
|
2913
|
-
</div>
|
|
2914
|
-
|
|
2915
|
-
<div class="relative container mx-auto px-4 text-center">
|
|
2916
|
-
<div class="animate-fade-in">
|
|
2917
|
-
<h1 class="text-4xl md:text-6xl lg:text-7xl font-bold text-white mb-6 drop-shadow-lg">
|
|
2918
|
-
{{ 'home.hero.title' | translate:{ projectName: projectName } }}
|
|
2919
|
-
</h1>
|
|
2920
|
-
<p class="text-lg md:text-xl lg:text-2xl text-primary-50 mb-8 max-w-3xl mx-auto leading-relaxed">
|
|
2921
|
-
{{ 'home.hero.subtitle' | translate }}
|
|
2922
|
-
</p>
|
|
2923
|
-
<div class="flex flex-col sm:flex-row gap-4 justify-center animate-slide-up">
|
|
2924
|
-
<app-button size="lg" class="transform hover:scale-105 transition-transform duration-200">
|
|
2925
|
-
<span class="flex items-center gap-2">
|
|
2926
|
-
<ng-icon name="heroRocketLaunch" size="20"></ng-icon>
|
|
2927
|
-
{{ 'home.hero.button' | translate }}
|
|
2928
|
-
</span>
|
|
2929
|
-
</app-button>
|
|
2930
|
-
<app-button size="lg" variant="secondary" [routerLink]="['/about']" class="transform hover:scale-105 transition-transform duration-200">
|
|
2931
|
-
<span class="flex items-center gap-2">
|
|
2932
|
-
{{ 'home.hero.learnMore' | translate }}
|
|
2933
|
-
<ng-icon name="heroShieldCheck" size="20"></ng-icon>
|
|
2934
|
-
</span>
|
|
2935
|
-
</app-button>
|
|
2936
|
-
</div>
|
|
2937
|
-
</div>
|
|
2938
|
-
</div>
|
|
2939
|
-
</section>
|
|
2940
|
-
|
|
2941
|
-
<!-- What's Included Section -->
|
|
2942
|
-
<section class="py-20 bg-white">
|
|
2943
|
-
<div class="container mx-auto px-4">
|
|
2944
|
-
<div class="text-center mb-16">
|
|
2945
|
-
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
|
|
2946
|
-
{{ 'home.productionReady.sectionTitle' | translate }}
|
|
2947
|
-
</h2>
|
|
2948
|
-
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
|
2949
|
-
{{ 'home.productionReady.title' | translate }}
|
|
2950
|
-
</p>
|
|
2951
|
-
</div>
|
|
2952
|
-
|
|
2953
|
-
<!-- Core Features Grid -->
|
|
2954
|
-
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
|
|
2955
|
-
<!-- Modern Architecture -->
|
|
2956
|
-
<app-card [hover]="true" [shadow]="true">
|
|
2957
|
-
<div class="flex items-start space-x-4">
|
|
2958
|
-
<div class="shrink-0">
|
|
2959
|
-
<div class="w-12 h-12 rounded-lg bg-primary-100 flex items-center justify-center">
|
|
2960
|
-
<ng-icon name="heroRocketLaunch" size="24" style="color: var(--color-primary-600);"></ng-icon>
|
|
2961
|
-
</div>
|
|
2962
|
-
</div>
|
|
2963
|
-
<div>
|
|
2964
|
-
<h3 class="text-lg font-semibold text-gray-900 mb-2">Modern Architecture</h3>
|
|
2965
|
-
<p class="text-sm text-gray-600">Standalone components, Signals, Zoneless support, Angular 20+</p>
|
|
2966
|
-
</div>
|
|
2967
|
-
</div>
|
|
2968
|
-
</app-card>
|
|
2969
|
-
|
|
2970
|
-
<!-- i18n System -->
|
|
2971
|
-
<app-card [hover]="true" [shadow]="true">
|
|
2972
|
-
<div class="flex items-start space-x-4">
|
|
2973
|
-
<div class="shrink-0">
|
|
2974
|
-
<div class="w-12 h-12 rounded-lg bg-accent-100 flex items-center justify-center">
|
|
2975
|
-
<ng-icon name="heroGlobeAlt" size="24" style="color: var(--color-accent-600);"></ng-icon>
|
|
2976
|
-
</div>
|
|
2977
|
-
</div>
|
|
2978
|
-
<div>
|
|
2979
|
-
<h3 class="text-lg font-semibold text-gray-900 mb-2">i18n Translation</h3>
|
|
2980
|
-
<p class="text-sm text-gray-600">English & Arabic with RTL support, language switcher in header</p>
|
|
2981
|
-
</div>
|
|
2982
|
-
</div>
|
|
2983
|
-
</app-card>
|
|
2984
|
-
|
|
2985
|
-
<!-- HTTP Interceptors -->
|
|
2986
|
-
<app-card [hover]="true" [shadow]="true">
|
|
2987
|
-
<div class="flex items-start space-x-4">
|
|
2988
|
-
<div class="shrink-0">
|
|
2989
|
-
<div class="w-12 h-12 rounded-lg bg-success-100 flex items-center justify-center">
|
|
2990
|
-
<ng-icon name="heroServer" size="24" style="color: var(--color-success-600);"></ng-icon>
|
|
2991
|
-
</div>
|
|
2992
|
-
</div>
|
|
2993
|
-
<div>
|
|
2994
|
-
<h3 class="text-lg font-semibold text-gray-900 mb-2">HTTP Interceptors</h3>
|
|
2995
|
-
<p class="text-sm text-gray-600">Auth, Error handling, Loading state, Response caching (5min TTL)</p>
|
|
2996
|
-
</div>
|
|
2997
|
-
</div>
|
|
2998
|
-
</app-card>
|
|
2999
|
-
|
|
3000
|
-
<!-- Tailwind CSS -->
|
|
3001
|
-
<app-card [hover]="true" [shadow]="true">
|
|
3002
|
-
<div class="flex items-start space-x-4">
|
|
3003
|
-
<div class="shrink-0">
|
|
3004
|
-
<div class="w-12 h-12 rounded-lg bg-secondary-100 flex items-center justify-center">
|
|
3005
|
-
<ng-icon name="heroPaintBrush" size="24" style="color: var(--color-secondary-600);"></ng-icon>
|
|
3006
|
-
</div>
|
|
3007
|
-
</div>
|
|
3008
|
-
<div>
|
|
3009
|
-
<h3 class="text-lg font-semibold text-gray-900 mb-2">Tailwind CSS v4</h3>
|
|
3010
|
-
<p class="text-sm text-gray-600">Modern PostCSS setup, responsive utilities, dark mode ready</p>
|
|
3011
|
-
</div>
|
|
3012
|
-
</div>
|
|
3013
|
-
</app-card>
|
|
3014
|
-
|
|
3015
|
-
<!-- UI Components -->
|
|
3016
|
-
<app-card [hover]="true" [shadow]="true">
|
|
3017
|
-
<div class="flex items-start space-x-4">
|
|
3018
|
-
<div class="shrink-0">
|
|
3019
|
-
<div class="w-12 h-12 rounded-lg bg-accent-100 flex items-center justify-center">
|
|
3020
|
-
<ng-icon name="heroCube" size="24" style="color: var(--color-accent-600);"></ng-icon>
|
|
3021
|
-
</div>
|
|
3022
|
-
</div>
|
|
3023
|
-
<div>
|
|
3024
|
-
<h3 class="text-lg font-semibold text-gray-900 mb-2">UI Components</h3>
|
|
3025
|
-
<p class="text-sm text-gray-600">Button, Card, Spinner, Toast, Modal with full customization</p>
|
|
3026
|
-
</div>
|
|
3027
|
-
</div>
|
|
3028
|
-
</app-card>
|
|
3029
|
-
|
|
3030
|
-
<!-- TypeScript -->
|
|
3031
|
-
<app-card [hover]="true" [shadow]="true">
|
|
3032
|
-
<div class="flex items-start space-x-4">
|
|
3033
|
-
<div class="shrink-0">
|
|
3034
|
-
<div class="w-12 h-12 rounded-lg bg-primary-100 flex items-center justify-center">
|
|
3035
|
-
<ng-icon name="heroShieldCheck" size="24" style="color: var(--color-primary-600);"></ng-icon>
|
|
3036
|
-
</div>
|
|
3037
|
-
</div>
|
|
3038
|
-
<div>
|
|
3039
|
-
<h3 class="text-lg font-semibold text-gray-900 mb-2">Type-Safe</h3>
|
|
3040
|
-
<p class="text-sm text-gray-600">TypeScript interfaces, models, strongly-typed services</p>
|
|
3041
|
-
</div>
|
|
3042
|
-
</div>
|
|
3043
|
-
</app-card>
|
|
3044
|
-
</div>
|
|
3045
|
-
|
|
3046
|
-
<!-- Features Breakdown -->
|
|
3047
|
-
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
3048
|
-
<!-- Core Services -->
|
|
3049
|
-
<app-card [title]="'home.coreServices.title' | translate" [shadow]="true">
|
|
3050
|
-
<ul class="space-y-2 text-gray-700">
|
|
3051
|
-
<li class="flex items-start">
|
|
3052
|
-
<ng-icon name="heroCheckCircle" size="20" class="mr-2 mt-0.5" style="color: var(--color-success-500);"></ng-icon>
|
|
3053
|
-
<span>{{ 'home.coreServices.authService' | translate }}</span>
|
|
3054
|
-
</li>
|
|
3055
|
-
<li class="flex items-start">
|
|
3056
|
-
<ng-icon name="heroCheckCircle" size="20" class="mr-2 mt-0.5" style="color: var(--color-success-500);"></ng-icon>
|
|
3057
|
-
<span>{{ 'home.coreServices.apiService' | translate }}</span>
|
|
3058
|
-
</li>
|
|
3059
|
-
<li class="flex items-start">
|
|
3060
|
-
<ng-icon name="heroCheckCircle" size="20" class="mr-2 mt-0.5" style="color: var(--color-success-500);"></ng-icon>
|
|
3061
|
-
<span>{{ 'home.coreServices.toastService' | translate }}</span>
|
|
3062
|
-
</li>
|
|
3063
|
-
<li class="flex items-start">
|
|
3064
|
-
<ng-icon name="heroCheckCircle" size="20" class="mr-2 mt-0.5" style="color: var(--color-success-500);"></ng-icon>
|
|
3065
|
-
<span>{{ 'home.coreServices.modalService' | translate }}</span>
|
|
3066
|
-
</li>
|
|
3067
|
-
<li class="flex items-start">
|
|
3068
|
-
<ng-icon name="heroCheckCircle" size="20" class="mr-2 mt-0.5" style="color: var(--color-success-500);"></ng-icon>
|
|
3069
|
-
<span>{{ 'home.coreServices.loadingService' | translate }}</span>
|
|
3070
|
-
</li>
|
|
3071
|
-
<li class="flex items-start">
|
|
3072
|
-
<ng-icon name="heroCheckCircle" size="20" class="mr-2 mt-0.5" style="color: var(--color-success-500);"></ng-icon>
|
|
3073
|
-
<span>{{ 'home.coreServices.cacheService' | translate }}</span>
|
|
3074
|
-
</li>
|
|
3075
|
-
<li class="flex items-start">
|
|
3076
|
-
<ng-icon name="heroCheckCircle" size="20" class="mr-2 mt-0.5" style="color: var(--color-success-500);"></ng-icon>
|
|
3077
|
-
<span>{{ 'home.coreServices.storageService' | translate }}</span>
|
|
3078
|
-
</li>
|
|
3079
|
-
<li class="flex items-start">
|
|
3080
|
-
<ng-icon name="heroCheckCircle" size="20" class="mr-2 mt-0.5" style="color: var(--color-success-500);"></ng-icon>
|
|
3081
|
-
<span>{{ 'home.coreServices.i18nService' | translate }}</span>
|
|
3082
|
-
</li>
|
|
3083
|
-
</ul>
|
|
3084
|
-
</app-card>
|
|
3085
|
-
|
|
3086
|
-
<!-- Shared Components & Utils -->
|
|
3087
|
-
<app-card [title]="'home.sharedComponents.title' | translate" [shadow]="true">
|
|
3088
|
-
<ul class="space-y-2 text-gray-700">
|
|
3089
|
-
<li class="flex items-start">
|
|
3090
|
-
<ng-icon name="heroCheckCircle" size="20" class="mr-2 mt-0.5" style="color: var(--color-primary-500);"></ng-icon>
|
|
3091
|
-
<span>{{ 'home.sharedComponents.button' | translate }}</span>
|
|
3092
|
-
</li>
|
|
3093
|
-
<li class="flex items-start">
|
|
3094
|
-
<ng-icon name="heroCheckCircle" size="20" class="mr-2 mt-0.5" style="color: var(--color-primary-500);"></ng-icon>
|
|
3095
|
-
<span>{{ 'home.sharedComponents.card' | translate }}</span>
|
|
3096
|
-
</li>
|
|
3097
|
-
<li class="flex items-start">
|
|
3098
|
-
<ng-icon name="heroCheckCircle" size="20" class="mr-2 mt-0.5" style="color: var(--color-primary-500);"></ng-icon>
|
|
3099
|
-
<span>{{ 'home.sharedComponents.spinner' | translate }}</span>
|
|
3100
|
-
</li>
|
|
3101
|
-
<li class="flex items-start">
|
|
3102
|
-
<ng-icon name="heroCheckCircle" size="20" class="mr-2 mt-0.5" style="color: var(--color-primary-500);"></ng-icon>
|
|
3103
|
-
<span>{{ 'home.sharedComponents.toast' | translate }}</span>
|
|
3104
|
-
</li>
|
|
3105
|
-
<li class="flex items-start">
|
|
3106
|
-
<ng-icon name="heroCheckCircle" size="20" class="mr-2 mt-0.5" style="color: var(--color-primary-500);"></ng-icon>
|
|
3107
|
-
<span>{{ 'home.sharedComponents.modal' | translate }}</span>
|
|
3108
|
-
</li>
|
|
3109
|
-
<li class="flex items-start">
|
|
3110
|
-
<ng-icon name="heroCheckCircle" size="20" class="mr-2 mt-0.5" style="color: var(--color-primary-500);"></ng-icon>
|
|
3111
|
-
<span>{{ 'home.sharedComponents.pipes' | translate }}</span>
|
|
3112
|
-
</li>
|
|
3113
|
-
<li class="flex items-start">
|
|
3114
|
-
<ng-icon name="heroCheckCircle" size="20" class="mr-2 mt-0.5" style="color: var(--color-primary-500);"></ng-icon>
|
|
3115
|
-
<span>{{ 'home.sharedComponents.directives' | translate }}</span>
|
|
3116
|
-
</li>
|
|
3117
|
-
</ul>
|
|
3118
|
-
</app-card>
|
|
3119
|
-
</div>
|
|
3120
|
-
</div>
|
|
3121
|
-
</section>
|
|
3122
|
-
|
|
3123
|
-
<!-- Package & Dependencies Section -->
|
|
3124
|
-
<section class="py-20 bg-gray-50">
|
|
3125
|
-
<div class="container mx-auto px-4">
|
|
3126
|
-
<div class="text-center mb-12">
|
|
3127
|
-
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
|
|
3128
|
-
{{ 'home.preConfigured.title' | translate }}
|
|
3129
|
-
</h2>
|
|
3130
|
-
<p class="text-xl text-gray-600 max-w-2xl mx-auto">
|
|
3131
|
-
{{ 'home.preConfigured.subtitle' | translate }}
|
|
3132
|
-
</p>
|
|
3133
|
-
</div>
|
|
3134
|
-
|
|
3135
|
-
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-6xl mx-auto">
|
|
3136
|
-
<!-- Core Dependencies -->
|
|
3137
|
-
<app-card [title]="'home.preConfigured.coreDependencies' | translate" [shadow]="true">
|
|
3138
|
-
<div class="space-y-3 text-sm">
|
|
3139
|
-
<div class="flex justify-between items-center p-2 bg-gray-50 rounded">
|
|
3140
|
-
<span class="font-mono text-gray-700">@angular/core</span>
|
|
3141
|
-
<span class="text-gray-500">{{ 'home.preConfigured.latest' | translate }}</span>
|
|
3142
|
-
</div>
|
|
3143
|
-
<div class="flex justify-between items-center p-2 bg-gray-50 rounded">
|
|
3144
|
-
<span class="font-mono text-gray-700">tailwindcss</span>
|
|
3145
|
-
<span class="text-gray-500">^4.0.0</span>
|
|
3146
|
-
</div>
|
|
3147
|
-
<div class="flex justify-between items-center p-2 bg-gray-50 rounded">
|
|
3148
|
-
<span class="font-mono text-gray-700">@tailwindcss/postcss</span>
|
|
3149
|
-
<span class="text-gray-500">{{ 'home.preConfigured.latest' | translate }}</span>
|
|
3150
|
-
</div>
|
|
3151
|
-
<div class="flex justify-between items-center p-2 bg-gray-50 rounded">
|
|
3152
|
-
<span class="font-mono text-gray-700">@ngx-translate/core</span>
|
|
3153
|
-
<span class="text-gray-500">{{ 'home.preConfigured.i18n' | translate }}</span>
|
|
3154
|
-
</div>
|
|
3155
|
-
<div class="flex justify-between items-center p-2 bg-gray-50 rounded">
|
|
3156
|
-
<span class="font-mono text-gray-700">@ng-icons/heroicons</span>
|
|
3157
|
-
<span class="text-gray-500">{{ 'home.preConfigured.iconLibrary' | translate }}</span>
|
|
3158
|
-
</div>
|
|
3159
|
-
</div>
|
|
3160
|
-
</app-card>
|
|
3161
|
-
|
|
3162
|
-
<!-- Dev Tools -->
|
|
3163
|
-
<app-card [title]="'home.preConfigured.developmentTools' | translate" [shadow]="true">
|
|
3164
|
-
<div class="space-y-3 text-sm">
|
|
3165
|
-
<div class="flex justify-between items-center p-2 bg-gray-50 rounded">
|
|
3166
|
-
<span class="font-mono text-gray-700">ESLint</span>
|
|
3167
|
-
<span class="text-gray-500">{{ 'home.preConfigured.linting' | translate }}</span>
|
|
3168
|
-
</div>
|
|
3169
|
-
<div class="flex justify-between items-center p-2 bg-gray-50 rounded">
|
|
3170
|
-
<span class="font-mono text-gray-700">Prettier</span>
|
|
3171
|
-
<span class="text-gray-500">{{ 'home.preConfigured.formatting' | translate }}</span>
|
|
3172
|
-
</div>
|
|
3173
|
-
<div class="flex justify-between items-center p-2 bg-gray-50 rounded">
|
|
3174
|
-
<span class="font-mono text-gray-700">simple-git-hooks</span>
|
|
3175
|
-
<span class="text-gray-500">{{ 'home.preConfigured.gitHooks' | translate }}</span>
|
|
3176
|
-
</div>
|
|
3177
|
-
<div class="flex justify-between items-center p-2 bg-gray-50 rounded">
|
|
3178
|
-
<span class="font-mono text-gray-700">TypeScript</span>
|
|
3179
|
-
<span class="text-gray-500">{{ 'home.preConfigured.typeSafety' | translate }}</span>
|
|
3180
|
-
</div>
|
|
3181
|
-
<div class="flex justify-between items-center p-2 bg-gray-50 rounded">
|
|
3182
|
-
<span class="font-mono text-gray-700">PWA Config</span>
|
|
3183
|
-
<span class="text-gray-500">{{ 'home.preConfigured.serviceWorker' | translate }}</span>
|
|
3184
|
-
</div>
|
|
3185
|
-
</div>
|
|
3186
|
-
</app-card>
|
|
3187
|
-
</div>
|
|
3188
|
-
|
|
3189
|
-
<!-- Path Aliases -->
|
|
3190
|
-
<div class="mt-8 max-w-4xl mx-auto">
|
|
3191
|
-
<app-card [title]="'home.pathAliases.title' | translate" [shadow]="true">
|
|
3192
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
3193
|
-
<div class="bg-gray-50 p-3 rounded font-mono">
|
|
3194
|
-
<span style="color: var(--color-accent-600);">{{ 'home.pathAliases.core' | translate }}</span>
|
|
3195
|
-
</div>
|
|
3196
|
-
<div class="bg-gray-50 p-3 rounded font-mono">
|
|
3197
|
-
<span style="color: var(--color-accent-600);">{{ 'home.pathAliases.shared' | translate }}</span>
|
|
3198
|
-
</div>
|
|
3199
|
-
<div class="bg-gray-50 p-3 rounded font-mono">
|
|
3200
|
-
<span style="color: var(--color-accent-600);">{{ 'home.pathAliases.features' | translate }}</span>
|
|
3201
|
-
</div>
|
|
3202
|
-
<div class="bg-gray-50 p-3 rounded font-mono">
|
|
3203
|
-
<span style="color: var(--color-accent-600);">{{ 'home.pathAliases.environments' | translate }}</span>
|
|
3204
|
-
</div>
|
|
3205
|
-
</div>
|
|
3206
|
-
</app-card>
|
|
3207
|
-
</div>
|
|
3208
|
-
</div>
|
|
3209
|
-
</section>
|
|
3210
|
-
|
|
3211
|
-
<!-- Project Structure Section -->
|
|
3212
|
-
<section class="py-20 bg-white">
|
|
3213
|
-
<div class="container mx-auto px-4">
|
|
3214
|
-
<div class="text-center mb-12">
|
|
3215
|
-
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
|
|
3216
|
-
{{ 'home.projectStructure.title' | translate }}
|
|
3217
|
-
</h2>
|
|
3218
|
-
<p class="text-xl text-gray-600 max-w-2xl mx-auto">
|
|
3219
|
-
{{ 'home.projectStructure.subtitle' | translate }}
|
|
3220
|
-
</p>
|
|
3221
|
-
</div>
|
|
3222
|
-
|
|
3223
|
-
<div class="max-w-4xl mx-auto">
|
|
3224
|
-
<app-card [shadow]="true">
|
|
3225
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm font-mono">
|
|
3226
|
-
<div>
|
|
3227
|
-
<div class="flex items-center mb-2">
|
|
3228
|
-
<ng-icon name="heroFolder" size="18" class="mr-2" style="color: var(--color-primary-500);"></ng-icon>
|
|
3229
|
-
<span class="font-semibold">src/app/core/</span>
|
|
3230
|
-
</div>
|
|
3231
|
-
<ul class="ml-6 space-y-1 text-gray-600">
|
|
3232
|
-
<li>├─ services/ (8 services)</li>
|
|
3233
|
-
<li>├─ guards/ (auth guard)</li>
|
|
3234
|
-
<li>├─ interceptors/ (4 types)</li>
|
|
3235
|
-
<li>└─ i18n/ (translation system)</li>
|
|
3236
|
-
</ul>
|
|
3237
|
-
</div>
|
|
3238
|
-
|
|
3239
|
-
<div>
|
|
3240
|
-
<div class="flex items-center mb-2">
|
|
3241
|
-
<ng-icon name="heroFolder" size="18" class="mr-2" style="color: var(--color-accent-500);"></ng-icon>
|
|
3242
|
-
<span class="font-semibold">src/app/shared/</span>
|
|
3243
|
-
</div>
|
|
3244
|
-
<ul class="ml-6 space-y-1 text-gray-600">
|
|
3245
|
-
<li>├─ components/ (5 components)</li>
|
|
3246
|
-
<li>├─ pipes/ (4 pipes)</li>
|
|
3247
|
-
<li>├─ directives/ (2 directives)</li>
|
|
3248
|
-
<li>└─ models/ (TypeScript interfaces)</li>
|
|
3249
|
-
</ul>
|
|
3250
|
-
</div>
|
|
3251
|
-
|
|
3252
|
-
<div>
|
|
3253
|
-
<div class="flex items-center mb-2">
|
|
3254
|
-
<ng-icon name="heroFolder" size="18" class="mr-2" style="color: var(--color-success-500);"></ng-icon>
|
|
3255
|
-
<span class="font-semibold">src/app/features/</span>
|
|
3256
|
-
</div>
|
|
3257
|
-
<ul class="ml-6 space-y-1 text-gray-600">
|
|
3258
|
-
<li>├─ home/</li>
|
|
3259
|
-
<li>├─ about/</li>
|
|
3260
|
-
<li>├─ contact/</li>
|
|
3261
|
-
<li>└─ auth/ (login, register, forgot)</li>
|
|
3262
|
-
</ul>
|
|
3263
|
-
</div>
|
|
3264
|
-
|
|
3265
|
-
<div>
|
|
3266
|
-
<div class="flex items-center mb-2">
|
|
3267
|
-
<ng-icon name="heroFolder" size="18" class="mr-2" style="color: var(--color-warning-500);"></ng-icon>
|
|
3268
|
-
<span class="font-semibold">src/app/layout/</span>
|
|
3269
|
-
</div>
|
|
3270
|
-
<ul class="ml-6 space-y-1 text-gray-600">
|
|
3271
|
-
<li>├─ main-layout/ (header + footer)</li>
|
|
3272
|
-
<li>└─ auth-layout/ (auth pages)</li>
|
|
3273
|
-
</ul>
|
|
3274
|
-
</div>
|
|
3275
|
-
</div>
|
|
3276
|
-
</app-card>
|
|
3277
|
-
</div>
|
|
3278
|
-
</div>
|
|
3279
|
-
</section>
|
|
3280
|
-
|
|
3281
|
-
<!-- Interactive Examples Section -->
|
|
3282
|
-
<section class="py-20 bg-gray-50">
|
|
3283
|
-
<div class="container mx-auto px-4">
|
|
3284
|
-
<div class="text-center mb-12 animate-fade-in">
|
|
3285
|
-
<div class="inline-flex items-center justify-center w-14 h-14 rounded-xl bg-primary-600 mb-4">
|
|
3286
|
-
<ng-icon name="heroBolt" size="28" style="color: white;"></ng-icon>
|
|
3287
|
-
</div>
|
|
3288
|
-
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-3">
|
|
3289
|
-
{{ 'home.interactiveExamples.title' | translate }}
|
|
3290
|
-
</h2>
|
|
3291
|
-
<p class="text-lg text-gray-600 max-w-2xl mx-auto">
|
|
3292
|
-
{{ 'home.interactiveExamples.subtitle' | translate }}
|
|
3293
|
-
</p>
|
|
3294
|
-
</div>
|
|
3295
|
-
|
|
3296
|
-
<div class="max-w-5xl mx-auto">
|
|
3297
|
-
<div class="bg-white rounded-xl shadow-lg p-8 md:p-10 border border-gray-200">
|
|
3298
|
-
<div class="space-y-10">
|
|
3299
|
-
<!-- Toast Examples -->
|
|
3300
|
-
<div class="animate-slide-up">
|
|
3301
|
-
<div class="flex items-center gap-3 mb-5">
|
|
3302
|
-
<div class="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center">
|
|
3303
|
-
<ng-icon name="heroCheckCircle" size="20" class="text-gray-700"></ng-icon>
|
|
3304
|
-
</div>
|
|
3305
|
-
<div>
|
|
3306
|
-
<h3 class="text-xl font-semibold text-gray-900">
|
|
3307
|
-
{{ 'home.interactiveExamples.toastNotifications.title' | translate }}
|
|
3308
|
-
</h3>
|
|
3309
|
-
<p class="text-sm text-gray-500">{{ 'home.interactiveExamples.toastNotifications.subtitle' | translate }}</p>
|
|
3310
|
-
</div>
|
|
3311
|
-
</div>
|
|
3312
|
-
<p class="text-gray-600 mb-5">
|
|
3313
|
-
{{ 'home.interactiveExamples.toastNotifications.description' | translate }}
|
|
3314
|
-
</p>
|
|
3315
|
-
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
3316
|
-
<app-button (click)="showSuccessToast()">
|
|
3317
|
-
<span class="flex items-center gap-2">
|
|
3318
|
-
<ng-icon name="heroCheckCircle" size="16"></ng-icon>
|
|
3319
|
-
{{ 'home.interactiveExamples.toastNotifications.success' | translate }}
|
|
3320
|
-
</span>
|
|
3321
|
-
</app-button>
|
|
3322
|
-
<app-button variant="danger" (click)="showErrorToast()">
|
|
3323
|
-
<span class="flex items-center gap-2">
|
|
3324
|
-
<ng-icon name="heroCheckCircle" size="16"></ng-icon>
|
|
3325
|
-
{{ 'home.interactiveExamples.toastNotifications.error' | translate }}
|
|
3326
|
-
</span>
|
|
3327
|
-
</app-button>
|
|
3328
|
-
<app-button variant="secondary" (click)="showWarningToast()">
|
|
3329
|
-
<span class="flex items-center gap-2">
|
|
3330
|
-
<ng-icon name="heroCheckCircle" size="16"></ng-icon>
|
|
3331
|
-
{{ 'home.interactiveExamples.toastNotifications.warning' | translate }}
|
|
3332
|
-
</span>
|
|
3333
|
-
</app-button>
|
|
3334
|
-
<app-button variant="outline" (click)="showInfoToast()">
|
|
3335
|
-
<span class="flex items-center gap-2">
|
|
3336
|
-
<ng-icon name="heroCheckCircle" size="16"></ng-icon>
|
|
3337
|
-
{{ 'home.interactiveExamples.toastNotifications.info' | translate }}
|
|
3338
|
-
</span>
|
|
3339
|
-
</app-button>
|
|
3340
|
-
</div>
|
|
3341
|
-
</div>
|
|
3342
|
-
|
|
3343
|
-
<div class="border-t border-gray-200"></div>
|
|
3344
|
-
|
|
3345
|
-
<!-- Modal Examples -->
|
|
3346
|
-
<div class="animate-slide-up animation-delay-200">
|
|
3347
|
-
<div class="flex items-center gap-3 mb-5">
|
|
3348
|
-
<div class="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center">
|
|
3349
|
-
<ng-icon name="heroCube" size="20" class="text-gray-700"></ng-icon>
|
|
3350
|
-
</div>
|
|
3351
|
-
<div>
|
|
3352
|
-
<h3 class="text-xl font-semibold text-gray-900">
|
|
3353
|
-
{{ 'home.interactiveExamples.modalDialogs.title' | translate }}
|
|
3354
|
-
</h3>
|
|
3355
|
-
<p class="text-sm text-gray-500">{{ 'home.interactiveExamples.modalDialogs.subtitle' | translate }}</p>
|
|
3356
|
-
</div>
|
|
3357
|
-
</div>
|
|
3358
|
-
<p class="text-gray-600 mb-5">
|
|
3359
|
-
{{ 'home.interactiveExamples.modalDialogs.description' | translate }}
|
|
3360
|
-
</p>
|
|
3361
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
3362
|
-
<app-button (click)="showConfirmModal()" size="lg">
|
|
3363
|
-
<span class="flex items-center gap-2 justify-center">
|
|
3364
|
-
<ng-icon name="heroShieldCheck" size="18"></ng-icon>
|
|
3365
|
-
{{ 'home.interactiveExamples.modalDialogs.showConfirm' | translate }}
|
|
3366
|
-
</span>
|
|
3367
|
-
</app-button>
|
|
3368
|
-
<app-button variant="secondary" (click)="showAlertModal()" size="lg">
|
|
3369
|
-
<span class="flex items-center gap-2 justify-center">
|
|
3370
|
-
<ng-icon name="heroBolt" size="18"></ng-icon>
|
|
3371
|
-
{{ 'home.interactiveExamples.modalDialogs.showAlert' | translate }}
|
|
3372
|
-
</span>
|
|
3373
|
-
</app-button>
|
|
3374
|
-
</div>
|
|
3375
|
-
</div>
|
|
3376
|
-
|
|
3377
|
-
<!-- Pro Tip -->
|
|
3378
|
-
<div class="bg-info-50 rounded-lg p-5 border border-info-100">
|
|
3379
|
-
<div class="flex items-start gap-3">
|
|
3380
|
-
<div class="shrink-0">
|
|
3381
|
-
<div class="w-8 h-8 rounded-lg bg-primary-600 flex items-center justify-center">
|
|
3382
|
-
<ng-icon name="heroBolt" size="16" style="color: white;"></ng-icon>
|
|
3383
|
-
</div>
|
|
3384
|
-
</div>
|
|
3385
|
-
<div>
|
|
3386
|
-
<h4 class="font-semibold text-gray-900 mb-1 text-sm">{{ 'home.interactiveExamples.proTip.title' | translate }}</h4>
|
|
3387
|
-
<p class="text-sm text-gray-600">
|
|
3388
|
-
{{ 'home.interactiveExamples.proTip.description' | translate }}
|
|
3389
|
-
</p>
|
|
3390
|
-
</div>
|
|
3391
|
-
</div>
|
|
3392
|
-
</div>
|
|
3393
|
-
</div>
|
|
3394
|
-
</div>
|
|
3395
|
-
</div>
|
|
3396
|
-
</div>
|
|
3397
|
-
</section>
|
|
3398
|
-
|
|
3399
|
-
<!-- Getting Started Section -->
|
|
3400
|
-
<section class="py-20 bg-white">
|
|
3401
|
-
<div class="container mx-auto px-4 text-center">
|
|
3402
|
-
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-6">
|
|
3403
|
-
{{ 'home.readyToBuild.title' | translate }}
|
|
3404
|
-
</h2>
|
|
3405
|
-
<p class="text-xl text-gray-600 mb-8 max-w-2xl mx-auto">
|
|
3406
|
-
{{ 'home.readyToBuild.subtitle' | translate }}
|
|
3407
|
-
</p>
|
|
3408
|
-
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
|
3409
|
-
<app-button [routerLink]="['/about']" size="lg">
|
|
3410
|
-
{{ 'home.readyToBuild.learnMore' | translate }}
|
|
3411
|
-
</app-button>
|
|
3412
|
-
<app-button [routerLink]="['/contact']" variant="secondary" size="lg">
|
|
3413
|
-
{{ 'home.readyToStart.contactUs' | translate }}
|
|
3414
|
-
</app-button>
|
|
3415
|
-
</div>
|
|
3416
|
-
</div>
|
|
3417
|
-
</section>
|
|
3418
|
-
`;
|
|
3419
|
-
|
|
3420
|
-
await fs.writeFile(
|
|
3421
|
-
path.join(config.fullPath, 'src/app/features/home/home.component.ts'),
|
|
3422
|
-
homeComponentTs
|
|
3423
|
-
);
|
|
3424
|
-
|
|
3425
|
-
await fs.writeFile(
|
|
3426
|
-
path.join(config.fullPath, 'src/app/features/home/home.component.html'),
|
|
3427
|
-
homeComponentHtml
|
|
3428
|
-
);
|
|
3429
|
-
},
|
|
3430
|
-
|
|
3431
|
-
async createAboutComponent(config) {
|
|
3432
|
-
const aboutComponentTs = `import { Component, OnInit, inject } from '@angular/core';
|
|
3433
|
-
import { RouterModule } from '@angular/router';
|
|
3434
|
-
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
3435
|
-
import {
|
|
3436
|
-
heroRocketLaunch,
|
|
3437
|
-
heroPaintBrush,
|
|
3438
|
-
heroShieldCheck,
|
|
3439
|
-
heroBolt,
|
|
3440
|
-
heroCodeBracket,
|
|
3441
|
-
heroCheckCircle,
|
|
3442
|
-
} from '@ng-icons/heroicons/outline';
|
|
3443
|
-
import { TranslateModule } from '@ngx-translate/core';
|
|
3444
|
-
import { ButtonComponent } from '@shared/components/button/button.component';
|
|
3445
|
-
import { SeoService } from '@core/services/seo.service';
|
|
3446
|
-
|
|
3447
|
-
@Component({
|
|
3448
|
-
selector: 'app-about',
|
|
3449
|
-
standalone: true,
|
|
3450
|
-
imports: [RouterModule, NgIconComponent, TranslateModule, ButtonComponent],
|
|
3451
|
-
viewProviders: [provideIcons({
|
|
3452
|
-
heroRocketLaunch,
|
|
3453
|
-
heroPaintBrush,
|
|
3454
|
-
heroShieldCheck,
|
|
3455
|
-
heroBolt,
|
|
3456
|
-
heroCodeBracket,
|
|
3457
|
-
heroCheckCircle,
|
|
3458
|
-
})],
|
|
3459
|
-
templateUrl: './about.component.html'
|
|
3460
|
-
})
|
|
3461
|
-
export class AboutComponent implements OnInit {
|
|
3462
|
-
protected readonly projectName = '${config.projectName}';
|
|
3463
|
-
private seo = inject(SeoService);
|
|
3464
|
-
|
|
3465
|
-
ngOnInit(): void {
|
|
3466
|
-
// Set SEO meta tags for About page
|
|
3467
|
-
this.seo.updateMeta({
|
|
3468
|
-
title: 'About Us',
|
|
3469
|
-
description: 'Learn more about ${config.projectName} and our mission to create modern web applications.',
|
|
3470
|
-
keywords: 'about, team, mission, company',
|
|
3471
|
-
ogType: 'website'
|
|
3472
|
-
});
|
|
3473
|
-
}
|
|
3474
|
-
}`;
|
|
3475
|
-
|
|
3476
|
-
const aboutComponentHtml = `<!-- Hero Section -->
|
|
3477
|
-
<section class="relative py-16 bg-linear-to-br from-primary-600 via-accent-600 to-secondary-600 overflow-hidden">
|
|
3478
|
-
<div class="absolute inset-0 opacity-10">
|
|
3479
|
-
<div class="absolute top-0 left-0 w-96 h-96 bg-white rounded-full mix-blend-multiply filter blur-3xl animate-blob"></div>
|
|
3480
|
-
<div class="absolute top-0 right-0 w-96 h-96 bg-warning-200 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-2000"></div>
|
|
3481
|
-
<div class="absolute bottom-0 left-1/2 w-96 h-96 bg-accent-200 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-4000"></div>
|
|
3482
|
-
</div>
|
|
3483
|
-
|
|
3484
|
-
<div class="relative container mx-auto px-4 text-center">
|
|
3485
|
-
<div class="animate-fade-in">
|
|
3486
|
-
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-4 drop-shadow-lg">
|
|
3487
|
-
{{ 'about.title' | translate:{ projectName: projectName } }}
|
|
3488
|
-
</h1>
|
|
3489
|
-
<p class="text-lg md:text-xl text-accent-50 max-w-3xl mx-auto leading-relaxed">
|
|
3490
|
-
{{ 'about.subtitle' | translate }}
|
|
3491
|
-
</p>
|
|
3492
|
-
</div>
|
|
3493
|
-
</div>
|
|
3494
|
-
</section>
|
|
3495
|
-
|
|
3496
|
-
<!-- Overview Section -->
|
|
3497
|
-
<section class="py-20 bg-white">
|
|
3498
|
-
<div class="container mx-auto px-4">
|
|
3499
|
-
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 max-w-6xl mx-auto">
|
|
3500
|
-
<!-- Project Overview Card -->
|
|
3501
|
-
<div class="animate-slide-up">
|
|
3502
|
-
<div class="bg-linear-to-br from-primary-50 to-accent-50 rounded-2xl p-8 border border-primary-100 h-full">
|
|
3503
|
-
<div class="flex items-center gap-3 mb-6">
|
|
3504
|
-
<div class="w-12 h-12 rounded-lg bg-linear-to-br from-primary-500 to-accent-600 flex items-center justify-center shadow-lg">
|
|
3505
|
-
<ng-icon name="heroRocketLaunch" size="24" style="color: white;"></ng-icon>
|
|
3506
|
-
</div>
|
|
3507
|
-
<h2 class="text-2xl font-bold text-gray-900">
|
|
3508
|
-
{{ 'about.overview.title' | translate }}
|
|
3509
|
-
</h2>
|
|
3510
|
-
</div>
|
|
3511
|
-
<p class="text-gray-700 mb-4 leading-relaxed">
|
|
3512
|
-
{{ 'about.overview.paragraph1' | translate }}
|
|
3513
|
-
</p>
|
|
3514
|
-
<p class="text-gray-700 leading-relaxed">
|
|
3515
|
-
{{ 'about.overview.paragraph2' | translate }}
|
|
3516
|
-
</p>
|
|
3517
|
-
</div>
|
|
3518
|
-
</div>
|
|
3519
|
-
|
|
3520
|
-
<!-- Features Card -->
|
|
3521
|
-
<div class="animate-slide-up animation-delay-200">
|
|
3522
|
-
<div class="bg-linear-to-br from-accent-50 to-secondary-50 rounded-2xl p-8 border border-accent-100 h-full">
|
|
3523
|
-
<div class="flex items-center gap-3 mb-6">
|
|
3524
|
-
<div class="w-12 h-12 rounded-lg bg-linear-to-br from-accent-500 to-secondary-600 flex items-center justify-center shadow-lg">
|
|
3525
|
-
<ng-icon name="heroCheckCircle" size="24" style="color: white;"></ng-icon>
|
|
3526
|
-
</div>
|
|
3527
|
-
<h2 class="text-2xl font-bold text-gray-900">
|
|
3528
|
-
{{ 'about.features.title' | translate }}
|
|
3529
|
-
</h2>
|
|
3530
|
-
</div>
|
|
3531
|
-
<ul class="space-y-4">
|
|
3532
|
-
<li class="flex items-start gap-3">
|
|
3533
|
-
<div class="shrink-0 w-6 h-6 rounded-full bg-success-100 flex items-center justify-center mt-0.5">
|
|
3534
|
-
<ng-icon name="heroCheckCircle" size="16" style="color: var(--color-success-600);"></ng-icon>
|
|
3535
|
-
</div>
|
|
3536
|
-
<span class="text-gray-700">{{ 'about.features.standalone' | translate }}</span>
|
|
3537
|
-
</li>
|
|
3538
|
-
<li class="flex items-start gap-3">
|
|
3539
|
-
<div class="shrink-0 w-6 h-6 rounded-full bg-success-100 flex items-center justify-center mt-0.5">
|
|
3540
|
-
<ng-icon name="heroCheckCircle" size="16" style="color: var(--color-success-600);"></ng-icon>
|
|
3541
|
-
</div>
|
|
3542
|
-
<span class="text-gray-700">{{ 'about.features.tailwind' | translate }}</span>
|
|
3543
|
-
</li>
|
|
3544
|
-
<li class="flex items-start gap-3">
|
|
3545
|
-
<div class="shrink-0 w-6 h-6 rounded-full bg-success-100 flex items-center justify-center mt-0.5">
|
|
3546
|
-
<ng-icon name="heroCheckCircle" size="16" style="color: var(--color-success-600);"></ng-icon>
|
|
3547
|
-
</div>
|
|
3548
|
-
<span class="text-gray-700">{{ 'about.features.responsive' | translate }}</span>
|
|
3549
|
-
</li>
|
|
3550
|
-
<li class="flex items-start gap-3">
|
|
3551
|
-
<div class="shrink-0 w-6 h-6 rounded-full bg-success-100 flex items-center justify-center mt-0.5">
|
|
3552
|
-
<ng-icon name="heroCheckCircle" size="16" style="color: var(--color-success-600);"></ng-icon>
|
|
3553
|
-
</div>
|
|
3554
|
-
<span class="text-gray-700">{{ 'about.features.typescript' | translate }}</span>
|
|
3555
|
-
</li>
|
|
3556
|
-
<li class="flex items-start gap-3">
|
|
3557
|
-
<div class="shrink-0 w-6 h-6 rounded-full bg-success-100 flex items-center justify-center mt-0.5">
|
|
3558
|
-
<ng-icon name="heroCheckCircle" size="16" style="color: var(--color-success-600);"></ng-icon>
|
|
3559
|
-
</div>
|
|
3560
|
-
<span class="text-gray-700">{{ 'about.features.production' | translate }}</span>
|
|
3561
|
-
</li>
|
|
3562
|
-
</ul>
|
|
3563
|
-
</div>
|
|
3564
|
-
</div>
|
|
3565
|
-
</div>
|
|
3566
|
-
</div>
|
|
3567
|
-
</section>
|
|
3568
|
-
|
|
3569
|
-
<!-- Tech Stack Section -->
|
|
3570
|
-
<section class="py-20 bg-linear-to-br from-gray-50 to-gray-100">
|
|
3571
|
-
<div class="container mx-auto px-4">
|
|
3572
|
-
<div class="text-center mb-12 animate-fade-in">
|
|
3573
|
-
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
|
|
3574
|
-
{{ 'about.techStack.title' | translate }}
|
|
3575
|
-
</h2>
|
|
3576
|
-
<p class="text-xl text-gray-600 max-w-2xl mx-auto">
|
|
3577
|
-
{{ 'home.techStack.title' | translate }}
|
|
3578
|
-
</p>
|
|
3579
|
-
</div>
|
|
3580
|
-
|
|
3581
|
-
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
|
|
3582
|
-
<!-- Angular -->
|
|
3583
|
-
<div class="bg-white rounded-xl p-6 shadow-lg hover:shadow-xl border border-gray-100 transform hover:-translate-y-1 transition-all">
|
|
3584
|
-
<div class="flex items-center gap-4 mb-4">
|
|
3585
|
-
<div class="w-12 h-12 rounded-lg bg-danger-100 flex items-center justify-center">
|
|
3586
|
-
<ng-icon name="heroCodeBracket" size="24" style="color: var(--color-danger-600);"></ng-icon>
|
|
3587
|
-
</div>
|
|
3588
|
-
<h3 class="text-xl font-bold text-gray-900">{{ 'home.techStack.angular.title' | translate }}</h3>
|
|
3589
|
-
</div>
|
|
3590
|
-
<p class="text-gray-600 text-sm">
|
|
3591
|
-
{{ 'home.techStack.angular.description' | translate }}
|
|
3592
|
-
</p>
|
|
3593
|
-
</div>
|
|
3594
|
-
|
|
3595
|
-
<!-- Tailwind CSS -->
|
|
3596
|
-
<div class="bg-white rounded-xl p-6 shadow-lg hover:shadow-xl border border-gray-100 transform hover:-translate-y-1 transition-all">
|
|
3597
|
-
<div class="flex items-center gap-4 mb-4">
|
|
3598
|
-
<div class="w-12 h-12 rounded-lg bg-secondary-100 flex items-center justify-center">
|
|
3599
|
-
<ng-icon name="heroPaintBrush" size="24" style="color: var(--color-secondary-600);"></ng-icon>
|
|
3600
|
-
</div>
|
|
3601
|
-
<h3 class="text-xl font-bold text-gray-900">{{ 'home.techStack.tailwind.title' | translate }}</h3>
|
|
3602
|
-
</div>
|
|
3603
|
-
<p class="text-gray-600 text-sm">
|
|
3604
|
-
{{ 'home.techStack.tailwind.description' | translate }}
|
|
3605
|
-
</p>
|
|
3606
|
-
</div>
|
|
3607
|
-
|
|
3608
|
-
<!-- TypeScript -->
|
|
3609
|
-
<div class="bg-white rounded-xl p-6 shadow-lg hover:shadow-xl border border-gray-100 transform hover:-translate-y-1 transition-all">
|
|
3610
|
-
<div class="flex items-center gap-4 mb-4">
|
|
3611
|
-
<div class="w-12 h-12 rounded-lg bg-primary-100 flex items-center justify-center">
|
|
3612
|
-
<ng-icon name="heroShieldCheck" size="24" style="color: var(--color-primary-600);"></ng-icon>
|
|
3613
|
-
</div>
|
|
3614
|
-
<h3 class="text-xl font-bold text-gray-900">{{ 'home.techStack.typescript.title' | translate }}</h3>
|
|
3615
|
-
</div>
|
|
3616
|
-
<p class="text-gray-600 text-sm">
|
|
3617
|
-
{{ 'home.techStack.typescript.description' | translate }}
|
|
3618
|
-
</p>
|
|
3619
|
-
</div>
|
|
3620
|
-
</div>
|
|
3621
|
-
</div>
|
|
3622
|
-
</section>
|
|
3623
|
-
|
|
3624
|
-
<!-- CTA Section -->
|
|
3625
|
-
<section class="py-20 bg-white">
|
|
3626
|
-
<div class="container mx-auto px-4 text-center">
|
|
3627
|
-
<div class="max-w-3xl mx-auto animate-slide-up">
|
|
3628
|
-
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-6">
|
|
3629
|
-
{{ 'home.readyToStart.title' | translate }}
|
|
3630
|
-
</h2>
|
|
3631
|
-
<p class="text-xl text-gray-600 mb-8">
|
|
3632
|
-
{{ 'home.readyToStart.subtitle' | translate }}
|
|
3633
|
-
</p>
|
|
3634
|
-
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
|
3635
|
-
<app-button [routerLink]="['/']" size="lg">
|
|
3636
|
-
<span class="flex items-center gap-2">
|
|
3637
|
-
<ng-icon name="heroRocketLaunch" size="20"></ng-icon>
|
|
3638
|
-
{{ 'home.readyToStart.viewHome' | translate }}
|
|
3639
|
-
</span>
|
|
3640
|
-
</app-button>
|
|
3641
|
-
<app-button [routerLink]="['/contact']" variant="secondary" size="lg">
|
|
3642
|
-
<span class="flex items-center gap-2">
|
|
3643
|
-
{{ 'home.readyToStart.contactUs' | translate }}
|
|
3644
|
-
<ng-icon name="heroBolt" size="20"></ng-icon>
|
|
3645
|
-
</span>
|
|
3646
|
-
</app-button>
|
|
3647
|
-
</div>
|
|
3648
|
-
</div>
|
|
3649
|
-
</div>
|
|
3650
|
-
</section>
|
|
3651
|
-
`;
|
|
3652
|
-
|
|
3653
|
-
await fs.writeFile(
|
|
3654
|
-
path.join(config.fullPath, 'src/app/features/about/about.component.ts'),
|
|
3655
|
-
aboutComponentTs
|
|
3656
|
-
);
|
|
3657
|
-
|
|
3658
|
-
await fs.writeFile(
|
|
3659
|
-
path.join(config.fullPath, 'src/app/features/about/about.component.html'),
|
|
3660
|
-
aboutComponentHtml
|
|
3661
|
-
);
|
|
3662
|
-
},
|
|
3663
|
-
|
|
3664
|
-
async updateTsConfig(config) {
|
|
3665
|
-
// Define path mapping configuration
|
|
3666
|
-
const pathMappingConfig = ` "baseUrl": "./",
|
|
3667
|
-
"paths": {
|
|
3668
|
-
"@/*": ["src/*"],
|
|
3669
|
-
"@app/*": ["src/app/*"],
|
|
3670
|
-
"@core/*": ["src/app/core/*"],
|
|
3671
|
-
"@shared/*": ["src/app/shared/*"],
|
|
3672
|
-
"@features/*": ["src/app/features/*"],
|
|
3673
|
-
"@layout/*": ["src/app/layout/*"],
|
|
3674
|
-
"@environments/*": ["src/environments/*"]
|
|
3675
|
-
},`;
|
|
3676
|
-
|
|
3677
|
-
// Helper function to add path mappings to a tsconfig file
|
|
3678
|
-
const addPathMappings = (content) => {
|
|
3679
|
-
if (content.includes('"compilerOptions": {')) {
|
|
3680
|
-
const compilerOptionsStart =
|
|
3681
|
-
content.indexOf('"compilerOptions": {') + '"compilerOptions": {'.length;
|
|
3682
|
-
const beforeOptions = content.substring(0, compilerOptionsStart);
|
|
3683
|
-
const afterOptions = content.substring(compilerOptionsStart);
|
|
3684
|
-
return beforeOptions + '\n' + pathMappingConfig + afterOptions;
|
|
3685
|
-
}
|
|
3686
|
-
return content;
|
|
3687
|
-
};
|
|
3688
|
-
|
|
3689
|
-
// Update tsconfig.json (base config) - CRITICAL for IDE support
|
|
3690
|
-
const tsConfigPath = path.join(config.fullPath, 'tsconfig.json');
|
|
3691
|
-
let tsConfigContent = await fs.readFile(tsConfigPath, 'utf8');
|
|
3692
|
-
tsConfigContent = addPathMappings(tsConfigContent);
|
|
3693
|
-
await fs.writeFile(tsConfigPath, tsConfigContent);
|
|
3694
|
-
|
|
3695
|
-
// Update tsconfig.app.json
|
|
3696
|
-
const tsConfigAppPath = path.join(config.fullPath, 'tsconfig.app.json');
|
|
3697
|
-
let tsConfigAppContent = await fs.readFile(tsConfigAppPath, 'utf8');
|
|
3698
|
-
tsConfigAppContent = addPathMappings(tsConfigAppContent);
|
|
3699
|
-
await fs.writeFile(tsConfigAppPath, tsConfigAppContent);
|
|
3700
|
-
|
|
3701
|
-
// Update tsconfig.spec.json
|
|
3702
|
-
const tsConfigSpecPath = path.join(config.fullPath, 'tsconfig.spec.json');
|
|
3703
|
-
let tsConfigSpecContent = await fs.readFile(tsConfigSpecPath, 'utf8');
|
|
3704
|
-
tsConfigSpecContent = addPathMappings(tsConfigSpecContent);
|
|
3705
|
-
await fs.writeFile(tsConfigSpecPath, tsConfigSpecContent);
|
|
3706
|
-
},
|
|
3707
|
-
|
|
3708
|
-
async setupLinting(config) {
|
|
3709
|
-
// Read current package.json
|
|
3710
|
-
const packageJsonPath = path.join(config.fullPath, 'package.json');
|
|
3711
|
-
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
3712
|
-
|
|
3713
|
-
// Add icons and PWA to regular dependencies
|
|
3714
|
-
if (!packageJson.dependencies) {
|
|
3715
|
-
packageJson.dependencies = {};
|
|
3716
|
-
}
|
|
3717
|
-
packageJson.dependencies['@ng-icons/core'] = '^29.7.0';
|
|
3718
|
-
packageJson.dependencies['@ng-icons/heroicons'] = '^29.7.0';
|
|
3719
|
-
packageJson.dependencies['@angular/service-worker'] = packageJson.dependencies['@angular/core'];
|
|
3720
|
-
|
|
3721
|
-
// Add linting dev dependencies
|
|
3722
|
-
const lintingDependencies = {
|
|
3723
|
-
'@eslint/js': '^9.23.0',
|
|
3724
|
-
eslint: '^9.23.0',
|
|
3725
|
-
'angular-eslint': '19.3.0',
|
|
3726
|
-
'typescript-eslint': '^8.20.0',
|
|
3727
|
-
prettier: '^3.5.3',
|
|
3728
|
-
'eslint-config-prettier': '^10.1.2',
|
|
3729
|
-
'prettier-plugin-tailwindcss': '^0.6.11',
|
|
3730
|
-
'simple-git-hooks': '^2.11.1',
|
|
3731
|
-
};
|
|
3732
|
-
|
|
3733
|
-
// Add enhanced scripts
|
|
3734
|
-
const enhancedScripts = {
|
|
3735
|
-
lint: 'ng lint',
|
|
3736
|
-
'lint:fix': 'ng lint --fix',
|
|
3737
|
-
format: 'prettier --write "**/*.{ts,html,css,json,md}"',
|
|
3738
|
-
'format:check': 'prettier --check "**/*.{ts,html,css,json,md}"',
|
|
3739
|
-
'format:ts': 'prettier --write "**/*.ts"',
|
|
3740
|
-
'code:check': 'npm run format:check && npm run lint',
|
|
3741
|
-
'code:fix': 'npm run format && npm run lint:fix',
|
|
3742
|
-
prepare: 'simple-git-hooks',
|
|
3743
|
-
};
|
|
3744
|
-
|
|
3745
|
-
// Add prettier configuration
|
|
3746
|
-
const prettierConfig = {
|
|
3747
|
-
printWidth: 100,
|
|
3748
|
-
singleQuote: true,
|
|
3749
|
-
trailingComma: 'es5',
|
|
3750
|
-
tabWidth: 2,
|
|
3751
|
-
semi: true,
|
|
3752
|
-
arrowParens: 'always',
|
|
3753
|
-
endOfLine: 'lf',
|
|
3754
|
-
plugins: ['prettier-plugin-tailwindcss'],
|
|
3755
|
-
overrides: [
|
|
3756
|
-
{
|
|
3757
|
-
files: '*.html',
|
|
3758
|
-
options: {
|
|
3759
|
-
parser: 'angular',
|
|
3760
|
-
},
|
|
3761
|
-
},
|
|
3762
|
-
],
|
|
3763
|
-
};
|
|
3764
|
-
|
|
3765
|
-
// simple-git-hooks configuration (lightweight pre-commit hooks)
|
|
3766
|
-
const simpleGitHooksConfig = {
|
|
3767
|
-
'pre-commit': 'npm run lint',
|
|
3768
|
-
};
|
|
3769
|
-
|
|
3770
|
-
// Update package.json
|
|
3771
|
-
packageJson.devDependencies = {
|
|
3772
|
-
...packageJson.devDependencies,
|
|
3773
|
-
...lintingDependencies,
|
|
3774
|
-
};
|
|
3775
|
-
packageJson.scripts = { ...packageJson.scripts, ...enhancedScripts };
|
|
3776
|
-
packageJson.prettier = prettierConfig;
|
|
3777
|
-
packageJson['simple-git-hooks'] = simpleGitHooksConfig;
|
|
3778
|
-
|
|
3779
|
-
// Write updated package.json
|
|
3780
|
-
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
3781
|
-
|
|
3782
|
-
// Configure angular.json for ESLint
|
|
3783
|
-
await this.configureAngularJson(config);
|
|
3784
|
-
|
|
3785
|
-
// Create ESLint configuration (ESLint 9+ flat config)
|
|
3786
|
-
const eslintConfig = `// @ts-check
|
|
3787
|
-
const eslint = require("@eslint/js");
|
|
3788
|
-
const tseslint = require("typescript-eslint");
|
|
3789
|
-
const angular = require("angular-eslint");
|
|
3790
|
-
const eslintConfigPrettier = require("eslint-config-prettier");
|
|
3791
|
-
|
|
3792
|
-
module.exports = tseslint.config(
|
|
3793
|
-
{
|
|
3794
|
-
files: ["**/*.ts"],
|
|
3795
|
-
extends: [
|
|
3796
|
-
eslint.configs.recommended,
|
|
3797
|
-
...tseslint.configs.recommended,
|
|
3798
|
-
...tseslint.configs.stylistic,
|
|
3799
|
-
...angular.configs.tsRecommended,
|
|
3800
|
-
eslintConfigPrettier,
|
|
3801
|
-
],
|
|
3802
|
-
processor: angular.processInlineTemplates,
|
|
3803
|
-
rules: {
|
|
3804
|
-
"@angular-eslint/directive-selector": [
|
|
3805
|
-
"error",
|
|
3806
|
-
{
|
|
3807
|
-
type: "attribute",
|
|
3808
|
-
prefix: "app",
|
|
3809
|
-
style: "camelCase",
|
|
3810
|
-
},
|
|
3811
|
-
],
|
|
3812
|
-
"@angular-eslint/component-selector": [
|
|
3813
|
-
"error",
|
|
3814
|
-
{
|
|
3815
|
-
type: "element",
|
|
3816
|
-
prefix: "app",
|
|
3817
|
-
style: "kebab-case",
|
|
3818
|
-
},
|
|
3819
|
-
],
|
|
3820
|
-
"@angular-eslint/component-class-suffix": [
|
|
3821
|
-
"error",
|
|
3822
|
-
{
|
|
3823
|
-
"suffixes": ["Component", "App"]
|
|
3824
|
-
}
|
|
3825
|
-
],
|
|
3826
|
-
"@typescript-eslint/no-unused-vars": [
|
|
3827
|
-
"error",
|
|
3828
|
-
{
|
|
3829
|
-
"argsIgnorePattern": "^_",
|
|
3830
|
-
"varsIgnorePattern": "^_"
|
|
3831
|
-
}
|
|
3832
|
-
],
|
|
3833
|
-
},
|
|
3834
|
-
},
|
|
3835
|
-
{
|
|
3836
|
-
files: ["**/*.html"],
|
|
3837
|
-
extends: [
|
|
3838
|
-
...angular.configs.templateRecommended,
|
|
3839
|
-
...angular.configs.templateAccessibility,
|
|
3840
|
-
],
|
|
3841
|
-
rules: {},
|
|
3842
|
-
}
|
|
3843
|
-
);
|
|
3844
|
-
`;
|
|
3845
|
-
|
|
3846
|
-
await fs.writeFile(path.join(config.fullPath, 'eslint.config.js'), eslintConfig);
|
|
3847
|
-
|
|
3848
|
-
// Create VS Code extensions recommendation
|
|
3849
|
-
await fs.ensureDir(path.join(config.fullPath, '.vscode'));
|
|
3850
|
-
|
|
3851
|
-
const vscodeExtensions = {
|
|
3852
|
-
recommendations: [
|
|
3853
|
-
'angular.ng-template',
|
|
3854
|
-
'esbenp.prettier-vscode',
|
|
3855
|
-
'dbaeumer.vscode-eslint',
|
|
3856
|
-
'bradlc.vscode-tailwindcss',
|
|
3857
|
-
],
|
|
3858
|
-
};
|
|
3859
|
-
|
|
3860
|
-
await fs.writeFile(
|
|
3861
|
-
path.join(config.fullPath, '.vscode/extensions.json'),
|
|
3862
|
-
JSON.stringify(vscodeExtensions, null, 2)
|
|
3863
|
-
);
|
|
3864
|
-
|
|
3865
|
-
// Create VS Code settings for format-on-save
|
|
3866
|
-
const vscodeSettings = {
|
|
3867
|
-
'editor.formatOnSave': true,
|
|
3868
|
-
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
3869
|
-
'editor.codeActionsOnSave': {
|
|
3870
|
-
'source.fixAll.eslint': 'explicit',
|
|
3871
|
-
},
|
|
3872
|
-
'[typescript]': {
|
|
3873
|
-
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
3874
|
-
'editor.formatOnSave': true,
|
|
3875
|
-
},
|
|
3876
|
-
'[html]': {
|
|
3877
|
-
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
3878
|
-
'editor.formatOnSave': true,
|
|
3879
|
-
},
|
|
3880
|
-
'[css]': {
|
|
3881
|
-
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
3882
|
-
'editor.formatOnSave': true,
|
|
3883
|
-
},
|
|
3884
|
-
'[json]': {
|
|
3885
|
-
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
3886
|
-
'editor.formatOnSave': true,
|
|
3887
|
-
},
|
|
3888
|
-
'[jsonc]': {
|
|
3889
|
-
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
3890
|
-
'editor.formatOnSave': true,
|
|
3891
|
-
},
|
|
3892
|
-
'prettier.requireConfig': false,
|
|
3893
|
-
'eslint.validate': ['javascript', 'typescript', 'html'],
|
|
3894
|
-
'files.eol': '\\n',
|
|
3895
|
-
'files.trimTrailingWhitespace': true,
|
|
3896
|
-
'files.insertFinalNewline': true,
|
|
3897
|
-
// Suppress CSS linting warnings for Tailwind CSS directives
|
|
3898
|
-
'css.lint.unknownAtRules': 'ignore',
|
|
3899
|
-
};
|
|
3900
|
-
|
|
3901
|
-
await fs.writeFile(
|
|
3902
|
-
path.join(config.fullPath, '.vscode/settings.json'),
|
|
3903
|
-
JSON.stringify(vscodeSettings, null, 2)
|
|
3904
|
-
);
|
|
3905
|
-
|
|
3906
|
-
// Create .prettierignore file
|
|
3907
|
-
const prettierIgnore = `# Build outputs
|
|
3908
|
-
dist/
|
|
3909
|
-
coverage/
|
|
3910
|
-
node_modules/
|
|
3911
|
-
|
|
3912
|
-
# Generated files
|
|
3913
|
-
*.d.ts
|
|
3914
|
-
`;
|
|
3915
|
-
|
|
3916
|
-
await fs.writeFile(path.join(config.fullPath, '.prettierignore'), prettierIgnore);
|
|
3917
|
-
|
|
3918
|
-
// Create .prettierrc.json file for better IDE support
|
|
3919
|
-
await fs.writeFile(
|
|
3920
|
-
path.join(config.fullPath, '.prettierrc.json'),
|
|
3921
|
-
JSON.stringify(prettierConfig, null, 2)
|
|
3922
|
-
);
|
|
3923
|
-
|
|
3924
|
-
// Install linting packages if not skipping install
|
|
3925
|
-
if (!config.skipInstall) {
|
|
3926
|
-
await this.installLintingPackages(config);
|
|
3927
|
-
}
|
|
3928
|
-
},
|
|
3929
|
-
|
|
3930
|
-
async configureAngularJson(config) {
|
|
3931
|
-
const angularJsonPath = path.join(config.fullPath, 'angular.json');
|
|
3932
|
-
const angularJson = JSON.parse(await fs.readFile(angularJsonPath, 'utf8'));
|
|
3933
|
-
|
|
3934
|
-
// Add lint architect target
|
|
3935
|
-
if (angularJson.projects && angularJson.projects[config.projectName]) {
|
|
3936
|
-
angularJson.projects[config.projectName].architect.lint = {
|
|
3937
|
-
builder: '@angular-eslint/builder:lint',
|
|
3938
|
-
options: {
|
|
3939
|
-
lintFilePatterns: ['src/**/*.ts', 'src/**/*.html'],
|
|
3940
|
-
},
|
|
3941
|
-
};
|
|
3942
|
-
}
|
|
3943
|
-
|
|
3944
|
-
await fs.writeFile(angularJsonPath, JSON.stringify(angularJson, null, 2));
|
|
3945
|
-
},
|
|
3946
|
-
|
|
3947
|
-
async installLintingPackages(config) {
|
|
3948
|
-
const execa = require('execa');
|
|
3949
|
-
|
|
3950
|
-
try {
|
|
3951
|
-
const packages = [
|
|
3952
|
-
'@eslint/js@^9.23.0',
|
|
3953
|
-
'eslint@^9.23.0',
|
|
3954
|
-
'angular-eslint@19.3.0',
|
|
3955
|
-
'typescript-eslint@^8.20.0',
|
|
3956
|
-
'prettier@^3.5.3',
|
|
3957
|
-
'eslint-config-prettier@^10.1.2',
|
|
3958
|
-
'prettier-plugin-tailwindcss@^0.6.11',
|
|
3959
|
-
'simple-git-hooks@^2.11.1',
|
|
3960
|
-
];
|
|
3961
|
-
|
|
3962
|
-
await execa.command(`npm install ${packages.join(' ')} --save-dev`, {
|
|
3963
|
-
cwd: config.fullPath,
|
|
3964
|
-
stdio: 'pipe',
|
|
3965
|
-
});
|
|
3966
|
-
} catch (error) {
|
|
3967
|
-
// Silently fail - packages will be installed when user runs npm install
|
|
3968
|
-
// Note: Linting packages will be installed with npm install
|
|
3969
|
-
}
|
|
3970
|
-
},
|
|
3971
|
-
|
|
3972
|
-
async formatCode(config) {
|
|
3973
|
-
const execa = require('execa');
|
|
3974
|
-
|
|
3975
|
-
try {
|
|
3976
|
-
// Run prettier to format all generated files (including config files)
|
|
3977
|
-
await execa.command(
|
|
3978
|
-
'npx prettier --write "**/*.{ts,html,css,json}" --ignore-path .gitignore --log-level silent',
|
|
3979
|
-
{
|
|
3980
|
-
cwd: config.fullPath,
|
|
3981
|
-
stdio: 'pipe',
|
|
3982
|
-
}
|
|
3983
|
-
);
|
|
3984
|
-
} catch (error) {
|
|
3985
|
-
// If prettier fails, it's not critical - user can run it manually
|
|
3986
|
-
// Silent failure - user can run "npm run format" later
|
|
3987
|
-
}
|
|
3988
|
-
},
|
|
3989
|
-
|
|
3990
|
-
async setupPWA(config) {
|
|
3991
|
-
// Create manifest.json
|
|
3992
|
-
const manifest = {
|
|
3993
|
-
name: config.projectName,
|
|
3994
|
-
short_name: config.projectName,
|
|
3995
|
-
theme_color: '#3b82f6',
|
|
3996
|
-
background_color: '#ffffff',
|
|
3997
|
-
display: 'standalone',
|
|
3998
|
-
scope: '/',
|
|
3999
|
-
start_url: '/',
|
|
4000
|
-
description: `${config.projectName} - Built with Angular and Tailwind CSS`,
|
|
4001
|
-
icons: [
|
|
4002
|
-
{
|
|
4003
|
-
src: 'assets/icons/android-chrome-192x192.svg',
|
|
4004
|
-
sizes: '192x192',
|
|
4005
|
-
type: 'image/svg+xml',
|
|
4006
|
-
purpose: 'maskable any',
|
|
4007
|
-
},
|
|
4008
|
-
{
|
|
4009
|
-
src: 'assets/icons/android-chrome-512x512.svg',
|
|
4010
|
-
sizes: '512x512',
|
|
4011
|
-
type: 'image/svg+xml',
|
|
4012
|
-
purpose: 'maskable any',
|
|
4013
|
-
},
|
|
4014
|
-
],
|
|
4015
|
-
};
|
|
4016
|
-
|
|
4017
|
-
// Write manifest to public directory (new Angular structure)
|
|
4018
|
-
await fs.ensureDir(path.join(config.fullPath, 'public'));
|
|
4019
|
-
await fs.writeFile(
|
|
4020
|
-
path.join(config.fullPath, 'public/manifest.webmanifest'),
|
|
4021
|
-
JSON.stringify(manifest, null, 2)
|
|
4022
|
-
);
|
|
4023
|
-
|
|
4024
|
-
// Create ngsw-config.json (Service Worker configuration)
|
|
4025
|
-
const ngswConfig = {
|
|
4026
|
-
$schema: './node_modules/@angular/service-worker/config/schema.json',
|
|
4027
|
-
index: '/index.html',
|
|
4028
|
-
assetGroups: [
|
|
4029
|
-
{
|
|
4030
|
-
name: 'app',
|
|
4031
|
-
installMode: 'prefetch',
|
|
4032
|
-
resources: {
|
|
4033
|
-
files: ['/favicon.ico', '/index.html', '/manifest.webmanifest', '/*.css', '/*.js'],
|
|
4034
|
-
},
|
|
4035
|
-
},
|
|
4036
|
-
{
|
|
4037
|
-
name: 'assets',
|
|
4038
|
-
installMode: 'lazy',
|
|
4039
|
-
updateMode: 'prefetch',
|
|
4040
|
-
resources: {
|
|
4041
|
-
files: [
|
|
4042
|
-
'/assets/**',
|
|
4043
|
-
'/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)',
|
|
4044
|
-
],
|
|
4045
|
-
},
|
|
4046
|
-
},
|
|
4047
|
-
],
|
|
4048
|
-
dataGroups: [
|
|
4049
|
-
{
|
|
4050
|
-
name: 'api',
|
|
4051
|
-
urls: ['https://api.example.com/**'],
|
|
4052
|
-
cacheConfig: {
|
|
4053
|
-
maxSize: 100,
|
|
4054
|
-
maxAge: '1h',
|
|
4055
|
-
timeout: '10s',
|
|
4056
|
-
strategy: 'freshness',
|
|
4057
|
-
},
|
|
4058
|
-
},
|
|
4059
|
-
],
|
|
4060
|
-
};
|
|
4061
|
-
|
|
4062
|
-
await fs.writeFile(
|
|
4063
|
-
path.join(config.fullPath, 'ngsw-config.json'),
|
|
4064
|
-
JSON.stringify(ngswConfig, null, 2)
|
|
4065
|
-
);
|
|
4066
|
-
|
|
4067
|
-
// Generate PWA icons (as SVG for now - users can replace with actual PNGs)
|
|
4068
|
-
await fs.ensureDir(path.join(config.fullPath, 'public/assets/icons'));
|
|
4069
|
-
|
|
4070
|
-
const iconSizes = [72, 96, 128, 144, 152, 192, 384, 512];
|
|
4071
|
-
const firstLetter = config.projectName.charAt(0).toUpperCase();
|
|
4072
|
-
|
|
4073
|
-
for (const size of iconSizes) {
|
|
4074
|
-
const iconSvg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
4075
|
-
<rect width="${size}" height="${size}" fill="#3B82F6" rx="${size * 0.2}"/>
|
|
4076
|
-
<text x="50%" y="55%" text-anchor="middle" fill="white" font-size="${
|
|
4077
|
-
size * 0.5
|
|
4078
|
-
}" font-family="Arial, sans-serif" font-weight="bold" dominant-baseline="middle">
|
|
4079
|
-
${firstLetter}
|
|
4080
|
-
</text>
|
|
4081
|
-
</svg>`;
|
|
4082
|
-
|
|
4083
|
-
// Save as SVG (users can convert to PNG later or use tools)
|
|
4084
|
-
await fs.writeFile(
|
|
4085
|
-
path.join(config.fullPath, `public/assets/icons/icon-${size}x${size}.svg`),
|
|
4086
|
-
iconSvg
|
|
4087
|
-
);
|
|
4088
|
-
}
|
|
4089
|
-
|
|
4090
|
-
// Create a README for icons
|
|
4091
|
-
const iconReadme = `# PWA Icons
|
|
4092
|
-
|
|
4093
|
-
These are placeholder SVG icons for your PWA.
|
|
4094
|
-
|
|
4095
|
-
## Generating PNG Icons
|
|
4096
|
-
|
|
4097
|
-
You should replace these with actual PNG icons for better compatibility. You can:
|
|
4098
|
-
|
|
4099
|
-
1. Use online tools like https://realfavicongenerator.net/
|
|
4100
|
-
2. Use a design tool to export PNG icons
|
|
4101
|
-
3. Use command-line tools like ImageMagick to convert SVGs to PNGs
|
|
4102
|
-
|
|
4103
|
-
## Required Sizes
|
|
4104
|
-
|
|
4105
|
-
- 72x72, 96x96, 128x128, 144x144, 152x152, 192x192, 384x384, 512x512
|
|
4106
|
-
|
|
4107
|
-
Update the paths in \`manifest.webmanifest\` if you change the file format.
|
|
4108
|
-
`;
|
|
4109
|
-
|
|
4110
|
-
await fs.writeFile(path.join(config.fullPath, 'public/assets/icons/README.md'), iconReadme);
|
|
4111
|
-
|
|
4112
|
-
// Update index.html to include manifest and theme
|
|
4113
|
-
const indexHtmlPath = path.join(config.fullPath, 'src/index.html');
|
|
4114
|
-
let indexHtml = await fs.readFile(indexHtmlPath, 'utf8');
|
|
4115
|
-
|
|
4116
|
-
if (!indexHtml.includes('manifest.webmanifest')) {
|
|
4117
|
-
indexHtml = indexHtml.replace(
|
|
4118
|
-
'</head>',
|
|
4119
|
-
` <link rel="manifest" href="manifest.webmanifest">
|
|
4120
|
-
<meta name="theme-color" content="#3b82f6">
|
|
4121
|
-
</head>`
|
|
4122
|
-
);
|
|
4123
|
-
await fs.writeFile(indexHtmlPath, indexHtml);
|
|
4124
|
-
}
|
|
4125
|
-
|
|
4126
|
-
// Update angular.json to include service worker
|
|
4127
|
-
const angularJsonPath = path.join(config.fullPath, 'angular.json');
|
|
4128
|
-
const angularJson = JSON.parse(await fs.readFile(angularJsonPath, 'utf8'));
|
|
4129
|
-
|
|
4130
|
-
const projectName = Object.keys(angularJson.projects)[0];
|
|
4131
|
-
const buildConfigs = angularJson.projects[projectName].architect.build.configurations;
|
|
4132
|
-
|
|
4133
|
-
// Add service worker to production configuration
|
|
4134
|
-
if (buildConfigs && buildConfigs.production && !buildConfigs.production.serviceWorker) {
|
|
4135
|
-
buildConfigs.production.serviceWorker = 'ngsw-config.json';
|
|
4136
|
-
}
|
|
4137
|
-
|
|
4138
|
-
await fs.writeFile(angularJsonPath, JSON.stringify(angularJson, null, 2));
|
|
4139
|
-
},
|
|
4140
132
|
};
|
|
4141
133
|
|
|
4142
134
|
module.exports = starter;
|