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,1371 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const starter = require('../starter');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SaaS Template
|
|
7
|
+
* Extends starter template with:
|
|
8
|
+
* - Pricing page with plan comparison
|
|
9
|
+
* - Subscription management
|
|
10
|
+
* - User dashboard
|
|
11
|
+
* - Feature highlights
|
|
12
|
+
* - FAQ section
|
|
13
|
+
* - Testimonials
|
|
14
|
+
*/
|
|
15
|
+
const saas = {
|
|
16
|
+
info: {
|
|
17
|
+
name: 'SaaS',
|
|
18
|
+
description: 'Software as a Service template with pricing, subscriptions, and user dashboard',
|
|
19
|
+
features: [
|
|
20
|
+
...starter.info.features,
|
|
21
|
+
'Pricing page with plan comparison',
|
|
22
|
+
'Subscription management UI',
|
|
23
|
+
'User dashboard with usage stats',
|
|
24
|
+
'Feature comparison table',
|
|
25
|
+
'FAQ accordion',
|
|
26
|
+
'Customer testimonials',
|
|
27
|
+
'CTA sections',
|
|
28
|
+
'Usage metrics display',
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async apply(config, spinner) {
|
|
33
|
+
const chalk = require('chalk');
|
|
34
|
+
|
|
35
|
+
const completeStep = (message) => {
|
|
36
|
+
if (spinner) {
|
|
37
|
+
spinner.stop();
|
|
38
|
+
console.log(chalk.green(' ✔') + chalk.white(' ' + message));
|
|
39
|
+
spinner.start();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Step 1: Apply starter template
|
|
44
|
+
if (spinner) spinner.update('Setting up starter foundation...');
|
|
45
|
+
await starter.apply(config, null);
|
|
46
|
+
|
|
47
|
+
// Step 2: Create SaaS-specific directories
|
|
48
|
+
if (spinner) spinner.update('Setting up SaaS structure...');
|
|
49
|
+
await this.createDirectories(config);
|
|
50
|
+
|
|
51
|
+
// Step 3: Create services
|
|
52
|
+
await this.createServices(config);
|
|
53
|
+
|
|
54
|
+
// Step 4: Create components
|
|
55
|
+
await this.createComponents(config);
|
|
56
|
+
|
|
57
|
+
// Step 5: Create pages
|
|
58
|
+
await this.createPages(config);
|
|
59
|
+
|
|
60
|
+
// Step 6: Update routing
|
|
61
|
+
await this.createRouting(config);
|
|
62
|
+
|
|
63
|
+
// Step 7: Add i18n translations
|
|
64
|
+
await this.updateI18n(config);
|
|
65
|
+
|
|
66
|
+
// Step 8: Format code
|
|
67
|
+
const base = require('../base');
|
|
68
|
+
await base.formatCode(config);
|
|
69
|
+
|
|
70
|
+
if (spinner) spinner.stop();
|
|
71
|
+
|
|
72
|
+
console.log('');
|
|
73
|
+
completeStep('SaaS template created');
|
|
74
|
+
completeStep('Pricing page with plans');
|
|
75
|
+
completeStep('User dashboard');
|
|
76
|
+
completeStep('Subscription management');
|
|
77
|
+
completeStep('Feature comparison');
|
|
78
|
+
console.log('');
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async createDirectories(config) {
|
|
82
|
+
const directories = [
|
|
83
|
+
'src/app/features/pricing',
|
|
84
|
+
'src/app/features/dashboard',
|
|
85
|
+
'src/app/features/dashboard/overview',
|
|
86
|
+
'src/app/features/dashboard/subscription',
|
|
87
|
+
'src/app/features/dashboard/usage',
|
|
88
|
+
'src/app/features/features',
|
|
89
|
+
'src/app/shared/components/pricing-card',
|
|
90
|
+
'src/app/shared/components/faq',
|
|
91
|
+
'src/app/shared/components/testimonial',
|
|
92
|
+
'src/app/shared/components/feature-card',
|
|
93
|
+
'src/app/shared/components/stats-card',
|
|
94
|
+
'src/app/shared/components/usage-chart',
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
for (const dir of directories) {
|
|
98
|
+
await fs.ensureDir(path.join(config.fullPath, dir));
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async createServices(config) {
|
|
103
|
+
const subscriptionService = `import { Injectable, signal, computed } from '@angular/core';
|
|
104
|
+
|
|
105
|
+
export interface Plan {
|
|
106
|
+
id: string;
|
|
107
|
+
name: string;
|
|
108
|
+
description: string;
|
|
109
|
+
price: number;
|
|
110
|
+
priceYearly: number;
|
|
111
|
+
features: string[];
|
|
112
|
+
highlighted?: boolean;
|
|
113
|
+
cta: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface Subscription {
|
|
117
|
+
planId: string;
|
|
118
|
+
status: 'active' | 'cancelled' | 'past_due' | 'trialing';
|
|
119
|
+
currentPeriodEnd: Date;
|
|
120
|
+
cancelAtPeriodEnd: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface UsageStats {
|
|
124
|
+
apiCalls: { used: number; limit: number };
|
|
125
|
+
storage: { used: number; limit: number };
|
|
126
|
+
teamMembers: { used: number; limit: number };
|
|
127
|
+
projects: { used: number; limit: number };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@Injectable({ providedIn: 'root' })
|
|
131
|
+
export class SubscriptionService {
|
|
132
|
+
private plansSignal = signal<Plan[]>(this.getPlans());
|
|
133
|
+
private subscriptionSignal = signal<Subscription | null>(this.getMockSubscription());
|
|
134
|
+
private usageSignal = signal<UsageStats>(this.getMockUsage());
|
|
135
|
+
|
|
136
|
+
plans = this.plansSignal.asReadonly();
|
|
137
|
+
subscription = this.subscriptionSignal.asReadonly();
|
|
138
|
+
usage = this.usageSignal.asReadonly();
|
|
139
|
+
|
|
140
|
+
currentPlan = computed(() => {
|
|
141
|
+
const sub = this.subscriptionSignal();
|
|
142
|
+
if (!sub) return null;
|
|
143
|
+
return this.plansSignal().find(p => p.id === sub.planId) || null;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
isSubscribed = computed(() => {
|
|
147
|
+
const sub = this.subscriptionSignal();
|
|
148
|
+
return sub !== null && sub.status === 'active';
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
daysRemaining = computed(() => {
|
|
152
|
+
const sub = this.subscriptionSignal();
|
|
153
|
+
if (!sub) return 0;
|
|
154
|
+
const now = new Date();
|
|
155
|
+
const end = new Date(sub.currentPeriodEnd);
|
|
156
|
+
const diff = end.getTime() - now.getTime();
|
|
157
|
+
return Math.ceil(diff / (1000 * 60 * 60 * 24));
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
getPlanById(id: string): Plan | undefined {
|
|
161
|
+
return this.plansSignal().find(p => p.id === id);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
subscribe(planId: string): Promise<boolean> {
|
|
165
|
+
// Mock subscription - in production, integrate with Stripe/Paddle
|
|
166
|
+
return new Promise(resolve => {
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
this.subscriptionSignal.set({
|
|
169
|
+
planId,
|
|
170
|
+
status: 'active',
|
|
171
|
+
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
|
172
|
+
cancelAtPeriodEnd: false,
|
|
173
|
+
});
|
|
174
|
+
resolve(true);
|
|
175
|
+
}, 1000);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
cancelSubscription(): Promise<boolean> {
|
|
180
|
+
return new Promise(resolve => {
|
|
181
|
+
setTimeout(() => {
|
|
182
|
+
this.subscriptionSignal.update(sub => sub ? { ...sub, cancelAtPeriodEnd: true } : null);
|
|
183
|
+
resolve(true);
|
|
184
|
+
}, 500);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private getPlans(): Plan[] {
|
|
189
|
+
return [
|
|
190
|
+
{
|
|
191
|
+
id: 'free',
|
|
192
|
+
name: 'Free',
|
|
193
|
+
description: 'Perfect for getting started',
|
|
194
|
+
price: 0,
|
|
195
|
+
priceYearly: 0,
|
|
196
|
+
features: [
|
|
197
|
+
'1,000 API calls/month',
|
|
198
|
+
'100MB storage',
|
|
199
|
+
'1 team member',
|
|
200
|
+
'3 projects',
|
|
201
|
+
'Community support',
|
|
202
|
+
],
|
|
203
|
+
cta: 'Get Started',
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 'pro',
|
|
207
|
+
name: 'Pro',
|
|
208
|
+
description: 'Best for growing teams',
|
|
209
|
+
price: 29,
|
|
210
|
+
priceYearly: 290,
|
|
211
|
+
features: [
|
|
212
|
+
'50,000 API calls/month',
|
|
213
|
+
'10GB storage',
|
|
214
|
+
'5 team members',
|
|
215
|
+
'Unlimited projects',
|
|
216
|
+
'Priority support',
|
|
217
|
+
'Advanced analytics',
|
|
218
|
+
'Custom integrations',
|
|
219
|
+
],
|
|
220
|
+
highlighted: true,
|
|
221
|
+
cta: 'Start Free Trial',
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 'enterprise',
|
|
225
|
+
name: 'Enterprise',
|
|
226
|
+
description: 'For large organizations',
|
|
227
|
+
price: 99,
|
|
228
|
+
priceYearly: 990,
|
|
229
|
+
features: [
|
|
230
|
+
'Unlimited API calls',
|
|
231
|
+
'100GB storage',
|
|
232
|
+
'Unlimited team members',
|
|
233
|
+
'Unlimited projects',
|
|
234
|
+
'24/7 dedicated support',
|
|
235
|
+
'Advanced analytics',
|
|
236
|
+
'Custom integrations',
|
|
237
|
+
'SLA guarantee',
|
|
238
|
+
'On-premise option',
|
|
239
|
+
],
|
|
240
|
+
cta: 'Contact Sales',
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private getMockSubscription(): Subscription {
|
|
246
|
+
return {
|
|
247
|
+
planId: 'pro',
|
|
248
|
+
status: 'active',
|
|
249
|
+
currentPeriodEnd: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000),
|
|
250
|
+
cancelAtPeriodEnd: false,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private getMockUsage(): UsageStats {
|
|
255
|
+
return {
|
|
256
|
+
apiCalls: { used: 32500, limit: 50000 },
|
|
257
|
+
storage: { used: 4.2, limit: 10 },
|
|
258
|
+
teamMembers: { used: 3, limit: 5 },
|
|
259
|
+
projects: { used: 8, limit: -1 }, // -1 means unlimited
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}`;
|
|
263
|
+
|
|
264
|
+
await fs.writeFile(
|
|
265
|
+
path.join(config.fullPath, 'src/app/core/services/subscription.service.ts'),
|
|
266
|
+
subscriptionService
|
|
267
|
+
);
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
async createComponents(config) {
|
|
271
|
+
// Pricing Card
|
|
272
|
+
const pricingCard = `import { Component, Input, Output, EventEmitter } from '@angular/core';
|
|
273
|
+
import { CurrencyPipe } from '@angular/common';
|
|
274
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
275
|
+
import { heroCheck } from '@ng-icons/heroicons/outline';
|
|
276
|
+
import { Plan } from '@core/services/subscription.service';
|
|
277
|
+
|
|
278
|
+
@Component({
|
|
279
|
+
selector: 'app-pricing-card',
|
|
280
|
+
standalone: true,
|
|
281
|
+
imports: [CurrencyPipe, NgIconComponent],
|
|
282
|
+
viewProviders: [provideIcons({ heroCheck })],
|
|
283
|
+
template: \`
|
|
284
|
+
<div
|
|
285
|
+
class="relative rounded-2xl border-2 p-8 transition-all"
|
|
286
|
+
[class.border-primary-500]="plan.highlighted"
|
|
287
|
+
[class.border-gray-200]="!plan.highlighted"
|
|
288
|
+
[class.shadow-xl]="plan.highlighted"
|
|
289
|
+
[class.scale-105]="plan.highlighted">
|
|
290
|
+
|
|
291
|
+
@if (plan.highlighted) {
|
|
292
|
+
<div class="absolute -top-4 left-1/2 -translate-x-1/2">
|
|
293
|
+
<span class="rounded-full bg-primary-500 px-4 py-1 text-sm font-semibold text-white">
|
|
294
|
+
Most Popular
|
|
295
|
+
</span>
|
|
296
|
+
</div>
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
<div class="text-center">
|
|
300
|
+
<h3 class="text-xl font-bold text-gray-900">{{ plan.name }}</h3>
|
|
301
|
+
<p class="mt-2 text-gray-600">{{ plan.description }}</p>
|
|
302
|
+
|
|
303
|
+
<div class="mt-6">
|
|
304
|
+
<span class="text-5xl font-bold text-gray-900">
|
|
305
|
+
{{ isYearly ? plan.priceYearly : plan.price | currency:'USD':'symbol':'1.0-0' }}
|
|
306
|
+
</span>
|
|
307
|
+
@if (plan.price > 0) {
|
|
308
|
+
<span class="text-gray-500">/{{ isYearly ? 'year' : 'month' }}</span>
|
|
309
|
+
}
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
@if (isYearly && plan.price > 0) {
|
|
313
|
+
<p class="mt-2 text-sm text-success-600">
|
|
314
|
+
Save {{ (plan.price * 12) - plan.priceYearly | currency:'USD':'symbol':'1.0-0' }}/year
|
|
315
|
+
</p>
|
|
316
|
+
}
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<ul class="mt-8 space-y-4">
|
|
320
|
+
@for (feature of plan.features; track feature) {
|
|
321
|
+
<li class="flex items-center gap-3">
|
|
322
|
+
<ng-icon name="heroCheck" size="20" class="text-success-500"></ng-icon>
|
|
323
|
+
<span class="text-gray-700">{{ feature }}</span>
|
|
324
|
+
</li>
|
|
325
|
+
}
|
|
326
|
+
</ul>
|
|
327
|
+
|
|
328
|
+
<button
|
|
329
|
+
(click)="onSelect.emit(plan)"
|
|
330
|
+
class="mt-8 w-full rounded-lg py-3 font-semibold transition-all"
|
|
331
|
+
[class.bg-primary-500]="plan.highlighted"
|
|
332
|
+
[class.text-white]="plan.highlighted"
|
|
333
|
+
[class.hover:bg-primary-600]="plan.highlighted"
|
|
334
|
+
[class.bg-gray-100]="!plan.highlighted"
|
|
335
|
+
[class.text-gray-900]="!plan.highlighted"
|
|
336
|
+
[class.hover:bg-gray-200]="!plan.highlighted">
|
|
337
|
+
{{ plan.cta }}
|
|
338
|
+
</button>
|
|
339
|
+
</div>
|
|
340
|
+
\`,
|
|
341
|
+
})
|
|
342
|
+
export class PricingCardComponent {
|
|
343
|
+
@Input({ required: true }) plan!: Plan;
|
|
344
|
+
@Input() isYearly = false;
|
|
345
|
+
@Output() onSelect = new EventEmitter<Plan>();
|
|
346
|
+
}`;
|
|
347
|
+
|
|
348
|
+
await fs.writeFile(
|
|
349
|
+
path.join(
|
|
350
|
+
config.fullPath,
|
|
351
|
+
'src/app/shared/components/pricing-card/pricing-card.component.ts'
|
|
352
|
+
),
|
|
353
|
+
pricingCard
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// FAQ Component
|
|
357
|
+
const faq = `import { Component, Input, signal } from '@angular/core';
|
|
358
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
359
|
+
import { heroChevronDown } from '@ng-icons/heroicons/outline';
|
|
360
|
+
|
|
361
|
+
export interface FaqItem {
|
|
362
|
+
question: string;
|
|
363
|
+
answer: string;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
@Component({
|
|
367
|
+
selector: 'app-faq',
|
|
368
|
+
standalone: true,
|
|
369
|
+
imports: [NgIconComponent],
|
|
370
|
+
viewProviders: [provideIcons({ heroChevronDown })],
|
|
371
|
+
template: \`
|
|
372
|
+
<div class="space-y-4">
|
|
373
|
+
@for (item of items; track item.question; let i = $index) {
|
|
374
|
+
<div class="rounded-xl border border-gray-200 bg-white">
|
|
375
|
+
<button
|
|
376
|
+
(click)="toggle(i)"
|
|
377
|
+
class="flex w-full items-center justify-between p-6 text-left">
|
|
378
|
+
<span class="font-semibold text-gray-900">{{ item.question }}</span>
|
|
379
|
+
<ng-icon
|
|
380
|
+
name="heroChevronDown"
|
|
381
|
+
size="20"
|
|
382
|
+
class="text-gray-500 transition-transform"
|
|
383
|
+
[class.rotate-180]="openIndex() === i">
|
|
384
|
+
</ng-icon>
|
|
385
|
+
</button>
|
|
386
|
+
@if (openIndex() === i) {
|
|
387
|
+
<div class="border-t border-gray-200 px-6 py-4">
|
|
388
|
+
<p class="text-gray-600">{{ item.answer }}</p>
|
|
389
|
+
</div>
|
|
390
|
+
}
|
|
391
|
+
</div>
|
|
392
|
+
}
|
|
393
|
+
</div>
|
|
394
|
+
\`,
|
|
395
|
+
})
|
|
396
|
+
export class FaqComponent {
|
|
397
|
+
@Input({ required: true }) items!: FaqItem[];
|
|
398
|
+
|
|
399
|
+
openIndex = signal<number | null>(null);
|
|
400
|
+
|
|
401
|
+
toggle(index: number): void {
|
|
402
|
+
this.openIndex.set(this.openIndex() === index ? null : index);
|
|
403
|
+
}
|
|
404
|
+
}`;
|
|
405
|
+
|
|
406
|
+
await fs.writeFile(
|
|
407
|
+
path.join(config.fullPath, 'src/app/shared/components/faq/faq.component.ts'),
|
|
408
|
+
faq
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// Stats Card
|
|
412
|
+
const statsCard = `import { Component, Input } from '@angular/core';
|
|
413
|
+
import { NgIconComponent } from '@ng-icons/core';
|
|
414
|
+
|
|
415
|
+
@Component({
|
|
416
|
+
selector: 'app-stats-card',
|
|
417
|
+
standalone: true,
|
|
418
|
+
imports: [NgIconComponent],
|
|
419
|
+
template: \`
|
|
420
|
+
<div class="rounded-xl border border-gray-200 bg-white p-6">
|
|
421
|
+
<div class="flex items-center justify-between">
|
|
422
|
+
<div>
|
|
423
|
+
<p class="text-sm font-medium text-gray-500">{{ label }}</p>
|
|
424
|
+
<p class="mt-1 text-3xl font-bold text-gray-900">{{ value }}</p>
|
|
425
|
+
@if (subtext) {
|
|
426
|
+
<p class="mt-1 text-sm" [class.text-success-600]="trend === 'up'" [class.text-danger-600]="trend === 'down'" [class.text-gray-500]="!trend">
|
|
427
|
+
{{ subtext }}
|
|
428
|
+
</p>
|
|
429
|
+
}
|
|
430
|
+
</div>
|
|
431
|
+
@if (icon) {
|
|
432
|
+
<div class="flex h-12 w-12 items-center justify-center rounded-xl" [class]="iconBgClass">
|
|
433
|
+
<ng-icon [name]="icon" size="24" [class]="iconClass"></ng-icon>
|
|
434
|
+
</div>
|
|
435
|
+
}
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
@if (showProgress) {
|
|
439
|
+
<div class="mt-4">
|
|
440
|
+
<div class="flex justify-between text-sm">
|
|
441
|
+
<span class="text-gray-500">{{ progressLabel }}</span>
|
|
442
|
+
<span class="font-medium text-gray-900">{{ progressPercent }}%</span>
|
|
443
|
+
</div>
|
|
444
|
+
<div class="mt-2 h-2 w-full overflow-hidden rounded-full bg-gray-100">
|
|
445
|
+
<div
|
|
446
|
+
class="h-full rounded-full transition-all"
|
|
447
|
+
[class.bg-primary-500]="progressPercent < 80"
|
|
448
|
+
[class.bg-warning-500]="progressPercent >= 80 && progressPercent < 95"
|
|
449
|
+
[class.bg-danger-500]="progressPercent >= 95"
|
|
450
|
+
[style.width.%]="progressPercent">
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
}
|
|
455
|
+
</div>
|
|
456
|
+
\`,
|
|
457
|
+
})
|
|
458
|
+
export class StatsCardComponent {
|
|
459
|
+
@Input({ required: true }) label!: string;
|
|
460
|
+
@Input({ required: true }) value!: string | number;
|
|
461
|
+
@Input() subtext?: string;
|
|
462
|
+
@Input() trend?: 'up' | 'down';
|
|
463
|
+
@Input() icon?: string;
|
|
464
|
+
@Input() iconBgClass = 'bg-primary-100';
|
|
465
|
+
@Input() iconClass = 'text-primary-600';
|
|
466
|
+
@Input() showProgress = false;
|
|
467
|
+
@Input() progressLabel = 'Used';
|
|
468
|
+
@Input() progressPercent = 0;
|
|
469
|
+
}`;
|
|
470
|
+
|
|
471
|
+
await fs.writeFile(
|
|
472
|
+
path.join(config.fullPath, 'src/app/shared/components/stats-card/stats-card.component.ts'),
|
|
473
|
+
statsCard
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// Feature Card
|
|
477
|
+
const featureCard = `import { Component, Input } from '@angular/core';
|
|
478
|
+
import { NgIconComponent } from '@ng-icons/core';
|
|
479
|
+
|
|
480
|
+
@Component({
|
|
481
|
+
selector: 'app-feature-card',
|
|
482
|
+
standalone: true,
|
|
483
|
+
imports: [NgIconComponent],
|
|
484
|
+
template: \`
|
|
485
|
+
<div class="rounded-xl border border-gray-200 bg-white p-6 transition-all hover:shadow-lg hover:-translate-y-1">
|
|
486
|
+
<div class="flex h-12 w-12 items-center justify-center rounded-xl" [class]="iconBgClass">
|
|
487
|
+
<ng-icon [name]="icon" size="24" [class]="iconClass"></ng-icon>
|
|
488
|
+
</div>
|
|
489
|
+
<h3 class="mt-4 text-lg font-semibold text-gray-900">{{ title }}</h3>
|
|
490
|
+
<p class="mt-2 text-gray-600">{{ description }}</p>
|
|
491
|
+
</div>
|
|
492
|
+
\`,
|
|
493
|
+
})
|
|
494
|
+
export class FeatureCardComponent {
|
|
495
|
+
@Input({ required: true }) icon!: string;
|
|
496
|
+
@Input({ required: true }) title!: string;
|
|
497
|
+
@Input({ required: true }) description!: string;
|
|
498
|
+
@Input() iconBgClass = 'bg-primary-100';
|
|
499
|
+
@Input() iconClass = 'text-primary-600';
|
|
500
|
+
}`;
|
|
501
|
+
|
|
502
|
+
await fs.writeFile(
|
|
503
|
+
path.join(
|
|
504
|
+
config.fullPath,
|
|
505
|
+
'src/app/shared/components/feature-card/feature-card.component.ts'
|
|
506
|
+
),
|
|
507
|
+
featureCard
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
// Testimonial Card
|
|
511
|
+
const testimonial = `import { Component, Input } from '@angular/core';
|
|
512
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
513
|
+
import { heroStarSolid } from '@ng-icons/heroicons/solid';
|
|
514
|
+
|
|
515
|
+
export interface Testimonial {
|
|
516
|
+
content: string;
|
|
517
|
+
author: string;
|
|
518
|
+
role: string;
|
|
519
|
+
company: string;
|
|
520
|
+
avatar: string;
|
|
521
|
+
rating: number;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
@Component({
|
|
525
|
+
selector: 'app-testimonial',
|
|
526
|
+
standalone: true,
|
|
527
|
+
imports: [NgIconComponent],
|
|
528
|
+
viewProviders: [provideIcons({ heroStarSolid })],
|
|
529
|
+
template: \`
|
|
530
|
+
<div class="rounded-2xl border border-gray-200 bg-white p-8">
|
|
531
|
+
<div class="flex gap-1 mb-4">
|
|
532
|
+
@for (star of stars; track star) {
|
|
533
|
+
<ng-icon name="heroStarSolid" size="20" class="text-amber-400"></ng-icon>
|
|
534
|
+
}
|
|
535
|
+
</div>
|
|
536
|
+
|
|
537
|
+
<p class="text-gray-700 italic">"{{ testimonial.content }}"</p>
|
|
538
|
+
|
|
539
|
+
<div class="mt-6 flex items-center gap-4">
|
|
540
|
+
<img [src]="testimonial.avatar" [alt]="testimonial.author" class="h-12 w-12 rounded-full object-cover" />
|
|
541
|
+
<div>
|
|
542
|
+
<p class="font-semibold text-gray-900">{{ testimonial.author }}</p>
|
|
543
|
+
<p class="text-sm text-gray-500">{{ testimonial.role }}, {{ testimonial.company }}</p>
|
|
544
|
+
</div>
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
\`,
|
|
548
|
+
})
|
|
549
|
+
export class TestimonialComponent {
|
|
550
|
+
@Input({ required: true }) testimonial!: Testimonial;
|
|
551
|
+
|
|
552
|
+
get stars(): number[] {
|
|
553
|
+
return Array.from({ length: this.testimonial.rating }, (_, i) => i);
|
|
554
|
+
}
|
|
555
|
+
}`;
|
|
556
|
+
|
|
557
|
+
await fs.writeFile(
|
|
558
|
+
path.join(config.fullPath, 'src/app/shared/components/testimonial/testimonial.component.ts'),
|
|
559
|
+
testimonial
|
|
560
|
+
);
|
|
561
|
+
},
|
|
562
|
+
|
|
563
|
+
async createPages(config) {
|
|
564
|
+
// Pricing Page
|
|
565
|
+
const pricing = `import { Component, inject, signal } from '@angular/core';
|
|
566
|
+
import { Router } from '@angular/router';
|
|
567
|
+
import { TranslateModule } from '@ngx-translate/core';
|
|
568
|
+
import { PricingCardComponent } from '@shared/components/pricing-card/pricing-card.component';
|
|
569
|
+
import { FaqComponent, FaqItem } from '@shared/components/faq/faq.component';
|
|
570
|
+
import { TestimonialComponent, Testimonial } from '@shared/components/testimonial/testimonial.component';
|
|
571
|
+
import { SubscriptionService, Plan } from '@core/services/subscription.service';
|
|
572
|
+
|
|
573
|
+
@Component({
|
|
574
|
+
selector: 'app-pricing',
|
|
575
|
+
standalone: true,
|
|
576
|
+
imports: [TranslateModule, PricingCardComponent, FaqComponent, TestimonialComponent],
|
|
577
|
+
template: \`
|
|
578
|
+
<div>
|
|
579
|
+
<!-- Hero -->
|
|
580
|
+
<section class="bg-gradient-to-b from-gray-50 to-white py-20">
|
|
581
|
+
<div class="mx-auto max-w-7xl px-4 text-center sm:px-6 lg:px-8">
|
|
582
|
+
<h1 class="text-4xl font-bold text-gray-900 sm:text-5xl">
|
|
583
|
+
{{ 'pricing.title' | translate }}
|
|
584
|
+
</h1>
|
|
585
|
+
<p class="mt-4 text-xl text-gray-600">
|
|
586
|
+
{{ 'pricing.subtitle' | translate }}
|
|
587
|
+
</p>
|
|
588
|
+
|
|
589
|
+
<!-- Billing Toggle -->
|
|
590
|
+
<div class="mt-8 flex items-center justify-center gap-4">
|
|
591
|
+
<span [class.text-gray-900]="!isYearly()" [class.text-gray-500]="isYearly()">Monthly</span>
|
|
592
|
+
<button
|
|
593
|
+
(click)="isYearly.set(!isYearly())"
|
|
594
|
+
class="relative h-8 w-14 rounded-full bg-primary-500 transition-colors">
|
|
595
|
+
<span
|
|
596
|
+
class="absolute top-1 h-6 w-6 rounded-full bg-white transition-all"
|
|
597
|
+
[class.left-1]="!isYearly()"
|
|
598
|
+
[class.left-7]="isYearly()">
|
|
599
|
+
</span>
|
|
600
|
+
</button>
|
|
601
|
+
<span [class.text-gray-900]="isYearly()" [class.text-gray-500]="!isYearly()">
|
|
602
|
+
Yearly
|
|
603
|
+
<span class="ml-1 rounded-full bg-success-100 px-2 py-0.5 text-xs font-medium text-success-700">
|
|
604
|
+
Save 20%
|
|
605
|
+
</span>
|
|
606
|
+
</span>
|
|
607
|
+
</div>
|
|
608
|
+
</div>
|
|
609
|
+
</section>
|
|
610
|
+
|
|
611
|
+
<!-- Pricing Cards -->
|
|
612
|
+
<section class="py-20">
|
|
613
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
614
|
+
<div class="grid gap-8 md:grid-cols-3">
|
|
615
|
+
@for (plan of subscriptionService.plans(); track plan.id) {
|
|
616
|
+
<app-pricing-card
|
|
617
|
+
[plan]="plan"
|
|
618
|
+
[isYearly]="isYearly()"
|
|
619
|
+
(onSelect)="selectPlan($event)">
|
|
620
|
+
</app-pricing-card>
|
|
621
|
+
}
|
|
622
|
+
</div>
|
|
623
|
+
</div>
|
|
624
|
+
</section>
|
|
625
|
+
|
|
626
|
+
<!-- Feature Comparison -->
|
|
627
|
+
<section class="bg-gray-50 py-20">
|
|
628
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
629
|
+
<h2 class="mb-12 text-center text-3xl font-bold text-gray-900">
|
|
630
|
+
{{ 'pricing.compareFeatures' | translate }}
|
|
631
|
+
</h2>
|
|
632
|
+
|
|
633
|
+
<div class="overflow-x-auto">
|
|
634
|
+
<table class="w-full">
|
|
635
|
+
<thead>
|
|
636
|
+
<tr class="border-b border-gray-200">
|
|
637
|
+
<th class="py-4 text-left font-semibold text-gray-900">Feature</th>
|
|
638
|
+
@for (plan of subscriptionService.plans(); track plan.id) {
|
|
639
|
+
<th class="py-4 text-center font-semibold text-gray-900">{{ plan.name }}</th>
|
|
640
|
+
}
|
|
641
|
+
</tr>
|
|
642
|
+
</thead>
|
|
643
|
+
<tbody>
|
|
644
|
+
@for (feature of comparisonFeatures; track feature.name) {
|
|
645
|
+
<tr class="border-b border-gray-100">
|
|
646
|
+
<td class="py-4 text-gray-700">{{ feature.name }}</td>
|
|
647
|
+
@for (value of feature.values; track $index) {
|
|
648
|
+
<td class="py-4 text-center">
|
|
649
|
+
@if (value === true) {
|
|
650
|
+
<span class="text-success-500">✓</span>
|
|
651
|
+
} @else if (value === false) {
|
|
652
|
+
<span class="text-gray-300">—</span>
|
|
653
|
+
} @else {
|
|
654
|
+
<span class="text-gray-700">{{ value }}</span>
|
|
655
|
+
}
|
|
656
|
+
</td>
|
|
657
|
+
}
|
|
658
|
+
</tr>
|
|
659
|
+
}
|
|
660
|
+
</tbody>
|
|
661
|
+
</table>
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
</section>
|
|
665
|
+
|
|
666
|
+
<!-- Testimonials -->
|
|
667
|
+
<section class="py-20">
|
|
668
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
669
|
+
<h2 class="mb-12 text-center text-3xl font-bold text-gray-900">
|
|
670
|
+
{{ 'pricing.trustedBy' | translate }}
|
|
671
|
+
</h2>
|
|
672
|
+
|
|
673
|
+
<div class="grid gap-8 md:grid-cols-3">
|
|
674
|
+
@for (testimonial of testimonials; track testimonial.author) {
|
|
675
|
+
<app-testimonial [testimonial]="testimonial"></app-testimonial>
|
|
676
|
+
}
|
|
677
|
+
</div>
|
|
678
|
+
</div>
|
|
679
|
+
</section>
|
|
680
|
+
|
|
681
|
+
<!-- FAQ -->
|
|
682
|
+
<section class="bg-gray-50 py-20">
|
|
683
|
+
<div class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
|
|
684
|
+
<h2 class="mb-12 text-center text-3xl font-bold text-gray-900">
|
|
685
|
+
{{ 'pricing.faq' | translate }}
|
|
686
|
+
</h2>
|
|
687
|
+
<app-faq [items]="faqItems"></app-faq>
|
|
688
|
+
</div>
|
|
689
|
+
</section>
|
|
690
|
+
|
|
691
|
+
<!-- CTA -->
|
|
692
|
+
<section class="py-20">
|
|
693
|
+
<div class="mx-auto max-w-4xl px-4 text-center sm:px-6 lg:px-8">
|
|
694
|
+
<h2 class="text-3xl font-bold text-gray-900">{{ 'pricing.ctaTitle' | translate }}</h2>
|
|
695
|
+
<p class="mt-4 text-xl text-gray-600">{{ 'pricing.ctaSubtitle' | translate }}</p>
|
|
696
|
+
<button class="mt-8 rounded-lg bg-primary-500 px-8 py-4 font-semibold text-white hover:bg-primary-600">
|
|
697
|
+
{{ 'pricing.startTrial' | translate }}
|
|
698
|
+
</button>
|
|
699
|
+
</div>
|
|
700
|
+
</section>
|
|
701
|
+
</div>
|
|
702
|
+
\`,
|
|
703
|
+
})
|
|
704
|
+
export class PricingComponent {
|
|
705
|
+
subscriptionService = inject(SubscriptionService);
|
|
706
|
+
private router = inject(Router);
|
|
707
|
+
|
|
708
|
+
isYearly = signal(false);
|
|
709
|
+
|
|
710
|
+
comparisonFeatures = [
|
|
711
|
+
{ name: 'API Calls', values: ['1,000/mo', '50,000/mo', 'Unlimited'] },
|
|
712
|
+
{ name: 'Storage', values: ['100MB', '10GB', '100GB'] },
|
|
713
|
+
{ name: 'Team Members', values: ['1', '5', 'Unlimited'] },
|
|
714
|
+
{ name: 'Projects', values: ['3', 'Unlimited', 'Unlimited'] },
|
|
715
|
+
{ name: 'Analytics', values: [false, true, true] },
|
|
716
|
+
{ name: 'Custom Integrations', values: [false, true, true] },
|
|
717
|
+
{ name: 'Priority Support', values: [false, true, true] },
|
|
718
|
+
{ name: 'SLA Guarantee', values: [false, false, true] },
|
|
719
|
+
{ name: 'On-Premise', values: [false, false, true] },
|
|
720
|
+
];
|
|
721
|
+
|
|
722
|
+
testimonials: Testimonial[] = [
|
|
723
|
+
{
|
|
724
|
+
content: 'This platform has transformed how we handle our data. The API is incredibly well-designed and the support team is amazing.',
|
|
725
|
+
author: 'Sarah Chen',
|
|
726
|
+
role: 'CTO',
|
|
727
|
+
company: 'TechStart Inc',
|
|
728
|
+
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150',
|
|
729
|
+
rating: 5,
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
content: 'We migrated from a competitor and the difference is night and day. Better performance, better pricing, better everything.',
|
|
733
|
+
author: 'Michael Rodriguez',
|
|
734
|
+
role: 'Lead Developer',
|
|
735
|
+
company: 'DataFlow',
|
|
736
|
+
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150',
|
|
737
|
+
rating: 5,
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
content: 'The enterprise features are exactly what we needed. Custom integrations and dedicated support made our rollout smooth.',
|
|
741
|
+
author: 'Emily Watson',
|
|
742
|
+
role: 'VP Engineering',
|
|
743
|
+
company: 'Enterprise Co',
|
|
744
|
+
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150',
|
|
745
|
+
rating: 5,
|
|
746
|
+
},
|
|
747
|
+
];
|
|
748
|
+
|
|
749
|
+
faqItems: FaqItem[] = [
|
|
750
|
+
{
|
|
751
|
+
question: 'Can I change plans at any time?',
|
|
752
|
+
answer: 'Yes, you can upgrade or downgrade your plan at any time. Changes take effect immediately and we\\'ll prorate any differences.',
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
question: 'What payment methods do you accept?',
|
|
756
|
+
answer: 'We accept all major credit cards (Visa, MasterCard, American Express) as well as PayPal. Enterprise customers can also pay via invoice.',
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
question: 'Is there a free trial?',
|
|
760
|
+
answer: 'Yes! All paid plans come with a 14-day free trial. No credit card required to start.',
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
question: 'What happens if I exceed my limits?',
|
|
764
|
+
answer: 'We\\'ll notify you when you\\'re approaching your limits. You can upgrade anytime, or we offer overage pricing for occasional spikes.',
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
question: 'Can I cancel anytime?',
|
|
768
|
+
answer: 'Absolutely. You can cancel your subscription at any time with no penalties. You\\'ll retain access until the end of your billing period.',
|
|
769
|
+
},
|
|
770
|
+
];
|
|
771
|
+
|
|
772
|
+
selectPlan(plan: Plan): void {
|
|
773
|
+
if (plan.id === 'enterprise') {
|
|
774
|
+
this.router.navigate(['/contact']);
|
|
775
|
+
} else {
|
|
776
|
+
this.router.navigate(['/auth/register'], { queryParams: { plan: plan.id } });
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}`;
|
|
780
|
+
|
|
781
|
+
await fs.writeFile(
|
|
782
|
+
path.join(config.fullPath, 'src/app/features/pricing/pricing.component.ts'),
|
|
783
|
+
pricing
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
// Dashboard Overview
|
|
787
|
+
const dashboardOverview = `import { Component, inject } from '@angular/core';
|
|
788
|
+
import { RouterModule } from '@angular/router';
|
|
789
|
+
import { DatePipe } from '@angular/common';
|
|
790
|
+
import { TranslateModule } from '@ngx-translate/core';
|
|
791
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
792
|
+
import {
|
|
793
|
+
heroChartBar,
|
|
794
|
+
heroCloud,
|
|
795
|
+
heroUsers,
|
|
796
|
+
heroFolder,
|
|
797
|
+
heroArrowTrendingUp,
|
|
798
|
+
heroArrowTrendingDown,
|
|
799
|
+
} from '@ng-icons/heroicons/outline';
|
|
800
|
+
import { StatsCardComponent } from '@shared/components/stats-card/stats-card.component';
|
|
801
|
+
import { SubscriptionService } from '@core/services/subscription.service';
|
|
802
|
+
|
|
803
|
+
@Component({
|
|
804
|
+
selector: 'app-dashboard-overview',
|
|
805
|
+
standalone: true,
|
|
806
|
+
imports: [RouterModule, DatePipe, TranslateModule, NgIconComponent, StatsCardComponent],
|
|
807
|
+
viewProviders: [provideIcons({ heroChartBar, heroCloud, heroUsers, heroFolder, heroArrowTrendingUp, heroArrowTrendingDown })],
|
|
808
|
+
template: \`
|
|
809
|
+
<div>
|
|
810
|
+
<div class="mb-8">
|
|
811
|
+
<h1 class="text-2xl font-bold text-gray-900">{{ 'dashboard.welcome' | translate }}</h1>
|
|
812
|
+
<p class="text-gray-600">{{ 'dashboard.overviewSubtitle' | translate }}</p>
|
|
813
|
+
</div>
|
|
814
|
+
|
|
815
|
+
<!-- Current Plan Banner -->
|
|
816
|
+
@if (subscriptionService.currentPlan()) {
|
|
817
|
+
<div class="mb-8 rounded-xl bg-gradient-to-r from-primary-500 to-purple-600 p-6 text-white">
|
|
818
|
+
<div class="flex items-center justify-between">
|
|
819
|
+
<div>
|
|
820
|
+
<p class="text-sm text-primary-100">Current Plan</p>
|
|
821
|
+
<p class="text-2xl font-bold">{{ subscriptionService.currentPlan()!.name }}</p>
|
|
822
|
+
<p class="mt-1 text-primary-100">
|
|
823
|
+
{{ subscriptionService.daysRemaining() }} days remaining
|
|
824
|
+
</p>
|
|
825
|
+
</div>
|
|
826
|
+
<a routerLink="/dashboard/subscription" class="rounded-lg bg-white/20 px-4 py-2 font-medium backdrop-blur-sm hover:bg-white/30">
|
|
827
|
+
Manage Subscription
|
|
828
|
+
</a>
|
|
829
|
+
</div>
|
|
830
|
+
</div>
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
<!-- Usage Stats -->
|
|
834
|
+
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
|
835
|
+
<app-stats-card
|
|
836
|
+
label="API Calls"
|
|
837
|
+
[value]="formatNumber(subscriptionService.usage().apiCalls.used)"
|
|
838
|
+
[subtext]="'of ' + formatNumber(subscriptionService.usage().apiCalls.limit)"
|
|
839
|
+
icon="heroChartBar"
|
|
840
|
+
iconBgClass="bg-blue-100"
|
|
841
|
+
iconClass="text-blue-600"
|
|
842
|
+
[showProgress]="true"
|
|
843
|
+
progressLabel="Used this month"
|
|
844
|
+
[progressPercent]="getPercent(subscriptionService.usage().apiCalls)">
|
|
845
|
+
</app-stats-card>
|
|
846
|
+
|
|
847
|
+
<app-stats-card
|
|
848
|
+
label="Storage"
|
|
849
|
+
[value]="subscriptionService.usage().storage.used + ' GB'"
|
|
850
|
+
[subtext]="'of ' + subscriptionService.usage().storage.limit + ' GB'"
|
|
851
|
+
icon="heroCloud"
|
|
852
|
+
iconBgClass="bg-purple-100"
|
|
853
|
+
iconClass="text-purple-600"
|
|
854
|
+
[showProgress]="true"
|
|
855
|
+
progressLabel="Used"
|
|
856
|
+
[progressPercent]="getPercent(subscriptionService.usage().storage)">
|
|
857
|
+
</app-stats-card>
|
|
858
|
+
|
|
859
|
+
<app-stats-card
|
|
860
|
+
label="Team Members"
|
|
861
|
+
[value]="subscriptionService.usage().teamMembers.used"
|
|
862
|
+
[subtext]="'of ' + subscriptionService.usage().teamMembers.limit + ' seats'"
|
|
863
|
+
icon="heroUsers"
|
|
864
|
+
iconBgClass="bg-green-100"
|
|
865
|
+
iconClass="text-green-600"
|
|
866
|
+
[showProgress]="true"
|
|
867
|
+
[progressPercent]="getPercent(subscriptionService.usage().teamMembers)">
|
|
868
|
+
</app-stats-card>
|
|
869
|
+
|
|
870
|
+
<app-stats-card
|
|
871
|
+
label="Projects"
|
|
872
|
+
[value]="subscriptionService.usage().projects.used"
|
|
873
|
+
[subtext]="subscriptionService.usage().projects.limit === -1 ? 'Unlimited' : 'of ' + subscriptionService.usage().projects.limit"
|
|
874
|
+
icon="heroFolder"
|
|
875
|
+
iconBgClass="bg-amber-100"
|
|
876
|
+
iconClass="text-amber-600">
|
|
877
|
+
</app-stats-card>
|
|
878
|
+
</div>
|
|
879
|
+
|
|
880
|
+
<!-- Recent Activity -->
|
|
881
|
+
<div class="mt-8 rounded-xl border border-gray-200 bg-white p-6">
|
|
882
|
+
<h2 class="mb-4 text-lg font-semibold text-gray-900">{{ 'dashboard.recentActivity' | translate }}</h2>
|
|
883
|
+
<div class="space-y-4">
|
|
884
|
+
@for (activity of recentActivity; track activity.id) {
|
|
885
|
+
<div class="flex items-center gap-4 rounded-lg bg-gray-50 p-4">
|
|
886
|
+
<div class="flex h-10 w-10 items-center justify-center rounded-full" [class]="activity.iconBg">
|
|
887
|
+
<ng-icon [name]="activity.icon" size="20" [class]="activity.iconColor"></ng-icon>
|
|
888
|
+
</div>
|
|
889
|
+
<div class="flex-1">
|
|
890
|
+
<p class="font-medium text-gray-900">{{ activity.title }}</p>
|
|
891
|
+
<p class="text-sm text-gray-500">{{ activity.description }}</p>
|
|
892
|
+
</div>
|
|
893
|
+
<span class="text-sm text-gray-500">{{ activity.time }}</span>
|
|
894
|
+
</div>
|
|
895
|
+
}
|
|
896
|
+
</div>
|
|
897
|
+
</div>
|
|
898
|
+
|
|
899
|
+
<!-- Quick Actions -->
|
|
900
|
+
<div class="mt-8 grid gap-4 md:grid-cols-3">
|
|
901
|
+
<a routerLink="/dashboard/usage" class="rounded-xl border border-gray-200 bg-white p-6 transition-all hover:shadow-lg">
|
|
902
|
+
<ng-icon name="heroChartBar" size="24" class="text-primary-500"></ng-icon>
|
|
903
|
+
<h3 class="mt-3 font-semibold text-gray-900">View Detailed Usage</h3>
|
|
904
|
+
<p class="mt-1 text-sm text-gray-600">See your API usage trends and analytics</p>
|
|
905
|
+
</a>
|
|
906
|
+
|
|
907
|
+
<a routerLink="/dashboard/subscription" class="rounded-xl border border-gray-200 bg-white p-6 transition-all hover:shadow-lg">
|
|
908
|
+
<ng-icon name="heroArrowTrendingUp" size="24" class="text-success-500"></ng-icon>
|
|
909
|
+
<h3 class="mt-3 font-semibold text-gray-900">Upgrade Plan</h3>
|
|
910
|
+
<p class="mt-1 text-sm text-gray-600">Get more features and higher limits</p>
|
|
911
|
+
</a>
|
|
912
|
+
|
|
913
|
+
<a routerLink="/contact" class="rounded-xl border border-gray-200 bg-white p-6 transition-all hover:shadow-lg">
|
|
914
|
+
<ng-icon name="heroUsers" size="24" class="text-purple-500"></ng-icon>
|
|
915
|
+
<h3 class="mt-3 font-semibold text-gray-900">Invite Team</h3>
|
|
916
|
+
<p class="mt-1 text-sm text-gray-600">Add team members to collaborate</p>
|
|
917
|
+
</a>
|
|
918
|
+
</div>
|
|
919
|
+
</div>
|
|
920
|
+
\`,
|
|
921
|
+
})
|
|
922
|
+
export class DashboardOverviewComponent {
|
|
923
|
+
subscriptionService = inject(SubscriptionService);
|
|
924
|
+
|
|
925
|
+
recentActivity = [
|
|
926
|
+
{ id: 1, title: 'API Key Generated', description: 'New production API key created', time: '2 hours ago', icon: 'heroChartBar', iconBg: 'bg-blue-100', iconColor: 'text-blue-600' },
|
|
927
|
+
{ id: 2, title: 'Storage Increased', description: 'Upgraded storage from 5GB to 10GB', time: '1 day ago', icon: 'heroCloud', iconBg: 'bg-purple-100', iconColor: 'text-purple-600' },
|
|
928
|
+
{ id: 3, title: 'New Team Member', description: 'john@example.com joined the team', time: '3 days ago', icon: 'heroUsers', iconBg: 'bg-green-100', iconColor: 'text-green-600' },
|
|
929
|
+
];
|
|
930
|
+
|
|
931
|
+
formatNumber(num: number): string {
|
|
932
|
+
return num.toLocaleString();
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
getPercent(usage: { used: number; limit: number }): number {
|
|
936
|
+
if (usage.limit === -1) return 0;
|
|
937
|
+
return Math.round((usage.used / usage.limit) * 100);
|
|
938
|
+
}
|
|
939
|
+
}`;
|
|
940
|
+
|
|
941
|
+
await fs.writeFile(
|
|
942
|
+
path.join(config.fullPath, 'src/app/features/dashboard/overview/overview.component.ts'),
|
|
943
|
+
dashboardOverview
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
// Subscription Management Page
|
|
947
|
+
const subscription = `import { Component, inject, signal } from '@angular/core';
|
|
948
|
+
import { RouterModule } from '@angular/router';
|
|
949
|
+
import { DatePipe, CurrencyPipe } from '@angular/common';
|
|
950
|
+
import { TranslateModule } from '@ngx-translate/core';
|
|
951
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
952
|
+
import { heroCheck, heroExclamationTriangle } from '@ng-icons/heroicons/outline';
|
|
953
|
+
import { SubscriptionService, Plan } from '@core/services/subscription.service';
|
|
954
|
+
import { ToastService } from '@core/services/toast.service';
|
|
955
|
+
|
|
956
|
+
@Component({
|
|
957
|
+
selector: 'app-subscription',
|
|
958
|
+
standalone: true,
|
|
959
|
+
imports: [RouterModule, DatePipe, CurrencyPipe, TranslateModule, NgIconComponent],
|
|
960
|
+
viewProviders: [provideIcons({ heroCheck, heroExclamationTriangle })],
|
|
961
|
+
template: \`
|
|
962
|
+
<div>
|
|
963
|
+
<div class="mb-8">
|
|
964
|
+
<h1 class="text-2xl font-bold text-gray-900">{{ 'subscription.title' | translate }}</h1>
|
|
965
|
+
<p class="text-gray-600">{{ 'subscription.subtitle' | translate }}</p>
|
|
966
|
+
</div>
|
|
967
|
+
|
|
968
|
+
<!-- Current Subscription -->
|
|
969
|
+
@if (subscriptionService.subscription()) {
|
|
970
|
+
<div class="mb-8 rounded-xl border border-gray-200 bg-white p-6">
|
|
971
|
+
<div class="flex items-start justify-between">
|
|
972
|
+
<div>
|
|
973
|
+
<h2 class="text-lg font-semibold text-gray-900">Current Plan</h2>
|
|
974
|
+
<p class="mt-1 text-3xl font-bold text-primary-600">
|
|
975
|
+
{{ subscriptionService.currentPlan()?.name }}
|
|
976
|
+
</p>
|
|
977
|
+
<p class="mt-2 text-gray-600">
|
|
978
|
+
{{ subscriptionService.currentPlan()?.price | currency }}/month
|
|
979
|
+
</p>
|
|
980
|
+
</div>
|
|
981
|
+
|
|
982
|
+
<div class="text-right">
|
|
983
|
+
<span
|
|
984
|
+
class="inline-flex items-center rounded-full px-3 py-1 text-sm font-medium"
|
|
985
|
+
[class.bg-success-100]="subscriptionService.subscription()?.status === 'active'"
|
|
986
|
+
[class.text-success-700]="subscriptionService.subscription()?.status === 'active'"
|
|
987
|
+
[class.bg-warning-100]="subscriptionService.subscription()?.status === 'trialing'"
|
|
988
|
+
[class.text-warning-700]="subscriptionService.subscription()?.status === 'trialing'">
|
|
989
|
+
{{ subscriptionService.subscription()?.status | titlecase }}
|
|
990
|
+
</span>
|
|
991
|
+
<p class="mt-2 text-sm text-gray-500">
|
|
992
|
+
Renews {{ subscriptionService.subscription()?.currentPeriodEnd | date:'mediumDate' }}
|
|
993
|
+
</p>
|
|
994
|
+
</div>
|
|
995
|
+
</div>
|
|
996
|
+
|
|
997
|
+
@if (subscriptionService.subscription()?.cancelAtPeriodEnd) {
|
|
998
|
+
<div class="mt-4 flex items-center gap-2 rounded-lg bg-warning-50 p-4 text-warning-700">
|
|
999
|
+
<ng-icon name="heroExclamationTriangle" size="20"></ng-icon>
|
|
1000
|
+
<span>Your subscription will cancel at the end of the billing period</span>
|
|
1001
|
+
</div>
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
<div class="mt-6 flex gap-4">
|
|
1005
|
+
@if (!subscriptionService.subscription()?.cancelAtPeriodEnd) {
|
|
1006
|
+
<button
|
|
1007
|
+
(click)="cancelSubscription()"
|
|
1008
|
+
[disabled]="loading()"
|
|
1009
|
+
class="rounded-lg border border-gray-300 px-4 py-2 font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50">
|
|
1010
|
+
Cancel Subscription
|
|
1011
|
+
</button>
|
|
1012
|
+
}
|
|
1013
|
+
<a routerLink="/pricing" class="rounded-lg bg-primary-500 px-4 py-2 font-medium text-white hover:bg-primary-600">
|
|
1014
|
+
Change Plan
|
|
1015
|
+
</a>
|
|
1016
|
+
</div>
|
|
1017
|
+
</div>
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
<!-- Plan Features -->
|
|
1021
|
+
@if (subscriptionService.currentPlan()) {
|
|
1022
|
+
<div class="rounded-xl border border-gray-200 bg-white p-6">
|
|
1023
|
+
<h2 class="mb-4 text-lg font-semibold text-gray-900">Plan Features</h2>
|
|
1024
|
+
<ul class="grid gap-3 md:grid-cols-2">
|
|
1025
|
+
@for (feature of subscriptionService.currentPlan()!.features; track feature) {
|
|
1026
|
+
<li class="flex items-center gap-2 text-gray-700">
|
|
1027
|
+
<ng-icon name="heroCheck" size="20" class="text-success-500"></ng-icon>
|
|
1028
|
+
{{ feature }}
|
|
1029
|
+
</li>
|
|
1030
|
+
}
|
|
1031
|
+
</ul>
|
|
1032
|
+
</div>
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
<!-- Billing History -->
|
|
1036
|
+
<div class="mt-8 rounded-xl border border-gray-200 bg-white p-6">
|
|
1037
|
+
<h2 class="mb-4 text-lg font-semibold text-gray-900">Billing History</h2>
|
|
1038
|
+
<div class="overflow-x-auto">
|
|
1039
|
+
<table class="w-full">
|
|
1040
|
+
<thead>
|
|
1041
|
+
<tr class="border-b border-gray-200 text-left">
|
|
1042
|
+
<th class="pb-3 font-medium text-gray-500">Date</th>
|
|
1043
|
+
<th class="pb-3 font-medium text-gray-500">Description</th>
|
|
1044
|
+
<th class="pb-3 font-medium text-gray-500">Amount</th>
|
|
1045
|
+
<th class="pb-3 font-medium text-gray-500">Status</th>
|
|
1046
|
+
<th class="pb-3 font-medium text-gray-500"></th>
|
|
1047
|
+
</tr>
|
|
1048
|
+
</thead>
|
|
1049
|
+
<tbody>
|
|
1050
|
+
@for (invoice of invoices; track invoice.id) {
|
|
1051
|
+
<tr class="border-b border-gray-100">
|
|
1052
|
+
<td class="py-4 text-gray-900">{{ invoice.date | date:'mediumDate' }}</td>
|
|
1053
|
+
<td class="py-4 text-gray-700">{{ invoice.description }}</td>
|
|
1054
|
+
<td class="py-4 text-gray-900">{{ invoice.amount | currency }}</td>
|
|
1055
|
+
<td class="py-4">
|
|
1056
|
+
<span class="rounded-full bg-success-100 px-2 py-1 text-xs font-medium text-success-700">
|
|
1057
|
+
{{ invoice.status }}
|
|
1058
|
+
</span>
|
|
1059
|
+
</td>
|
|
1060
|
+
<td class="py-4">
|
|
1061
|
+
<button class="text-primary-600 hover:text-primary-700">Download</button>
|
|
1062
|
+
</td>
|
|
1063
|
+
</tr>
|
|
1064
|
+
}
|
|
1065
|
+
</tbody>
|
|
1066
|
+
</table>
|
|
1067
|
+
</div>
|
|
1068
|
+
</div>
|
|
1069
|
+
</div>
|
|
1070
|
+
\`,
|
|
1071
|
+
})
|
|
1072
|
+
export class SubscriptionComponent {
|
|
1073
|
+
subscriptionService = inject(SubscriptionService);
|
|
1074
|
+
private toast = inject(ToastService);
|
|
1075
|
+
|
|
1076
|
+
loading = signal(false);
|
|
1077
|
+
|
|
1078
|
+
invoices = [
|
|
1079
|
+
{ id: 1, date: new Date('2025-01-01'), description: 'Pro Plan - Monthly', amount: 29, status: 'Paid' },
|
|
1080
|
+
{ id: 2, date: new Date('2024-12-01'), description: 'Pro Plan - Monthly', amount: 29, status: 'Paid' },
|
|
1081
|
+
{ id: 3, date: new Date('2024-11-01'), description: 'Pro Plan - Monthly', amount: 29, status: 'Paid' },
|
|
1082
|
+
];
|
|
1083
|
+
|
|
1084
|
+
async cancelSubscription(): Promise<void> {
|
|
1085
|
+
if (confirm('Are you sure you want to cancel your subscription?')) {
|
|
1086
|
+
this.loading.set(true);
|
|
1087
|
+
try {
|
|
1088
|
+
await this.subscriptionService.cancelSubscription();
|
|
1089
|
+
this.toast.success('Subscription cancelled. You will retain access until the end of your billing period.');
|
|
1090
|
+
} catch {
|
|
1091
|
+
this.toast.error('Failed to cancel subscription');
|
|
1092
|
+
} finally {
|
|
1093
|
+
this.loading.set(false);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}`;
|
|
1098
|
+
|
|
1099
|
+
await fs.writeFile(
|
|
1100
|
+
path.join(
|
|
1101
|
+
config.fullPath,
|
|
1102
|
+
'src/app/features/dashboard/subscription/subscription.component.ts'
|
|
1103
|
+
),
|
|
1104
|
+
subscription
|
|
1105
|
+
);
|
|
1106
|
+
|
|
1107
|
+
// Features Page
|
|
1108
|
+
const features = `import { Component } from '@angular/core';
|
|
1109
|
+
import { RouterModule } from '@angular/router';
|
|
1110
|
+
import { TranslateModule } from '@ngx-translate/core';
|
|
1111
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
1112
|
+
import {
|
|
1113
|
+
heroBolt,
|
|
1114
|
+
heroShieldCheck,
|
|
1115
|
+
heroChartBar,
|
|
1116
|
+
heroCloud,
|
|
1117
|
+
heroCog,
|
|
1118
|
+
heroUsers,
|
|
1119
|
+
heroGlobeAlt,
|
|
1120
|
+
heroLockClosed,
|
|
1121
|
+
} from '@ng-icons/heroicons/outline';
|
|
1122
|
+
import { FeatureCardComponent } from '@shared/components/feature-card/feature-card.component';
|
|
1123
|
+
|
|
1124
|
+
@Component({
|
|
1125
|
+
selector: 'app-features',
|
|
1126
|
+
standalone: true,
|
|
1127
|
+
imports: [RouterModule, TranslateModule, NgIconComponent, FeatureCardComponent],
|
|
1128
|
+
viewProviders: [provideIcons({ heroBolt, heroShieldCheck, heroChartBar, heroCloud, heroCog, heroUsers, heroGlobeAlt, heroLockClosed })],
|
|
1129
|
+
template: \`
|
|
1130
|
+
<div>
|
|
1131
|
+
<!-- Hero -->
|
|
1132
|
+
<section class="bg-gradient-to-b from-primary-600 to-primary-800 py-20 text-white">
|
|
1133
|
+
<div class="mx-auto max-w-7xl px-4 text-center sm:px-6 lg:px-8">
|
|
1134
|
+
<h1 class="text-4xl font-bold sm:text-5xl">{{ 'features.title' | translate }}</h1>
|
|
1135
|
+
<p class="mt-4 text-xl text-primary-100">{{ 'features.subtitle' | translate }}</p>
|
|
1136
|
+
</div>
|
|
1137
|
+
</section>
|
|
1138
|
+
|
|
1139
|
+
<!-- Features Grid -->
|
|
1140
|
+
<section class="py-20">
|
|
1141
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
1142
|
+
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-4">
|
|
1143
|
+
@for (feature of features; track feature.title) {
|
|
1144
|
+
<app-feature-card
|
|
1145
|
+
[icon]="feature.icon"
|
|
1146
|
+
[title]="feature.title"
|
|
1147
|
+
[description]="feature.description"
|
|
1148
|
+
[iconBgClass]="feature.iconBg"
|
|
1149
|
+
[iconClass]="feature.iconColor">
|
|
1150
|
+
</app-feature-card>
|
|
1151
|
+
}
|
|
1152
|
+
</div>
|
|
1153
|
+
</div>
|
|
1154
|
+
</section>
|
|
1155
|
+
|
|
1156
|
+
<!-- Feature Details -->
|
|
1157
|
+
<section class="bg-gray-50 py-20">
|
|
1158
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
1159
|
+
@for (detail of featureDetails; track detail.title; let i = $index) {
|
|
1160
|
+
<div class="mb-20 last:mb-0 grid items-center gap-12 lg:grid-cols-2" [class.lg:flex-row-reverse]="i % 2 === 1">
|
|
1161
|
+
<div [class.lg:order-2]="i % 2 === 1">
|
|
1162
|
+
<h2 class="text-3xl font-bold text-gray-900">{{ detail.title }}</h2>
|
|
1163
|
+
<p class="mt-4 text-lg text-gray-600">{{ detail.description }}</p>
|
|
1164
|
+
<ul class="mt-6 space-y-3">
|
|
1165
|
+
@for (point of detail.points; track point) {
|
|
1166
|
+
<li class="flex items-center gap-3 text-gray-700">
|
|
1167
|
+
<span class="flex h-6 w-6 items-center justify-center rounded-full bg-primary-100 text-primary-600">✓</span>
|
|
1168
|
+
{{ point }}
|
|
1169
|
+
</li>
|
|
1170
|
+
}
|
|
1171
|
+
</ul>
|
|
1172
|
+
</div>
|
|
1173
|
+
<div [class.lg:order-1]="i % 2 === 1" class="rounded-2xl bg-linear-to-br p-8" [class]="detail.gradientClass">
|
|
1174
|
+
<ng-icon [name]="detail.icon" size="120" class="mx-auto text-white/80"></ng-icon>
|
|
1175
|
+
</div>
|
|
1176
|
+
</div>
|
|
1177
|
+
}
|
|
1178
|
+
</div>
|
|
1179
|
+
</section>
|
|
1180
|
+
|
|
1181
|
+
<!-- CTA -->
|
|
1182
|
+
<section class="py-20">
|
|
1183
|
+
<div class="mx-auto max-w-4xl px-4 text-center sm:px-6 lg:px-8">
|
|
1184
|
+
<h2 class="text-3xl font-bold text-gray-900">{{ 'features.ctaTitle' | translate }}</h2>
|
|
1185
|
+
<p class="mt-4 text-xl text-gray-600">{{ 'features.ctaSubtitle' | translate }}</p>
|
|
1186
|
+
<div class="mt-8 flex flex-wrap justify-center gap-4">
|
|
1187
|
+
<a routerLink="/pricing" class="rounded-lg bg-primary-500 px-8 py-4 font-semibold text-white hover:bg-primary-600">
|
|
1188
|
+
View Pricing
|
|
1189
|
+
</a>
|
|
1190
|
+
<a routerLink="/contact" class="rounded-lg border border-gray-300 px-8 py-4 font-semibold text-gray-700 hover:bg-gray-50">
|
|
1191
|
+
Contact Sales
|
|
1192
|
+
</a>
|
|
1193
|
+
</div>
|
|
1194
|
+
</div>
|
|
1195
|
+
</section>
|
|
1196
|
+
</div>
|
|
1197
|
+
\`,
|
|
1198
|
+
})
|
|
1199
|
+
export class FeaturesComponent {
|
|
1200
|
+
features = [
|
|
1201
|
+
{ icon: 'heroBolt', title: 'Lightning Fast', description: 'Sub-millisecond response times with our globally distributed infrastructure.', iconBg: 'bg-amber-100', iconColor: 'text-amber-600' },
|
|
1202
|
+
{ icon: 'heroShieldCheck', title: 'Enterprise Security', description: 'SOC 2 compliant with end-to-end encryption and advanced access controls.', iconBg: 'bg-green-100', iconColor: 'text-green-600' },
|
|
1203
|
+
{ icon: 'heroChartBar', title: 'Advanced Analytics', description: 'Real-time insights and custom dashboards to track your performance.', iconBg: 'bg-blue-100', iconColor: 'text-blue-600' },
|
|
1204
|
+
{ icon: 'heroCloud', title: 'Scalable Storage', description: 'Automatically scales with your needs, from megabytes to petabytes.', iconBg: 'bg-purple-100', iconColor: 'text-purple-600' },
|
|
1205
|
+
{ icon: 'heroCog', title: 'Custom Integrations', description: 'Connect with 100+ tools or build your own with our flexible API.', iconBg: 'bg-gray-100', iconColor: 'text-gray-600' },
|
|
1206
|
+
{ icon: 'heroUsers', title: 'Team Collaboration', description: 'Real-time collaboration with role-based permissions and audit logs.', iconBg: 'bg-pink-100', iconColor: 'text-pink-600' },
|
|
1207
|
+
{ icon: 'heroGlobeAlt', title: 'Global CDN', description: 'Content delivery from 200+ edge locations worldwide.', iconBg: 'bg-cyan-100', iconColor: 'text-cyan-600' },
|
|
1208
|
+
{ icon: 'heroLockClosed', title: 'SSO & SAML', description: 'Enterprise single sign-on with support for all major providers.', iconBg: 'bg-red-100', iconColor: 'text-red-600' },
|
|
1209
|
+
];
|
|
1210
|
+
|
|
1211
|
+
featureDetails = [
|
|
1212
|
+
{
|
|
1213
|
+
icon: 'heroChartBar',
|
|
1214
|
+
title: 'Powerful Analytics Dashboard',
|
|
1215
|
+
description: 'Get deep insights into your data with our comprehensive analytics suite. Track trends, identify patterns, and make data-driven decisions.',
|
|
1216
|
+
points: ['Real-time data visualization', 'Custom report builder', 'Automated insights', 'Export to any format'],
|
|
1217
|
+
gradientClass: 'from-blue-500 to-indigo-600',
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
icon: 'heroShieldCheck',
|
|
1221
|
+
title: 'Enterprise-Grade Security',
|
|
1222
|
+
description: 'Your data security is our top priority. We employ industry-leading security measures to protect your sensitive information.',
|
|
1223
|
+
points: ['End-to-end encryption', 'SOC 2 Type II certified', 'GDPR compliant', '99.99% uptime SLA'],
|
|
1224
|
+
gradientClass: 'from-green-500 to-emerald-600',
|
|
1225
|
+
},
|
|
1226
|
+
{
|
|
1227
|
+
icon: 'heroCog',
|
|
1228
|
+
title: 'Seamless Integrations',
|
|
1229
|
+
description: 'Connect your favorite tools and automate your workflows with our extensive integration library and flexible API.',
|
|
1230
|
+
points: ['100+ pre-built integrations', 'RESTful API', 'Webhooks support', 'Custom SDK available'],
|
|
1231
|
+
gradientClass: 'from-purple-500 to-pink-600',
|
|
1232
|
+
},
|
|
1233
|
+
];
|
|
1234
|
+
}`;
|
|
1235
|
+
|
|
1236
|
+
await fs.writeFile(
|
|
1237
|
+
path.join(config.fullPath, 'src/app/features/features/features.component.ts'),
|
|
1238
|
+
features
|
|
1239
|
+
);
|
|
1240
|
+
},
|
|
1241
|
+
|
|
1242
|
+
async createRouting(config) {
|
|
1243
|
+
const routes = `import { Routes } from '@angular/router';
|
|
1244
|
+
|
|
1245
|
+
export const routes: Routes = [
|
|
1246
|
+
// Public routes
|
|
1247
|
+
{
|
|
1248
|
+
path: '',
|
|
1249
|
+
loadComponent: () => import('./features/home/home.component').then(c => c.HomeComponent)
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
path: 'about',
|
|
1253
|
+
loadComponent: () => import('./features/about/about.component').then(c => c.AboutComponent)
|
|
1254
|
+
},
|
|
1255
|
+
{
|
|
1256
|
+
path: 'contact',
|
|
1257
|
+
loadComponent: () => import('./features/contact/contact.component').then(c => c.ContactComponent)
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
path: 'pricing',
|
|
1261
|
+
loadComponent: () => import('./features/pricing/pricing.component').then(c => c.PricingComponent)
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
path: 'features',
|
|
1265
|
+
loadComponent: () => import('./features/features/features.component').then(c => c.FeaturesComponent)
|
|
1266
|
+
},
|
|
1267
|
+
|
|
1268
|
+
// Dashboard routes
|
|
1269
|
+
{
|
|
1270
|
+
path: 'dashboard',
|
|
1271
|
+
children: [
|
|
1272
|
+
{ path: '', redirectTo: 'overview', pathMatch: 'full' },
|
|
1273
|
+
{ path: 'overview', loadComponent: () => import('./features/dashboard/overview/overview.component').then(c => c.DashboardOverviewComponent) },
|
|
1274
|
+
{ path: 'subscription', loadComponent: () => import('./features/dashboard/subscription/subscription.component').then(c => c.SubscriptionComponent) },
|
|
1275
|
+
]
|
|
1276
|
+
},
|
|
1277
|
+
|
|
1278
|
+
// Auth routes
|
|
1279
|
+
{
|
|
1280
|
+
path: 'auth',
|
|
1281
|
+
loadComponent: () => import('./layout/auth/auth-layout.component').then(c => c.AuthLayoutComponent),
|
|
1282
|
+
children: [
|
|
1283
|
+
{ path: '', redirectTo: 'login', pathMatch: 'full' },
|
|
1284
|
+
{ path: 'login', loadComponent: () => import('./features/auth/login/login.component').then(c => c.LoginComponent) },
|
|
1285
|
+
{ path: 'register', loadComponent: () => import('./features/auth/register/register.component').then(c => c.RegisterComponent) },
|
|
1286
|
+
{ path: 'forgot-password', loadComponent: () => import('./features/auth/forgot-password/forgot-password.component').then(c => c.ForgotPasswordComponent) }
|
|
1287
|
+
]
|
|
1288
|
+
},
|
|
1289
|
+
|
|
1290
|
+
{ path: '**', redirectTo: '' }
|
|
1291
|
+
];`;
|
|
1292
|
+
|
|
1293
|
+
await fs.writeFile(path.join(config.fullPath, 'src/app/app.routes.ts'), routes);
|
|
1294
|
+
},
|
|
1295
|
+
|
|
1296
|
+
async updateI18n(config) {
|
|
1297
|
+
const enPath = path.join(config.fullPath, 'public/assets/i18n/en.json');
|
|
1298
|
+
const arPath = path.join(config.fullPath, 'public/assets/i18n/ar.json');
|
|
1299
|
+
|
|
1300
|
+
let en = {};
|
|
1301
|
+
let ar = {};
|
|
1302
|
+
|
|
1303
|
+
try {
|
|
1304
|
+
en = JSON.parse(await fs.readFile(enPath, 'utf-8'));
|
|
1305
|
+
ar = JSON.parse(await fs.readFile(arPath, 'utf-8'));
|
|
1306
|
+
} catch {
|
|
1307
|
+
// Files don't exist yet
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
const saasEn = {
|
|
1311
|
+
pricing: {
|
|
1312
|
+
title: 'Simple, Transparent Pricing',
|
|
1313
|
+
subtitle: 'Choose the plan that fits your needs. No hidden fees.',
|
|
1314
|
+
compareFeatures: 'Compare Features',
|
|
1315
|
+
trustedBy: 'Trusted by Industry Leaders',
|
|
1316
|
+
faq: 'Frequently Asked Questions',
|
|
1317
|
+
ctaTitle: 'Ready to Get Started?',
|
|
1318
|
+
ctaSubtitle: 'Start your 14-day free trial today. No credit card required.',
|
|
1319
|
+
startTrial: 'Start Free Trial',
|
|
1320
|
+
},
|
|
1321
|
+
subscription: {
|
|
1322
|
+
title: 'Subscription',
|
|
1323
|
+
subtitle: 'Manage your subscription and billing',
|
|
1324
|
+
},
|
|
1325
|
+
dashboard: {
|
|
1326
|
+
welcome: 'Welcome Back!',
|
|
1327
|
+
overviewSubtitle: "Here's what's happening with your account",
|
|
1328
|
+
recentActivity: 'Recent Activity',
|
|
1329
|
+
},
|
|
1330
|
+
features: {
|
|
1331
|
+
title: 'Powerful Features for Modern Teams',
|
|
1332
|
+
subtitle: 'Everything you need to build, deploy, and scale your applications',
|
|
1333
|
+
ctaTitle: 'Ready to Transform Your Workflow?',
|
|
1334
|
+
ctaSubtitle: 'Join thousands of teams already using our platform',
|
|
1335
|
+
},
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
const saasAr = {
|
|
1339
|
+
pricing: {
|
|
1340
|
+
title: 'أسعار بسيطة وشفافة',
|
|
1341
|
+
subtitle: 'اختر الخطة التي تناسب احتياجاتك. بدون رسوم خفية.',
|
|
1342
|
+
compareFeatures: 'مقارنة الميزات',
|
|
1343
|
+
trustedBy: 'موثوق به من قبل رواد الصناعة',
|
|
1344
|
+
faq: 'الأسئلة الشائعة',
|
|
1345
|
+
ctaTitle: 'هل أنت مستعد للبدء؟',
|
|
1346
|
+
ctaSubtitle: 'ابدأ تجربتك المجانية لمدة 14 يومًا اليوم. لا حاجة لبطاقة ائتمان.',
|
|
1347
|
+
startTrial: 'ابدأ التجربة المجانية',
|
|
1348
|
+
},
|
|
1349
|
+
subscription: {
|
|
1350
|
+
title: 'الاشتراك',
|
|
1351
|
+
subtitle: 'إدارة اشتراكك والفواتير',
|
|
1352
|
+
},
|
|
1353
|
+
dashboard: {
|
|
1354
|
+
welcome: 'مرحبًا بعودتك!',
|
|
1355
|
+
overviewSubtitle: 'إليك ما يحدث في حسابك',
|
|
1356
|
+
recentActivity: 'النشاط الأخير',
|
|
1357
|
+
},
|
|
1358
|
+
features: {
|
|
1359
|
+
title: 'ميزات قوية للفرق الحديثة',
|
|
1360
|
+
subtitle: 'كل ما تحتاجه لبناء ونشر وتوسيع تطبيقاتك',
|
|
1361
|
+
ctaTitle: 'هل أنت مستعد لتحويل سير عملك؟',
|
|
1362
|
+
ctaSubtitle: 'انضم إلى آلاف الفرق التي تستخدم منصتنا بالفعل',
|
|
1363
|
+
},
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
await fs.writeFile(enPath, JSON.stringify({ ...en, ...saasEn }, null, 2));
|
|
1367
|
+
await fs.writeFile(arPath, JSON.stringify({ ...ar, ...saasAr }, null, 2));
|
|
1368
|
+
},
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
module.exports = saas;
|