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,1117 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const starter = require('../starter');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Landing Page Template
|
|
7
|
+
* Single-page marketing template with:
|
|
8
|
+
* - Animated hero section
|
|
9
|
+
* - Feature highlights
|
|
10
|
+
* - How it works section
|
|
11
|
+
* - Testimonials
|
|
12
|
+
* - Pricing section
|
|
13
|
+
* - FAQ
|
|
14
|
+
* - CTA sections
|
|
15
|
+
* - Newsletter signup
|
|
16
|
+
*/
|
|
17
|
+
const landing = {
|
|
18
|
+
info: {
|
|
19
|
+
name: 'Landing Page',
|
|
20
|
+
description:
|
|
21
|
+
'Marketing landing page with hero, features, testimonials, pricing, and CTA sections',
|
|
22
|
+
features: [
|
|
23
|
+
...starter.info.features,
|
|
24
|
+
'Animated hero with CTA',
|
|
25
|
+
'Feature cards with icons',
|
|
26
|
+
'How it works section',
|
|
27
|
+
'Social proof / logos',
|
|
28
|
+
'Testimonials carousel',
|
|
29
|
+
'Pricing comparison',
|
|
30
|
+
'FAQ accordion',
|
|
31
|
+
'Newsletter signup',
|
|
32
|
+
'Sticky navigation',
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async apply(config, spinner) {
|
|
37
|
+
const chalk = require('chalk');
|
|
38
|
+
|
|
39
|
+
const completeStep = (message) => {
|
|
40
|
+
if (spinner) {
|
|
41
|
+
spinner.stop();
|
|
42
|
+
console.log(chalk.green(' ✔') + chalk.white(' ' + message));
|
|
43
|
+
spinner.start();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Step 1: Apply starter template
|
|
48
|
+
if (spinner) spinner.update('Setting up starter foundation...');
|
|
49
|
+
await starter.apply(config, null);
|
|
50
|
+
|
|
51
|
+
// Step 2: Create landing-specific directories
|
|
52
|
+
if (spinner) spinner.update('Setting up landing page structure...');
|
|
53
|
+
await this.createDirectories(config);
|
|
54
|
+
|
|
55
|
+
// Step 3: Create components
|
|
56
|
+
await this.createComponents(config);
|
|
57
|
+
|
|
58
|
+
// Step 4: Create landing page
|
|
59
|
+
await this.createLandingPage(config);
|
|
60
|
+
|
|
61
|
+
// Step 5: Update layout (sticky header)
|
|
62
|
+
await this.updateLayout(config);
|
|
63
|
+
|
|
64
|
+
// Step 6: Update routing
|
|
65
|
+
await this.createRouting(config);
|
|
66
|
+
|
|
67
|
+
// Step 7: Add i18n translations
|
|
68
|
+
await this.updateI18n(config);
|
|
69
|
+
|
|
70
|
+
// Step 8: Format code
|
|
71
|
+
const base = require('../base');
|
|
72
|
+
await base.formatCode(config);
|
|
73
|
+
|
|
74
|
+
if (spinner) spinner.stop();
|
|
75
|
+
|
|
76
|
+
console.log('');
|
|
77
|
+
completeStep('Landing page template created');
|
|
78
|
+
completeStep('Hero section with animations');
|
|
79
|
+
completeStep('Features & benefits');
|
|
80
|
+
completeStep('Pricing section');
|
|
81
|
+
completeStep('Testimonials');
|
|
82
|
+
completeStep('FAQ & Newsletter');
|
|
83
|
+
console.log('');
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
async createDirectories(config) {
|
|
87
|
+
const directories = [
|
|
88
|
+
'src/app/features/landing',
|
|
89
|
+
'src/app/shared/components/hero',
|
|
90
|
+
'src/app/shared/components/features-section',
|
|
91
|
+
'src/app/shared/components/how-it-works',
|
|
92
|
+
'src/app/shared/components/social-proof',
|
|
93
|
+
'src/app/shared/components/pricing-section',
|
|
94
|
+
'src/app/shared/components/testimonials-section',
|
|
95
|
+
'src/app/shared/components/faq-section',
|
|
96
|
+
'src/app/shared/components/newsletter',
|
|
97
|
+
'src/app/shared/components/cta-section',
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
for (const dir of directories) {
|
|
101
|
+
await fs.ensureDir(path.join(config.fullPath, dir));
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
async createComponents(config) {
|
|
106
|
+
// Hero Component
|
|
107
|
+
const hero = `import { Component, Input } from '@angular/core';
|
|
108
|
+
import { RouterModule } from '@angular/router';
|
|
109
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
110
|
+
import { heroPlay, heroArrowRight } from '@ng-icons/heroicons/outline';
|
|
111
|
+
import { heroStarSolid } from '@ng-icons/heroicons/solid';
|
|
112
|
+
|
|
113
|
+
@Component({
|
|
114
|
+
selector: 'app-hero',
|
|
115
|
+
standalone: true,
|
|
116
|
+
imports: [RouterModule, NgIconComponent],
|
|
117
|
+
viewProviders: [provideIcons({ heroPlay, heroArrowRight, heroStarSolid })],
|
|
118
|
+
template: \`
|
|
119
|
+
<section class="relative min-h-screen overflow-hidden bg-linear-to-br from-slate-900 via-purple-900 to-slate-900">
|
|
120
|
+
<!-- Animated Background -->
|
|
121
|
+
<div class="absolute inset-0">
|
|
122
|
+
<div class="animate-blob absolute -left-4 top-20 h-96 w-96 rounded-full bg-purple-500 opacity-20 mix-blend-multiply blur-3xl"></div>
|
|
123
|
+
<div class="animate-blob animation-delay-2000 absolute right-0 top-0 h-96 w-96 rounded-full bg-cyan-500 opacity-20 mix-blend-multiply blur-3xl"></div>
|
|
124
|
+
<div class="animate-blob animation-delay-4000 absolute bottom-0 left-1/3 h-96 w-96 rounded-full bg-pink-500 opacity-20 mix-blend-multiply blur-3xl"></div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<!-- Grid Pattern -->
|
|
128
|
+
<div class="absolute inset-0 bg-[linear-gradient(to_right,#ffffff08_1px,transparent_1px),linear-gradient(to_bottom,#ffffff08_1px,transparent_1px)] bg-[size:4rem_4rem]"></div>
|
|
129
|
+
|
|
130
|
+
<div class="relative mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8 lg:py-32">
|
|
131
|
+
<div class="text-center">
|
|
132
|
+
<!-- Badge -->
|
|
133
|
+
<div class="mb-8 inline-flex items-center gap-2 rounded-full bg-white/10 px-4 py-2 backdrop-blur-sm">
|
|
134
|
+
<span class="relative flex h-2 w-2">
|
|
135
|
+
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
|
|
136
|
+
<span class="relative inline-flex h-2 w-2 rounded-full bg-green-400"></span>
|
|
137
|
+
</span>
|
|
138
|
+
<span class="text-sm font-medium text-white">{{ badge }}</span>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<!-- Headline -->
|
|
142
|
+
<h1 class="mx-auto max-w-4xl text-4xl font-bold tracking-tight text-white sm:text-6xl lg:text-7xl">
|
|
143
|
+
{{ title }}
|
|
144
|
+
<span class="bg-gradient-to-r from-cyan-400 to-purple-400 bg-clip-text text-transparent">{{ highlight }}</span>
|
|
145
|
+
</h1>
|
|
146
|
+
|
|
147
|
+
<!-- Subtitle -->
|
|
148
|
+
<p class="mx-auto mt-6 max-w-2xl text-lg text-gray-300 sm:text-xl">
|
|
149
|
+
{{ subtitle }}
|
|
150
|
+
</p>
|
|
151
|
+
|
|
152
|
+
<!-- CTA Buttons -->
|
|
153
|
+
<div class="mt-10 flex flex-wrap justify-center gap-4">
|
|
154
|
+
<a
|
|
155
|
+
[routerLink]="primaryCta.link"
|
|
156
|
+
class="group inline-flex items-center gap-2 rounded-full bg-white px-8 py-4 font-semibold text-gray-900 shadow-xl transition-all hover:shadow-2xl hover:scale-105">
|
|
157
|
+
{{ primaryCta.text }}
|
|
158
|
+
<ng-icon name="heroArrowRight" size="20" class="transition-transform group-hover:translate-x-1"></ng-icon>
|
|
159
|
+
</a>
|
|
160
|
+
@if (secondaryCta) {
|
|
161
|
+
<button
|
|
162
|
+
(click)="onWatchDemo()"
|
|
163
|
+
class="inline-flex items-center gap-2 rounded-full border-2 border-white/30 px-8 py-4 font-semibold text-white backdrop-blur-sm transition-all hover:bg-white/10">
|
|
164
|
+
<ng-icon name="heroPlay" size="20"></ng-icon>
|
|
165
|
+
{{ secondaryCta.text }}
|
|
166
|
+
</button>
|
|
167
|
+
}
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<!-- Social Proof -->
|
|
171
|
+
<div class="mt-16">
|
|
172
|
+
<p class="mb-4 text-sm text-gray-400">Trusted by 10,000+ companies worldwide</p>
|
|
173
|
+
<div class="flex flex-wrap justify-center items-center gap-8 opacity-60">
|
|
174
|
+
@for (logo of logos; track logo) {
|
|
175
|
+
<div class="text-2xl font-bold text-white">{{ logo }}</div>
|
|
176
|
+
}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<!-- Stats -->
|
|
181
|
+
<div class="mt-16 grid grid-cols-2 gap-8 sm:grid-cols-4">
|
|
182
|
+
@for (stat of stats; track stat.label) {
|
|
183
|
+
<div class="text-center">
|
|
184
|
+
<div class="text-3xl font-bold text-white sm:text-4xl">{{ stat.value }}</div>
|
|
185
|
+
<div class="mt-1 text-sm text-gray-400">{{ stat.label }}</div>
|
|
186
|
+
</div>
|
|
187
|
+
}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- Scroll Indicator -->
|
|
193
|
+
<div class="absolute bottom-8 left-1/2 -translate-x-1/2">
|
|
194
|
+
<div class="flex flex-col items-center gap-2">
|
|
195
|
+
<span class="text-xs text-gray-400">Scroll to explore</span>
|
|
196
|
+
<div class="h-12 w-6 rounded-full border-2 border-white/30 p-1">
|
|
197
|
+
<div class="h-2 w-2 animate-bounce rounded-full bg-white"></div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</section>
|
|
202
|
+
\`,
|
|
203
|
+
})
|
|
204
|
+
export class HeroComponent {
|
|
205
|
+
@Input() badge = 'New Feature Available';
|
|
206
|
+
@Input() title = 'Build something ';
|
|
207
|
+
@Input() highlight = 'amazing today';
|
|
208
|
+
@Input() subtitle = 'The all-in-one platform to build, launch, and grow your digital products faster than ever.';
|
|
209
|
+
@Input() primaryCta = { text: 'Get Started Free', link: '/auth/register' };
|
|
210
|
+
@Input() secondaryCta?: { text: string } = { text: 'Watch Demo' };
|
|
211
|
+
@Input() logos = ['Google', 'Microsoft', 'Airbnb', 'Spotify', 'Slack'];
|
|
212
|
+
@Input() stats = [
|
|
213
|
+
{ value: '10K+', label: 'Active Users' },
|
|
214
|
+
{ value: '99.9%', label: 'Uptime' },
|
|
215
|
+
{ value: '50M+', label: 'API Requests' },
|
|
216
|
+
{ value: '4.9/5', label: 'User Rating' },
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
onWatchDemo(): void {
|
|
220
|
+
// Open video modal or navigate to demo
|
|
221
|
+
console.log('Watch demo clicked');
|
|
222
|
+
}
|
|
223
|
+
}`;
|
|
224
|
+
|
|
225
|
+
await fs.writeFile(
|
|
226
|
+
path.join(config.fullPath, 'src/app/shared/components/hero/hero.component.ts'),
|
|
227
|
+
hero
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Features Section
|
|
231
|
+
const features = `import { Component, Input } from '@angular/core';
|
|
232
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
233
|
+
import {
|
|
234
|
+
heroBolt,
|
|
235
|
+
heroShieldCheck,
|
|
236
|
+
heroChartBar,
|
|
237
|
+
heroCog,
|
|
238
|
+
heroUsers,
|
|
239
|
+
heroGlobeAlt,
|
|
240
|
+
} from '@ng-icons/heroicons/outline';
|
|
241
|
+
|
|
242
|
+
export interface Feature {
|
|
243
|
+
icon: string;
|
|
244
|
+
title: string;
|
|
245
|
+
description: string;
|
|
246
|
+
iconBg?: string;
|
|
247
|
+
iconColor?: string;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@Component({
|
|
251
|
+
selector: 'app-features-section',
|
|
252
|
+
standalone: true,
|
|
253
|
+
imports: [NgIconComponent],
|
|
254
|
+
viewProviders: [provideIcons({ heroBolt, heroShieldCheck, heroChartBar, heroCog, heroUsers, heroGlobeAlt })],
|
|
255
|
+
template: \`
|
|
256
|
+
<section class="py-24 bg-white" [id]="sectionId">
|
|
257
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
258
|
+
<div class="text-center mb-16">
|
|
259
|
+
<span class="text-primary-600 font-semibold text-sm uppercase tracking-wider">{{ badge }}</span>
|
|
260
|
+
<h2 class="mt-2 text-3xl font-bold text-gray-900 sm:text-4xl">{{ title }}</h2>
|
|
261
|
+
<p class="mt-4 text-xl text-gray-600 max-w-2xl mx-auto">{{ subtitle }}</p>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
|
265
|
+
@for (feature of features; track feature.title) {
|
|
266
|
+
<div class="group rounded-2xl border border-gray-200 bg-white p-8 transition-all hover:shadow-xl hover:-translate-y-1 hover:border-primary-200">
|
|
267
|
+
<div
|
|
268
|
+
class="flex h-14 w-14 items-center justify-center rounded-xl transition-colors"
|
|
269
|
+
[class]="feature.iconBg || 'bg-primary-100 group-hover:bg-primary-500'">
|
|
270
|
+
<ng-icon
|
|
271
|
+
[name]="feature.icon"
|
|
272
|
+
size="28"
|
|
273
|
+
[class]="feature.iconColor || 'text-primary-600 group-hover:text-white'">
|
|
274
|
+
</ng-icon>
|
|
275
|
+
</div>
|
|
276
|
+
<h3 class="mt-6 text-xl font-bold text-gray-900">{{ feature.title }}</h3>
|
|
277
|
+
<p class="mt-2 text-gray-600">{{ feature.description }}</p>
|
|
278
|
+
</div>
|
|
279
|
+
}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</section>
|
|
283
|
+
\`,
|
|
284
|
+
})
|
|
285
|
+
export class FeaturesSectionComponent {
|
|
286
|
+
@Input() sectionId = 'features';
|
|
287
|
+
@Input() badge = 'Features';
|
|
288
|
+
@Input() title = 'Everything you need';
|
|
289
|
+
@Input() subtitle = 'Powerful features to help you build, launch, and scale faster';
|
|
290
|
+
@Input() features: Feature[] = [
|
|
291
|
+
{ icon: 'heroBolt', title: 'Lightning Fast', description: 'Optimized for speed with sub-millisecond response times and global CDN distribution.' },
|
|
292
|
+
{ icon: 'heroShieldCheck', title: 'Secure by Default', description: 'Enterprise-grade security with end-to-end encryption and SOC 2 compliance.' },
|
|
293
|
+
{ icon: 'heroChartBar', title: 'Advanced Analytics', description: 'Real-time insights and custom dashboards to track your key metrics.' },
|
|
294
|
+
{ icon: 'heroCog', title: 'Easy Integration', description: 'Connect with 100+ tools through our REST API and native integrations.' },
|
|
295
|
+
{ icon: 'heroUsers', title: 'Team Collaboration', description: 'Real-time collaboration with role-based access and audit logs.' },
|
|
296
|
+
{ icon: 'heroGlobeAlt', title: 'Global Scale', description: 'Deploy worldwide with automatic scaling and 99.99% uptime guarantee.' },
|
|
297
|
+
];
|
|
298
|
+
}`;
|
|
299
|
+
|
|
300
|
+
await fs.writeFile(
|
|
301
|
+
path.join(
|
|
302
|
+
config.fullPath,
|
|
303
|
+
'src/app/shared/components/features-section/features-section.component.ts'
|
|
304
|
+
),
|
|
305
|
+
features
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// How It Works
|
|
309
|
+
const howItWorks = `import { Component, Input } from '@angular/core';
|
|
310
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
311
|
+
import { heroArrowRight } from '@ng-icons/heroicons/outline';
|
|
312
|
+
|
|
313
|
+
export interface Step {
|
|
314
|
+
number: number;
|
|
315
|
+
title: string;
|
|
316
|
+
description: string;
|
|
317
|
+
image?: string;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
@Component({
|
|
321
|
+
selector: 'app-how-it-works',
|
|
322
|
+
standalone: true,
|
|
323
|
+
imports: [NgIconComponent],
|
|
324
|
+
viewProviders: [provideIcons({ heroArrowRight })],
|
|
325
|
+
template: \`
|
|
326
|
+
<section class="py-24 bg-gray-50" [id]="sectionId">
|
|
327
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
328
|
+
<div class="text-center mb-16">
|
|
329
|
+
<span class="text-primary-600 font-semibold text-sm uppercase tracking-wider">{{ badge }}</span>
|
|
330
|
+
<h2 class="mt-2 text-3xl font-bold text-gray-900 sm:text-4xl">{{ title }}</h2>
|
|
331
|
+
<p class="mt-4 text-xl text-gray-600 max-w-2xl mx-auto">{{ subtitle }}</p>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<div class="relative">
|
|
335
|
+
<!-- Connection Line -->
|
|
336
|
+
<div class="absolute left-1/2 top-0 h-full w-0.5 -translate-x-1/2 bg-gray-200 hidden lg:block"></div>
|
|
337
|
+
|
|
338
|
+
<div class="space-y-12 lg:space-y-24">
|
|
339
|
+
@for (step of steps; track step.number; let i = $index) {
|
|
340
|
+
<div class="relative grid items-center gap-8 lg:grid-cols-2" [class.lg:flex-row-reverse]="i % 2 === 1">
|
|
341
|
+
<!-- Step Number -->
|
|
342
|
+
<div class="absolute left-1/2 -translate-x-1/2 hidden lg:flex h-12 w-12 items-center justify-center rounded-full bg-primary-500 text-white font-bold text-lg z-10">
|
|
343
|
+
{{ step.number }}
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
<!-- Content -->
|
|
347
|
+
<div [class.lg:text-right]="i % 2 === 0" [class.lg:pr-16]="i % 2 === 0" [class.lg:pl-16]="i % 2 === 1">
|
|
348
|
+
<div class="lg:hidden mb-4 inline-flex h-10 w-10 items-center justify-center rounded-full bg-primary-500 text-white font-bold">
|
|
349
|
+
{{ step.number }}
|
|
350
|
+
</div>
|
|
351
|
+
<h3 class="text-2xl font-bold text-gray-900">{{ step.title }}</h3>
|
|
352
|
+
<p class="mt-4 text-gray-600">{{ step.description }}</p>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
<!-- Image/Illustration -->
|
|
356
|
+
<div [class.lg:order-first]="i % 2 === 1">
|
|
357
|
+
@if (step.image) {
|
|
358
|
+
<img [src]="step.image" [alt]="step.title" class="rounded-2xl shadow-xl" />
|
|
359
|
+
} @else {
|
|
360
|
+
<div class="aspect-video rounded-2xl bg-linear-to-br from-primary-100 to-purple-100 flex items-center justify-center">
|
|
361
|
+
<span class="text-6xl font-bold text-primary-300">{{ step.number }}</span>
|
|
362
|
+
</div>
|
|
363
|
+
}
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
}
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
</section>
|
|
371
|
+
\`,
|
|
372
|
+
})
|
|
373
|
+
export class HowItWorksComponent {
|
|
374
|
+
@Input() sectionId = 'how-it-works';
|
|
375
|
+
@Input() badge = 'How It Works';
|
|
376
|
+
@Input() title = 'Get started in minutes';
|
|
377
|
+
@Input() subtitle = 'Simple steps to transform your workflow';
|
|
378
|
+
@Input() steps: Step[] = [
|
|
379
|
+
{ number: 1, title: 'Create Your Account', description: 'Sign up in seconds with just your email. No credit card required to get started.' },
|
|
380
|
+
{ number: 2, title: 'Configure Your Workspace', description: 'Set up your workspace, invite team members, and customize your settings.' },
|
|
381
|
+
{ number: 3, title: 'Start Building', description: 'Use our intuitive tools and templates to build your first project in minutes.' },
|
|
382
|
+
{ number: 4, title: 'Launch & Scale', description: 'Deploy with one click and scale automatically as your business grows.' },
|
|
383
|
+
];
|
|
384
|
+
}`;
|
|
385
|
+
|
|
386
|
+
await fs.writeFile(
|
|
387
|
+
path.join(
|
|
388
|
+
config.fullPath,
|
|
389
|
+
'src/app/shared/components/how-it-works/how-it-works.component.ts'
|
|
390
|
+
),
|
|
391
|
+
howItWorks
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
// Testimonials Section
|
|
395
|
+
const testimonials = `import { Component, Input, signal } from '@angular/core';
|
|
396
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
397
|
+
import { heroChevronLeft, heroChevronRight } from '@ng-icons/heroicons/outline';
|
|
398
|
+
import { heroStarSolid } from '@ng-icons/heroicons/solid';
|
|
399
|
+
|
|
400
|
+
export interface LandingTestimonial {
|
|
401
|
+
content: string;
|
|
402
|
+
author: string;
|
|
403
|
+
role: string;
|
|
404
|
+
company: string;
|
|
405
|
+
avatar: string;
|
|
406
|
+
rating: number;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
@Component({
|
|
410
|
+
selector: 'app-testimonials-section',
|
|
411
|
+
standalone: true,
|
|
412
|
+
imports: [NgIconComponent],
|
|
413
|
+
viewProviders: [provideIcons({ heroChevronLeft, heroChevronRight, heroStarSolid })],
|
|
414
|
+
template: \`
|
|
415
|
+
<section class="py-24 bg-white" [id]="sectionId">
|
|
416
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
417
|
+
<div class="text-center mb-16">
|
|
418
|
+
<span class="text-primary-600 font-semibold text-sm uppercase tracking-wider">{{ badge }}</span>
|
|
419
|
+
<h2 class="mt-2 text-3xl font-bold text-gray-900 sm:text-4xl">{{ title }}</h2>
|
|
420
|
+
<p class="mt-4 text-xl text-gray-600 max-w-2xl mx-auto">{{ subtitle }}</p>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
<!-- Testimonials Grid -->
|
|
424
|
+
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
|
425
|
+
@for (testimonial of testimonials; track testimonial.author) {
|
|
426
|
+
<div class="rounded-2xl border border-gray-200 bg-white p-8 shadow-sm hover:shadow-lg transition-shadow">
|
|
427
|
+
<!-- Rating -->
|
|
428
|
+
<div class="flex gap-1 mb-4">
|
|
429
|
+
@for (star of getStars(testimonial.rating); track $index) {
|
|
430
|
+
<ng-icon name="heroStarSolid" size="20" class="text-amber-400"></ng-icon>
|
|
431
|
+
}
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
<!-- Content -->
|
|
435
|
+
<p class="text-gray-700 italic leading-relaxed">"{{ testimonial.content }}"</p>
|
|
436
|
+
|
|
437
|
+
<!-- Author -->
|
|
438
|
+
<div class="mt-6 flex items-center gap-4">
|
|
439
|
+
<img [src]="testimonial.avatar" [alt]="testimonial.author" class="h-12 w-12 rounded-full object-cover" />
|
|
440
|
+
<div>
|
|
441
|
+
<p class="font-semibold text-gray-900">{{ testimonial.author }}</p>
|
|
442
|
+
<p class="text-sm text-gray-500">{{ testimonial.role }}, {{ testimonial.company }}</p>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
}
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
</section>
|
|
450
|
+
\`,
|
|
451
|
+
})
|
|
452
|
+
export class TestimonialsSectionComponent {
|
|
453
|
+
@Input() sectionId = 'testimonials';
|
|
454
|
+
@Input() badge = 'Testimonials';
|
|
455
|
+
@Input() title = 'Loved by thousands';
|
|
456
|
+
@Input() subtitle = 'See what our customers have to say about us';
|
|
457
|
+
@Input() testimonials: LandingTestimonial[] = [
|
|
458
|
+
{
|
|
459
|
+
content: 'This platform has completely transformed how we work. The speed and reliability are unmatched.',
|
|
460
|
+
author: 'Sarah Chen',
|
|
461
|
+
role: 'CTO',
|
|
462
|
+
company: 'TechStart Inc',
|
|
463
|
+
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150',
|
|
464
|
+
rating: 5,
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
content: 'We migrated our entire infrastructure and saw a 40% improvement in performance immediately.',
|
|
468
|
+
author: 'Michael Rodriguez',
|
|
469
|
+
role: 'Engineering Lead',
|
|
470
|
+
company: 'DataFlow',
|
|
471
|
+
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150',
|
|
472
|
+
rating: 5,
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
content: 'The customer support is incredible. Any question we have is answered within minutes.',
|
|
476
|
+
author: 'Emily Watson',
|
|
477
|
+
role: 'Product Manager',
|
|
478
|
+
company: 'Scale Labs',
|
|
479
|
+
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150',
|
|
480
|
+
rating: 5,
|
|
481
|
+
},
|
|
482
|
+
];
|
|
483
|
+
|
|
484
|
+
getStars(rating: number): number[] {
|
|
485
|
+
return Array.from({ length: rating }, (_, i) => i);
|
|
486
|
+
}
|
|
487
|
+
}`;
|
|
488
|
+
|
|
489
|
+
await fs.writeFile(
|
|
490
|
+
path.join(
|
|
491
|
+
config.fullPath,
|
|
492
|
+
'src/app/shared/components/testimonials-section/testimonials-section.component.ts'
|
|
493
|
+
),
|
|
494
|
+
testimonials
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
// Pricing Section
|
|
498
|
+
const pricing = `import { Component, Input, signal } from '@angular/core';
|
|
499
|
+
import { RouterModule } from '@angular/router';
|
|
500
|
+
import { CurrencyPipe } from '@angular/common';
|
|
501
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
502
|
+
import { heroCheck } from '@ng-icons/heroicons/outline';
|
|
503
|
+
|
|
504
|
+
export interface PricingPlan {
|
|
505
|
+
name: string;
|
|
506
|
+
description: string;
|
|
507
|
+
price: number;
|
|
508
|
+
priceYearly: number;
|
|
509
|
+
features: string[];
|
|
510
|
+
cta: string;
|
|
511
|
+
highlighted?: boolean;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
@Component({
|
|
515
|
+
selector: 'app-pricing-section',
|
|
516
|
+
standalone: true,
|
|
517
|
+
imports: [RouterModule, CurrencyPipe, NgIconComponent],
|
|
518
|
+
viewProviders: [provideIcons({ heroCheck })],
|
|
519
|
+
template: \`
|
|
520
|
+
<section class="py-24 bg-gray-50" [id]="sectionId">
|
|
521
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
522
|
+
<div class="text-center mb-16">
|
|
523
|
+
<span class="text-primary-600 font-semibold text-sm uppercase tracking-wider">{{ badge }}</span>
|
|
524
|
+
<h2 class="mt-2 text-3xl font-bold text-gray-900 sm:text-4xl">{{ title }}</h2>
|
|
525
|
+
<p class="mt-4 text-xl text-gray-600 max-w-2xl mx-auto">{{ subtitle }}</p>
|
|
526
|
+
|
|
527
|
+
<!-- Billing Toggle -->
|
|
528
|
+
<div class="mt-8 flex items-center justify-center gap-4">
|
|
529
|
+
<span [class.text-gray-900]="!isYearly()" [class.text-gray-500]="isYearly()">Monthly</span>
|
|
530
|
+
<button
|
|
531
|
+
(click)="isYearly.set(!isYearly())"
|
|
532
|
+
class="relative h-8 w-14 rounded-full bg-primary-500 transition-colors">
|
|
533
|
+
<span
|
|
534
|
+
class="absolute top-1 h-6 w-6 rounded-full bg-white transition-all"
|
|
535
|
+
[class.left-1]="!isYearly()"
|
|
536
|
+
[class.left-7]="isYearly()">
|
|
537
|
+
</span>
|
|
538
|
+
</button>
|
|
539
|
+
<span [class.text-gray-900]="isYearly()" [class.text-gray-500]="!isYearly()">
|
|
540
|
+
Yearly
|
|
541
|
+
<span class="ml-1 rounded-full bg-success-100 px-2 py-0.5 text-xs font-medium text-success-700">Save 20%</span>
|
|
542
|
+
</span>
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
|
|
546
|
+
<div class="grid gap-8 lg:grid-cols-3">
|
|
547
|
+
@for (plan of plans; track plan.name) {
|
|
548
|
+
<div
|
|
549
|
+
class="relative rounded-2xl border-2 bg-white p-8 shadow-sm transition-all hover:shadow-xl"
|
|
550
|
+
[class.border-primary-500]="plan.highlighted"
|
|
551
|
+
[class.border-gray-200]="!plan.highlighted"
|
|
552
|
+
[class.scale-105]="plan.highlighted">
|
|
553
|
+
|
|
554
|
+
@if (plan.highlighted) {
|
|
555
|
+
<div class="absolute -top-4 left-1/2 -translate-x-1/2">
|
|
556
|
+
<span class="rounded-full bg-primary-500 px-4 py-1 text-sm font-semibold text-white">Most Popular</span>
|
|
557
|
+
</div>
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
<div class="text-center">
|
|
561
|
+
<h3 class="text-xl font-bold text-gray-900">{{ plan.name }}</h3>
|
|
562
|
+
<p class="mt-2 text-gray-600">{{ plan.description }}</p>
|
|
563
|
+
|
|
564
|
+
<div class="mt-6">
|
|
565
|
+
<span class="text-5xl font-bold text-gray-900">
|
|
566
|
+
{{ (isYearly() ? plan.priceYearly : plan.price) | currency:'USD':'symbol':'1.0-0' }}
|
|
567
|
+
</span>
|
|
568
|
+
@if (plan.price > 0) {
|
|
569
|
+
<span class="text-gray-500">/{{ isYearly() ? 'year' : 'month' }}</span>
|
|
570
|
+
}
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
|
|
574
|
+
<ul class="mt-8 space-y-4">
|
|
575
|
+
@for (feature of plan.features; track feature) {
|
|
576
|
+
<li class="flex items-center gap-3">
|
|
577
|
+
<ng-icon name="heroCheck" size="20" class="text-success-500"></ng-icon>
|
|
578
|
+
<span class="text-gray-700">{{ feature }}</span>
|
|
579
|
+
</li>
|
|
580
|
+
}
|
|
581
|
+
</ul>
|
|
582
|
+
|
|
583
|
+
<a
|
|
584
|
+
[routerLink]="['/auth/register']"
|
|
585
|
+
[queryParams]="{ plan: plan.name.toLowerCase() }"
|
|
586
|
+
class="mt-8 block w-full rounded-lg py-3 text-center font-semibold transition-all"
|
|
587
|
+
[class.bg-primary-500]="plan.highlighted"
|
|
588
|
+
[class.text-white]="plan.highlighted"
|
|
589
|
+
[class.hover:bg-primary-600]="plan.highlighted"
|
|
590
|
+
[class.bg-gray-100]="!plan.highlighted"
|
|
591
|
+
[class.text-gray-900]="!plan.highlighted"
|
|
592
|
+
[class.hover:bg-gray-200]="!plan.highlighted">
|
|
593
|
+
{{ plan.cta }}
|
|
594
|
+
</a>
|
|
595
|
+
</div>
|
|
596
|
+
}
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
</section>
|
|
600
|
+
\`,
|
|
601
|
+
})
|
|
602
|
+
export class PricingSectionComponent {
|
|
603
|
+
@Input() sectionId = 'pricing';
|
|
604
|
+
@Input() badge = 'Pricing';
|
|
605
|
+
@Input() title = 'Simple, transparent pricing';
|
|
606
|
+
@Input() subtitle = 'Choose the plan that fits your needs. No hidden fees.';
|
|
607
|
+
@Input() plans: PricingPlan[] = [
|
|
608
|
+
{
|
|
609
|
+
name: 'Starter',
|
|
610
|
+
description: 'Perfect for individuals',
|
|
611
|
+
price: 0,
|
|
612
|
+
priceYearly: 0,
|
|
613
|
+
features: ['1 user', '5 projects', '1GB storage', 'Basic support'],
|
|
614
|
+
cta: 'Get Started Free',
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
name: 'Pro',
|
|
618
|
+
description: 'Best for growing teams',
|
|
619
|
+
price: 29,
|
|
620
|
+
priceYearly: 290,
|
|
621
|
+
features: ['10 users', 'Unlimited projects', '100GB storage', 'Priority support', 'Advanced analytics'],
|
|
622
|
+
cta: 'Start Free Trial',
|
|
623
|
+
highlighted: true,
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: 'Enterprise',
|
|
627
|
+
description: 'For large organizations',
|
|
628
|
+
price: 99,
|
|
629
|
+
priceYearly: 990,
|
|
630
|
+
features: ['Unlimited users', 'Unlimited projects', '1TB storage', '24/7 support', 'Custom integrations', 'SLA'],
|
|
631
|
+
cta: 'Contact Sales',
|
|
632
|
+
},
|
|
633
|
+
];
|
|
634
|
+
|
|
635
|
+
isYearly = signal(false);
|
|
636
|
+
}`;
|
|
637
|
+
|
|
638
|
+
await fs.writeFile(
|
|
639
|
+
path.join(
|
|
640
|
+
config.fullPath,
|
|
641
|
+
'src/app/shared/components/pricing-section/pricing-section.component.ts'
|
|
642
|
+
),
|
|
643
|
+
pricing
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
// FAQ Section
|
|
647
|
+
const faq = `import { Component, Input, signal } from '@angular/core';
|
|
648
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
649
|
+
import { heroChevronDown } from '@ng-icons/heroicons/outline';
|
|
650
|
+
|
|
651
|
+
export interface FaqItem {
|
|
652
|
+
question: string;
|
|
653
|
+
answer: string;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
@Component({
|
|
657
|
+
selector: 'app-faq-section',
|
|
658
|
+
standalone: true,
|
|
659
|
+
imports: [NgIconComponent],
|
|
660
|
+
viewProviders: [provideIcons({ heroChevronDown })],
|
|
661
|
+
template: \`
|
|
662
|
+
<section class="py-24 bg-white" [id]="sectionId">
|
|
663
|
+
<div class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
|
|
664
|
+
<div class="text-center mb-16">
|
|
665
|
+
<span class="text-primary-600 font-semibold text-sm uppercase tracking-wider">{{ badge }}</span>
|
|
666
|
+
<h2 class="mt-2 text-3xl font-bold text-gray-900 sm:text-4xl">{{ title }}</h2>
|
|
667
|
+
<p class="mt-4 text-xl text-gray-600">{{ subtitle }}</p>
|
|
668
|
+
</div>
|
|
669
|
+
|
|
670
|
+
<div class="space-y-4">
|
|
671
|
+
@for (item of items; track item.question; let i = $index) {
|
|
672
|
+
<div class="rounded-xl border border-gray-200 bg-white overflow-hidden">
|
|
673
|
+
<button
|
|
674
|
+
(click)="toggle(i)"
|
|
675
|
+
class="flex w-full items-center justify-between p-6 text-left hover:bg-gray-50">
|
|
676
|
+
<span class="font-semibold text-gray-900">{{ item.question }}</span>
|
|
677
|
+
<ng-icon
|
|
678
|
+
name="heroChevronDown"
|
|
679
|
+
size="20"
|
|
680
|
+
class="text-gray-500 transition-transform"
|
|
681
|
+
[class.rotate-180]="openIndex() === i">
|
|
682
|
+
</ng-icon>
|
|
683
|
+
</button>
|
|
684
|
+
@if (openIndex() === i) {
|
|
685
|
+
<div class="border-t border-gray-200 px-6 py-4 bg-gray-50">
|
|
686
|
+
<p class="text-gray-600">{{ item.answer }}</p>
|
|
687
|
+
</div>
|
|
688
|
+
}
|
|
689
|
+
</div>
|
|
690
|
+
}
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
</section>
|
|
694
|
+
\`,
|
|
695
|
+
})
|
|
696
|
+
export class FaqSectionComponent {
|
|
697
|
+
@Input() sectionId = 'faq';
|
|
698
|
+
@Input() badge = 'FAQ';
|
|
699
|
+
@Input() title = 'Frequently asked questions';
|
|
700
|
+
@Input() subtitle = 'Everything you need to know';
|
|
701
|
+
@Input() items: FaqItem[] = [
|
|
702
|
+
{ question: 'How do I get started?', answer: 'Simply sign up for a free account and you can start using the platform immediately. No credit card required.' },
|
|
703
|
+
{ question: 'Can I change my plan later?', answer: 'Yes, you can upgrade or downgrade your plan at any time. Changes take effect at the start of your next billing cycle.' },
|
|
704
|
+
{ question: 'Is there a free trial?', answer: 'Yes! All paid plans come with a 14-day free trial. No credit card required to start.' },
|
|
705
|
+
{ question: 'What payment methods do you accept?', answer: 'We accept all major credit cards (Visa, MasterCard, American Express) as well as PayPal and bank transfers for enterprise plans.' },
|
|
706
|
+
{ question: 'Can I cancel anytime?', answer: 'Absolutely. You can cancel your subscription at any time with no penalties or hidden fees.' },
|
|
707
|
+
];
|
|
708
|
+
|
|
709
|
+
openIndex = signal<number | null>(0);
|
|
710
|
+
|
|
711
|
+
toggle(index: number): void {
|
|
712
|
+
this.openIndex.set(this.openIndex() === index ? null : index);
|
|
713
|
+
}
|
|
714
|
+
}`;
|
|
715
|
+
|
|
716
|
+
await fs.writeFile(
|
|
717
|
+
path.join(config.fullPath, 'src/app/shared/components/faq-section/faq-section.component.ts'),
|
|
718
|
+
faq
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
// Newsletter
|
|
722
|
+
const newsletter = `import { Component, Input, signal } from '@angular/core';
|
|
723
|
+
import { FormsModule } from '@angular/forms';
|
|
724
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
725
|
+
import { heroPaperAirplane } from '@ng-icons/heroicons/outline';
|
|
726
|
+
|
|
727
|
+
@Component({
|
|
728
|
+
selector: 'app-newsletter',
|
|
729
|
+
standalone: true,
|
|
730
|
+
imports: [FormsModule, NgIconComponent],
|
|
731
|
+
viewProviders: [provideIcons({ heroPaperAirplane })],
|
|
732
|
+
template: \`
|
|
733
|
+
<section class="py-24 bg-gradient-to-r from-primary-600 to-purple-600">
|
|
734
|
+
<div class="mx-auto max-w-4xl px-4 text-center sm:px-6 lg:px-8">
|
|
735
|
+
<h2 class="text-3xl font-bold text-white sm:text-4xl">{{ title }}</h2>
|
|
736
|
+
<p class="mt-4 text-xl text-primary-100">{{ subtitle }}</p>
|
|
737
|
+
|
|
738
|
+
@if (!submitted()) {
|
|
739
|
+
<form (ngSubmit)="subscribe()" class="mt-8 flex flex-col gap-4 sm:flex-row sm:justify-center">
|
|
740
|
+
<input
|
|
741
|
+
type="email"
|
|
742
|
+
[(ngModel)]="email"
|
|
743
|
+
name="email"
|
|
744
|
+
[placeholder]="placeholder"
|
|
745
|
+
required
|
|
746
|
+
class="w-full rounded-full px-6 py-4 text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-white sm:w-80"
|
|
747
|
+
/>
|
|
748
|
+
<button
|
|
749
|
+
type="submit"
|
|
750
|
+
[disabled]="loading()"
|
|
751
|
+
class="inline-flex items-center justify-center gap-2 rounded-full bg-white px-8 py-4 font-semibold text-primary-600 hover:bg-gray-100 disabled:opacity-50">
|
|
752
|
+
@if (loading()) {
|
|
753
|
+
<span class="h-5 w-5 animate-spin rounded-full border-2 border-primary-600 border-t-transparent"></span>
|
|
754
|
+
} @else {
|
|
755
|
+
<ng-icon name="heroPaperAirplane" size="20"></ng-icon>
|
|
756
|
+
}
|
|
757
|
+
{{ buttonText }}
|
|
758
|
+
</button>
|
|
759
|
+
</form>
|
|
760
|
+
<p class="mt-4 text-sm text-primary-200">{{ disclaimer }}</p>
|
|
761
|
+
} @else {
|
|
762
|
+
<div class="mt-8 rounded-2xl bg-white/10 p-8 backdrop-blur-sm">
|
|
763
|
+
<p class="text-xl font-semibold text-white">{{ successMessage }}</p>
|
|
764
|
+
</div>
|
|
765
|
+
}
|
|
766
|
+
</div>
|
|
767
|
+
</section>
|
|
768
|
+
\`,
|
|
769
|
+
})
|
|
770
|
+
export class NewsletterComponent {
|
|
771
|
+
@Input() title = 'Stay in the loop';
|
|
772
|
+
@Input() subtitle = 'Get the latest updates, tips, and special offers delivered to your inbox.';
|
|
773
|
+
@Input() placeholder = 'Enter your email';
|
|
774
|
+
@Input() buttonText = 'Subscribe';
|
|
775
|
+
@Input() disclaimer = 'No spam, unsubscribe at any time.';
|
|
776
|
+
@Input() successMessage = 'Thanks for subscribing! Check your inbox for confirmation.';
|
|
777
|
+
|
|
778
|
+
email = '';
|
|
779
|
+
loading = signal(false);
|
|
780
|
+
submitted = signal(false);
|
|
781
|
+
|
|
782
|
+
subscribe(): void {
|
|
783
|
+
if (!this.email) return;
|
|
784
|
+
|
|
785
|
+
this.loading.set(true);
|
|
786
|
+
// Simulate API call
|
|
787
|
+
setTimeout(() => {
|
|
788
|
+
this.loading.set(false);
|
|
789
|
+
this.submitted.set(true);
|
|
790
|
+
}, 1000);
|
|
791
|
+
}
|
|
792
|
+
}`;
|
|
793
|
+
|
|
794
|
+
await fs.writeFile(
|
|
795
|
+
path.join(config.fullPath, 'src/app/shared/components/newsletter/newsletter.component.ts'),
|
|
796
|
+
newsletter
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
// CTA Section
|
|
800
|
+
const cta = `import { Component, Input } from '@angular/core';
|
|
801
|
+
import { RouterModule } from '@angular/router';
|
|
802
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
803
|
+
import { heroArrowRight } from '@ng-icons/heroicons/outline';
|
|
804
|
+
|
|
805
|
+
@Component({
|
|
806
|
+
selector: 'app-cta-section',
|
|
807
|
+
standalone: true,
|
|
808
|
+
imports: [RouterModule, NgIconComponent],
|
|
809
|
+
viewProviders: [provideIcons({ heroArrowRight })],
|
|
810
|
+
template: \`
|
|
811
|
+
<section class="py-24 bg-gray-900">
|
|
812
|
+
<div class="mx-auto max-w-4xl px-4 text-center sm:px-6 lg:px-8">
|
|
813
|
+
<h2 class="text-3xl font-bold text-white sm:text-4xl">{{ title }}</h2>
|
|
814
|
+
<p class="mt-4 text-xl text-gray-400">{{ subtitle }}</p>
|
|
815
|
+
|
|
816
|
+
<div class="mt-10 flex flex-wrap justify-center gap-4">
|
|
817
|
+
<a
|
|
818
|
+
[routerLink]="primaryCta.link"
|
|
819
|
+
class="group inline-flex items-center gap-2 rounded-full bg-white px-8 py-4 font-semibold text-gray-900 hover:bg-gray-100 transition-all">
|
|
820
|
+
{{ primaryCta.text }}
|
|
821
|
+
<ng-icon name="heroArrowRight" size="20" class="transition-transform group-hover:translate-x-1"></ng-icon>
|
|
822
|
+
</a>
|
|
823
|
+
@if (secondaryCta) {
|
|
824
|
+
<a
|
|
825
|
+
[routerLink]="secondaryCta.link"
|
|
826
|
+
class="inline-flex items-center gap-2 rounded-full border-2 border-white/30 px-8 py-4 font-semibold text-white hover:bg-white/10 transition-all">
|
|
827
|
+
{{ secondaryCta.text }}
|
|
828
|
+
</a>
|
|
829
|
+
}
|
|
830
|
+
</div>
|
|
831
|
+
</div>
|
|
832
|
+
</section>
|
|
833
|
+
\`,
|
|
834
|
+
})
|
|
835
|
+
export class CtaSectionComponent {
|
|
836
|
+
@Input() title = 'Ready to get started?';
|
|
837
|
+
@Input() subtitle = 'Join thousands of satisfied customers using our platform.';
|
|
838
|
+
@Input() primaryCta = { text: 'Start Free Trial', link: '/auth/register' };
|
|
839
|
+
@Input() secondaryCta?: { text: string; link: string } = { text: 'Contact Sales', link: '/contact' };
|
|
840
|
+
}`;
|
|
841
|
+
|
|
842
|
+
await fs.writeFile(
|
|
843
|
+
path.join(config.fullPath, 'src/app/shared/components/cta-section/cta-section.component.ts'),
|
|
844
|
+
cta
|
|
845
|
+
);
|
|
846
|
+
},
|
|
847
|
+
|
|
848
|
+
async createLandingPage(config) {
|
|
849
|
+
const landingPage = `import { Component } from '@angular/core';
|
|
850
|
+
import { HeroComponent } from '@shared/components/hero/hero.component';
|
|
851
|
+
import { FeaturesSectionComponent } from '@shared/components/features-section/features-section.component';
|
|
852
|
+
import { HowItWorksComponent } from '@shared/components/how-it-works/how-it-works.component';
|
|
853
|
+
import { TestimonialsSectionComponent } from '@shared/components/testimonials-section/testimonials-section.component';
|
|
854
|
+
import { PricingSectionComponent } from '@shared/components/pricing-section/pricing-section.component';
|
|
855
|
+
import { FaqSectionComponent } from '@shared/components/faq-section/faq-section.component';
|
|
856
|
+
import { NewsletterComponent } from '@shared/components/newsletter/newsletter.component';
|
|
857
|
+
import { CtaSectionComponent } from '@shared/components/cta-section/cta-section.component';
|
|
858
|
+
|
|
859
|
+
@Component({
|
|
860
|
+
selector: 'app-landing',
|
|
861
|
+
standalone: true,
|
|
862
|
+
imports: [
|
|
863
|
+
HeroComponent,
|
|
864
|
+
FeaturesSectionComponent,
|
|
865
|
+
HowItWorksComponent,
|
|
866
|
+
TestimonialsSectionComponent,
|
|
867
|
+
PricingSectionComponent,
|
|
868
|
+
FaqSectionComponent,
|
|
869
|
+
NewsletterComponent,
|
|
870
|
+
CtaSectionComponent,
|
|
871
|
+
],
|
|
872
|
+
template: \`
|
|
873
|
+
<app-hero></app-hero>
|
|
874
|
+
<app-features-section></app-features-section>
|
|
875
|
+
<app-how-it-works></app-how-it-works>
|
|
876
|
+
<app-testimonials-section></app-testimonials-section>
|
|
877
|
+
<app-pricing-section></app-pricing-section>
|
|
878
|
+
<app-faq-section></app-faq-section>
|
|
879
|
+
<app-newsletter></app-newsletter>
|
|
880
|
+
<app-cta-section></app-cta-section>
|
|
881
|
+
\`,
|
|
882
|
+
})
|
|
883
|
+
export class LandingComponent {}`;
|
|
884
|
+
|
|
885
|
+
await fs.writeFile(
|
|
886
|
+
path.join(config.fullPath, 'src/app/features/landing/landing.component.ts'),
|
|
887
|
+
landingPage
|
|
888
|
+
);
|
|
889
|
+
},
|
|
890
|
+
|
|
891
|
+
async updateLayout(config) {
|
|
892
|
+
// Update header to be sticky and transparent on landing
|
|
893
|
+
const header = `import { Component, inject, signal, HostListener } from '@angular/core';
|
|
894
|
+
import { RouterModule } from '@angular/router';
|
|
895
|
+
import { TranslateModule } from '@ngx-translate/core';
|
|
896
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
897
|
+
import { heroBars3, heroXMark, heroGlobeAlt } from '@ng-icons/heroicons/outline';
|
|
898
|
+
import { TranslationService } from '@core/i18n/translation.service';
|
|
899
|
+
|
|
900
|
+
@Component({
|
|
901
|
+
selector: 'app-header',
|
|
902
|
+
standalone: true,
|
|
903
|
+
imports: [RouterModule, TranslateModule, NgIconComponent],
|
|
904
|
+
viewProviders: [provideIcons({ heroBars3, heroXMark, heroGlobeAlt })],
|
|
905
|
+
template: \`
|
|
906
|
+
<header
|
|
907
|
+
class="fixed top-0 left-0 right-0 z-50 transition-all duration-300"
|
|
908
|
+
[class.bg-white]="scrolled()"
|
|
909
|
+
[class.shadow-md]="scrolled()"
|
|
910
|
+
[class.bg-transparent]="!scrolled()">
|
|
911
|
+
<nav class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
912
|
+
<div class="flex h-16 items-center justify-between lg:h-20">
|
|
913
|
+
<!-- Logo -->
|
|
914
|
+
<a routerLink="/" class="text-xl font-bold" [class.text-gray-900]="scrolled()" [class.text-white]="!scrolled()">
|
|
915
|
+
{{ 'app.name' | translate }}
|
|
916
|
+
</a>
|
|
917
|
+
|
|
918
|
+
<!-- Desktop Navigation -->
|
|
919
|
+
<div class="hidden lg:flex lg:items-center lg:gap-8">
|
|
920
|
+
@for (link of navLinks; track link.path) {
|
|
921
|
+
<a
|
|
922
|
+
[routerLink]="link.path"
|
|
923
|
+
[fragment]="link.fragment"
|
|
924
|
+
class="text-sm font-medium transition-colors"
|
|
925
|
+
[class.text-gray-700]="scrolled()"
|
|
926
|
+
[class.hover:text-primary-600]="scrolled()"
|
|
927
|
+
[class.text-white/80]="!scrolled()"
|
|
928
|
+
[class.hover:text-white]="!scrolled()">
|
|
929
|
+
{{ link.label | translate }}
|
|
930
|
+
</a>
|
|
931
|
+
}
|
|
932
|
+
</div>
|
|
933
|
+
|
|
934
|
+
<!-- Right Actions -->
|
|
935
|
+
<div class="flex items-center gap-4">
|
|
936
|
+
<!-- Language Switcher -->
|
|
937
|
+
<button
|
|
938
|
+
(click)="toggleLanguage()"
|
|
939
|
+
class="flex items-center gap-1 rounded-lg p-2 transition-colors"
|
|
940
|
+
[class.text-gray-600]="scrolled()"
|
|
941
|
+
[class.hover:bg-gray-100]="scrolled()"
|
|
942
|
+
[class.text-white/80]="!scrolled()"
|
|
943
|
+
[class.hover:bg-white/10]="!scrolled()">
|
|
944
|
+
<ng-icon name="heroGlobeAlt" size="20"></ng-icon>
|
|
945
|
+
<span class="text-sm font-medium">{{ translationService.currentLang() === 'en' ? 'AR' : 'EN' }}</span>
|
|
946
|
+
</button>
|
|
947
|
+
|
|
948
|
+
<!-- Auth Buttons -->
|
|
949
|
+
<div class="hidden lg:flex lg:items-center lg:gap-3">
|
|
950
|
+
<a
|
|
951
|
+
routerLink="/auth/login"
|
|
952
|
+
class="text-sm font-medium transition-colors"
|
|
953
|
+
[class.text-gray-700]="scrolled()"
|
|
954
|
+
[class.hover:text-primary-600]="scrolled()"
|
|
955
|
+
[class.text-white/80]="!scrolled()"
|
|
956
|
+
[class.hover:text-white]="!scrolled()">
|
|
957
|
+
{{ 'nav.login' | translate }}
|
|
958
|
+
</a>
|
|
959
|
+
<a
|
|
960
|
+
routerLink="/auth/register"
|
|
961
|
+
class="rounded-full px-4 py-2 text-sm font-semibold transition-all"
|
|
962
|
+
[class.bg-primary-500]="scrolled()"
|
|
963
|
+
[class.text-white]="scrolled()"
|
|
964
|
+
[class.hover:bg-primary-600]="scrolled()"
|
|
965
|
+
[class.bg-white]="!scrolled()"
|
|
966
|
+
[class.text-gray-900]="!scrolled()"
|
|
967
|
+
[class.hover:bg-gray-100]="!scrolled()">
|
|
968
|
+
{{ 'nav.getStarted' | translate }}
|
|
969
|
+
</a>
|
|
970
|
+
</div>
|
|
971
|
+
|
|
972
|
+
<!-- Mobile Menu Button -->
|
|
973
|
+
<button
|
|
974
|
+
(click)="mobileMenuOpen.set(!mobileMenuOpen())"
|
|
975
|
+
class="lg:hidden p-2 rounded-lg"
|
|
976
|
+
[class.text-gray-600]="scrolled()"
|
|
977
|
+
[class.text-white]="!scrolled()">
|
|
978
|
+
<ng-icon [name]="mobileMenuOpen() ? 'heroXMark' : 'heroBars3'" size="24"></ng-icon>
|
|
979
|
+
</button>
|
|
980
|
+
</div>
|
|
981
|
+
</div>
|
|
982
|
+
|
|
983
|
+
<!-- Mobile Menu -->
|
|
984
|
+
@if (mobileMenuOpen()) {
|
|
985
|
+
<div class="lg:hidden border-t border-gray-200 bg-white py-4">
|
|
986
|
+
@for (link of navLinks; track link.path) {
|
|
987
|
+
<a
|
|
988
|
+
[routerLink]="link.path"
|
|
989
|
+
[fragment]="link.fragment"
|
|
990
|
+
(click)="mobileMenuOpen.set(false)"
|
|
991
|
+
class="block px-4 py-2 text-gray-700 hover:bg-gray-50">
|
|
992
|
+
{{ link.label | translate }}
|
|
993
|
+
</a>
|
|
994
|
+
}
|
|
995
|
+
<div class="mt-4 border-t border-gray-200 pt-4 px-4 space-y-2">
|
|
996
|
+
<a routerLink="/auth/login" class="block py-2 text-gray-700">{{ 'nav.login' | translate }}</a>
|
|
997
|
+
<a routerLink="/auth/register" class="block rounded-lg bg-primary-500 py-2 text-center text-white">
|
|
998
|
+
{{ 'nav.getStarted' | translate }}
|
|
999
|
+
</a>
|
|
1000
|
+
</div>
|
|
1001
|
+
</div>
|
|
1002
|
+
}
|
|
1003
|
+
</nav>
|
|
1004
|
+
</header>
|
|
1005
|
+
\`,
|
|
1006
|
+
})
|
|
1007
|
+
export class HeaderComponent {
|
|
1008
|
+
translationService = inject(TranslationService);
|
|
1009
|
+
|
|
1010
|
+
mobileMenuOpen = signal(false);
|
|
1011
|
+
scrolled = signal(false);
|
|
1012
|
+
|
|
1013
|
+
navLinks = [
|
|
1014
|
+
{ path: '/', fragment: 'features', label: 'nav.features' },
|
|
1015
|
+
{ path: '/', fragment: 'how-it-works', label: 'nav.howItWorks' },
|
|
1016
|
+
{ path: '/', fragment: 'testimonials', label: 'nav.testimonials' },
|
|
1017
|
+
{ path: '/', fragment: 'pricing', label: 'nav.pricing' },
|
|
1018
|
+
{ path: '/', fragment: 'faq', label: 'nav.faq' },
|
|
1019
|
+
];
|
|
1020
|
+
|
|
1021
|
+
@HostListener('window:scroll')
|
|
1022
|
+
onScroll(): void {
|
|
1023
|
+
this.scrolled.set(window.scrollY > 50);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
toggleLanguage(): void {
|
|
1027
|
+
const newLang = this.translationService.currentLang() === 'en' ? 'ar' : 'en';
|
|
1028
|
+
this.translationService.setLanguage(newLang);
|
|
1029
|
+
}
|
|
1030
|
+
}`;
|
|
1031
|
+
|
|
1032
|
+
await fs.writeFile(
|
|
1033
|
+
path.join(config.fullPath, 'src/app/layout/header/header.component.ts'),
|
|
1034
|
+
header
|
|
1035
|
+
);
|
|
1036
|
+
},
|
|
1037
|
+
|
|
1038
|
+
async createRouting(config) {
|
|
1039
|
+
const routes = `import { Routes } from '@angular/router';
|
|
1040
|
+
|
|
1041
|
+
export const routes: Routes = [
|
|
1042
|
+
// Landing page as home
|
|
1043
|
+
{
|
|
1044
|
+
path: '',
|
|
1045
|
+
loadComponent: () => import('./features/landing/landing.component').then(c => c.LandingComponent)
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
path: 'about',
|
|
1049
|
+
loadComponent: () => import('./features/about/about.component').then(c => c.AboutComponent)
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
path: 'contact',
|
|
1053
|
+
loadComponent: () => import('./features/contact/contact.component').then(c => c.ContactComponent)
|
|
1054
|
+
},
|
|
1055
|
+
|
|
1056
|
+
// Auth routes
|
|
1057
|
+
{
|
|
1058
|
+
path: 'auth',
|
|
1059
|
+
loadComponent: () => import('./layout/auth/auth-layout.component').then(c => c.AuthLayoutComponent),
|
|
1060
|
+
children: [
|
|
1061
|
+
{ path: '', redirectTo: 'login', pathMatch: 'full' },
|
|
1062
|
+
{ path: 'login', loadComponent: () => import('./features/auth/login/login.component').then(c => c.LoginComponent) },
|
|
1063
|
+
{ path: 'register', loadComponent: () => import('./features/auth/register/register.component').then(c => c.RegisterComponent) },
|
|
1064
|
+
{ path: 'forgot-password', loadComponent: () => import('./features/auth/forgot-password/forgot-password.component').then(c => c.ForgotPasswordComponent) }
|
|
1065
|
+
]
|
|
1066
|
+
},
|
|
1067
|
+
|
|
1068
|
+
{ path: '**', redirectTo: '' }
|
|
1069
|
+
];`;
|
|
1070
|
+
|
|
1071
|
+
await fs.writeFile(path.join(config.fullPath, 'src/app/app.routes.ts'), routes);
|
|
1072
|
+
},
|
|
1073
|
+
|
|
1074
|
+
async updateI18n(config) {
|
|
1075
|
+
const enPath = path.join(config.fullPath, 'public/assets/i18n/en.json');
|
|
1076
|
+
const arPath = path.join(config.fullPath, 'public/assets/i18n/ar.json');
|
|
1077
|
+
|
|
1078
|
+
let en = {};
|
|
1079
|
+
let ar = {};
|
|
1080
|
+
|
|
1081
|
+
try {
|
|
1082
|
+
en = JSON.parse(await fs.readFile(enPath, 'utf-8'));
|
|
1083
|
+
ar = JSON.parse(await fs.readFile(arPath, 'utf-8'));
|
|
1084
|
+
} catch {
|
|
1085
|
+
// Files don't exist yet
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const landingEn = {
|
|
1089
|
+
nav: {
|
|
1090
|
+
features: 'Features',
|
|
1091
|
+
howItWorks: 'How It Works',
|
|
1092
|
+
testimonials: 'Testimonials',
|
|
1093
|
+
pricing: 'Pricing',
|
|
1094
|
+
faq: 'FAQ',
|
|
1095
|
+
login: 'Log In',
|
|
1096
|
+
getStarted: 'Get Started',
|
|
1097
|
+
},
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
const landingAr = {
|
|
1101
|
+
nav: {
|
|
1102
|
+
features: 'الميزات',
|
|
1103
|
+
howItWorks: 'كيف يعمل',
|
|
1104
|
+
testimonials: 'الشهادات',
|
|
1105
|
+
pricing: 'الأسعار',
|
|
1106
|
+
faq: 'الأسئلة الشائعة',
|
|
1107
|
+
login: 'تسجيل الدخول',
|
|
1108
|
+
getStarted: 'ابدأ الآن',
|
|
1109
|
+
},
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
await fs.writeFile(enPath, JSON.stringify({ ...en, ...landingEn }, null, 2));
|
|
1113
|
+
await fs.writeFile(arPath, JSON.stringify({ ...ar, ...landingAr }, null, 2));
|
|
1114
|
+
},
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
module.exports = landing;
|