@digilogiclabs/create-saas-app 2.5.0 → 2.6.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.
Files changed (26) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/cli/commands/create.d.ts +4 -0
  4. package/dist/cli/commands/create.d.ts.map +1 -1
  5. package/dist/cli/commands/create.js +2 -0
  6. package/dist/cli/commands/create.js.map +1 -1
  7. package/dist/cli/index.d.ts.map +1 -1
  8. package/dist/cli/index.js +2 -0
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/generators/template-generator.d.ts +2 -0
  11. package/dist/generators/template-generator.d.ts.map +1 -1
  12. package/dist/generators/template-generator.js +32 -0
  13. package/dist/generators/template-generator.js.map +1 -1
  14. package/dist/templates/shared/design/web/src/config/design.config.ts +51 -0
  15. package/dist/templates/shared/landing/web/src/components/LandingPage.tsx +97 -0
  16. package/dist/templates/shared/landing/web/src/components/PricingSection.tsx +80 -0
  17. package/dist/templates/shared/quality/web/lighthouserc.js +41 -0
  18. package/dist/templates/shared/quality/web/src/__tests__/accessibility.test.tsx +140 -0
  19. package/dist/templates/shared/security/web/lib/api-security.ts +76 -243
  20. package/package.json +2 -2
  21. package/src/templates/shared/design/web/src/config/design.config.ts +51 -0
  22. package/src/templates/shared/landing/web/src/components/LandingPage.tsx +97 -0
  23. package/src/templates/shared/landing/web/src/components/PricingSection.tsx +80 -0
  24. package/src/templates/shared/quality/web/lighthouserc.js +41 -0
  25. package/src/templates/shared/quality/web/src/__tests__/accessibility.test.tsx +140 -0
  26. package/src/templates/shared/security/web/lib/api-security.ts +76 -243
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Accessibility Test Suite
3
+ *
4
+ * Uses axe-core to automatically scan pages for WCAG 2.2 AA violations.
5
+ * Run: npm test -- accessibility
6
+ *
7
+ * Setup: npm i -D @axe-core/react axe-core vitest
8
+ *
9
+ * Generated by @digilogiclabs/create-saas-app
10
+ */
11
+ import { describe, it, expect } from 'vitest';
12
+ import { render } from '@testing-library/react';
13
+
14
+ // Dynamically import axe-core to avoid build errors if not installed
15
+ async function checkAccessibility(container: HTMLElement) {
16
+ try {
17
+ const axe = await import('axe-core');
18
+ const results = await axe.default.run(container);
19
+ return results;
20
+ } catch {
21
+ // axe-core not installed — skip gracefully
22
+ return null;
23
+ }
24
+ }
25
+
26
+ describe('Accessibility', () => {
27
+ it('should have no critical accessibility violations on a basic page', async () => {
28
+ const { container } = render(
29
+ <main>
30
+ <h1>Test Page</h1>
31
+ <nav aria-label="Main navigation">
32
+ <a href="/">Home</a>
33
+ <a href="/about">About</a>
34
+ </nav>
35
+ <section aria-label="Content">
36
+ <p>Test content</p>
37
+ <button type="button">Action</button>
38
+ </section>
39
+ </main>
40
+ );
41
+
42
+ const results = await checkAccessibility(container);
43
+
44
+ if (results) {
45
+ // Filter to only critical and serious violations
46
+ const critical = results.violations.filter(
47
+ (v) => v.impact === 'critical' || v.impact === 'serious'
48
+ );
49
+
50
+ if (critical.length > 0) {
51
+ const summary = critical
52
+ .map((v) => `[${v.impact}] ${v.id}: ${v.description}`)
53
+ .join('\n');
54
+ expect(critical, `Accessibility violations found:\n${summary}`).toHaveLength(0);
55
+ }
56
+ } else {
57
+ // axe-core not installed — test passes with a warning
58
+ console.warn(
59
+ 'axe-core not installed. Run: npm i -D axe-core\n' +
60
+ 'Accessibility testing will be enabled once installed.'
61
+ );
62
+ }
63
+ });
64
+
65
+ it('should have proper heading hierarchy', () => {
66
+ const { container } = render(
67
+ <main>
68
+ <h1>Page Title</h1>
69
+ <section>
70
+ <h2>Section</h2>
71
+ <h3>Subsection</h3>
72
+ </section>
73
+ </main>
74
+ );
75
+
76
+ const headings = container.querySelectorAll('h1, h2, h3, h4, h5, h6');
77
+ const levels = Array.from(headings).map((h) =>
78
+ parseInt(h.tagName.replace('H', ''))
79
+ );
80
+
81
+ // Verify no heading level is skipped (e.g., h1 -> h3 without h2)
82
+ for (let i = 1; i < levels.length; i++) {
83
+ const jump = levels[i]! - levels[i - 1]!;
84
+ expect(
85
+ jump,
86
+ `Heading level jumped from h${levels[i - 1]} to h${levels[i]}`
87
+ ).toBeLessThanOrEqual(1);
88
+ }
89
+ });
90
+
91
+ it('should have accessible form labels', () => {
92
+ const { container } = render(
93
+ <form>
94
+ <label htmlFor="email">Email</label>
95
+ <input id="email" type="email" aria-required="true" />
96
+ <label htmlFor="password">Password</label>
97
+ <input id="password" type="password" aria-required="true" />
98
+ <button type="submit">Submit</button>
99
+ </form>
100
+ );
101
+
102
+ const inputs = container.querySelectorAll('input');
103
+ inputs.forEach((input) => {
104
+ const id = input.getAttribute('id');
105
+ if (id) {
106
+ const label = container.querySelector(`label[for="${id}"]`);
107
+ expect(
108
+ label,
109
+ `Input #${id} is missing a label`
110
+ ).not.toBeNull();
111
+ }
112
+ });
113
+ });
114
+
115
+ it('should have sufficient touch targets (44px minimum)', () => {
116
+ const { container } = render(
117
+ <div>
118
+ <button style={{ minHeight: '44px', minWidth: '44px' }}>
119
+ Click me
120
+ </button>
121
+ <a href="/" style={{ display: 'inline-block', minHeight: '44px', padding: '12px' }}>
122
+ Link
123
+ </a>
124
+ </div>
125
+ );
126
+
127
+ const buttons = container.querySelectorAll('button');
128
+ buttons.forEach((button) => {
129
+ const style = window.getComputedStyle(button);
130
+ const height = parseFloat(style.minHeight) || button.offsetHeight;
131
+ // Only check if explicit minHeight is set
132
+ if (style.minHeight && style.minHeight !== 'auto') {
133
+ expect(
134
+ height,
135
+ `Button "${button.textContent}" has touch target ${height}px (minimum 44px)`
136
+ ).toBeGreaterThanOrEqual(44);
137
+ }
138
+ });
139
+ });
140
+ });
@@ -1,17 +1,30 @@
1
1
  /**
2
2
  * Shared API security utilities.
3
3
  *
4
- * Built on platform-core/auth building blocks no local reimplementations.
5
- * Uses constantTimeEqual, classifyError, rate limiting, and audit from the package.
4
+ * Built on platform-core's createSecureHandlerFactoryapps configure auth,
5
+ * rate limiting, and error handling once, then use composable wrappers per route.
6
6
  *
7
- * Two usage patterns:
8
- * 1. Wrappers: withPublicApi / withAuthenticatedApi / withAdminApi (recommended)
9
- * 2. Primitives: enforceRateLimit, isAdminRequest, errorResponse (manual composition)
7
+ * Usage:
8
+ * // Authenticated route
9
+ * export const POST = withAuthenticatedApi({
10
+ * rateLimit: 'authMutation',
11
+ * validate: CreateItemSchema,
12
+ * }, async (request, { session, validated }) => {
13
+ * return Response.json({ created: true })
14
+ * })
15
+ *
16
+ * // Public route (manual composition)
17
+ * export async function GET(request: NextRequest) {
18
+ * const rl = await enforceRateLimit(request, 'list', AppRateLimits.publicRead)
19
+ * if (rl) return rl
20
+ * return NextResponse.json({ items: [] })
21
+ * }
10
22
  */
