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
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// Import modular components
|
|
5
|
+
const { createCoreServices } = require('./services');
|
|
6
|
+
const { createSharedComponents } = require('./components');
|
|
7
|
+
const {
|
|
8
|
+
createDirectoryStructure,
|
|
9
|
+
createEnvironments,
|
|
10
|
+
createGuards,
|
|
11
|
+
createModels,
|
|
12
|
+
createPipes,
|
|
13
|
+
} = require('./infrastructure');
|
|
14
|
+
const {
|
|
15
|
+
createVSCodeSettings,
|
|
16
|
+
updateTsConfig,
|
|
17
|
+
setupLinting,
|
|
18
|
+
formatCode,
|
|
19
|
+
} = require('./linting');
|
|
20
|
+
const { setupPWA } = require('./pwa');
|
|
21
|
+
|
|
22
|
+
// Import existing modular components
|
|
23
|
+
const {
|
|
24
|
+
createAuthInterceptor,
|
|
25
|
+
createErrorInterceptor,
|
|
26
|
+
createLoadingInterceptor,
|
|
27
|
+
createCachingInterceptor,
|
|
28
|
+
createToastService,
|
|
29
|
+
createToastComponent,
|
|
30
|
+
createLoadingService,
|
|
31
|
+
createCacheService,
|
|
32
|
+
} = require('./advanced-features');
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
createModalService,
|
|
36
|
+
createModalComponent,
|
|
37
|
+
createTruncatePipe,
|
|
38
|
+
createClickOutsideDirective,
|
|
39
|
+
createTooltipDirective,
|
|
40
|
+
} = require('./ui-features');
|
|
41
|
+
|
|
42
|
+
const { createSEOService, createStructuredDataUtil, createRobotsTxt } = require('./seo-features');
|
|
43
|
+
|
|
44
|
+
const {
|
|
45
|
+
createDefaultOGImage,
|
|
46
|
+
createFavicon,
|
|
47
|
+
createAppleTouchIcon,
|
|
48
|
+
createAndroidIcon,
|
|
49
|
+
createAndroidIconLarge,
|
|
50
|
+
createLogo,
|
|
51
|
+
} = require('./seo-assets');
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Base Template
|
|
55
|
+
* Contains all core infrastructure: services, guards, interceptors, pipes, directives, etc.
|
|
56
|
+
* UI templates (starter, dashboard, ecommerce) extend this base.
|
|
57
|
+
*/
|
|
58
|
+
const base = {
|
|
59
|
+
info: {
|
|
60
|
+
name: 'Base',
|
|
61
|
+
description: 'Core infrastructure template with services, guards, interceptors, and shared components',
|
|
62
|
+
features: [
|
|
63
|
+
'Modern standalone Angular 20+ architecture with signals',
|
|
64
|
+
'HTTP Interceptors (Auth, Error, Loading, Caching)',
|
|
65
|
+
'Core services (API, Auth, Storage, Cache, Loading, SEO)',
|
|
66
|
+
'Toast/Notification system with auto-dismiss',
|
|
67
|
+
'Modal/Dialog system with confirm & alert',
|
|
68
|
+
'Essential UI components (Button, Card, Spinner, Toast, Modal)',
|
|
69
|
+
'Truncate pipe for text truncation',
|
|
70
|
+
'Useful Directives (ClickOutside, Tooltip)',
|
|
71
|
+
'TypeScript interfaces and models',
|
|
72
|
+
'ESLint + Prettier + simple-git-hooks (pre-commit)',
|
|
73
|
+
'PWA-ready with service worker configuration',
|
|
74
|
+
'Icons library (@ng-icons/heroicons)',
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async apply(config, spinner) {
|
|
79
|
+
this.spinner = spinner;
|
|
80
|
+
const chalk = require('chalk');
|
|
81
|
+
|
|
82
|
+
const completeStep = (message) => {
|
|
83
|
+
if (spinner) {
|
|
84
|
+
spinner.stop();
|
|
85
|
+
console.log(chalk.green(' ✔') + chalk.white(' ' + message));
|
|
86
|
+
spinner.start();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Create core infrastructure
|
|
91
|
+
if (spinner) spinner.update('Setting up base project structure...');
|
|
92
|
+
await createDirectoryStructure(config);
|
|
93
|
+
await createEnvironments(config);
|
|
94
|
+
await createCoreServices(config);
|
|
95
|
+
await this.createSEOFeatures(config);
|
|
96
|
+
await this.createHttpInterceptors(config);
|
|
97
|
+
await createGuards(config);
|
|
98
|
+
await createSharedComponents(config);
|
|
99
|
+
await createLoadingService(config);
|
|
100
|
+
await createCacheService(config);
|
|
101
|
+
await this.createToastSystem(config);
|
|
102
|
+
await this.createModalSystem(config);
|
|
103
|
+
await createPipes(config);
|
|
104
|
+
await this.createAdditionalPipes(config);
|
|
105
|
+
await this.createDirectives(config);
|
|
106
|
+
await createModels(config);
|
|
107
|
+
await createVSCodeSettings(config);
|
|
108
|
+
await updateTsConfig(config);
|
|
109
|
+
await setupLinting(config);
|
|
110
|
+
await setupPWA(config);
|
|
111
|
+
|
|
112
|
+
if (spinner) spinner.stop();
|
|
113
|
+
|
|
114
|
+
// Show base template summary
|
|
115
|
+
console.log('');
|
|
116
|
+
completeStep('Core infrastructure configured');
|
|
117
|
+
completeStep('HTTP interceptors (Auth, Error, Loading, Cache)');
|
|
118
|
+
completeStep('Core services (9 services including SEO)');
|
|
119
|
+
completeStep('UI components (Button, Card, Toast, Modal, Spinner)');
|
|
120
|
+
completeStep('Utility pipes & directives');
|
|
121
|
+
completeStep('PWA support configured');
|
|
122
|
+
completeStep('ESLint + Prettier + simple-git-hooks');
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async createSEOFeatures(config) {
|
|
126
|
+
// Create SEO Service
|
|
127
|
+
const seoService = createSEOService();
|
|
128
|
+
await fs.writeFile(
|
|
129
|
+
path.join(config.fullPath, 'src/app/core/services/seo.service.ts'),
|
|
130
|
+
seoService
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Create Structured Data Utility
|
|
134
|
+
const structuredDataUtil = createStructuredDataUtil();
|
|
135
|
+
await fs.writeFile(
|
|
136
|
+
path.join(config.fullPath, 'src/app/core/utils/structured-data.ts'),
|
|
137
|
+
structuredDataUtil
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Create robots.txt in public folder
|
|
141
|
+
const robotsTxt = createRobotsTxt();
|
|
142
|
+
await fs.writeFile(path.join(config.fullPath, 'public/robots.txt'), robotsTxt);
|
|
143
|
+
|
|
144
|
+
// Create default OG image (SVG as placeholder)
|
|
145
|
+
const ogImage = createDefaultOGImage(config.projectName);
|
|
146
|
+
await fs.writeFile(path.join(config.fullPath, 'public/assets/images/og-default.svg'), ogImage);
|
|
147
|
+
|
|
148
|
+
// Create favicon
|
|
149
|
+
const favicon = createFavicon(config.projectName);
|
|
150
|
+
await fs.writeFile(path.join(config.fullPath, 'public/favicon.svg'), favicon);
|
|
151
|
+
|
|
152
|
+
// Create Apple Touch Icon (SVG as placeholder)
|
|
153
|
+
const appleTouchIcon = createAppleTouchIcon(config.projectName);
|
|
154
|
+
await fs.writeFile(
|
|
155
|
+
path.join(config.fullPath, 'public/assets/icons/apple-touch-icon.svg'),
|
|
156
|
+
appleTouchIcon
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Create Android Chrome icons (SVG as placeholder)
|
|
160
|
+
const androidIcon = createAndroidIcon(config.projectName);
|
|
161
|
+
await fs.writeFile(
|
|
162
|
+
path.join(config.fullPath, 'public/assets/icons/android-chrome-192x192.svg'),
|
|
163
|
+
androidIcon
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const androidIconLarge = createAndroidIconLarge(config.projectName);
|
|
167
|
+
await fs.writeFile(
|
|
168
|
+
path.join(config.fullPath, 'public/assets/icons/android-chrome-512x512.svg'),
|
|
169
|
+
androidIconLarge
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Create logo
|
|
173
|
+
const logo = createLogo(config.projectName);
|
|
174
|
+
await fs.writeFile(path.join(config.fullPath, 'public/assets/images/logo.svg'), logo);
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
async createHttpInterceptors(config) {
|
|
178
|
+
await createAuthInterceptor(config);
|
|
179
|
+
await createErrorInterceptor(config);
|
|
180
|
+
await createLoadingInterceptor(config);
|
|
181
|
+
await createCachingInterceptor(config);
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
async createToastSystem(config) {
|
|
185
|
+
await createToastService(config);
|
|
186
|
+
await createToastComponent(config);
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
async createModalSystem(config) {
|
|
190
|
+
await createModalService(config);
|
|
191
|
+
await createModalComponent(config);
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
async createAdditionalPipes(config) {
|
|
195
|
+
await createTruncatePipe(config);
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
async createDirectives(config) {
|
|
199
|
+
await createClickOutsideDirective(config);
|
|
200
|
+
await createTooltipDirective(config);
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
// Expose formatCode for child templates to use
|
|
204
|
+
formatCode,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
module.exports = base;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create directory structure for base template
|
|
6
|
+
*/
|
|
7
|
+
async function createDirectoryStructure(config) {
|
|
8
|
+
const directories = [
|
|
9
|
+
'src/environments',
|
|
10
|
+
'src/app/core/services',
|
|
11
|
+
'src/app/core/guards',
|
|
12
|
+
'src/app/core/interceptors',
|
|
13
|
+
'src/app/core/utils',
|
|
14
|
+
'src/app/shared/components/button',
|
|
15
|
+
'src/app/shared/components/card',
|
|
16
|
+
'src/app/shared/components/loading-spinner',
|
|
17
|
+
'src/app/shared/components/toast',
|
|
18
|
+
'src/app/shared/components/modal',
|
|
19
|
+
'src/app/shared/pipes',
|
|
20
|
+
'src/app/shared/directives',
|
|
21
|
+
'src/app/shared/models',
|
|
22
|
+
'public/assets/images',
|
|
23
|
+
'public/assets/icons',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
for (const dir of directories) {
|
|
27
|
+
await fs.ensureDir(path.join(config.fullPath, dir));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create environment files
|
|
33
|
+
*/
|
|
34
|
+
async function createEnvironments(config) {
|
|
35
|
+
// Development environment
|
|
36
|
+
const devEnvironment = `export const environment = {
|
|
37
|
+
production: false,
|
|
38
|
+
apiUrl: 'http://localhost:3000/api',
|
|
39
|
+
appName: '${config.projectName}',
|
|
40
|
+
version: '1.0.0',
|
|
41
|
+
enableDevTools: true,
|
|
42
|
+
logLevel: 'debug' as 'debug' | 'info' | 'warn' | 'error',
|
|
43
|
+
features: {
|
|
44
|
+
enableAnalytics: false,
|
|
45
|
+
enableLogging: true,
|
|
46
|
+
enableMocking: true
|
|
47
|
+
},
|
|
48
|
+
auth: {
|
|
49
|
+
tokenKey: 'auth_token',
|
|
50
|
+
refreshTokenKey: 'refresh_token',
|
|
51
|
+
tokenExpirationBuffer: 300000 // 5 minutes
|
|
52
|
+
},
|
|
53
|
+
api: {
|
|
54
|
+
timeout: 30000,
|
|
55
|
+
retryAttempts: 3
|
|
56
|
+
}
|
|
57
|
+
};`;
|
|
58
|
+
|
|
59
|
+
await fs.writeFile(
|
|
60
|
+
path.join(config.fullPath, 'src/environments/environment.ts'),
|
|
61
|
+
devEnvironment
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Production environment
|
|
65
|
+
const prodEnvironment = `export const environment = {
|
|
66
|
+
production: true,
|
|
67
|
+
apiUrl: 'https://api.${config.projectName}.com/api',
|
|
68
|
+
appName: '${config.projectName}',
|
|
69
|
+
version: '1.0.0',
|
|
70
|
+
enableDevTools: false,
|
|
71
|
+
logLevel: 'error' as 'debug' | 'info' | 'warn' | 'error',
|
|
72
|
+
features: {
|
|
73
|
+
enableAnalytics: true,
|
|
74
|
+
enableLogging: false,
|
|
75
|
+
enableMocking: false
|
|
76
|
+
},
|
|
77
|
+
auth: {
|
|
78
|
+
tokenKey: 'auth_token',
|
|
79
|
+
refreshTokenKey: 'refresh_token',
|
|
80
|
+
tokenExpirationBuffer: 300000 // 5 minutes
|
|
81
|
+
},
|
|
82
|
+
api: {
|
|
83
|
+
timeout: 30000,
|
|
84
|
+
retryAttempts: 1
|
|
85
|
+
}
|
|
86
|
+
};`;
|
|
87
|
+
|
|
88
|
+
await fs.writeFile(
|
|
89
|
+
path.join(config.fullPath, 'src/environments/environment.prod.ts'),
|
|
90
|
+
prodEnvironment
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create auth guard
|
|
96
|
+
*/
|
|
97
|
+
async function createGuards(config) {
|
|
98
|
+
const authGuard = `import { CanActivateFn, Router } from '@angular/router';
|
|
99
|
+
import { inject } from '@angular/core';
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Simple authentication guard
|
|
103
|
+
* Replace with your actual authentication logic
|
|
104
|
+
*/
|
|
105
|
+
export const authGuard: CanActivateFn = (_route, _state) => {
|
|
106
|
+
const router = inject(Router);
|
|
107
|
+
|
|
108
|
+
// Replace with your actual authentication check
|
|
109
|
+
const isAuthenticated = localStorage.getItem('auth-token') !== null;
|
|
110
|
+
|
|
111
|
+
if (!isAuthenticated) {
|
|
112
|
+
// Redirect to login page
|
|
113
|
+
router.navigate(['/auth/login']);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return true;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Example: Check if user has specific role
|
|
122
|
+
*/
|
|
123
|
+
export const roleGuard = (requiredRole: string): CanActivateFn => {
|
|
124
|
+
return (_route, _state) => {
|
|
125
|
+
const router = inject(Router);
|
|
126
|
+
|
|
127
|
+
// Replace with your actual role checking logic
|
|
128
|
+
const userRole = localStorage.getItem('user-role');
|
|
129
|
+
|
|
130
|
+
if (userRole !== requiredRole) {
|
|
131
|
+
router.navigate(['/unauthorized']);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return true;
|
|
136
|
+
};
|
|
137
|
+
};`;
|
|
138
|
+
|
|
139
|
+
await fs.writeFile(path.join(config.fullPath, 'src/app/core/guards/auth.guard.ts'), authGuard);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create model interfaces
|
|
144
|
+
*/
|
|
145
|
+
async function createModels(config) {
|
|
146
|
+
// User Interface
|
|
147
|
+
const userInterface = `export interface User {
|
|
148
|
+
id: string;
|
|
149
|
+
email: string;
|
|
150
|
+
firstName: string;
|
|
151
|
+
lastName: string;
|
|
152
|
+
avatar?: string;
|
|
153
|
+
role: 'user' | 'admin' | 'moderator';
|
|
154
|
+
createdAt: Date;
|
|
155
|
+
updatedAt: Date;
|
|
156
|
+
isActive: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface UserProfile {
|
|
160
|
+
user: User;
|
|
161
|
+
preferences: {
|
|
162
|
+
notifications: boolean;
|
|
163
|
+
language: string;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface CreateUserRequest {
|
|
168
|
+
email: string;
|
|
169
|
+
password: string;
|
|
170
|
+
firstName: string;
|
|
171
|
+
lastName: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface UpdateUserRequest {
|
|
175
|
+
firstName?: string;
|
|
176
|
+
lastName?: string;
|
|
177
|
+
avatar?: string;
|
|
178
|
+
}`;
|
|
179
|
+
|
|
180
|
+
await fs.writeFile(
|
|
181
|
+
path.join(config.fullPath, 'src/app/shared/models/user.interface.ts'),
|
|
182
|
+
userInterface
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// API Response Interface
|
|
186
|
+
const apiResponseInterface = `export interface ApiResponse<T = unknown> {
|
|
187
|
+
data: T;
|
|
188
|
+
message: string;
|
|
189
|
+
success: boolean;
|
|
190
|
+
errors?: string[];
|
|
191
|
+
meta?: {
|
|
192
|
+
total: number;
|
|
193
|
+
page: number;
|
|
194
|
+
perPage: number;
|
|
195
|
+
totalPages: number;
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface ApiError {
|
|
200
|
+
message: string;
|
|
201
|
+
code: string;
|
|
202
|
+
status: number;
|
|
203
|
+
timestamp: string;
|
|
204
|
+
details?: Record<string, string[]>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface PaginatedResponse<T> {
|
|
208
|
+
data: T[];
|
|
209
|
+
meta: {
|
|
210
|
+
total: number;
|
|
211
|
+
page: number;
|
|
212
|
+
perPage: number;
|
|
213
|
+
totalPages: number;
|
|
214
|
+
hasNextPage: boolean;
|
|
215
|
+
hasPrevPage: boolean;
|
|
216
|
+
};
|
|
217
|
+
}`;
|
|
218
|
+
|
|
219
|
+
await fs.writeFile(
|
|
220
|
+
path.join(config.fullPath, 'src/app/shared/models/api-response.interface.ts'),
|
|
221
|
+
apiResponseInterface
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Create pipes (Truncate, TimeAgo)
|
|
227
|
+
*/
|
|
228
|
+
async function createPipes(config) {
|
|
229
|
+
// Truncate Pipe
|
|
230
|
+
const truncatePipe = `import { Pipe, PipeTransform } from '@angular/core';
|
|
231
|
+
|
|
232
|
+
@Pipe({
|
|
233
|
+
name: 'truncate',
|
|
234
|
+
standalone: true
|
|
235
|
+
})
|
|
236
|
+
export class TruncatePipe implements PipeTransform {
|
|
237
|
+
transform(value: string | null | undefined, limit = 50, suffix = '...'): string {
|
|
238
|
+
if (!value) return '';
|
|
239
|
+
|
|
240
|
+
if (value.length <= limit) {
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return value.substring(0, limit).trim() + suffix;
|
|
245
|
+
}
|
|
246
|
+
}`;
|
|
247
|
+
|
|
248
|
+
await fs.writeFile(
|
|
249
|
+
path.join(config.fullPath, 'src/app/shared/pipes/truncate.pipe.ts'),
|
|
250
|
+
truncatePipe
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Time Ago Pipe
|
|
254
|
+
const timeAgoPipe = `import { Pipe, PipeTransform } from '@angular/core';
|
|
255
|
+
|
|
256
|
+
@Pipe({
|
|
257
|
+
name: 'timeAgo',
|
|
258
|
+
standalone: true
|
|
259
|
+
})
|
|
260
|
+
export class TimeAgoPipe implements PipeTransform {
|
|
261
|
+
transform(value: string | number | Date | null | undefined): string {
|
|
262
|
+
if (!value) return '';
|
|
263
|
+
|
|
264
|
+
const date = new Date(value);
|
|
265
|
+
const now = new Date();
|
|
266
|
+
const secondsAgo = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
267
|
+
|
|
268
|
+
if (secondsAgo < 60) {
|
|
269
|
+
return 'just now';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const minutesAgo = Math.floor(secondsAgo / 60);
|
|
273
|
+
if (minutesAgo < 60) {
|
|
274
|
+
return \`\${minutesAgo} minute\${minutesAgo === 1 ? '' : 's'} ago\`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const hoursAgo = Math.floor(minutesAgo / 60);
|
|
278
|
+
if (hoursAgo < 24) {
|
|
279
|
+
return \`\${hoursAgo} hour\${hoursAgo === 1 ? '' : 's'} ago\`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const daysAgo = Math.floor(hoursAgo / 24);
|
|
283
|
+
if (daysAgo < 7) {
|
|
284
|
+
return \`\${daysAgo} day\${daysAgo === 1 ? '' : 's'} ago\`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const weeksAgo = Math.floor(daysAgo / 7);
|
|
288
|
+
if (weeksAgo < 4) {
|
|
289
|
+
return \`\${weeksAgo} week\${weeksAgo === 1 ? '' : 's'} ago\`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const monthsAgo = Math.floor(daysAgo / 30);
|
|
293
|
+
if (monthsAgo < 12) {
|
|
294
|
+
return \`\${monthsAgo} month\${monthsAgo === 1 ? '' : 's'} ago\`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const yearsAgo = Math.floor(daysAgo / 365);
|
|
298
|
+
return \`\${yearsAgo} year\${yearsAgo === 1 ? '' : 's'} ago\`;
|
|
299
|
+
}
|
|
300
|
+
}`;
|
|
301
|
+
|
|
302
|
+
await fs.writeFile(
|
|
303
|
+
path.join(config.fullPath, 'src/app/shared/pipes/time-ago.pipe.ts'),
|
|
304
|
+
timeAgoPipe
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
createDirectoryStructure,
|
|
310
|
+
createEnvironments,
|
|
311
|
+
createGuards,
|
|
312
|
+
createModels,
|
|
313
|
+
createPipes,
|
|
314
|
+
};
|