create-ng-tailwind 1.0.0 → 2.0.2

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.
@@ -0,0 +1,528 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+
4
+ // ==================== HTTP INTERCEPTORS ====================
5
+
6
+ async function createAuthInterceptor(config) {
7
+ const authInterceptor = `import { HttpInterceptorFn } from '@angular/common/http';
8
+ import { inject } from '@angular/core';
9
+ import { AuthService } from '@core/services/auth.service';
10
+
11
+ /**
12
+ * Authentication Interceptor
13
+ * Adds JWT token to outgoing requests
14
+ */
15
+ export const authInterceptor: HttpInterceptorFn = (req, next) => {
16
+ const authService = inject(AuthService);
17
+ const token = authService.getToken();
18
+
19
+ // Skip adding token for login/register endpoints
20
+ const skipAuthUrls = ['/auth/login', '/auth/register', '/auth/refresh'];
21
+ const shouldSkip = skipAuthUrls.some(url => req.url.includes(url));
22
+
23
+ if (token && !shouldSkip) {
24
+ // Clone the request and add the authorization header
25
+ req = req.clone({
26
+ setHeaders: {
27
+ Authorization: \`Bearer \${token}\`
28
+ }
29
+ });
30
+ }
31
+
32
+ return next(req);
33
+ };`;
34
+
35
+ await fs.writeFile(
36
+ path.join(config.fullPath, "src/app/core/interceptors/auth.interceptor.ts"),
37
+ authInterceptor,
38
+ );
39
+ }
40
+
41
+ async function createErrorInterceptor(config) {
42
+ const errorInterceptor = `import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
43
+ import { inject } from '@angular/core';
44
+ import { Router } from '@angular/router';
45
+ import { catchError, throwError } from 'rxjs';
46
+ import { ToastService } from '@core/services/toast.service';
47
+
48
+ /**
49
+ * Error Handling Interceptor
50
+ * Handles HTTP errors globally and shows user-friendly messages
51
+ */
52
+ export const errorInterceptor: HttpInterceptorFn = (req, next) => {
53
+ const router = inject(Router);
54
+ const toastService = inject(ToastService);
55
+
56
+ return next(req).pipe(
57
+ catchError((error: HttpErrorResponse) => {
58
+ let errorMessage = 'An error occurred';
59
+
60
+ if (error.error instanceof ErrorEvent) {
61
+ // Client-side error
62
+ errorMessage = \`Client Error: \${error.error.message}\`;
63
+ } else {
64
+ // Server-side error
65
+ switch (error.status) {
66
+ case 0:
67
+ errorMessage = 'No internet connection';
68
+ break;
69
+ case 400:
70
+ errorMessage = error.error?.message || 'Bad request';
71
+ break;
72
+ case 401:
73
+ errorMessage = 'Unauthorized. Please login again.';
74
+ // Redirect to login
75
+ router.navigate(['/auth/login']);
76
+ break;
77
+ case 403:
78
+ errorMessage = 'Access forbidden';
79
+ break;
80
+ case 404:
81
+ errorMessage = 'Resource not found';
82
+ break;
83
+ case 500:
84
+ errorMessage = 'Internal server error';
85
+ break;
86
+ case 503:
87
+ errorMessage = 'Service unavailable';
88
+ break;
89
+ default:
90
+ errorMessage = error.error?.message || \`Error: \${error.status}\`;
91
+ }
92
+ }
93
+
94
+ // Show error toast
95
+ toastService.error(errorMessage);
96
+
97
+ console.error('HTTP Error:', error);
98
+ return throwError(() => new Error(errorMessage));
99
+ })
100
+ );
101
+ };`;
102
+
103
+ await fs.writeFile(
104
+ path.join(
105
+ config.fullPath,
106
+ "src/app/core/interceptors/error.interceptor.ts",
107
+ ),
108
+ errorInterceptor,
109
+ );
110
+ }
111
+
112
+ async function createLoadingInterceptor(config) {
113
+ const loadingInterceptor = `import { HttpInterceptorFn } from '@angular/common/http';
114
+ import { inject } from '@angular/core';
115
+ import { finalize } from 'rxjs';
116
+ import { LoadingService } from '@core/services/loading.service';
117
+
118
+ /**
119
+ * Loading Interceptor
120
+ * Shows/hides loading indicator for HTTP requests
121
+ */
122
+ export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
123
+ const loadingService = inject(LoadingService);
124
+
125
+ // Skip loading indicator for certain endpoints (optional)
126
+ const skipLoadingUrls = ['/api/polling', '/api/heartbeat'];
127
+ const shouldSkip = skipLoadingUrls.some(url => req.url.includes(url));
128
+
129
+ if (!shouldSkip) {
130
+ loadingService.show();
131
+ }
132
+
133
+ return next(req).pipe(
134
+ finalize(() => {
135
+ if (!shouldSkip) {
136
+ loadingService.hide();
137
+ }
138
+ })
139
+ );
140
+ };`;
141
+
142
+ await fs.writeFile(
143
+ path.join(
144
+ config.fullPath,
145
+ "src/app/core/interceptors/loading.interceptor.ts",
146
+ ),
147
+ loadingInterceptor,
148
+ );
149
+ }
150
+
151
+ async function createCachingInterceptor(config) {
152
+ const cachingInterceptor = `import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
153
+ import { inject } from '@angular/core';
154
+ import { of, tap } from 'rxjs';
155
+ import { CacheService } from '@core/services/cache.service';
156
+
157
+ /**
158
+ * Caching Interceptor
159
+ * Caches GET requests for better performance
160
+ */
161
+ export const cachingInterceptor: HttpInterceptorFn = (req, next) => {
162
+ const cacheService = inject(CacheService);
163
+
164
+ // Only cache GET requests
165
+ if (req.method !== 'GET') {
166
+ return next(req);
167
+ }
168
+
169
+ // Check if request has cache bypass header
170
+ if (req.headers.has('X-Bypass-Cache')) {
171
+ return next(req);
172
+ }
173
+
174
+ // Check cache
175
+ const cachedResponse = cacheService.get(req.url);
176
+ if (cachedResponse) {
177
+ return of(cachedResponse);
178
+ }
179
+
180
+ // If not cached, make request and cache the response
181
+ return next(req).pipe(
182
+ tap(event => {
183
+ if (event instanceof HttpResponse) {
184
+ cacheService.set(req.url, event);
185
+ }
186
+ })
187
+ );
188
+ };`;
189
+
190
+ await fs.writeFile(
191
+ path.join(
192
+ config.fullPath,
193
+ "src/app/core/interceptors/caching.interceptor.ts",
194
+ ),
195
+ cachingInterceptor,
196
+ );
197
+ }
198
+
199
+ // ==================== TOAST SERVICE & COMPONENT ====================
200
+
201
+ async function createToastService(config) {
202
+ const toastService = `import { Injectable, signal } from '@angular/core';
203
+
204
+ export interface Toast {
205
+ id: string;
206
+ type: 'success' | 'error' | 'warning' | 'info';
207
+ message: string;
208
+ duration?: number;
209
+ }
210
+
211
+ @Injectable({
212
+ providedIn: 'root'
213
+ })
214
+ export class ToastService {
215
+ // Reactive signal for toast state
216
+ toasts = signal<Toast[]>([]);
217
+
218
+ private toastId = 0;
219
+
220
+ /**
221
+ * Show a success toast
222
+ */
223
+ success(message: string, duration = 5000): void {
224
+ this.show('success', message, duration);
225
+ }
226
+
227
+ /**
228
+ * Show an error toast
229
+ */
230
+ error(message: string, duration = 7000): void {
231
+ this.show('error', message, duration);
232
+ }
233
+
234
+ /**
235
+ * Show a warning toast
236
+ */
237
+ warning(message: string, duration = 6000): void {
238
+ this.show('warning', message, duration);
239
+ }
240
+
241
+ /**
242
+ * Show an info toast
243
+ */
244
+ info(message: string, duration = 5000): void {
245
+ this.show('info', message, duration);
246
+ }
247
+
248
+ /**
249
+ * Show a toast with custom type
250
+ */
251
+ private show(type: Toast['type'], message: string, duration: number): void {
252
+ const id = \`toast-\${++this.toastId}\`;
253
+ const toast: Toast = { id, type, message, duration };
254
+
255
+ // Add toast to the list
256
+ this.toasts.update(toasts => [...toasts, toast]);
257
+
258
+ // Auto-remove after duration
259
+ if (duration > 0) {
260
+ setTimeout(() => this.remove(id), duration);
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Remove a toast by ID
266
+ */
267
+ remove(id: string): void {
268
+ this.toasts.update(toasts => toasts.filter(t => t.id !== id));
269
+ }
270
+
271
+ /**
272
+ * Clear all toasts
273
+ */
274
+ clear(): void {
275
+ this.toasts.set([]);
276
+ }
277
+ }`;
278
+
279
+ await fs.writeFile(
280
+ path.join(config.fullPath, "src/app/core/services/toast.service.ts"),
281
+ toastService,
282
+ );
283
+ }
284
+
285
+ async function createToastComponent(config) {
286
+ const toastComponent = `import { Component, inject } from '@angular/core';
287
+ import { CommonModule } from '@angular/common';
288
+ import { ToastService, Toast } from '@core/services/toast.service';
289
+
290
+ @Component({
291
+ selector: 'app-toast',
292
+ imports: [CommonModule],
293
+ template: \`
294
+ <!-- Toast Container -->
295
+ <div class="fixed top-4 right-4 z-50 space-y-2 max-w-sm w-full">
296
+ @for (toast of toastService.toasts(); track toast.id) {
297
+ <div
298
+ [class]="getToastClasses(toast)"
299
+ class="p-4 rounded-lg shadow-lg flex items-start space-x-3 animate-slide-in-right">
300
+
301
+ <!-- Icon -->
302
+ <div class="shrink-0">
303
+ @switch (toast.type) {
304
+ @case ('success') {
305
+ <svg class="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
306
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
307
+ </svg>
308
+ }
309
+ @case ('error') {
310
+ <svg class="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
311
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
312
+ </svg>
313
+ }
314
+ @case ('warning') {
315
+ <svg class="h-6 w-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
316
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
317
+ </svg>
318
+ }
319
+ @case ('info') {
320
+ <svg class="h-6 w-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
321
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
322
+ </svg>
323
+ }
324
+ }
325
+ </div>
326
+
327
+ <!-- Message -->
328
+ <div class="flex-1 pt-0.5">
329
+ <p class="text-sm font-medium text-gray-900">{{ toast.message }}</p>
330
+ </div>
331
+
332
+ <!-- Close Button -->
333
+ <button
334
+ (click)="toastService.remove(toast.id)"
335
+ class="shrink-0 text-gray-400 hover:text-gray-600 transition-colors">
336
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
337
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
338
+ </svg>
339
+ </button>
340
+ </div>
341
+ }
342
+ </div>
343
+ \`,
344
+ styles: [\`
345
+ @keyframes slide-in-right {
346
+ from {
347
+ opacity: 0;
348
+ transform: translateX(100%);
349
+ }
350
+ to {
351
+ opacity: 1;
352
+ transform: translateX(0);
353
+ }
354
+ }
355
+
356
+ .animate-slide-in-right {
357
+ animation: slide-in-right 0.3s ease-out;
358
+ }
359
+ \`]
360
+ })
361
+ export class ToastComponent {
362
+ toastService = inject(ToastService);
363
+
364
+ getToastClasses(toast: Toast): string {
365
+ const baseClasses = 'border-l-4';
366
+ const typeClasses = {
367
+ success: 'bg-green-50 border-green-500',
368
+ error: 'bg-red-50 border-red-500',
369
+ warning: 'bg-yellow-50 border-yellow-500',
370
+ info: 'bg-blue-50 border-blue-500'
371
+ };
372
+
373
+ return \`\${baseClasses} \${typeClasses[toast.type]}\`;
374
+ }
375
+ }`;
376
+
377
+ await fs.writeFile(
378
+ path.join(
379
+ config.fullPath,
380
+ "src/app/shared/components/toast/toast.component.ts",
381
+ ),
382
+ toastComponent,
383
+ );
384
+ }
385
+
386
+ // ==================== LOADING SERVICE ====================
387
+
388
+ async function createLoadingService(config) {
389
+ const loadingService = `import { Injectable, signal } from '@angular/core';
390
+
391
+ @Injectable({
392
+ providedIn: 'root'
393
+ })
394
+ export class LoadingService {
395
+ // Reactive signal for loading state
396
+ private loadingCount = 0;
397
+ isLoading = signal(false);
398
+
399
+ /**
400
+ * Show loading indicator
401
+ */
402
+ show(): void {
403
+ this.loadingCount++;
404
+ this.isLoading.set(true);
405
+ }
406
+
407
+ /**
408
+ * Hide loading indicator
409
+ */
410
+ hide(): void {
411
+ this.loadingCount = Math.max(0, this.loadingCount - 1);
412
+ if (this.loadingCount === 0) {
413
+ this.isLoading.set(false);
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Force hide loading (reset counter)
419
+ */
420
+ forceHide(): void {
421
+ this.loadingCount = 0;
422
+ this.isLoading.set(false);
423
+ }
424
+ }`;
425
+
426
+ await fs.writeFile(
427
+ path.join(config.fullPath, "src/app/core/services/loading.service.ts"),
428
+ loadingService,
429
+ );
430
+ }
431
+
432
+ // ==================== CACHE SERVICE ====================
433
+
434
+ async function createCacheService(config) {
435
+ const cacheService = `import { Injectable } from '@angular/core';
436
+ import { HttpResponse } from '@angular/common/http';
437
+
438
+ interface CacheEntry {
439
+ response: HttpResponse<unknown>;
440
+ timestamp: number;
441
+ }
442
+
443
+ @Injectable({
444
+ providedIn: 'root'
445
+ })
446
+ export class CacheService {
447
+ private cache = new Map<string, CacheEntry>();
448
+ private readonly maxAge = 5 * 60 * 1000; // 5 minutes
449
+
450
+ /**
451
+ * Get cached response
452
+ */
453
+ get(url: string): HttpResponse<unknown> | null {
454
+ const entry = this.cache.get(url);
455
+
456
+ if (!entry) {
457
+ return null;
458
+ }
459
+
460
+ // Check if cache is expired
461
+ const isExpired = Date.now() - entry.timestamp > this.maxAge;
462
+ if (isExpired) {
463
+ this.cache.delete(url);
464
+ return null;
465
+ }
466
+
467
+ return entry.response;
468
+ }
469
+
470
+ /**
471
+ * Set cache entry
472
+ */
473
+ set(url: string, response: HttpResponse<unknown>): void {
474
+ this.cache.set(url, {
475
+ response,
476
+ timestamp: Date.now()
477
+ });
478
+ }
479
+
480
+ /**
481
+ * Clear specific cache entry
482
+ */
483
+ delete(url: string): void {
484
+ this.cache.delete(url);
485
+ }
486
+
487
+ /**
488
+ * Clear all cache
489
+ */
490
+ clear(): void {
491
+ this.cache.clear();
492
+ }
493
+
494
+ /**
495
+ * Clear expired cache entries
496
+ */
497
+ clearExpired(): void {
498
+ const now = Date.now();
499
+ for (const [url, entry] of this.cache.entries()) {
500
+ if (now - entry.timestamp > this.maxAge) {
501
+ this.cache.delete(url);
502
+ }
503
+ }
504
+ }
505
+ }`;
506
+
507
+ await fs.writeFile(
508
+ path.join(config.fullPath, "src/app/core/services/cache.service.ts"),
509
+ cacheService,
510
+ );
511
+ }
512
+
513
+ // Export all functions
514
+ module.exports = {
515
+ // Interceptors
516
+ createAuthInterceptor,
517
+ createErrorInterceptor,
518
+ createLoadingInterceptor,
519
+ createCachingInterceptor,
520
+
521
+ // Toast System
522
+ createToastService,
523
+ createToastComponent,
524
+
525
+ // Supporting Services
526
+ createLoadingService,
527
+ createCacheService,
528
+ };