popeye-cli 1.4.6 → 1.5.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 (140) hide show
  1. package/README.md +222 -63
  2. package/dist/adapters/gemini.d.ts +1 -0
  3. package/dist/adapters/gemini.d.ts.map +1 -1
  4. package/dist/adapters/gemini.js +9 -4
  5. package/dist/adapters/gemini.js.map +1 -1
  6. package/dist/adapters/grok.d.ts +1 -0
  7. package/dist/adapters/grok.d.ts.map +1 -1
  8. package/dist/adapters/grok.js +9 -4
  9. package/dist/adapters/grok.js.map +1 -1
  10. package/dist/adapters/openai.d.ts +1 -1
  11. package/dist/adapters/openai.d.ts.map +1 -1
  12. package/dist/adapters/openai.js +35 -9
  13. package/dist/adapters/openai.js.map +1 -1
  14. package/dist/cli/interactive.d.ts.map +1 -1
  15. package/dist/cli/interactive.js +42 -0
  16. package/dist/cli/interactive.js.map +1 -1
  17. package/dist/generators/all.d.ts +4 -1
  18. package/dist/generators/all.d.ts.map +1 -1
  19. package/dist/generators/all.js +2 -1
  20. package/dist/generators/all.js.map +1 -1
  21. package/dist/generators/doc-parser.d.ts +49 -0
  22. package/dist/generators/doc-parser.d.ts.map +1 -0
  23. package/dist/generators/doc-parser.js +336 -0
  24. package/dist/generators/doc-parser.js.map +1 -0
  25. package/dist/generators/templates/index.d.ts +4 -0
  26. package/dist/generators/templates/index.d.ts.map +1 -1
  27. package/dist/generators/templates/index.js +4 -0
  28. package/dist/generators/templates/index.js.map +1 -1
  29. package/dist/generators/templates/website-components.d.ts +33 -0
  30. package/dist/generators/templates/website-components.d.ts.map +1 -0
  31. package/dist/generators/templates/website-components.js +278 -0
  32. package/dist/generators/templates/website-components.js.map +1 -0
  33. package/dist/generators/templates/website-config.d.ts +41 -0
  34. package/dist/generators/templates/website-config.d.ts.map +1 -0
  35. package/dist/generators/templates/website-config.js +283 -0
  36. package/dist/generators/templates/website-config.js.map +1 -0
  37. package/dist/generators/templates/website-conversion.d.ts +27 -0
  38. package/dist/generators/templates/website-conversion.d.ts.map +1 -0
  39. package/dist/generators/templates/website-conversion.js +326 -0
  40. package/dist/generators/templates/website-conversion.js.map +1 -0
  41. package/dist/generators/templates/website-seo.d.ts +76 -0
  42. package/dist/generators/templates/website-seo.d.ts.map +1 -0
  43. package/dist/generators/templates/website-seo.js +326 -0
  44. package/dist/generators/templates/website-seo.js.map +1 -0
  45. package/dist/generators/templates/website.d.ts +14 -47
  46. package/dist/generators/templates/website.d.ts.map +1 -1
  47. package/dist/generators/templates/website.js +412 -499
  48. package/dist/generators/templates/website.js.map +1 -1
  49. package/dist/generators/website-context.d.ts +83 -0
  50. package/dist/generators/website-context.d.ts.map +1 -0
  51. package/dist/generators/website-context.js +190 -0
  52. package/dist/generators/website-context.js.map +1 -0
  53. package/dist/generators/website.d.ts +3 -0
  54. package/dist/generators/website.d.ts.map +1 -1
  55. package/dist/generators/website.js +73 -10
  56. package/dist/generators/website.js.map +1 -1
  57. package/dist/state/index.d.ts +27 -0
  58. package/dist/state/index.d.ts.map +1 -1
  59. package/dist/state/index.js +30 -0
  60. package/dist/state/index.js.map +1 -1
  61. package/dist/types/consensus.d.ts +3 -0
  62. package/dist/types/consensus.d.ts.map +1 -1
  63. package/dist/types/consensus.js +1 -0
  64. package/dist/types/consensus.js.map +1 -1
  65. package/dist/types/website-strategy.d.ts +263 -0
  66. package/dist/types/website-strategy.d.ts.map +1 -0
  67. package/dist/types/website-strategy.js +105 -0
  68. package/dist/types/website-strategy.js.map +1 -0
  69. package/dist/types/workflow.d.ts +15 -0
  70. package/dist/types/workflow.d.ts.map +1 -1
  71. package/dist/types/workflow.js +6 -0
  72. package/dist/types/workflow.js.map +1 -1
  73. package/dist/workflow/auto-fix.d.ts +7 -1
  74. package/dist/workflow/auto-fix.d.ts.map +1 -1
  75. package/dist/workflow/auto-fix.js +55 -3
  76. package/dist/workflow/auto-fix.js.map +1 -1
  77. package/dist/workflow/consensus.d.ts.map +1 -1
  78. package/dist/workflow/consensus.js +2 -0
  79. package/dist/workflow/consensus.js.map +1 -1
  80. package/dist/workflow/execution-mode.d.ts.map +1 -1
  81. package/dist/workflow/execution-mode.js +18 -0
  82. package/dist/workflow/execution-mode.js.map +1 -1
  83. package/dist/workflow/index.d.ts +3 -0
  84. package/dist/workflow/index.d.ts.map +1 -1
  85. package/dist/workflow/index.js +25 -0
  86. package/dist/workflow/index.js.map +1 -1
  87. package/dist/workflow/overview.d.ts +89 -0
  88. package/dist/workflow/overview.d.ts.map +1 -0
  89. package/dist/workflow/overview.js +354 -0
  90. package/dist/workflow/overview.js.map +1 -0
  91. package/dist/workflow/plan-mode.d.ts +2 -1
  92. package/dist/workflow/plan-mode.d.ts.map +1 -1
  93. package/dist/workflow/plan-mode.js +83 -5
  94. package/dist/workflow/plan-mode.js.map +1 -1
  95. package/dist/workflow/website-strategy.d.ts +70 -0
  96. package/dist/workflow/website-strategy.d.ts.map +1 -0
  97. package/dist/workflow/website-strategy.js +238 -0
  98. package/dist/workflow/website-strategy.js.map +1 -0
  99. package/dist/workflow/website-updater.d.ts +17 -0
  100. package/dist/workflow/website-updater.d.ts.map +1 -0
  101. package/dist/workflow/website-updater.js +105 -0
  102. package/dist/workflow/website-updater.js.map +1 -0
  103. package/dist/workflow/workflow-logger.d.ts +1 -1
  104. package/dist/workflow/workflow-logger.d.ts.map +1 -1
  105. package/dist/workflow/workflow-logger.js.map +1 -1
  106. package/package.json +1 -1
  107. package/src/adapters/gemini.ts +10 -4
  108. package/src/adapters/grok.ts +10 -4
  109. package/src/adapters/openai.ts +38 -6
  110. package/src/cli/interactive.ts +47 -0
  111. package/src/generators/all.ts +6 -1
  112. package/src/generators/doc-parser.ts +372 -0
  113. package/src/generators/templates/index.ts +4 -0
  114. package/src/generators/templates/website-components.ts +305 -0
  115. package/src/generators/templates/website-config.ts +291 -0
  116. package/src/generators/templates/website-conversion.ts +341 -0
  117. package/src/generators/templates/website-seo.ts +370 -0
  118. package/src/generators/templates/website.ts +451 -505
  119. package/src/generators/website-context.ts +265 -0
  120. package/src/generators/website.ts +109 -19
  121. package/src/state/index.ts +42 -0
  122. package/src/types/consensus.ts +3 -0
  123. package/src/types/website-strategy.ts +243 -0
  124. package/src/types/workflow.ts +15 -0
  125. package/src/workflow/auto-fix.ts +57 -3
  126. package/src/workflow/consensus.ts +2 -0
  127. package/src/workflow/execution-mode.ts +21 -0
  128. package/src/workflow/index.ts +25 -0
  129. package/src/workflow/overview.ts +469 -0
  130. package/src/workflow/plan-mode.ts +115 -4
  131. package/src/workflow/website-strategy.ts +305 -0
  132. package/src/workflow/website-updater.ts +131 -0
  133. package/src/workflow/workflow-logger.ts +1 -0
  134. package/tests/adapters/persona-switching.test.ts +63 -0
  135. package/tests/generators/website-components.test.ts +159 -0
  136. package/tests/generators/website-context.test.ts +222 -0
  137. package/tests/generators/website-seo-quality.test.ts +246 -0
  138. package/tests/workflow/auto-fix-enhanced.test.ts +61 -1
  139. package/tests/workflow/overview.test.ts +392 -0
  140. package/tests/workflow/website-strategy.test.ts +191 -0
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Website configuration templates (non-content)
3
+ * Package configs, build tools, Docker, vitest, and env declarations
4
+ */
5
+
6
+ /**
7
+ * Generate Next.js package.json
8
+ */
9
+ export function generateWebsitePackageJson(projectName: string): string {
10
+ return `{
11
+ "name": "${projectName}-website",
12
+ "version": "1.0.0",
13
+ "private": true,
14
+ "scripts": {
15
+ "dev": "next dev -p 3001",
16
+ "build": "next build",
17
+ "start": "next start -p 3001",
18
+ "lint": "next lint",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "typecheck": "tsc --noEmit"
22
+ },
23
+ "dependencies": {
24
+ "next": "^14.1.0",
25
+ "react": "^18.2.0",
26
+ "react-dom": "^18.2.0",
27
+ "lucide-react": "^0.312.0",
28
+ "clsx": "^2.1.0",
29
+ "tailwind-merge": "^2.2.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^20.11.0",
33
+ "@types/react": "^18.2.0",
34
+ "@types/react-dom": "^18.2.0",
35
+ "autoprefixer": "^10.4.17",
36
+ "postcss": "^8.4.33",
37
+ "tailwindcss": "^3.4.1",
38
+ "typescript": "^5.3.3",
39
+ "@testing-library/react": "^14.1.2",
40
+ "@vitejs/plugin-react": "^4.2.1",
41
+ "vitest": "^1.2.0",
42
+ "jsdom": "^24.0.0"
43
+ }
44
+ }
45
+ `;
46
+ }
47
+
48
+ /**
49
+ * Generate Next.js config
50
+ */
51
+ export function generateNextConfig(): string {
52
+ return `/** @type {import('next').NextConfig} */
53
+ const nextConfig = {
54
+ // Enable React Strict Mode for better development
55
+ reactStrictMode: true,
56
+
57
+ // Image optimization
58
+ images: {
59
+ domains: [],
60
+ formats: ['image/avif', 'image/webp'],
61
+ },
62
+
63
+ // Disable x-powered-by header
64
+ poweredByHeader: false,
65
+
66
+ // Trailing slash config
67
+ trailingSlash: false,
68
+
69
+ // Headers for security
70
+ async headers() {
71
+ return [
72
+ {
73
+ source: '/:path*',
74
+ headers: [
75
+ {
76
+ key: 'X-DNS-Prefetch-Control',
77
+ value: 'on',
78
+ },
79
+ {
80
+ key: 'X-Content-Type-Options',
81
+ value: 'nosniff',
82
+ },
83
+ ],
84
+ },
85
+ ];
86
+ },
87
+ };
88
+
89
+ module.exports = nextConfig;
90
+ `;
91
+ }
92
+
93
+ /**
94
+ * Generate website tsconfig.json
95
+ */
96
+ export function generateWebsiteTsconfig(): string {
97
+ return `{
98
+ "compilerOptions": {
99
+ "target": "ES2017",
100
+ "lib": ["dom", "dom.iterable", "esnext"],
101
+ "allowJs": true,
102
+ "skipLibCheck": true,
103
+ "strict": true,
104
+ "noEmit": true,
105
+ "esModuleInterop": true,
106
+ "module": "esnext",
107
+ "moduleResolution": "bundler",
108
+ "resolveJsonModule": true,
109
+ "isolatedModules": true,
110
+ "jsx": "preserve",
111
+ "incremental": true,
112
+ "plugins": [
113
+ {
114
+ "name": "next"
115
+ }
116
+ ],
117
+ "paths": {
118
+ "@/*": ["./src/*"]
119
+ }
120
+ },
121
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
122
+ "exclude": ["node_modules"]
123
+ }
124
+ `;
125
+ }
126
+
127
+ /**
128
+ * Generate Tailwind config for website
129
+ */
130
+ export function generateWebsiteTailwindConfig(): string {
131
+ return `import type { Config } from 'tailwindcss';
132
+
133
+ const config: Config = {
134
+ content: [
135
+ './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
136
+ './src/components/**/*.{js,ts,jsx,tsx,mdx}',
137
+ './src/app/**/*.{js,ts,jsx,tsx,mdx}',
138
+ ],
139
+ theme: {
140
+ extend: {
141
+ colors: {
142
+ primary: {
143
+ 50: '#f0f9ff',
144
+ 100: '#e0f2fe',
145
+ 200: '#bae6fd',
146
+ 300: '#7dd3fc',
147
+ 400: '#38bdf8',
148
+ 500: '#0ea5e9',
149
+ 600: '#0284c7',
150
+ 700: '#0369a1',
151
+ 800: '#075985',
152
+ 900: '#0c4a6e',
153
+ },
154
+ },
155
+ fontFamily: {
156
+ sans: ['var(--font-inter)', 'system-ui', 'sans-serif'],
157
+ },
158
+ },
159
+ },
160
+ plugins: [],
161
+ };
162
+
163
+ export default config;
164
+ `;
165
+ }
166
+
167
+ /**
168
+ * Generate PostCSS config for website
169
+ */
170
+ export function generateWebsitePostcssConfig(): string {
171
+ return `module.exports = {
172
+ plugins: {
173
+ tailwindcss: {},
174
+ autoprefixer: {},
175
+ },
176
+ };
177
+ `;
178
+ }
179
+
180
+ /**
181
+ * Generate website Dockerfile
182
+ */
183
+ export function generateWebsiteDockerfile(): string {
184
+ return `# Build stage
185
+ FROM node:20-alpine AS builder
186
+
187
+ WORKDIR /app
188
+
189
+ # Copy package files
190
+ COPY package*.json ./
191
+
192
+ # Install dependencies
193
+ RUN npm ci
194
+
195
+ # Copy source
196
+ COPY . .
197
+
198
+ # Build
199
+ RUN npm run build
200
+
201
+ # Production stage
202
+ FROM node:20-alpine AS runner
203
+
204
+ WORKDIR /app
205
+
206
+ ENV NODE_ENV=production
207
+ ENV NEXT_TELEMETRY_DISABLED=1
208
+
209
+ # Create non-root user
210
+ RUN addgroup --system --gid 1001 nodejs
211
+ RUN adduser --system --uid 1001 nextjs
212
+
213
+ # Copy built assets
214
+ COPY --from=builder /app/public ./public
215
+ COPY --from=builder /app/.next/standalone ./
216
+ COPY --from=builder /app/.next/static ./.next/static
217
+
218
+ USER nextjs
219
+
220
+ EXPOSE 3000
221
+
222
+ ENV PORT=3000
223
+ ENV HOSTNAME="0.0.0.0"
224
+
225
+ CMD ["node", "server.js"]
226
+ `;
227
+ }
228
+
229
+ /**
230
+ * Generate vitest config for website
231
+ */
232
+ export function generateWebsiteVitestConfig(): string {
233
+ return `import { defineConfig } from 'vitest/config';
234
+ import react from '@vitejs/plugin-react';
235
+ import path from 'path';
236
+
237
+ export default defineConfig({
238
+ plugins: [react()],
239
+ test: {
240
+ environment: 'jsdom',
241
+ include: ['**/*.test.{ts,tsx}'],
242
+ globals: true,
243
+ setupFiles: ['./tests/setup.ts'],
244
+ },
245
+ resolve: {
246
+ alias: {
247
+ '@': path.resolve(__dirname, './src'),
248
+ },
249
+ },
250
+ });
251
+ `;
252
+ }
253
+
254
+ /**
255
+ * Generate vitest setup for website
256
+ */
257
+ export function generateWebsiteVitestSetup(): string {
258
+ return `import '@testing-library/jest-dom';
259
+
260
+ // Mock next/navigation
261
+ vi.mock('next/navigation', () => ({
262
+ useRouter: () => ({
263
+ push: vi.fn(),
264
+ replace: vi.fn(),
265
+ prefetch: vi.fn(),
266
+ }),
267
+ useSearchParams: () => new URLSearchParams(),
268
+ usePathname: () => '/',
269
+ }));
270
+
271
+ // Mock next/image
272
+ vi.mock('next/image', () => ({
273
+ default: (props: Record<string, unknown>) => {
274
+ // eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text
275
+ return <img {...props} />;
276
+ },
277
+ }));
278
+ `;
279
+ }
280
+
281
+ /**
282
+ * Generate Next.js environment declaration
283
+ */
284
+ export function generateWebsiteNextEnv(): string {
285
+ return `/// <reference types="next" />
286
+ /// <reference types="next/image-types/global" />
287
+
288
+ // NOTE: This file should not be edited
289
+ // see https://nextjs.org/docs/basic-features/typescript for more information.
290
+ `;
291
+ }
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Lead capture and conversion templates
3
+ * Generates API route handlers for lead capture and contact form components
4
+ */
5
+
6
+ import type { WebsiteStrategyDocument } from '../../types/website-strategy.js';
7
+
8
+ /**
9
+ * Escape a string for safe use inside JSX template literals
10
+ */
11
+ function escapeJsx(str: string): string {
12
+ return str
13
+ .replace(/\\/g, '\\\\')
14
+ .replace(/'/g, "\\'")
15
+ .replace(/`/g, '\\`')
16
+ .replace(/\$/g, '\\$');
17
+ }
18
+
19
+ /**
20
+ * Generate lead capture API route handler
21
+ *
22
+ * @param provider - Lead capture provider type
23
+ * @returns API route source code (src/app/api/lead/route.ts)
24
+ */
25
+ export function generateLeadCaptureRoute(
26
+ provider: 'none' | 'webhook' | 'resend' | 'postmark' = 'webhook'
27
+ ): string {
28
+ if (provider === 'none') {
29
+ return `import { NextResponse } from 'next/server';
30
+
31
+ export async function POST() {
32
+ return NextResponse.json(
33
+ { error: 'Lead capture not configured' },
34
+ { status: 501 }
35
+ );
36
+ }
37
+ `;
38
+ }
39
+
40
+ if (provider === 'resend') {
41
+ return `import { NextResponse } from 'next/server';
42
+
43
+ interface LeadPayload {
44
+ name: string;
45
+ email: string;
46
+ message?: string;
47
+ }
48
+
49
+ export async function POST(request: Request) {
50
+ try {
51
+ const body: LeadPayload = await request.json();
52
+
53
+ if (!body.name || !body.email) {
54
+ return NextResponse.json(
55
+ { error: 'Name and email are required' },
56
+ { status: 400 }
57
+ );
58
+ }
59
+
60
+ const apiKey = process.env.RESEND_API_KEY;
61
+ if (!apiKey) {
62
+ console.error('RESEND_API_KEY not configured');
63
+ return NextResponse.json(
64
+ { error: 'Lead capture not configured' },
65
+ { status: 500 }
66
+ );
67
+ }
68
+
69
+ const response = await fetch('https://api.resend.com/emails', {
70
+ method: 'POST',
71
+ headers: {
72
+ 'Content-Type': 'application/json',
73
+ 'Authorization': \`Bearer \${apiKey}\`,
74
+ },
75
+ body: JSON.stringify({
76
+ from: process.env.RESEND_FROM_EMAIL || 'onboarding@resend.dev',
77
+ to: process.env.LEAD_NOTIFICATION_EMAIL || 'team@example.com',
78
+ subject: \`New lead: \${body.name}\`,
79
+ text: \`Name: \${body.name}\\nEmail: \${body.email}\\nMessage: \${body.message || 'N/A'}\`,
80
+ }),
81
+ });
82
+
83
+ if (!response.ok) {
84
+ console.error('Resend API error:', await response.text());
85
+ return NextResponse.json({ error: 'Failed to send' }, { status: 500 });
86
+ }
87
+
88
+ return NextResponse.json({ success: true });
89
+ } catch (error) {
90
+ console.error('Lead capture error:', error);
91
+ return NextResponse.json({ error: 'Internal error' }, { status: 500 });
92
+ }
93
+ }
94
+ `;
95
+ }
96
+
97
+ if (provider === 'postmark') {
98
+ return `import { NextResponse } from 'next/server';
99
+
100
+ interface LeadPayload {
101
+ name: string;
102
+ email: string;
103
+ message?: string;
104
+ }
105
+
106
+ export async function POST(request: Request) {
107
+ try {
108
+ const body: LeadPayload = await request.json();
109
+
110
+ if (!body.name || !body.email) {
111
+ return NextResponse.json(
112
+ { error: 'Name and email are required' },
113
+ { status: 400 }
114
+ );
115
+ }
116
+
117
+ const apiKey = process.env.POSTMARK_API_KEY;
118
+ if (!apiKey) {
119
+ console.error('POSTMARK_API_KEY not configured');
120
+ return NextResponse.json(
121
+ { error: 'Lead capture not configured' },
122
+ { status: 500 }
123
+ );
124
+ }
125
+
126
+ const response = await fetch('https://api.postmarkapp.com/email', {
127
+ method: 'POST',
128
+ headers: {
129
+ 'Content-Type': 'application/json',
130
+ 'X-Postmark-Server-Token': apiKey,
131
+ },
132
+ body: JSON.stringify({
133
+ From: process.env.POSTMARK_FROM_EMAIL || 'no-reply@example.com',
134
+ To: process.env.LEAD_NOTIFICATION_EMAIL || 'team@example.com',
135
+ Subject: \`New lead: \${body.name}\`,
136
+ TextBody: \`Name: \${body.name}\\nEmail: \${body.email}\\nMessage: \${body.message || 'N/A'}\`,
137
+ }),
138
+ });
139
+
140
+ if (!response.ok) {
141
+ console.error('Postmark API error:', await response.text());
142
+ return NextResponse.json({ error: 'Failed to send' }, { status: 500 });
143
+ }
144
+
145
+ return NextResponse.json({ success: true });
146
+ } catch (error) {
147
+ console.error('Lead capture error:', error);
148
+ return NextResponse.json({ error: 'Internal error' }, { status: 500 });
149
+ }
150
+ }
151
+ `;
152
+ }
153
+
154
+ // Default: webhook provider
155
+ return `import { NextResponse } from 'next/server';
156
+
157
+ interface LeadPayload {
158
+ name: string;
159
+ email: string;
160
+ message?: string;
161
+ }
162
+
163
+ export async function POST(request: Request) {
164
+ try {
165
+ const body: LeadPayload = await request.json();
166
+
167
+ if (!body.name || !body.email) {
168
+ return NextResponse.json(
169
+ { error: 'Name and email are required' },
170
+ { status: 400 }
171
+ );
172
+ }
173
+
174
+ const webhookUrl = process.env.LEAD_WEBHOOK_URL;
175
+ if (!webhookUrl) {
176
+ console.error('LEAD_WEBHOOK_URL not configured');
177
+ return NextResponse.json(
178
+ { error: 'Lead capture not configured' },
179
+ { status: 500 }
180
+ );
181
+ }
182
+
183
+ const response = await fetch(webhookUrl, {
184
+ method: 'POST',
185
+ headers: { 'Content-Type': 'application/json' },
186
+ body: JSON.stringify({
187
+ name: body.name,
188
+ email: body.email,
189
+ message: body.message || '',
190
+ timestamp: new Date().toISOString(),
191
+ source: 'website',
192
+ }),
193
+ });
194
+
195
+ if (!response.ok) {
196
+ console.error('Webhook error:', response.status);
197
+ return NextResponse.json({ error: 'Failed to submit' }, { status: 500 });
198
+ }
199
+
200
+ return NextResponse.json({ success: true });
201
+ } catch (error) {
202
+ console.error('Lead capture error:', error);
203
+ return NextResponse.json({ error: 'Internal error' }, { status: 500 });
204
+ }
205
+ }
206
+ `;
207
+ }
208
+
209
+ /**
210
+ * Generate contact form component
211
+ *
212
+ * @param strategy - Optional strategy for CTA text
213
+ * @returns ContactForm component source code
214
+ */
215
+ export function generateContactForm(
216
+ strategy?: WebsiteStrategyDocument
217
+ ): string {
218
+ const ctaText = strategy?.conversionStrategy.primaryCta.text || 'Get Started';
219
+
220
+ return `'use client';
221
+
222
+ import { useState, type FormEvent } from 'react';
223
+
224
+ /**
225
+ * Lead capture contact form
226
+ * Submits to /api/lead endpoint
227
+ */
228
+ export default function ContactForm() {
229
+ const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
230
+
231
+ async function handleSubmit(e: FormEvent<HTMLFormElement>) {
232
+ e.preventDefault();
233
+ setStatus('submitting');
234
+
235
+ const formData = new FormData(e.currentTarget);
236
+ const data = {
237
+ name: formData.get('name') as string,
238
+ email: formData.get('email') as string,
239
+ message: formData.get('message') as string,
240
+ };
241
+
242
+ try {
243
+ const response = await fetch('/api/lead', {
244
+ method: 'POST',
245
+ headers: { 'Content-Type': 'application/json' },
246
+ body: JSON.stringify(data),
247
+ });
248
+
249
+ if (response.ok) {
250
+ setStatus('success');
251
+ e.currentTarget.reset();
252
+ } else {
253
+ setStatus('error');
254
+ }
255
+ } catch {
256
+ setStatus('error');
257
+ }
258
+ }
259
+
260
+ if (status === 'success') {
261
+ return (
262
+ <div className="rounded-lg bg-green-50 p-6 text-center">
263
+ <p className="text-lg font-medium text-green-800">Thank you for reaching out!</p>
264
+ <p className="mt-2 text-sm text-green-700">We will get back to you shortly.</p>
265
+ </div>
266
+ );
267
+ }
268
+
269
+ return (
270
+ <form onSubmit={handleSubmit} className="space-y-4">
271
+ <div>
272
+ <label htmlFor="name" className="block text-sm font-medium text-gray-700">
273
+ Name
274
+ </label>
275
+ <input
276
+ type="text"
277
+ id="name"
278
+ name="name"
279
+ required
280
+ className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
281
+ />
282
+ </div>
283
+ <div>
284
+ <label htmlFor="email" className="block text-sm font-medium text-gray-700">
285
+ Email
286
+ </label>
287
+ <input
288
+ type="email"
289
+ id="email"
290
+ name="email"
291
+ required
292
+ className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
293
+ />
294
+ </div>
295
+ <div>
296
+ <label htmlFor="message" className="block text-sm font-medium text-gray-700">
297
+ Message
298
+ </label>
299
+ <textarea
300
+ id="message"
301
+ name="message"
302
+ rows={4}
303
+ className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
304
+ />
305
+ </div>
306
+ <button
307
+ type="submit"
308
+ disabled={status === 'submitting'}
309
+ className="w-full rounded-md bg-primary-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-primary-500 disabled:opacity-50"
310
+ >
311
+ {status === 'submitting' ? 'Sending...' : '${escapeJsx(ctaText)}'}
312
+ </button>
313
+ {status === 'error' && (
314
+ <p className="text-sm text-red-600">Something went wrong. Please try again.</p>
315
+ )}
316
+ </form>
317
+ );
318
+ }
319
+ `;
320
+ }
321
+
322
+ /**
323
+ * Generate .env.example entries for lead capture provider
324
+ *
325
+ * @param provider - Lead capture provider type
326
+ * @returns Environment variable example lines
327
+ */
328
+ export function generateLeadCaptureEnvExample(
329
+ provider: 'none' | 'webhook' | 'resend' | 'postmark'
330
+ ): string {
331
+ switch (provider) {
332
+ case 'webhook':
333
+ return 'LEAD_WEBHOOK_URL=https://your-webhook-endpoint.com/leads\n';
334
+ case 'resend':
335
+ return 'RESEND_API_KEY=re_xxxxxxxxxxxx\nRESEND_FROM_EMAIL=onboarding@resend.dev\nLEAD_NOTIFICATION_EMAIL=team@example.com\n';
336
+ case 'postmark':
337
+ return 'POSTMARK_API_KEY=xxxxxxxxxxxx\nPOSTMARK_FROM_EMAIL=no-reply@example.com\nLEAD_NOTIFICATION_EMAIL=team@example.com\n';
338
+ default:
339
+ return '';
340
+ }
341
+ }