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.
- package/README.md +222 -63
- package/dist/adapters/gemini.d.ts +1 -0
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +9 -4
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/grok.d.ts +1 -0
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js +9 -4
- package/dist/adapters/grok.js.map +1 -1
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +35 -9
- package/dist/adapters/openai.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +42 -0
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts +4 -1
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +2 -1
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +49 -0
- package/dist/generators/doc-parser.d.ts.map +1 -0
- package/dist/generators/doc-parser.js +336 -0
- package/dist/generators/doc-parser.js.map +1 -0
- package/dist/generators/templates/index.d.ts +4 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +4 -0
- package/dist/generators/templates/index.js.map +1 -1
- package/dist/generators/templates/website-components.d.ts +33 -0
- package/dist/generators/templates/website-components.d.ts.map +1 -0
- package/dist/generators/templates/website-components.js +278 -0
- package/dist/generators/templates/website-components.js.map +1 -0
- package/dist/generators/templates/website-config.d.ts +41 -0
- package/dist/generators/templates/website-config.d.ts.map +1 -0
- package/dist/generators/templates/website-config.js +283 -0
- package/dist/generators/templates/website-config.js.map +1 -0
- package/dist/generators/templates/website-conversion.d.ts +27 -0
- package/dist/generators/templates/website-conversion.d.ts.map +1 -0
- package/dist/generators/templates/website-conversion.js +326 -0
- package/dist/generators/templates/website-conversion.js.map +1 -0
- package/dist/generators/templates/website-seo.d.ts +76 -0
- package/dist/generators/templates/website-seo.d.ts.map +1 -0
- package/dist/generators/templates/website-seo.js +326 -0
- package/dist/generators/templates/website-seo.js.map +1 -0
- package/dist/generators/templates/website.d.ts +14 -47
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +412 -499
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-context.d.ts +83 -0
- package/dist/generators/website-context.d.ts.map +1 -0
- package/dist/generators/website-context.js +190 -0
- package/dist/generators/website-context.js.map +1 -0
- package/dist/generators/website.d.ts +3 -0
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +73 -10
- package/dist/generators/website.js.map +1 -1
- package/dist/state/index.d.ts +27 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +30 -0
- package/dist/state/index.js.map +1 -1
- package/dist/types/consensus.d.ts +3 -0
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +1 -0
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/website-strategy.d.ts +263 -0
- package/dist/types/website-strategy.d.ts.map +1 -0
- package/dist/types/website-strategy.js +105 -0
- package/dist/types/website-strategy.js.map +1 -0
- package/dist/types/workflow.d.ts +15 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +6 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/workflow/auto-fix.d.ts +7 -1
- package/dist/workflow/auto-fix.d.ts.map +1 -1
- package/dist/workflow/auto-fix.js +55 -3
- package/dist/workflow/auto-fix.js.map +1 -1
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +2 -0
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/execution-mode.d.ts.map +1 -1
- package/dist/workflow/execution-mode.js +18 -0
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +3 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +25 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/overview.d.ts +89 -0
- package/dist/workflow/overview.d.ts.map +1 -0
- package/dist/workflow/overview.js +354 -0
- package/dist/workflow/overview.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +2 -1
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +83 -5
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts +70 -0
- package/dist/workflow/website-strategy.d.ts.map +1 -0
- package/dist/workflow/website-strategy.js +238 -0
- package/dist/workflow/website-strategy.js.map +1 -0
- package/dist/workflow/website-updater.d.ts +17 -0
- package/dist/workflow/website-updater.d.ts.map +1 -0
- package/dist/workflow/website-updater.js +105 -0
- package/dist/workflow/website-updater.js.map +1 -0
- package/dist/workflow/workflow-logger.d.ts +1 -1
- package/dist/workflow/workflow-logger.d.ts.map +1 -1
- package/dist/workflow/workflow-logger.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/gemini.ts +10 -4
- package/src/adapters/grok.ts +10 -4
- package/src/adapters/openai.ts +38 -6
- package/src/cli/interactive.ts +47 -0
- package/src/generators/all.ts +6 -1
- package/src/generators/doc-parser.ts +372 -0
- package/src/generators/templates/index.ts +4 -0
- package/src/generators/templates/website-components.ts +305 -0
- package/src/generators/templates/website-config.ts +291 -0
- package/src/generators/templates/website-conversion.ts +341 -0
- package/src/generators/templates/website-seo.ts +370 -0
- package/src/generators/templates/website.ts +451 -505
- package/src/generators/website-context.ts +265 -0
- package/src/generators/website.ts +109 -19
- package/src/state/index.ts +42 -0
- package/src/types/consensus.ts +3 -0
- package/src/types/website-strategy.ts +243 -0
- package/src/types/workflow.ts +15 -0
- package/src/workflow/auto-fix.ts +57 -3
- package/src/workflow/consensus.ts +2 -0
- package/src/workflow/execution-mode.ts +21 -0
- package/src/workflow/index.ts +25 -0
- package/src/workflow/overview.ts +469 -0
- package/src/workflow/plan-mode.ts +115 -4
- package/src/workflow/website-strategy.ts +305 -0
- package/src/workflow/website-updater.ts +131 -0
- package/src/workflow/workflow-logger.ts +1 -0
- package/tests/adapters/persona-switching.test.ts +63 -0
- package/tests/generators/website-components.test.ts +159 -0
- package/tests/generators/website-context.test.ts +222 -0
- package/tests/generators/website-seo-quality.test.ts +246 -0
- package/tests/workflow/auto-fix-enhanced.test.ts +61 -1
- package/tests/workflow/overview.test.ts +392 -0
- 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
|
+
}
|