11
23
  import 'server-only';
12
24
  import { NextRequest, NextResponse } from 'next/server';
13
- import { randomUUID } from 'crypto';
14
25
  import {
26
+ // Factory for composable wrappers
27
+ createSecureHandlerFactory,
15
28
  // Security primitives
16
29
  constantTimeEqual,
17
30
  // Rate limiting
@@ -33,18 +46,12 @@ export { escapeHtml } from '@digilogiclabs/platform-core/auth';
33
46
  export { errorResponse, zodErrorResponse };
34
47
 
35
48
  // ---------------------------------------------------------------------------
36
- // Request ID / Correlation ID
49
+ // Rate limiting Redis-backed with in-memory fallback
37
50
  // ---------------------------------------------------------------------------
38
51
 
39
- /** Generate or extract a request ID for correlation. */
40
- export function getRequestId(request: NextRequest): string {
41
- return request.headers.get('x-request-id') || randomUUID();
42
- }
43
-
44
52
  /**
45
- * Rate-limit a request using Redis-backed store (if REDIS_URL is set)
46
- * or in-memory fallback. Wraps platform-core's enforceRateLimit to
47
- * automatically inject the store.
53
+ * Rate-limit a request. Wraps platform-core's enforceRateLimit to
54
+ * automatically inject the Redis store.
48
55
  */
49
56
  export async function enforceRateLimit(
50
57
  request: { headers: { get(name: string): string | null } },
@@ -65,254 +72,80 @@ export async function enforceRateLimit(
65
72
  }
66
73
 
67
74
  // ---------------------------------------------------------------------------
68
- // Admin / Cron auth helpers
75
+ // Rate limit presets tune for your app's traffic patterns
69
76
  // ---------------------------------------------------------------------------
70
77
 
71
- /** Extract bearer token from Authorization header. */
72
- function extractBearerToken(request: NextRequest): string | null {
73
- const header = request.headers.get('authorization');
74
- if (!header?.startsWith('Bearer ')) return null;
75
- return header.slice(7);
76
- }
77
-
78
- /** Check if request has a valid admin bearer token (ADMIN_SECRET). Timing-safe. */
79
- export function isAdminRequest(request: NextRequest): boolean {
80
- const secret = config.adminSecret;
81
- if (!secret) return false;
82
- const token = extractBearerToken(request);
83
- if (!token) return false;
84
- return constantTimeEqual(token, secret);
85
- }
86
-
87
- /** Check if request has a valid cron bearer token (CRON_SECRET). Timing-safe. */
88
- export function isCronRequest(request: NextRequest): boolean {
89
- const secret = config.cronSecret;
90
- if (!secret) return false;
91
- const token = extractBearerToken(request);
92
- if (!token) return false;
93
- return constantTimeEqual(token, secret);
94
- }
95
-
96
- // ---------------------------------------------------------------------------
97
- // Rate limiting presets — tune for your app's traffic patterns
98
- // ---------------------------------------------------------------------------
99
-
100
- /** App-specific rate limit presets. Extend or override as needed. */
101
78
  export const AppRateLimits = {
102
- /** Public read endpoints */
103
79
  publicRead: { limit: 60, windowSeconds: 60 } satisfies RateLimitRule,
104
- /** Authenticated mutations */
105
80
  authMutation: { limit: 30, windowSeconds: 60 } satisfies RateLimitRule,
106
- /** Admin endpoints */
107
81
  admin: CommonRateLimits.adminAction,
108
- /** Beta code validation */
109
82
  betaValidation: CommonRateLimits.betaValidation,
110
- /** Webhook endpoints (generous — Stripe retries) */
111
83
  webhook: { limit: 100, windowSeconds: 60 } satisfies RateLimitRule,
112
84
  } as const;
113
85
 
86
+ type AppRateLimitPreset = keyof typeof AppRateLimits;
87
+
114
88
  // ---------------------------------------------------------------------------
115
- // API Wrapper Types
89
+ // Rate limit enforcer for the factory
116
90
  // ---------------------------------------------------------------------------
117
91
 
118
- interface ApiWrapperConfig {
119
- /** Rate limit rule (defaults to preset based on wrapper type) */
120
- rateLimit?: RateLimitRule;
121
- /** Operation name for rate limiting and logging */
122
- operation?: string;
123
- }
124
-
125
- interface AuthenticatedApiContext {
126
- /** The authenticated session */
127
- session: { user: { id: string; email: string; roles?: string[] } };
128
- /** Request correlation ID */
129
- requestId: string;
130
- }
131
-
132
- interface PublicApiContext {
133
- /** Request correlation ID */
134
- requestId: string;
92
+ async function enforcePreset(
93
+ request: Request,
94
+ operation: string,
95
+ preset: AppRateLimitPreset,
96
+ userId?: string,
97
+ ): Promise<Response | null> {
98
+ const rule = AppRateLimits[preset];
99
+ return enforceRateLimit(request, operation, rule, userId ? { userId } : undefined);
135
100
  }
136
101
 
137
- type PublicApiHandler = (request: NextRequest, context: PublicApiContext) => Promise<NextResponse>;
138
-
139
- type AuthenticatedApiHandler = (
140
- request: NextRequest,
141
- context: AuthenticatedApiContext
142
- ) => Promise<NextResponse>;
143
-
144
- type AdminApiHandler = (request: NextRequest, context: PublicApiContext) => Promise<NextResponse>;
145
-
146
- type CronApiHandler = (request: NextRequest, context: PublicApiContext) => Promise<NextResponse>;
147
-
148
102
  // ---------------------------------------------------------------------------
149
- // API Wrappers compose auth, rate limiting, error handling automatically
103
+ // Composable API security wrappers (via platform-core factory)
150
104
  // ---------------------------------------------------------------------------
151
105
 
152
- /**
153
- * Wrap a public API route with rate limiting and error handling.
154
- * No authentication required.
155
- *
156
- * @example
157
- * export const GET = withPublicApi({ operation: 'list-items' }, async (req, ctx) => {
158
- * return NextResponse.json({ items: [] });
159
- * });
160
- */
161
- export function withPublicApi(
162
- handlerConfig: ApiWrapperConfig,
163
- handler: PublicApiHandler
164
- ): (request: NextRequest) => Promise<NextResponse> {
165
- return async (request: NextRequest) => {
166
- const requestId = getRequestId(request);
167
- const operation = handlerConfig.operation || 'public';
168
-
169
- try {
170
- // Rate limiting
171
- const rateLimited = await enforceRateLimit(
172
- request,
173
- operation,
174
- handlerConfig.rateLimit || AppRateLimits.publicRead
175
- );
176
- if (rateLimited) return rateLimited as NextResponse;
177
-
178
- const response = await handler(request, { requestId });
179
- response.headers.set('x-request-id', requestId);
180
- return response;
181
- } catch (error) {
182
- const { status, body } = classifyError(error, process.env.NODE_ENV === 'development');
183
- return NextResponse.json({ ...body, requestId }, { status });
184
- }
185
- };
186
- }
187
-
188
- /**
189
- * Wrap an authenticated API route with session validation, rate limiting, and error handling.
190
- * Requires a valid Auth.js session.
191
- *
192
- * @example
193
- * export const POST = withAuthenticatedApi({ operation: 'create-item' }, async (req, ctx) => {
194
- * const { session, requestId } = ctx;
195
- * return NextResponse.json({ userId: session.user.id });
196
- * });
197
- */
198
- export function withAuthenticatedApi(
199
- handlerConfig: ApiWrapperConfig,
200
- handler: AuthenticatedApiHandler
201
- ): (request: NextRequest) => Promise<NextResponse> {
202
- return async (request: NextRequest) => {
203
- const requestId = getRequestId(request);
204
- const operation = handlerConfig.operation || 'authenticated';
205
-
206
- try {
207
- // Dynamic import to avoid Edge runtime issues
208
- const { auth } = await import('@/auth');
209
- const session = await auth();
210
-
211
- if (!session?.user?.id) {
212
- return NextResponse.json({ error: 'Unauthorized', requestId }, { status: 401 });
213
- }
214
-
215
- // Rate limiting (per-user)
216
- const rateLimited = await enforceRateLimit(
217
- request,
218
- operation,
219
- handlerConfig.rateLimit || AppRateLimits.authMutation,
220
- { userId: session.user.id }
221
- );
222
- if (rateLimited) return rateLimited as NextResponse;
223
-
224
- const response = await handler(request, {
225
- session: session as AuthenticatedApiContext['session'],
226
- requestId,
227
- });
228
- response.headers.set('x-request-id', requestId);
229
- return response;
230
- } catch (error) {
231
- const { status, body } = classifyError(error, process.env.NODE_ENV === 'development');
232
- return NextResponse.json({ ...body, requestId }, { status });
233
- }
234
- };
235
- }
236
-
237
- /**
238
- * Wrap an admin API route with Bearer token auth, rate limiting, and error handling.
239
- * Requires ADMIN_SECRET Bearer token.
240
- *
241
- * @example
242
- * export const POST = withAdminApi({ operation: 'admin-action' }, async (req, ctx) => {
243
- * return NextResponse.json({ success: true });
244
- * });
245
- */
246
- export function withAdminApi(
247
- handlerConfig: ApiWrapperConfig,
248
- handler: AdminApiHandler
249
- ): (request: NextRequest) => Promise<NextResponse> {
250
- return async (request: NextRequest) => {
251
- const requestId = getRequestId(request);
252
- const operation = handlerConfig.operation || 'admin';
253
-
254
- try {
255
- if (!isAdminRequest(request)) {
256
- return NextResponse.json({ error: 'Forbidden', requestId }, { status: 403 });
257
- }
106
+ export const {
107
+ withPublicApi,
108
+ withAuthenticatedApi,
109
+ withAdminApi,
110
+ withLegacyAdminApi,
111
+ createSecureHandler,
112
+ } = createSecureHandlerFactory<
113
+ { user?: { id?: string; email?: string | null; name?: string | null; roles?: string[] } },
114
+ AppRateLimitPreset
115
+ >({
116
+ getSession: async () => {
117
+ const { auth } = await import('@/auth');
118
+ return auth();
119
+ },
120
+ isAdmin: (session) => session?.user?.roles?.includes('admin') ?? false,
121
+ rateLimiter: {
122
+ enforce: enforcePreset,
123
+ publicDefault: 'publicRead',
124
+ authDefault: 'authMutation',
125
+ adminDefault: 'admin',
126
+ },
127
+ adminSecret: config.adminSecret,
128
+ classifyError: (error, isDev) => classifyError(error, isDev),
129
+ });
258
130
 
259
- // Rate limiting
260
- const rateLimited = await enforceRateLimit(
261
- request,
262
- operation,
263
- handlerConfig.rateLimit || AppRateLimits.admin
264
- );
265
- if (rateLimited) return rateLimited as NextResponse;
131
+ // ---------------------------------------------------------------------------
132
+ // Legacy helpers for routes that need manual composition
133
+ // ---------------------------------------------------------------------------
266
134
 
267
- const response = await handler(request, { requestId });
268
- response.headers.set('x-request-id', requestId);
269
- return response;
270
- } catch (error) {
271
- const { status, body } = classifyError(error, process.env.NODE_ENV === 'development');
272
- return NextResponse.json({ ...body, requestId }, { status });
273
- }
274
- };
135
+ /** Check if request has a valid admin bearer token. Timing-safe. */
136
+ export function isAdminRequest(request: NextRequest): boolean {
137
+ const secret = config.adminSecret;
138
+ if (!secret) return false;
139
+ const header = request.headers.get('authorization');
140
+ if (!header?.startsWith('Bearer ')) return false;
141
+ return constantTimeEqual(header.slice(7), secret);
275
142
  }
276
143
 
277
- /**
278
- * Wrap a cron/scheduled API route with CRON_SECRET Bearer token auth,
279
- * falling back to admin session check. Uses generous rate limits since
280
- * cron jobs are server-to-server.
281
- *
282
- * @example
283
- * export const POST = withCronApi({ operation: 'daily-digest' }, async (req, ctx) => {
284
- * // Run scheduled task...
285
- * return NextResponse.json({ processed: 42 });
286
- * });
287
- */
288
- export function withCronApi(
289
- handlerConfig: ApiWrapperConfig,
290
- handler: CronApiHandler
291
- ): (request: NextRequest) => Promise<NextResponse> {
292
- return async (request: NextRequest) => {
293
- const requestId = getRequestId(request);
294
- const operation = handlerConfig.operation || 'cron';
295
-
296
- try {
297
- // Accept CRON_SECRET Bearer token or fall back to ADMIN_SECRET
298
- if (!isCronRequest(request) && !isAdminRequest(request)) {
299
- return NextResponse.json({ error: 'Forbidden', requestId }, { status: 403 });
300
- }
301
-
302
- // Rate limiting (generous — cron jobs are server-to-server)
303
- const rateLimited = await enforceRateLimit(
304
- request,
305
- operation,
306
- handlerConfig.rateLimit || AppRateLimits.webhook
307
- );
308
- if (rateLimited) return rateLimited as NextResponse;
309
-
310
- const response = await handler(request, { requestId });
311
- response.headers.set('x-request-id', requestId);
312
- return response;
313
- } catch (error) {
314
- const { status, body } = classifyError(error, process.env.NODE_ENV === 'development');
315
- return NextResponse.json({ ...body, requestId }, { status });
316
- }
317
- };
144
+ /** Check if request has a valid cron bearer token. Timing-safe. */
145
+ export function isCronRequest(request: NextRequest): boolean {
146
+ const secret = config.cronSecret;
147
+ if (!secret) return false;
148
+ const header = request.headers.get('authorization');
149
+ if (!header?.startsWith('Bearer ')) return false;
150
+ return constantTimeEqual(header.slice(7), secret);
318
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digilogiclabs/create-saas-app",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Create modern SaaS applications with DLL Platform - tier-aware scaffolding with platform-core and app-sdk",
5
5
  "main": "dist/cli/index.js",
6
6
  "bin": {
@@ -69,7 +69,7 @@
69
69
  "ora": "^5.4.1",
70
70
  "semver": "^7.5.4",
71
71
  "validate-npm-package-name": "^5.0.0",
72
- "zod": "^3.22.4"
72
+ "zod": "^4.1.0"
73
73
  },
74
74
  "devDependencies": {
75
75
  "@changesets/cli": "^2.26.2",
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Design System Configuration
3
+ *
4
+ * Central design config for {{projectName}}.
5
+ * Values here drive the CSS custom properties in globals.css.
6
+ * Override tokens per-app — never hardcode colors/spacing in components.
7
+ *
8
+ * Generated by @digilogiclabs/create-saas-app
9
+ */
10
+
11
+ export const designConfig = {
12
+ /** App theme preset */
13
+ theme: "{{theme}}" as const,
14
+
15
+ /** Primary accent color */
16
+ accent: "{{themeColor}}" as const,
17
+
18
+ /** Default color mode */
19
+ defaultMode: "{{defaultTheme}}" as const,
20
+
21
+ /** Content density */
22
+ density: "comfortable" as const,
23
+
24
+ /** Border radius scale */
25
+ radius: "lg" as const,
26
+
27
+ /** Motion preference */
28
+ motion: "{{motion}}" as const,
29
+
30
+ /** Landing page style */
31
+ landingStyle: "{{landingStyle}}" as const,
32
+
33
+ /** Layout archetype */
34
+ layout: "dashboard" as const,
35
+ } as const;
36
+
37
+ /**
38
+ * Color presets mapped to CSS custom property values.
39
+ * Used by ThemeProvider to set --color-accent at runtime.
40
+ */
41
+ export const accentColors = {
42
+ blue: { light: "#3b82f6", dark: "#60a5fa" },
43
+ green: { light: "#10b981", dark: "#34d399" },
44
+ purple: { light: "#8b5cf6", dark: "#a78bfa" },
45
+ orange: { light: "#f97316", dark: "#fb923c" },
46
+ red: { light: "#ef4444", dark: "#f87171" },
47
+ slate: { light: "#475569", dark: "#94a3b8" },
48
+ } as const;
49
+
50
+ export type AccentColor = keyof typeof accentColors;
51
+ export type DesignConfig = typeof designConfig;
@@ -0,0 +1,97 @@
1
+ 'use client'
2
+
3
+ import {
4
+ PageShell,
5
+ PageHeader,
6
+ SectionBlock,
7
+ FeatureGrid,
8
+ CTACluster,
9
+ Hero,
10
+ Button,
11
+ } from '@digilogiclabs/saas-factory-ui'
12
+ import { Zap, Shield, Rocket, BarChart3 } from 'lucide-react'
13
+ import Link from 'next/link'
14
+
15
+ /**
16
+ * LandingPage — Design-system-aligned landing page component.
17
+ *
18
+ * Uses PageShell, FeatureGrid, CTACluster, and SectionBlock from
19
+ * @digilogiclabs/saas-factory-ui for consistent structure.
20
+ *
21
+ * Generated by @digilogiclabs/create-saas-app
22
+ */
23
+ export function LandingPage() {
24
+ return (
25
+ <PageShell maxWidth="xl" padding="none">
26
+ {/* Hero Section */}
27
+ <Hero
28
+ title="Build Your SaaS, Faster"
29
+ subtitle="Everything you need to launch — authentication, payments, and a beautiful UI. All in one platform."
30
+ variant="centered"
31
+ size="lg"
32
+ primaryAction={{
33
+ text: "Get Started",
34
+ onClick: () => (window.location.href = '/signup'),
35
+ }}
36
+ secondaryAction={{
37
+ text: "View Pricing",
38
+ onClick: () => {
39
+ document.getElementById('pricing')?.scrollIntoView({ behavior: 'smooth' })
40
+ },
41
+ variant: "outline",
42
+ }}
43
+ className="py-20 px-4"
44
+ />
45
+
46
+ {/* Features Section */}
47
+ <SectionBlock spacing="lg">
48
+ <FeatureGrid
49
+ title="Why Choose Us"
50
+ description="Built on battle-tested infrastructure with modern design patterns."
51
+ columns={4}
52
+ variant="cards"
53
+ features={[
54
+ {
55
+ icon: <Zap className="w-5 h-5" />,
56
+ title: "Lightning Fast",
57
+ description: "Optimized for Core Web Vitals with server-first rendering and smart caching.",
58
+ },
59
+ {
60
+ icon: <Shield className="w-5 h-5" />,
61
+ title: "Enterprise Security",
62
+ description: "Self-hosted auth via Keycloak, rate limiting, CSRF protection, and audit logging.",
63
+ },
64
+ {
65
+ icon: <Rocket className="w-5 h-5" />,
66
+ title: "Ship in Days",
67
+ description: "Pre-built auth flows, payment integration, and a complete component library.",
68
+ },
69
+ {
70
+ icon: <BarChart3 className="w-5 h-5" />,
71
+ title: "Full Observability",
72
+ description: "Health checks, structured logging, metrics, and distributed tracing out of the box.",
73
+ },
74
+ ]}
75
+ />
76
+ </SectionBlock>
77
+
78
+ {/* CTA Section */}
79
+ <SectionBlock spacing="lg">
80
+ <div className="text-center py-16 px-4 rounded-2xl bg-gradient-to-br from-primary/5 to-accent/5">
81
+ <h2 className="text-2xl sm:text-3xl font-bold mb-4">
82
+ Ready to launch?
83
+ </h2>
84
+ <p className="text-muted-foreground mb-8 max-w-lg mx-auto">
85
+ Start building today with a complete SaaS foundation. No vendor lock-in.
86
+ </p>
87
+ <CTACluster
88
+ align="center"
89
+ size="lg"
90
+ primary={{ text: "Start Free Trial", href: "/signup" }}
91
+ secondary={{ text: "View Documentation", href: "/docs", variant: "outline" }}
92
+ />
93
+ </div>
94
+ </SectionBlock>
95
+ </PageShell>
96
+ )
97
+ }