Ashraf Al-Kassem Claude Opus 4.6 commited on
Commit
6acfd72
·
1 Parent(s): 740c3bd

feat: Mission 33 — Marketing Website Alignment + DB-Driven Content

Browse files

Convert all website pages from hardcoded static arrays to DB-driven
content via public catalog API endpoints. Removes phantom "Pro" tier
(website showed 4 plans, DB only has 3). Adds Templates and Integrations
pages, parity audit script, and scroll-reveal design system classes.

- Backend: public GET /catalog/templates endpoint (unauthenticated, 60s cache)
- Website: api.ts client, moduleMap.ts for module-to-marketing mapping
- Pages converted: home, plans (3 tiers), features (module-driven)
- New pages: /templates (12 templates, category filters), /integrations (3 providers)
- Components: FeatureCard gains iconName + comingSoon props
- Nav: Header + Footer updated with Templates and Integrations links
- Design: catalog-card, badge-platform, badge-category, scroll-reveal classes
- Audit: scripts/parity-audit.ts verifies no fake feature claims (CI-friendly)
- Tests: 3 new catalog tests (305 total passing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Website/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
Website/package.json CHANGED
@@ -6,7 +6,8 @@
6
  "dev": "next dev",
7
  "build": "next build",
8
  "start": "next start",
9
- "lint": "eslint"
 
10
  },
11
  "dependencies": {
12
  "clsx": "^2.1.1",
@@ -24,6 +25,7 @@
24
  "eslint": "^9",
25
  "eslint-config-next": "16.1.6",
26
  "tailwindcss": "^4",
 
27
  "typescript": "^5"
28
  }
29
  }
 
6
  "dev": "next dev",
7
  "build": "next build",
8
  "start": "next start",
9
+ "lint": "eslint",
10
+ "audit:parity": "npx ts-node scripts/parity-audit.ts"
11
  },
12
  "dependencies": {
13
  "clsx": "^2.1.1",
 
25
  "eslint": "^9",
26
  "eslint-config-next": "16.1.6",
27
  "tailwindcss": "^4",
28
+ "ts-node": "^10",
29
  "typescript": "^5"
30
  }
31
  }
Website/scripts/parity-audit.ts ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Parity Audit — Mission 33
3
+ *
4
+ * Verifies the marketing website does not claim features, plans, or providers
5
+ * that don't exist in the backend database.
6
+ *
7
+ * Usage:
8
+ * AUDIT_API_URL=http://localhost:8000 npx ts-node scripts/parity-audit.ts
9
+ *
10
+ * Exit code 0 = PASS, 1 = FAIL (violations found)
11
+ */
12
+
13
+ import * as fs from "fs";
14
+ import * as path from "path";
15
+
16
+ const API_BASE = process.env.AUDIT_API_URL ?? "http://localhost:8000";
17
+ const WEBSITE_SRC = path.join(__dirname, "../src");
18
+
19
+ interface Violation {
20
+ type: string;
21
+ value: string;
22
+ file: string;
23
+ line: number;
24
+ }
25
+
26
+ interface Envelope<T> {
27
+ success: boolean;
28
+ data: T;
29
+ }
30
+
31
+ async function fetchData<T>(endpoint: string): Promise<T[]> {
32
+ try {
33
+ const res = await fetch(`${API_BASE}/api/v1/catalog${endpoint}`);
34
+ if (!res.ok) {
35
+ console.error(` WARN: ${endpoint} returned ${res.status}`);
36
+ return [];
37
+ }
38
+ const json: Envelope<T[]> = await res.json();
39
+ return json.success ? json.data : [];
40
+ } catch (err) {
41
+ console.error(` WARN: Failed to fetch ${endpoint}:`, err);
42
+ return [];
43
+ }
44
+ }
45
+
46
+ function scanFiles(dir: string, ext: string): { filePath: string; content: string; lines: string[] }[] {
47
+ const results: { filePath: string; content: string; lines: string[] }[] = [];
48
+
49
+ function walk(d: string) {
50
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
51
+ const full = path.join(d, entry.name);
52
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
53
+ walk(full);
54
+ } else if (entry.isFile() && entry.name.endsWith(ext)) {
55
+ const content = fs.readFileSync(full, "utf-8");
56
+ results.push({
57
+ filePath: path.relative(WEBSITE_SRC, full),
58
+ content,
59
+ lines: content.split("\n"),
60
+ });
61
+ }
62
+ }
63
+ }
64
+
65
+ walk(dir);
66
+ return results;
67
+ }
68
+
69
+ async function main() {
70
+ console.log("Parity Audit — LeadPilot Website vs Backend DB");
71
+ console.log(`API: ${API_BASE}`);
72
+ console.log(`Source: ${WEBSITE_SRC}\n`);
73
+
74
+ // Fetch all catalog data
75
+ const [plans, providers] = await Promise.all([
76
+ fetchData<{ name: string; display_name: string }>("/plans"),
77
+ fetchData<{ key: string; label: string }>("/integration-providers"),
78
+ ]);
79
+
80
+ const dbPlanNames = new Set(plans.map((p) => p.name.toLowerCase()));
81
+ const dbPlanDisplayNames = new Set(plans.map((p) => p.display_name.toLowerCase()));
82
+ const dbProviderKeys = new Set(providers.map((p) => p.key.toLowerCase()));
83
+
84
+ console.log(`DB plans: ${[...dbPlanNames].join(", ")}`);
85
+ console.log(`DB providers: ${[...dbProviderKeys].join(", ")}\n`);
86
+
87
+ // Known phantom plan names that should NOT appear
88
+ const phantomPlanNames = ["pro", "starter", "basic", "professional", "premium"];
89
+ const phantomPlanPatterns = phantomPlanNames
90
+ .filter((name) => !dbPlanNames.has(name) && !dbPlanDisplayNames.has(name))
91
+ .map((name) => ({
92
+ name,
93
+ // Match as standalone word (plan name in quotes, or as plan title)
94
+ regex: new RegExp(`(?:name|tier|plan).*?["'\`]${name}["'\`]|["'\`]${name}["'\`].*?(?:plan|tier)`, "gi"),
95
+ }));
96
+
97
+ // Scan source files
98
+ const files = scanFiles(path.join(WEBSITE_SRC, "app"), ".tsx");
99
+ const violations: Violation[] = [];
100
+
101
+ for (const { filePath, lines } of files) {
102
+ for (let i = 0; i < lines.length; i++) {
103
+ const line = lines[i];
104
+
105
+ // Check for phantom plan names
106
+ for (const { name, regex } of phantomPlanPatterns) {
107
+ regex.lastIndex = 0;
108
+ if (regex.test(line)) {
109
+ violations.push({
110
+ type: "PHANTOM_PLAN",
111
+ value: name,
112
+ file: filePath,
113
+ line: i + 1,
114
+ });
115
+ }
116
+ }
117
+
118
+ // Also check for standalone "Pro" as a plan name in plan arrays/objects
119
+ // This catches hardcoded plan arrays like { name: "Pro", ... }
120
+ if (/name:\s*["']Pro["']/i.test(line) && !dbPlanNames.has("pro")) {
121
+ violations.push({
122
+ type: "PHANTOM_PLAN_PROPERTY",
123
+ value: "Pro",
124
+ file: filePath,
125
+ line: i + 1,
126
+ });
127
+ }
128
+ }
129
+ }
130
+
131
+ // Output
132
+ const result = {
133
+ audit_timestamp: new Date().toISOString(),
134
+ api_base: API_BASE,
135
+ plans_in_db: [...dbPlanNames],
136
+ providers_in_db: [...dbProviderKeys],
137
+ violations,
138
+ status: violations.length === 0 ? "PASS" : "FAIL",
139
+ };
140
+
141
+ console.log(JSON.stringify(result, null, 2));
142
+
143
+ if (violations.length > 0) {
144
+ console.error(`\n${violations.length} violation(s) found.`);
145
+ process.exit(1);
146
+ } else {
147
+ console.log("\nNo violations. Website is in parity with backend.");
148
+ process.exit(0);
149
+ }
150
+ }
151
+
152
+ main().catch((err) => {
153
+ console.error("Audit script failed:", err);
154
+ process.exit(2);
155
+ });
Website/src/app/features/page.tsx CHANGED
@@ -1,20 +1,10 @@
1
  import type { Metadata } from "next";
2
- import {
3
- Brain,
4
- Globe,
5
- GitMerge,
6
- Inbox,
7
- BarChart3,
8
- BookOpen,
9
- Shield,
10
- Database,
11
- FileText,
12
- Zap,
13
- Bell,
14
- Settings,
15
- } from "lucide-react";
16
  import FeatureCard from "@/components/FeatureCard";
17
  import CTASection from "@/components/CTASection";
 
 
18
 
19
  export const metadata: Metadata = {
20
  title: "Features — Everything LeadPilot Does",
@@ -22,122 +12,29 @@ export const metadata: Metadata = {
22
  "AI qualification & scoring, multi-channel capture, smart routing, CRM sync, team inbox, analytics, playbooks, and more. The full LeadPilot feature set.",
23
  };
24
 
25
- const features = [
26
- {
27
- icon: Brain,
28
- benefit: "Qualify at scale",
29
- title: "AI Qualification & Scoring",
30
- description:
31
- "Custom AI prompts engage every lead the moment they arrive. Scores are assigned based on intent, fit, and urgency — so your reps only work leads worth their time.",
32
- },
33
- {
34
- icon: Globe,
35
- benefit: "Zero leakage",
36
- title: "Multi-Channel Lead Capture",
37
- description:
38
- "Embed capture forms, integrate your existing chat widget, accept webhook payloads, and connect partner referral links — everything flows into one structured pipeline.",
39
- },
40
- {
41
- icon: Database,
42
- benefit: "Stay connected",
43
- title: "CRM Sync & Integrations",
44
- description:
45
- "Push qualified leads directly to your CRM of choice. Map fields, set sync rules, and keep your sales stack in perfect alignment without manual exports.",
46
- },
47
- {
48
- icon: GitMerge,
49
- benefit: "Right rep, right time",
50
- title: "Smart Routing Rules",
51
- description:
52
- "Route by territory, vertical, product line, rep availability, or any custom attribute. Rules compose like building blocks — no code required.",
53
- },
54
- {
55
- icon: Inbox,
56
- benefit: "Stay in the loop",
57
- title: "Team Inbox & Human Handover",
58
- description:
59
- "All conversations flow into a shared team inbox. Reps can pick up context instantly, send messages, and mark leads without switching tools.",
60
- },
61
- {
62
- icon: BarChart3,
63
- benefit: "Measure what matters",
64
- title: "Analytics & Conversion Tracking",
65
- description:
66
- "Track lead volume, qualification rate, response time, conversion by channel and rep, and pipeline velocity — all in real time from a single dashboard.",
67
- },
68
- {
69
- icon: BookOpen,
70
- benefit: "Start fast",
71
- title: "Playbooks & Templates",
72
- description:
73
- "Pre-built qualification playbooks for common industries and use cases. Start with a template, customise the prompts, and go live in minutes.",
74
- },
75
- {
76
- icon: Shield,
77
- benefit: "Stay in control",
78
- title: "Role-Based Access Control",
79
- description:
80
- "Assign owner, manager, and viewer roles. Control who can see which leads, edit routing rules, or access reporting — down to workspace level.",
81
- },
82
- {
83
- icon: FileText,
84
- benefit: "Full transparency",
85
- title: "Audit Trail & Activity Log",
86
- description:
87
- "Every action on every lead is logged: capture, score change, routing event, message sent, rep assigned. Immutable, exportable, always available.",
88
- },
89
- {
90
- icon: Database,
91
- benefit: "Richer profiles",
92
- title: "Custom Fields & Lead Enrichment",
93
- description:
94
- "Extend the lead model with custom fields that match your business. Enrich profiles with company data, source metadata, and conversation history automatically.",
95
- },
96
- {
97
- icon: Bell,
98
- benefit: "Never miss a beat",
99
- title: "Real-Time Notifications",
100
- description:
101
- "Email and in-app alerts the moment a lead is assigned, qualified, or hits a threshold. Configure notification rules per rep, per team, or per lead stage.",
102
- },
103
- {
104
- icon: Settings,
105
- benefit: "Your rules",
106
- title: "Workflow Automation",
107
- description:
108
- "Build trigger-based workflows that act on lead status changes, score thresholds, or time-based events. Automate follow-ups, escalations, and re-routing without code.",
109
- },
110
- ];
111
 
112
- const categories = [
113
- {
114
- label: "Capture & Intake",
115
- icon: Zap,
116
- features: ["Multi-Channel Capture", "Custom Fields & Lead Enrichment"],
117
- },
118
- {
119
- label: "Intelligence",
120
- icon: Brain,
121
- features: ["AI Qualification & Scoring", "Playbooks & Templates", "Workflow Automation"],
122
- },
123
- {
124
- label: "Operations",
125
- icon: GitMerge,
126
- features: ["Smart Routing Rules", "Team Inbox & Human Handover", "Real-Time Notifications"],
127
- },
128
- {
129
- label: "Visibility",
130
- icon: BarChart3,
131
- features: ["Analytics & Conversion Tracking", "Audit Trail & Activity Log"],
132
- },
133
- {
134
- label: "Platform",
135
- icon: Shield,
136
- features: ["CRM Sync & Integrations", "Role-Based Access Control"],
137
- },
138
- ];
139
 
140
- export default function FeaturesPage() {
141
  return (
142
  <>
143
  {/* ─── HERO ─────────────────────────────────────────────────── */}
@@ -155,7 +52,6 @@ export default function FeaturesPage() {
155
  "radial-gradient(ellipse 60% 50% at 50% 0%, rgba(15,118,110,0.25) 0%, transparent 70%)",
156
  }}
157
  />
158
-
159
  <div className="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
160
  <p
161
  className="text-xs font-bold uppercase tracking-widest mb-4"
@@ -193,16 +89,20 @@ export default function FeaturesPage() {
193
  >
194
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
195
  <div className="flex flex-wrap items-center gap-2 justify-center">
196
- {categories.map(({ label, icon: Icon }) => (
197
- <a
198
- key={label}
199
- href={`#${label.toLowerCase().replace(/ & /g, "-").replace(/ /g, "-")}`}
200
- className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-medium category-link"
201
- >
202
- <Icon className="w-3.5 h-3.5" aria-hidden="true" />
203
- {label}
204
- </a>
205
- ))}
 
 
 
 
206
  </div>
207
  </div>
208
  </section>
@@ -214,20 +114,27 @@ export default function FeaturesPage() {
214
  aria-labelledby="features-grid-heading"
215
  >
216
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
217
- <h2 id="features-grid-heading" className="sr-only">
218
- All features
219
- </h2>
220
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
221
- {features.map((feat) => (
222
- <FeatureCard key={feat.title} {...feat} dark />
 
 
 
 
 
 
 
 
223
  ))}
224
  </div>
225
  </div>
226
  </section>
227
 
228
  {/* ─── CATEGORIES DEEP DIVE ─────────────────────────────────── */}
229
- {categories.map(({ label, icon: Icon, features: catFeats }) => {
230
  const anchorId = label.toLowerCase().replace(/ & /g, "-").replace(/ /g, "-");
 
231
  return (
232
  <section
233
  key={label}
@@ -242,7 +149,7 @@ export default function FeaturesPage() {
242
  className="w-10 h-10 rounded-xl flex items-center justify-center"
243
  style={{ background: "rgba(15,118,110,0.1)", border: "1px solid rgba(15,118,110,0.2)" }}
244
  >
245
- <Icon className="w-5 h-5" style={{ color: "#0F766E" }} aria-hidden="true" />
246
  </div>
247
  <h2
248
  id={`${anchorId}-heading`}
@@ -253,11 +160,16 @@ export default function FeaturesPage() {
253
  </h2>
254
  </div>
255
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
256
- {features
257
- .filter((f) => catFeats.includes(f.title))
258
- .map((feat) => (
259
- <FeatureCard key={feat.title} {...feat} />
260
- ))}
 
 
 
 
 
261
  </div>
262
  </div>
263
  </section>
 
1
  import type { Metadata } from "next";
2
+ import { getModules } from "@/lib/api";
3
+ import { MODULE_MARKETING_MAP, FEATURE_CATEGORIES } from "@/lib/moduleMap";
 
 
 
 
 
 
 
 
 
 
 
 
4
  import FeatureCard from "@/components/FeatureCard";
5
  import CTASection from "@/components/CTASection";
6
+ import * as Icons from "lucide-react";
7
+ import { type LucideIcon } from "lucide-react";
8
 
9
  export const metadata: Metadata = {
10
  title: "Features — Everything LeadPilot Does",
 
12
  "AI qualification & scoring, multi-channel capture, smart routing, CRM sync, team inbox, analytics, playbooks, and more. The full LeadPilot feature set.",
13
  };
14
 
15
+ function getIcon(iconName: string): LucideIcon {
16
+ if (iconName in Icons) return (Icons as unknown as Record<string, LucideIcon>)[iconName];
17
+ return Icons.Zap;
18
+ }
19
+
20
+ export default async function FeaturesPage() {
21
+ const modules = await getModules();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
+ // Map DB modules to marketing features
24
+ const allFeatures = modules
25
+ .filter((m) => MODULE_MARKETING_MAP[m.key])
26
+ .map((m) => ({
27
+ ...MODULE_MARKETING_MAP[m.key],
28
+ isEnabled: m.is_enabled,
29
+ moduleKey: m.key,
30
+ }));
31
+
32
+ // Categories with their features
33
+ const categoriesWithFeatures = FEATURE_CATEGORIES.map((cat) => ({
34
+ ...cat,
35
+ features: allFeatures.filter((f) => f.category === cat.key),
36
+ })).filter((cat) => cat.features.length > 0);
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
 
38
  return (
39
  <>
40
  {/* ─── HERO ─────────────────────────────────────────────────── */}
 
52
  "radial-gradient(ellipse 60% 50% at 50% 0%, rgba(15,118,110,0.25) 0%, transparent 70%)",
53
  }}
54
  />
 
55
  <div className="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
56
  <p
57
  className="text-xs font-bold uppercase tracking-widest mb-4"
 
89
  >
90
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
91
  <div className="flex flex-wrap items-center gap-2 justify-center">
92
+ {categoriesWithFeatures.map(({ label, iconName }) => {
93
+ const CatIcon = getIcon(iconName);
94
+ const anchorId = label.toLowerCase().replace(/ & /g, "-").replace(/ /g, "-");
95
+ return (
96
+ <a
97
+ key={label}
98
+ href={`#${anchorId}`}
99
+ className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-medium category-link"
100
+ >
101
+ <CatIcon className="w-3.5 h-3.5" aria-hidden="true" />
102
+ {label}
103
+ </a>
104
+ );
105
+ })}
106
  </div>
107
  </div>
108
  </section>
 
114
  aria-labelledby="features-grid-heading"
115
  >
116
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
117
+ <h2 id="features-grid-heading" className="sr-only">All features</h2>
 
 
118
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
119
+ {allFeatures.map((feat) => (
120
+ <FeatureCard
121
+ key={feat.moduleKey}
122
+ iconName={feat.iconName}
123
+ benefit={feat.benefit}
124
+ title={feat.marketingTitle}
125
+ description={feat.description}
126
+ dark
127
+ comingSoon={!feat.isEnabled}
128
+ />
129
  ))}
130
  </div>
131
  </div>
132
  </section>
133
 
134
  {/* ─── CATEGORIES DEEP DIVE ─────────────────────────────────── */}
135
+ {categoriesWithFeatures.map(({ label, iconName, features: catFeats }) => {
136
  const anchorId = label.toLowerCase().replace(/ & /g, "-").replace(/ /g, "-");
137
+ const CatIcon = getIcon(iconName);
138
  return (
139
  <section
140
  key={label}
 
149
  className="w-10 h-10 rounded-xl flex items-center justify-center"
150
  style={{ background: "rgba(15,118,110,0.1)", border: "1px solid rgba(15,118,110,0.2)" }}
151
  >
152
+ <CatIcon className="w-5 h-5" style={{ color: "#0F766E" }} aria-hidden="true" />
153
  </div>
154
  <h2
155
  id={`${anchorId}-heading`}
 
160
  </h2>
161
  </div>
162
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
163
+ {catFeats.map((feat) => (
164
+ <FeatureCard
165
+ key={feat.moduleKey}
166
+ iconName={feat.iconName}
167
+ benefit={feat.benefit}
168
+ title={feat.marketingTitle}
169
+ description={feat.description}
170
+ comingSoon={!feat.isEnabled}
171
+ />
172
+ ))}
173
  </div>
174
  </div>
175
  </section>
Website/src/app/globals.css CHANGED
@@ -291,3 +291,121 @@ body {
291
  linear-gradient(90deg, rgba(15, 118, 110, 0.08) 1px, transparent 1px);
292
  background-size: 40px 40px;
293
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  linear-gradient(90deg, rgba(15, 118, 110, 0.08) 1px, transparent 1px);
292
  background-size: 40px 40px;
293
  }
294
+
295
+ /* ─── Mission 33: Scroll-reveal + Catalog cards ──────────────────── */
296
+
297
+ @keyframes revealUp {
298
+ from { opacity: 0; transform: translateY(30px); }
299
+ to { opacity: 1; transform: translateY(0); }
300
+ }
301
+
302
+ @keyframes shimmer {
303
+ 0% { background-position: -200% 0; }
304
+ 100% { background-position: 200% 0; }
305
+ }
306
+
307
+ /* Initial hidden state for scroll-reveal sections */
308
+ .reveal-hidden {
309
+ opacity: 0;
310
+ transform: translateY(30px);
311
+ }
312
+ .reveal-visible {
313
+ animation: revealUp 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
314
+ }
315
+
316
+ /* Staggered reveals — add delay-N class alongside reveal-hidden */
317
+ .reveal-delay-1 { animation-delay: 0.1s; }
318
+ .reveal-delay-2 { animation-delay: 0.2s; }
319
+ .reveal-delay-3 { animation-delay: 0.3s; }
320
+ .reveal-delay-4 { animation-delay: 0.4s; }
321
+ .reveal-delay-5 { animation-delay: 0.5s; }
322
+
323
+ /* Dark catalog card (templates, integrations) */
324
+ .catalog-card {
325
+ background: rgba(255,255,255,0.03);
326
+ border: 1px solid rgba(255,255,255,0.07);
327
+ border-radius: 1rem;
328
+ transition: border-color 0.3s, background 0.3s, transform 0.3s, box-shadow 0.3s;
329
+ }
330
+ .catalog-card:hover {
331
+ border-color: rgba(15,118,110,0.4);
332
+ background: rgba(15,118,110,0.06);
333
+ transform: translateY(-2px);
334
+ box-shadow: 0 8px 24px rgba(15,118,110,0.12);
335
+ }
336
+
337
+ /* Muted variant for coming-soon / disabled items */
338
+ .catalog-card-muted {
339
+ background: rgba(255,255,255,0.02);
340
+ border: 1px solid rgba(255,255,255,0.05);
341
+ border-radius: 1rem;
342
+ opacity: 0.55;
343
+ transition: opacity 0.3s;
344
+ }
345
+ .catalog-card-muted:hover {
346
+ opacity: 0.7;
347
+ }
348
+
349
+ /* Platform badge pills */
350
+ .badge-platform {
351
+ display: inline-flex;
352
+ align-items: center;
353
+ gap: 4px;
354
+ padding: 2px 8px;
355
+ border-radius: 9999px;
356
+ font-size: 10px;
357
+ font-weight: 600;
358
+ text-transform: uppercase;
359
+ letter-spacing: 0.05em;
360
+ background: rgba(255,255,255,0.06);
361
+ border: 1px solid rgba(255,255,255,0.1);
362
+ color: rgba(148,163,184,0.9);
363
+ }
364
+
365
+ /* Category badge pills */
366
+ .badge-category {
367
+ display: inline-flex;
368
+ align-items: center;
369
+ padding: 2px 8px;
370
+ border-radius: 9999px;
371
+ font-size: 10px;
372
+ font-weight: 700;
373
+ text-transform: uppercase;
374
+ letter-spacing: 0.06em;
375
+ }
376
+ .badge-category[data-category="lead_generation"] {
377
+ background: rgba(20,184,166,0.15);
378
+ color: #14B8A6;
379
+ border: 1px solid rgba(20,184,166,0.3);
380
+ }
381
+ .badge-category[data-category="sales"] {
382
+ background: rgba(59,130,246,0.15);
383
+ color: #60A5FA;
384
+ border: 1px solid rgba(59,130,246,0.3);
385
+ }
386
+ .badge-category[data-category="customer_support"] {
387
+ background: rgba(245,158,11,0.15);
388
+ color: #FBBF24;
389
+ border: 1px solid rgba(245,158,11,0.3);
390
+ }
391
+ .badge-category[data-category="onboarding"] {
392
+ background: rgba(168,85,247,0.15);
393
+ color: #C084FC;
394
+ border: 1px solid rgba(168,85,247,0.3);
395
+ }
396
+
397
+ /* Skeleton loading line */
398
+ .skeleton-line {
399
+ height: 12px;
400
+ border-radius: 6px;
401
+ background: linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.04) 75%);
402
+ background-size: 200% 100%;
403
+ animation: shimmer 1.5s ease-in-out infinite;
404
+ }
405
+
406
+ /* Active category filter tab */
407
+ .category-link-active {
408
+ background: rgba(15,118,110,0.15);
409
+ border: 1px solid rgba(20,184,166,0.4);
410
+ color: var(--secondary);
411
+ }
Website/src/app/integrations/page.tsx ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import Link from "next/link";
3
+ import { ArrowRight, Cable, RefreshCw, Zap } from "lucide-react";
4
+ import { getIntegrationProviders } from "@/lib/api";
5
+ import CTASection from "@/components/CTASection";
6
+
7
+ export const metadata: Metadata = {
8
+ title: "Integrations — Connect Your Entire Stack",
9
+ description:
10
+ "LeadPilot integrates natively with WhatsApp Cloud API, Meta (Instagram/Messenger), and Zoho CRM. Connect, sync, and automate.",
11
+ };
12
+
13
+ const PROVIDER_COLORS: Record<string, string> = {
14
+ whatsapp: "#25D366",
15
+ meta: "#1877F2",
16
+ zoho: "#E42527",
17
+ };
18
+
19
+ const howItWorks = [
20
+ {
21
+ icon: Cable,
22
+ step: "01",
23
+ title: "Connect",
24
+ description:
25
+ "Add your integration credentials from the Integrations Hub. LeadPilot handles authentication and connection health.",
26
+ },
27
+ {
28
+ icon: RefreshCw,
29
+ step: "02",
30
+ title: "Sync",
31
+ description:
32
+ "Leads, contacts, and conversations flow bidirectionally between LeadPilot and your connected platforms in real time.",
33
+ },
34
+ {
35
+ icon: Zap,
36
+ step: "03",
37
+ title: "Automate",
38
+ description:
39
+ "Build automation flows that span platforms — capture on WhatsApp, qualify with AI, push to Zoho CRM. All in one pipeline.",
40
+ },
41
+ ];
42
+
43
+ export default async function IntegrationsPage() {
44
+ const providers = await getIntegrationProviders();
45
+
46
+ return (
47
+ <>
48
+ {/* ─── HERO ─────────────────────────────────────────────────── */}
49
+ <section
50
+ className="relative py-28 overflow-hidden"
51
+ style={{ background: "#0B1320" }}
52
+ aria-labelledby="integrations-hero-heading"
53
+ >
54
+ <div className="absolute inset-0 cockpit-grid opacity-40" aria-hidden="true" />
55
+ <div
56
+ className="absolute inset-0 pointer-events-none"
57
+ aria-hidden="true"
58
+ style={{
59
+ background:
60
+ "radial-gradient(ellipse 60% 50% at 50% 0%, rgba(15,118,110,0.25) 0%, transparent 70%)",
61
+ }}
62
+ />
63
+ <div className="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
64
+ <p
65
+ className="text-xs font-bold uppercase tracking-widest mb-4"
66
+ style={{ color: "#14B8A6" }}
67
+ >
68
+ Integrations
69
+ </p>
70
+ <h1
71
+ id="integrations-hero-heading"
72
+ className="text-4xl sm:text-5xl md:text-6xl font-bold text-white mb-6 tracking-tight"
73
+ >
74
+ Connect your entire stack.
75
+ <br />
76
+ <span className="gradient-text">No code required.</span>
77
+ </h1>
78
+ <p
79
+ className="text-lg max-w-2xl mx-auto leading-relaxed"
80
+ style={{ color: "rgba(148,163,184,0.9)" }}
81
+ >
82
+ Native integrations with the messaging platforms and CRM your revenue team already uses.
83
+ Set up in minutes, stay connected forever.
84
+ </p>
85
+ </div>
86
+ </section>
87
+
88
+ {/* ─── PROVIDER CARDS ───────────────────────────────────────── */}
89
+ <section
90
+ className="py-24"
91
+ style={{ background: "#0B1320" }}
92
+ aria-labelledby="providers-heading"
93
+ >
94
+ <div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
95
+ <h2 id="providers-heading" className="sr-only">Available integrations</h2>
96
+
97
+ {providers.length === 0 ? (
98
+ <p className="text-center text-lg" style={{ color: "#64748B" }}>
99
+ Integrations are loading. Check back soon.
100
+ </p>
101
+ ) : (
102
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
103
+ {providers.map((p) => {
104
+ const color = PROVIDER_COLORS[p.key] ?? "#0F766E";
105
+ return (
106
+ <div
107
+ key={p.key}
108
+ className="catalog-card p-8 flex flex-col"
109
+ >
110
+ {/* Icon */}
111
+ <div
112
+ className="w-14 h-14 rounded-xl flex items-center justify-center mb-5"
113
+ style={{
114
+ background: `${color}15`,
115
+ border: `1px solid ${color}30`,
116
+ }}
117
+ >
118
+ <span className="text-2xl font-bold" style={{ color }}>
119
+ {p.label.charAt(0)}
120
+ </span>
121
+ </div>
122
+
123
+ {/* Name + description */}
124
+ <h3 className="text-white font-bold text-lg mb-2">{p.label}</h3>
125
+ <p
126
+ className="text-sm leading-relaxed mb-5 flex-1"
127
+ style={{ color: "rgba(148,163,184,0.85)" }}
128
+ >
129
+ {p.description}
130
+ </p>
131
+
132
+ {/* Required fields */}
133
+ {p.fields.length > 0 && (
134
+ <div>
135
+ <p className="text-[10px] font-semibold uppercase tracking-wider mb-2" style={{ color: "#475569" }}>
136
+ What you need
137
+ </p>
138
+ <ul className="space-y-1">
139
+ {p.fields.map((f) => (
140
+ <li key={f.name} className="text-xs" style={{ color: "#94A3B8" }}>
141
+ • {f.label}
142
+ </li>
143
+ ))}
144
+ </ul>
145
+ </div>
146
+ )}
147
+ </div>
148
+ );
149
+ })}
150
+ </div>
151
+ )}
152
+ </div>
153
+ </section>
154
+
155
+ {/* ─── HOW IT WORKS ─────────────────────────────────────────── */}
156
+ <section
157
+ className="py-24 border-t"
158
+ style={{ background: "#0E1826", borderColor: "rgba(255,255,255,0.06)" }}
159
+ aria-labelledby="how-integrations-work"
160
+ >
161
+ <div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
162
+ <div className="text-center mb-16">
163
+ <p
164
+ className="text-xs font-bold uppercase tracking-widest mb-3"
165
+ style={{ color: "#14B8A6" }}
166
+ >
167
+ How It Works
168
+ </p>
169
+ <h2
170
+ id="how-integrations-work"
171
+ className="text-3xl sm:text-4xl font-bold text-white tracking-tight"
172
+ >
173
+ Three steps to a connected pipeline
174
+ </h2>
175
+ </div>
176
+
177
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
178
+ {howItWorks.map(({ icon: Icon, step, title, description }) => (
179
+ <div key={step} className="text-center">
180
+ <div
181
+ className="w-12 h-12 rounded-xl flex items-center justify-center mx-auto mb-5"
182
+ style={{
183
+ background: "rgba(15,118,110,0.15)",
184
+ border: "1px solid rgba(20,184,166,0.3)",
185
+ }}
186
+ >
187
+ <Icon className="w-5 h-5" style={{ color: "#14B8A6" }} aria-hidden="true" />
188
+ </div>
189
+ <span
190
+ className="text-xl font-bold block mb-2"
191
+ style={{
192
+ background: "linear-gradient(135deg, #0F766E, #14B8A6)",
193
+ WebkitBackgroundClip: "text",
194
+ WebkitTextFillColor: "transparent",
195
+ backgroundClip: "text",
196
+ }}
197
+ >
198
+ {step}
199
+ </span>
200
+ <h3 className="text-white font-semibold text-base mb-2">{title}</h3>
201
+ <p className="text-sm leading-relaxed" style={{ color: "#94A3B8" }}>
202
+ {description}
203
+ </p>
204
+ </div>
205
+ ))}
206
+ </div>
207
+
208
+ <div className="text-center mt-12">
209
+ <Link
210
+ href="/contact"
211
+ className="inline-flex items-center gap-2 text-sm font-semibold text-link-teal"
212
+ >
213
+ Need help connecting? Contact us
214
+ <ArrowRight className="w-4 h-4" aria-hidden="true" />
215
+ </Link>
216
+ </div>
217
+ </div>
218
+ </section>
219
+
220
+ {/* ─── CTA ──────────────────────────────────────────────────── */}
221
+ <CTASection
222
+ headline="Ready to connect your stack?"
223
+ subheadline="Set up integrations in minutes and start automating your lead pipeline today."
224
+ primaryLabel="Book a Demo"
225
+ primaryHref="/contact"
226
+ secondaryLabel="See Templates"
227
+ secondaryHref="/templates"
228
+ />
229
+ </>
230
+ );
231
+ }
Website/src/app/page.tsx CHANGED
@@ -1,12 +1,6 @@
1
  import type { Metadata } from "next";
2
  import Link from "next/link";
3
  import {
4
- Zap,
5
- Target,
6
- GitMerge,
7
- BarChart3,
8
- MessageSquare,
9
- Shield,
10
  ArrowRight,
11
  CheckCircle,
12
  Users,
@@ -16,6 +10,8 @@ import {
16
  import CTASection from "@/components/CTASection";
17
  import FeatureCard from "@/components/FeatureCard";
18
  import PlanCard from "@/components/PlanCard";
 
 
19
 
20
  export const metadata: Metadata = {
21
  title: "LeadPilot — AI-Native Lead Capture, Qualification & Routing",
@@ -23,136 +19,65 @@ export const metadata: Metadata = {
23
  "LeadPilot is the AI-native platform that captures, qualifies, and routes leads to the right rep automatically. Close more deals without growing headcount.",
24
  };
25
 
26
- const features = [
27
- {
28
- icon: Zap,
29
- benefit: "Convert faster",
30
- title: "AI Lead Qualification",
31
- description:
32
- "Custom AI prompts score and qualify every inbound lead in seconds. No manual filtering, no missed opportunities.",
33
- },
34
- {
35
- icon: Target,
36
- benefit: "Zero leakage",
37
- title: "Multi-Channel Capture",
38
- description:
39
- "Capture leads from web forms, chat widgets, WhatsApp, and more — all routed into one unified pipeline.",
40
- },
41
- {
42
- icon: GitMerge,
43
- benefit: "Right rep, right time",
44
- title: "Smart Routing Rules",
45
- description:
46
- "Route leads by territory, product line, availability, or any custom logic you define. Instant assignment.",
47
- },
48
- {
49
- icon: BarChart3,
50
- benefit: "Improve decisions",
51
- title: "Conversion Analytics",
52
- description:
53
- "Track lead-to-close conversion rates, response times, and team performance across the full funnel.",
54
- },
55
- {
56
- icon: MessageSquare,
57
- benefit: "Stay human",
58
- title: "Team Inbox & Handover",
59
- description:
60
- "AI handles first touch and qualification; your team steps in exactly when a human connection matters.",
61
- },
62
- {
63
- icon: Shield,
64
- benefit: "Audit-ready",
65
- title: "Activity Log & Access Control",
66
- description:
67
- "Role-based permissions and full audit trail. Know who touched what, and when.",
68
- },
69
- ];
70
 
71
  const steps = [
72
- {
73
- number: "01",
74
- title: "Capture",
75
- description:
76
- "Leads arrive from any channel your forms, chat, or integrations. LeadPilot ingests them all instantly.",
77
- },
78
- {
79
- number: "02",
80
- title: "Qualify",
81
- description:
82
- "Our AI engages, asks the right questions, and scores every lead based on your ideal customer profile.",
83
- },
84
- {
85
- number: "03",
86
- title: "Route",
87
- description:
88
- "Qualified leads are dispatched to the right rep or team — automatically, with zero manual hand-off.",
89
- },
90
- {
91
- number: "04",
92
- title: "Follow Up",
93
- description:
94
- "Automated follow-up sequences keep leads warm while your team focuses on closing.",
95
- },
96
- {
97
- number: "05",
98
- title: "Report",
99
- description:
100
- "Real-time dashboards show conversion rates, pipeline velocity, and where you're losing leads.",
101
- },
102
  ];
103
 
104
- const plans = [
105
- {
106
- name: "Free",
107
- tagline: "Start capturing leads today",
108
- price: "$0",
109
- priceNote: "/ month",
110
- features: [
111
- "Basic lead capture (up to a limited number of leads)",
112
- "AI qualification prompts",
113
- "Manual routing",
114
- "Email notifications",
115
- "Basic analytics dashboard",
116
- "1 team seat",
117
- ],
118
- ctaLabel: "Get Started Free",
119
- ctaHref: "/plans",
120
- available: true,
121
- highlighted: true,
122
- },
123
- {
124
- name: "Growth",
125
- tagline: "For growing sales teams",
126
- price: "Coming Soon",
127
- features: [
128
- "Everything in Free",
129
- "Unlimited lead capture",
130
- "Advanced AI qualification",
131
- "Automated routing rules",
132
- "CRM integrations",
133
- "Up to 5 team seats",
134
- ],
135
- ctaLabel: "Coming Soon",
136
- available: false,
137
- },
138
- {
139
- name: "Pro",
140
- tagline: "For high-volume pipelines",
141
- price: "Coming Soon",
142
- features: [
143
- "Everything in Growth",
144
- "Multi-channel capture",
145
- "Custom AI playbooks",
146
- "Team inbox & handover",
147
- "Priority support",
148
- "Unlimited seats",
149
- ],
150
- ctaLabel: "Coming Soon",
151
- available: false,
152
- },
153
- ];
154
 
155
- export default function HomePage() {
156
  return (
157
  <>
158
  {/* ─── HERO ─────────────────────────────────────────────────── */}
@@ -161,56 +86,36 @@ export default function HomePage() {
161
  style={{ background: "#0B1320" }}
162
  aria-labelledby="hero-heading"
163
  >
164
- {/* Cockpit grid background */}
165
  <div className="absolute inset-0 cockpit-grid opacity-60" aria-hidden="true" />
166
-
167
- {/* Radial glow */}
168
  <div
169
  className="absolute inset-0 pointer-events-none"
170
  aria-hidden="true"
171
  style={{
172
- background:
173
- "radial-gradient(ellipse 70% 60% at 50% 0%, rgba(15,118,110,0.3) 0%, transparent 65%)",
174
  }}
175
  />
176
-
177
- {/* Subtle side glow */}
178
  <div
179
  className="absolute top-1/3 left-0 w-64 h-64 rounded-full pointer-events-none"
180
  aria-hidden="true"
181
- style={{
182
- background: "rgba(15,118,110,0.12)",
183
- filter: "blur(80px)",
184
- }}
185
  />
186
  <div
187
  className="absolute top-1/3 right-0 w-64 h-64 rounded-full pointer-events-none"
188
  aria-hidden="true"
189
- style={{
190
- background: "rgba(20,184,166,0.1)",
191
- filter: "blur(80px)",
192
- }}
193
  />
194
 
195
  <div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-32 text-center">
196
- {/* Badge */}
197
- <div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full mb-8"
198
- style={{
199
- background: "rgba(15,118,110,0.15)",
200
- border: "1px solid rgba(20,184,166,0.3)",
201
- }}
202
  >
203
- <span
204
- className="w-2 h-2 rounded-full animate-pulse-glow"
205
- style={{ background: "#14B8A6" }}
206
- aria-hidden="true"
207
- />
208
  <span className="text-xs font-semibold tracking-wider" style={{ color: "#14B8A6" }}>
209
  AI-NATIVE LEAD PLATFORM
210
  </span>
211
  </div>
212
 
213
- {/* Headline */}
214
  <h1
215
  id="hero-heading"
216
  className="text-5xl sm:text-6xl md:text-7xl font-bold text-white mb-6 tracking-tight leading-none"
@@ -229,10 +134,7 @@ export default function HomePage() {
229
  </p>
230
 
231
  <div className="flex flex-col sm:flex-row items-center justify-center gap-4">
232
- <Link
233
- href="/contact"
234
- className="px-8 py-4 rounded-xl font-semibold text-base btn-primary"
235
- >
236
  Book a Demo
237
  </Link>
238
  <Link
@@ -244,7 +146,6 @@ export default function HomePage() {
244
  </Link>
245
  </div>
246
 
247
- {/* Trust stats */}
248
  <div className="mt-20 grid grid-cols-3 gap-4 max-w-lg mx-auto">
249
  {[
250
  { icon: TrendingUp, label: "More pipeline", sub: "per rep" },
@@ -260,51 +161,26 @@ export default function HomePage() {
260
  </div>
261
  </div>
262
 
263
- {/* Scroll indicator */}
264
- <div
265
- className="absolute bottom-8 left-1/2 -translate-x-1/2 flex flex-col items-center gap-1"
266
- aria-hidden="true"
267
- >
268
  <span className="text-xs" style={{ color: "#475569" }}>scroll</span>
269
- <div
270
- className="w-px h-8 animate-pulse"
271
- style={{ background: "linear-gradient(to bottom, #0F766E, transparent)" }}
272
- />
273
  </div>
274
  </section>
275
 
276
  {/* ─── SOCIAL PROOF STRIP ───────────────────────────────────── */}
277
  <section
278
  className="py-12 border-y"
279
- style={{
280
- background: "#0E1826",
281
- borderColor: "rgba(255,255,255,0.06)",
282
- }}
283
  aria-label="Trusted by teams across industries"
284
  >
285
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
286
- <p
287
- className="text-center text-xs font-semibold uppercase tracking-widest mb-8"
288
- style={{ color: "#475569" }}
289
- >
290
  Trusted by revenue teams in
291
  </p>
292
  <div className="grid grid-cols-3 md:grid-cols-6 gap-8 items-center">
293
- {[
294
- "Real Estate",
295
- "Healthcare Clinics",
296
- "Marketing Agencies",
297
- "B2B Services",
298
- "Automotive",
299
- "Retail & SMB",
300
- ].map((industry) => (
301
  <div key={industry} className="text-center">
302
- <span
303
- className="text-sm font-medium"
304
- style={{ color: "rgba(148,163,184,0.5)" }}
305
- >
306
- {industry}
307
- </span>
308
  </div>
309
  ))}
310
  </div>
@@ -319,19 +195,9 @@ export default function HomePage() {
319
  >
320
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
321
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
322
- {/* Problem */}
323
  <div>
324
- <p
325
- className="text-xs font-bold uppercase tracking-widest mb-3"
326
- style={{ color: "#EF4444" }}
327
- >
328
- The Problem
329
- </p>
330
- <h2
331
- id="problem-heading"
332
- className="text-3xl sm:text-4xl font-bold mb-6 tracking-tight"
333
- style={{ color: "#0F172A" }}
334
- >
335
  Leads slip through the cracks every day.
336
  </h2>
337
  <ul className="space-y-4">
@@ -342,31 +208,15 @@ export default function HomePage() {
342
  "You can't see where the pipeline breaks without real-time data.",
343
  ].map((point) => (
344
  <li key={point} className="flex items-start gap-3">
345
- <div
346
- className="mt-1.5 w-1.5 h-1.5 rounded-full shrink-0"
347
- style={{ background: "#EF4444" }}
348
- aria-hidden="true"
349
- />
350
- <p className="text-sm leading-relaxed" style={{ color: "#64748B" }}>
351
- {point}
352
- </p>
353
  </li>
354
  ))}
355
  </ul>
356
  </div>
357
-
358
- {/* Solution */}
359
  <div>
360
- <p
361
- className="text-xs font-bold uppercase tracking-widest mb-3"
362
- style={{ color: "#0F766E" }}
363
- >
364
- The Solution
365
- </p>
366
- <h2
367
- className="text-3xl sm:text-4xl font-bold mb-6 tracking-tight"
368
- style={{ color: "#0F172A" }}
369
- >
370
  LeadPilot handles the pipeline. You handle the close.
371
  </h2>
372
  <ul className="space-y-4">
@@ -377,21 +227,12 @@ export default function HomePage() {
377
  "Real-time analytics show you exactly where revenue is being left behind.",
378
  ].map((point) => (
379
  <li key={point} className="flex items-start gap-3">
380
- <CheckCircle
381
- className="w-4 h-4 mt-0.5 shrink-0"
382
- style={{ color: "#0F766E" }}
383
- aria-hidden="true"
384
- />
385
- <p className="text-sm leading-relaxed" style={{ color: "#64748B" }}>
386
- {point}
387
- </p>
388
  </li>
389
  ))}
390
  </ul>
391
- <Link
392
- href="/product"
393
- className="inline-flex items-center gap-2 mt-8 text-sm font-semibold text-link-primary"
394
- >
395
  See how it works
396
  <ArrowRight className="w-4 h-4" aria-hidden="true" />
397
  </Link>
@@ -409,67 +250,40 @@ export default function HomePage() {
409
  <div
410
  className="absolute inset-0 pointer-events-none"
411
  aria-hidden="true"
412
- style={{
413
- background:
414
- "radial-gradient(ellipse 50% 60% at 50% 100%, rgba(15,118,110,0.2) 0%, transparent 70%)",
415
- }}
416
  />
417
  <div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
418
  <div className="text-center mb-16">
419
- <p
420
- className="text-xs font-bold uppercase tracking-widest mb-3"
421
- style={{ color: "#14B8A6" }}
422
- >
423
- How It Works
424
- </p>
425
- <h2
426
- id="how-it-works-heading"
427
- className="text-3xl sm:text-4xl font-bold text-white tracking-tight"
428
- >
429
  Five steps from lead to revenue
430
  </h2>
431
  </div>
432
-
433
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-6">
434
  {steps.map((step, i) => (
435
  <div
436
  key={step.number}
437
  className="relative rounded-xl p-6"
438
- style={{
439
- background: "rgba(255,255,255,0.03)",
440
- border: "1px solid rgba(255,255,255,0.07)",
441
- }}
442
  >
443
- {/* Connector line */}
444
  {i < steps.length - 1 && (
445
- <div
446
- className="hidden lg:block absolute top-10 -right-3 w-6 h-px"
447
- aria-hidden="true"
448
- style={{ background: "rgba(15,118,110,0.4)" }}
449
- />
450
  )}
451
  <span
452
  className="text-3xl font-bold block mb-4"
453
- style={{
454
- background: "linear-gradient(135deg, #0F766E, #14B8A6)",
455
- WebkitBackgroundClip: "text",
456
- WebkitTextFillColor: "transparent",
457
- backgroundClip: "text",
458
- }}
459
  >
460
  {step.number}
461
  </span>
462
  <h3 className="text-white font-semibold mb-2">{step.title}</h3>
463
- <p className="text-xs leading-relaxed" style={{ color: "#64748B" }}>
464
- {step.description}
465
- </p>
466
  </div>
467
  ))}
468
  </div>
469
  </div>
470
  </section>
471
 
472
- {/* ─── FEATURE HIGHLIGHTS ───────────────────────────────────── */}
473
  <section
474
  className="py-24"
475
  style={{ background: "#F8FAFC" }}
@@ -477,35 +291,27 @@ export default function HomePage() {
477
  >
478
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
479
  <div className="text-center mb-16">
480
- <p
481
- className="text-xs font-bold uppercase tracking-widest mb-3"
482
- style={{ color: "#0F766E" }}
483
- >
484
- Platform Features
485
- </p>
486
- <h2
487
- id="features-heading"
488
- className="text-3xl sm:text-4xl font-bold tracking-tight"
489
- style={{ color: "#0F172A" }}
490
- >
491
  Everything your revenue team needs
492
  </h2>
493
  <p className="mt-4 text-base max-w-xl mx-auto" style={{ color: "#64748B" }}>
494
  One platform for the entire lead lifecycle — from first touch to closed deal.
495
  </p>
496
  </div>
497
-
498
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
499
- {features.map((feat) => (
500
- <FeatureCard key={feat.title} {...feat} />
 
 
 
 
 
 
501
  ))}
502
  </div>
503
-
504
  <div className="text-center mt-10">
505
- <Link
506
- href="/features"
507
- className="inline-flex items-center gap-2 text-sm font-semibold text-link-primary"
508
- >
509
  View all features
510
  <ArrowRight className="w-4 h-4" aria-hidden="true" />
511
  </Link>
@@ -513,7 +319,115 @@ export default function HomePage() {
513
  </div>
514
  </section>
515
 
516
- {/* ─── MINI PLANS PREVIEW ───────────────────────────────────── */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  <section
518
  className="py-24 relative overflow-hidden"
519
  style={{ background: "#0B1320" }}
@@ -522,43 +436,49 @@ export default function HomePage() {
522
  <div
523
  className="absolute inset-0 pointer-events-none"
524
  aria-hidden="true"
525
- style={{
526
- background:
527
- "radial-gradient(ellipse 60% 50% at 50% 50%, rgba(15,118,110,0.15) 0%, transparent 70%)",
528
- }}
529
  />
530
  <div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
531
  <div className="text-center mb-16">
532
- <p
533
- className="text-xs font-bold uppercase tracking-widest mb-3"
534
- style={{ color: "#14B8A6" }}
535
- >
536
- Plans
537
- </p>
538
- <h2
539
- id="plans-preview-heading"
540
- className="text-3xl sm:text-4xl font-bold text-white tracking-tight"
541
- >
542
  Start free. Scale when ready.
543
  </h2>
544
  </div>
545
 
546
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-4xl mx-auto">
547
- {plans.map((plan) => (
548
- <PlanCard key={plan.name} {...plan} />
549
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  </div>
551
 
552
- <p className="text-center mt-8 text-sm" style={{ color: "#475569" }}>
553
- Join the waitlist for Growth and Pro —{" "}
554
- <Link
555
- href="/plans"
556
- className="underline underline-offset-2 transition-colors duration-200"
557
- style={{ color: "#14B8A6" }}
558
- >
559
- see Plans page
560
- </Link>
561
- </p>
562
  </div>
563
  </section>
564
 
 
1
  import type { Metadata } from "next";
2
  import Link from "next/link";
3
  import {
 
 
 
 
 
 
4
  ArrowRight,
5
  CheckCircle,
6
  Users,
 
10
  import CTASection from "@/components/CTASection";
11
  import FeatureCard from "@/components/FeatureCard";
12
  import PlanCard from "@/components/PlanCard";
13
+ import { getPlans, getModules, getIntegrationProviders, getPublicTemplates } from "@/lib/api";
14
+ import { MODULE_MARKETING_MAP } from "@/lib/moduleMap";
15
 
16
  export const metadata: Metadata = {
17
  title: "LeadPilot — AI-Native Lead Capture, Qualification & Routing",
 
19
  "LeadPilot is the AI-native platform that captures, qualifies, and routes leads to the right rep automatically. Close more deals without growing headcount.",
20
  };
21
 
22
+ // ── Pricing lookup (same as plans page) ─────────────────────────────
23
+
24
+ const PLAN_PRICING: Record<string, { price: string; priceNote: string; tagline: string; available: boolean }> = {
25
+ free: { price: "$0", priceNote: "/ month", tagline: "Start capturing leads today", available: true },
26
+ growth: { price: "Coming Soon", priceNote: "", tagline: "For growing sales teams", available: false },
27
+ enterprise: { price: "Contact Us", priceNote: "", tagline: "Custom deployment for large organisations", available: false },
28
+ };
29
+
30
+ function entitlementToString(moduleKey: string, hardLimit: number | null): string | null {
31
+ const info = MODULE_MARKETING_MAP[moduleKey];
32
+ if (!info) return null;
33
+ if (hardLimit === null) return `${info.marketingTitle} (Unlimited)`;
34
+ if (hardLimit > 0) return `${info.marketingTitle} (up to ${hardLimit.toLocaleString()})`;
35
+ return info.marketingTitle;
36
+ }
37
+
38
+ // ── Static content (narrative, not feature claims) ──────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  const steps = [
41
+ { number: "01", title: "Capture", description: "Leads arrive from any channel — your forms, chat, or integrations. LeadPilot ingests them all instantly." },
42
+ { number: "02", title: "Qualify", description: "Our AI engages, asks the right questions, and scores every lead based on your ideal customer profile." },
43
+ { number: "03", title: "Route", description: "Qualified leads are dispatched to the right rep or team — automatically, with zero manual hand-off." },
44
+ { number: "04", title: "Follow Up", description: "Automated follow-up sequences keep leads warm while your team focuses on closing." },
45
+ { number: "05", title: "Report", description: "Real-time dashboards show conversion rates, pipeline velocity, and where you're losing leads." },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  ];
47
 
48
+ // ── Provider icon map ───────────────────────────────────────────────
49
+
50
+ const PROVIDER_COLORS: Record<string, string> = {
51
+ whatsapp: "#25D366",
52
+ meta: "#1877F2",
53
+ zoho: "#E42527",
54
+ };
55
+
56
+ export default async function HomePage() {
57
+ const [plans, modules, providers, templates] = await Promise.all([
58
+ getPlans(),
59
+ getModules(),
60
+ getIntegrationProviders(),
61
+ getPublicTemplates(),
62
+ ]);
63
+
64
+ // Feature highlights — up to 6 enabled modules
65
+ const featureHighlights = modules
66
+ .filter((m) => MODULE_MARKETING_MAP[m.key] && m.is_enabled)
67
+ .slice(0, 6)
68
+ .map((m) => ({
69
+ ...MODULE_MARKETING_MAP[m.key],
70
+ moduleKey: m.key,
71
+ }));
72
+
73
+ // Featured templates
74
+ const featuredTemplates = templates.filter((t) => t.is_featured).slice(0, 5);
75
+
76
+ // Waitlist plan names
77
+ const waitlistNames = plans
78
+ .filter((p) => !PLAN_PRICING[p.name]?.available)
79
+ .map((p) => p.display_name);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
 
81
  return (
82
  <>
83
  {/* ─── HERO ─────────────────────────────────────────────────── */}
 
86
  style={{ background: "#0B1320" }}
87
  aria-labelledby="hero-heading"
88
  >
 
89
  <div className="absolute inset-0 cockpit-grid opacity-60" aria-hidden="true" />
 
 
90
  <div
91
  className="absolute inset-0 pointer-events-none"
92
  aria-hidden="true"
93
  style={{
94
+ background: "radial-gradient(ellipse 70% 60% at 50% 0%, rgba(15,118,110,0.3) 0%, transparent 65%)",
 
95
  }}
96
  />
 
 
97
  <div
98
  className="absolute top-1/3 left-0 w-64 h-64 rounded-full pointer-events-none"
99
  aria-hidden="true"
100
+ style={{ background: "rgba(15,118,110,0.12)", filter: "blur(80px)" }}
 
 
 
101
  />
102
  <div
103
  className="absolute top-1/3 right-0 w-64 h-64 rounded-full pointer-events-none"
104
  aria-hidden="true"
105
+ style={{ background: "rgba(20,184,166,0.1)", filter: "blur(80px)" }}
 
 
 
106
  />
107
 
108
  <div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-32 text-center">
109
+ <div
110
+ className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full mb-8"
111
+ style={{ background: "rgba(15,118,110,0.15)", border: "1px solid rgba(20,184,166,0.3)" }}
 
 
 
112
  >
113
+ <span className="w-2 h-2 rounded-full animate-pulse-glow" style={{ background: "#14B8A6" }} aria-hidden="true" />
 
 
 
 
114
  <span className="text-xs font-semibold tracking-wider" style={{ color: "#14B8A6" }}>
115
  AI-NATIVE LEAD PLATFORM
116
  </span>
117
  </div>
118
 
 
119
  <h1
120
  id="hero-heading"
121
  className="text-5xl sm:text-6xl md:text-7xl font-bold text-white mb-6 tracking-tight leading-none"
 
134
  </p>
135
 
136
  <div className="flex flex-col sm:flex-row items-center justify-center gap-4">
137
+ <Link href="/contact" className="px-8 py-4 rounded-xl font-semibold text-base btn-primary">
 
 
 
138
  Book a Demo
139
  </Link>
140
  <Link
 
146
  </Link>
147
  </div>
148
 
 
149
  <div className="mt-20 grid grid-cols-3 gap-4 max-w-lg mx-auto">
150
  {[
151
  { icon: TrendingUp, label: "More pipeline", sub: "per rep" },
 
161
  </div>
162
  </div>
163
 
164
+ <div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex flex-col items-center gap-1" aria-hidden="true">
 
 
 
 
165
  <span className="text-xs" style={{ color: "#475569" }}>scroll</span>
166
+ <div className="w-px h-8 animate-pulse" style={{ background: "linear-gradient(to bottom, #0F766E, transparent)" }} />
 
 
 
167
  </div>
168
  </section>
169
 
170
  {/* ─── SOCIAL PROOF STRIP ───────────────────────────────────── */}
171
  <section
172
  className="py-12 border-y"
173
+ style={{ background: "#0E1826", borderColor: "rgba(255,255,255,0.06)" }}
 
 
 
174
  aria-label="Trusted by teams across industries"
175
  >
176
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
177
+ <p className="text-center text-xs font-semibold uppercase tracking-widest mb-8" style={{ color: "#475569" }}>
 
 
 
178
  Trusted by revenue teams in
179
  </p>
180
  <div className="grid grid-cols-3 md:grid-cols-6 gap-8 items-center">
181
+ {["Real Estate", "Healthcare Clinics", "Marketing Agencies", "B2B Services", "Automotive", "Retail & SMB"].map((industry) => (
 
 
 
 
 
 
 
182
  <div key={industry} className="text-center">
183
+ <span className="text-sm font-medium" style={{ color: "rgba(148,163,184,0.5)" }}>{industry}</span>
 
 
 
 
 
184
  </div>
185
  ))}
186
  </div>
 
195
  >
196
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
197
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
 
198
  <div>
199
+ <p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: "#EF4444" }}>The Problem</p>
200
+ <h2 id="problem-heading" className="text-3xl sm:text-4xl font-bold mb-6 tracking-tight" style={{ color: "#0F172A" }}>
 
 
 
 
 
 
 
 
 
201
  Leads slip through the cracks every day.
202
  </h2>
203
  <ul className="space-y-4">
 
208
  "You can't see where the pipeline breaks without real-time data.",
209
  ].map((point) => (
210
  <li key={point} className="flex items-start gap-3">
211
+ <div className="mt-1.5 w-1.5 h-1.5 rounded-full shrink-0" style={{ background: "#EF4444" }} aria-hidden="true" />
212
+ <p className="text-sm leading-relaxed" style={{ color: "#64748B" }}>{point}</p>
 
 
 
 
 
 
213
  </li>
214
  ))}
215
  </ul>
216
  </div>
 
 
217
  <div>
218
+ <p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: "#0F766E" }}>The Solution</p>
219
+ <h2 className="text-3xl sm:text-4xl font-bold mb-6 tracking-tight" style={{ color: "#0F172A" }}>
 
 
 
 
 
 
 
 
220
  LeadPilot handles the pipeline. You handle the close.
221
  </h2>
222
  <ul className="space-y-4">
 
227
  "Real-time analytics show you exactly where revenue is being left behind.",
228
  ].map((point) => (
229
  <li key={point} className="flex items-start gap-3">
230
+ <CheckCircle className="w-4 h-4 mt-0.5 shrink-0" style={{ color: "#0F766E" }} aria-hidden="true" />
231
+ <p className="text-sm leading-relaxed" style={{ color: "#64748B" }}>{point}</p>
 
 
 
 
 
 
232
  </li>
233
  ))}
234
  </ul>
235
+ <Link href="/product" className="inline-flex items-center gap-2 mt-8 text-sm font-semibold text-link-primary">
 
 
 
236
  See how it works
237
  <ArrowRight className="w-4 h-4" aria-hidden="true" />
238
  </Link>
 
250
  <div
251
  className="absolute inset-0 pointer-events-none"
252
  aria-hidden="true"
253
+ style={{ background: "radial-gradient(ellipse 50% 60% at 50% 100%, rgba(15,118,110,0.2) 0%, transparent 70%)" }}
 
 
 
254
  />
255
  <div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
256
  <div className="text-center mb-16">
257
+ <p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: "#14B8A6" }}>How It Works</p>
258
+ <h2 id="how-it-works-heading" className="text-3xl sm:text-4xl font-bold text-white tracking-tight">
 
 
 
 
 
 
 
 
259
  Five steps from lead to revenue
260
  </h2>
261
  </div>
 
262
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-6">
263
  {steps.map((step, i) => (
264
  <div
265
  key={step.number}
266
  className="relative rounded-xl p-6"
267
+ style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.07)" }}
 
 
 
268
  >
 
269
  {i < steps.length - 1 && (
270
+ <div className="hidden lg:block absolute top-10 -right-3 w-6 h-px" aria-hidden="true" style={{ background: "rgba(15,118,110,0.4)" }} />
 
 
 
 
271
  )}
272
  <span
273
  className="text-3xl font-bold block mb-4"
274
+ style={{ background: "linear-gradient(135deg, #0F766E, #14B8A6)", WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", backgroundClip: "text" }}
 
 
 
 
 
275
  >
276
  {step.number}
277
  </span>
278
  <h3 className="text-white font-semibold mb-2">{step.title}</h3>
279
+ <p className="text-xs leading-relaxed" style={{ color: "#64748B" }}>{step.description}</p>
 
 
280
  </div>
281
  ))}
282
  </div>
283
  </div>
284
  </section>
285
 
286
+ {/* ─── FEATURE HIGHLIGHTS (DB-driven) ───────────────────────── */}
287
  <section
288
  className="py-24"
289
  style={{ background: "#F8FAFC" }}
 
291
  >
292
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
293
  <div className="text-center mb-16">
294
+ <p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: "#0F766E" }}>Platform Features</p>
295
+ <h2 id="features-heading" className="text-3xl sm:text-4xl font-bold tracking-tight" style={{ color: "#0F172A" }}>
 
 
 
 
 
 
 
 
 
296
  Everything your revenue team needs
297
  </h2>
298
  <p className="mt-4 text-base max-w-xl mx-auto" style={{ color: "#64748B" }}>
299
  One platform for the entire lead lifecycle — from first touch to closed deal.
300
  </p>
301
  </div>
 
302
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
303
+ {featureHighlights.map((feat) => (
304
+ <FeatureCard
305
+ key={feat.moduleKey}
306
+ iconName={feat.iconName}
307
+ benefit={feat.benefit}
308
+ title={feat.marketingTitle}
309
+ description={feat.description}
310
+ />
311
  ))}
312
  </div>
 
313
  <div className="text-center mt-10">
314
+ <Link href="/features" className="inline-flex items-center gap-2 text-sm font-semibold text-link-primary">
 
 
 
315
  View all features
316
  <ArrowRight className="w-4 h-4" aria-hidden="true" />
317
  </Link>
 
319
  </div>
320
  </section>
321
 
322
+ {/* ─── TEMPLATES TEASER (DB-driven) ─────────────────────────── */}
323
+ {featuredTemplates.length > 0 && (
324
+ <section
325
+ className="py-24 relative overflow-hidden"
326
+ style={{ background: "#0B1320" }}
327
+ aria-labelledby="templates-teaser-heading"
328
+ >
329
+ <div className="absolute inset-0 cockpit-grid opacity-30" aria-hidden="true" />
330
+ <div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
331
+ <div className="text-center mb-16">
332
+ <p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: "#14B8A6" }}>
333
+ Templates
334
+ </p>
335
+ <h2 id="templates-teaser-heading" className="text-3xl sm:text-4xl font-bold text-white tracking-tight">
336
+ Ready-made automation flows
337
+ </h2>
338
+ <p className="mt-4 text-base max-w-xl mx-auto" style={{ color: "#64748B" }}>
339
+ Clone a template, customise it for your business, and go live in minutes.
340
+ </p>
341
+ </div>
342
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4">
343
+ {featuredTemplates.map((t) => (
344
+ <div key={t.id} className="catalog-card p-5">
345
+ <span className="badge-category" data-category={t.category}>
346
+ {t.category.replace(/_/g, " ")}
347
+ </span>
348
+ <h3 className="text-white font-semibold text-sm mt-3 mb-1">{t.name}</h3>
349
+ <p className="text-xs leading-relaxed line-clamp-2" style={{ color: "rgba(148,163,184,0.8)" }}>
350
+ {t.description}
351
+ </p>
352
+ <div className="flex flex-wrap gap-1.5 mt-3">
353
+ {t.platforms.map((p) => (
354
+ <span key={p} className="badge-platform">{p}</span>
355
+ ))}
356
+ </div>
357
+ {t.clone_count > 0 && (
358
+ <p className="text-[10px] mt-2" style={{ color: "#475569" }}>
359
+ {t.clone_count.toLocaleString()} clones
360
+ </p>
361
+ )}
362
+ </div>
363
+ ))}
364
+ </div>
365
+ <div className="text-center mt-10">
366
+ <Link href="/templates" className="inline-flex items-center gap-2 text-sm font-semibold text-link-teal">
367
+ Browse all templates
368
+ <ArrowRight className="w-4 h-4" aria-hidden="true" />
369
+ </Link>
370
+ </div>
371
+ </div>
372
+ </section>
373
+ )}
374
+
375
+ {/* ─── INTEGRATIONS TEASER (DB-driven) ──────────────────────── */}
376
+ {providers.length > 0 && (
377
+ <section
378
+ className="py-24"
379
+ style={{ background: "#F8FAFC" }}
380
+ aria-labelledby="integrations-teaser-heading"
381
+ >
382
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
383
+ <div className="text-center mb-16">
384
+ <p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: "#0F766E" }}>
385
+ Integrations
386
+ </p>
387
+ <h2 id="integrations-teaser-heading" className="text-3xl sm:text-4xl font-bold tracking-tight" style={{ color: "#0F172A" }}>
388
+ Connect your entire stack
389
+ </h2>
390
+ <p className="mt-4 text-base max-w-xl mx-auto" style={{ color: "#64748B" }}>
391
+ Native integrations with the platforms your revenue team already uses.
392
+ </p>
393
+ </div>
394
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-6 max-w-3xl mx-auto">
395
+ {providers.map((p) => (
396
+ <div
397
+ key={p.key}
398
+ className="rounded-xl p-6 text-center feature-card-light"
399
+ >
400
+ <div
401
+ className="w-12 h-12 rounded-xl flex items-center justify-center mx-auto mb-4"
402
+ style={{
403
+ background: `${PROVIDER_COLORS[p.key] ?? "#0F766E"}15`,
404
+ border: `1px solid ${PROVIDER_COLORS[p.key] ?? "#0F766E"}30`,
405
+ }}
406
+ >
407
+ <span className="text-lg font-bold" style={{ color: PROVIDER_COLORS[p.key] ?? "#0F766E" }}>
408
+ {p.label.charAt(0)}
409
+ </span>
410
+ </div>
411
+ <h3 className="font-semibold text-base mb-1" style={{ color: "#1E293B" }}>
412
+ {p.label}
413
+ </h3>
414
+ <p className="text-xs leading-relaxed" style={{ color: "#64748B" }}>
415
+ {p.description}
416
+ </p>
417
+ </div>
418
+ ))}
419
+ </div>
420
+ <div className="text-center mt-10">
421
+ <Link href="/integrations" className="inline-flex items-center gap-2 text-sm font-semibold text-link-primary">
422
+ View all integrations
423
+ <ArrowRight className="w-4 h-4" aria-hidden="true" />
424
+ </Link>
425
+ </div>
426
+ </div>
427
+ </section>
428
+ )}
429
+
430
+ {/* ─── MINI PLANS PREVIEW (DB-driven) ───────────────────────── */}
431
  <section
432
  className="py-24 relative overflow-hidden"
433
  style={{ background: "#0B1320" }}
 
436
  <div
437
  className="absolute inset-0 pointer-events-none"
438
  aria-hidden="true"
439
+ style={{ background: "radial-gradient(ellipse 60% 50% at 50% 50%, rgba(15,118,110,0.15) 0%, transparent 70%)" }}
 
 
 
440
  />
441
  <div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
442
  <div className="text-center mb-16">
443
+ <p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: "#14B8A6" }}>Plans</p>
444
+ <h2 id="plans-preview-heading" className="text-3xl sm:text-4xl font-bold text-white tracking-tight">
 
 
 
 
 
 
 
 
445
  Start free. Scale when ready.
446
  </h2>
447
  </div>
448
 
449
+ <div className={`grid grid-cols-1 ${plans.length === 3 ? "md:grid-cols-3" : "md:grid-cols-2"} gap-6 max-w-4xl mx-auto`}>
450
+ {plans.map((plan) => {
451
+ const pricing = PLAN_PRICING[plan.name] ?? { price: "Contact Us", priceNote: "", tagline: "", available: false };
452
+ const featureLines = plan.entitlements
453
+ .map((e) => entitlementToString(e.module_key, e.hard_limit))
454
+ .filter((s): s is string => s !== null)
455
+ .slice(0, 6);
456
+
457
+ return (
458
+ <PlanCard
459
+ key={plan.id}
460
+ name={plan.display_name}
461
+ tagline={pricing.tagline || plan.description || ""}
462
+ price={pricing.price}
463
+ priceNote={pricing.priceNote}
464
+ features={featureLines}
465
+ ctaLabel={pricing.available ? "Get Started Free" : "Coming Soon"}
466
+ ctaHref="/plans"
467
+ available={pricing.available}
468
+ highlighted={plan.name === "free"}
469
+ />
470
+ );
471
+ })}
472
  </div>
473
 
474
+ {waitlistNames.length > 0 && (
475
+ <p className="text-center mt-8 text-sm" style={{ color: "#475569" }}>
476
+ Join the waitlist for {waitlistNames.join(" and ")} —{" "}
477
+ <Link href="/plans" className="underline underline-offset-2 transition-colors duration-200" style={{ color: "#14B8A6" }}>
478
+ see Plans page
479
+ </Link>
480
+ </p>
481
+ )}
 
 
482
  </div>
483
  </section>
484
 
Website/src/app/plans/PlansClientContent.tsx ADDED
@@ -0,0 +1,467 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import {
5
+ Check,
6
+ Clock,
7
+ ChevronDown,
8
+ ChevronUp,
9
+ Loader2,
10
+ CheckCircle,
11
+ AlertCircle,
12
+ } from "lucide-react";
13
+ import Link from "next/link";
14
+ import type { CatalogPlan } from "@/lib/api";
15
+ import { MODULE_MARKETING_MAP } from "@/lib/moduleMap";
16
+
17
+ // ── Pricing config (not in DB — marketing copy) ─────────────────────
18
+
19
+ const PLAN_PRICING: Record<string, { price: string; priceNote: string; tagline: string; available: boolean }> = {
20
+ free: {
21
+ price: "$0",
22
+ priceNote: "/ month, forever",
23
+ tagline: "Start capturing and qualifying leads today",
24
+ available: true,
25
+ },
26
+ growth: {
27
+ price: "Coming Soon",
28
+ priceNote: "",
29
+ tagline: "For growing sales teams with higher volume",
30
+ available: false,
31
+ },
32
+ enterprise: {
33
+ price: "Contact Us",
34
+ priceNote: "",
35
+ tagline: "Custom deployment for large organisations",
36
+ available: false,
37
+ },
38
+ };
39
+
40
+ function entitlementToString(moduleKey: string, hardLimit: number | null): string | null {
41
+ const info = MODULE_MARKETING_MAP[moduleKey];
42
+ if (!info) return null;
43
+ if (hardLimit === null) return `${info.marketingTitle} (Unlimited)`;
44
+ if (hardLimit > 0) return `${info.marketingTitle} (up to ${hardLimit.toLocaleString()})`;
45
+ return info.marketingTitle;
46
+ }
47
+
48
+ // ── FAQs (marketing copy, not feature claims) ───────────────────────
49
+
50
+ const faqs = [
51
+ {
52
+ question: "Is the Free plan actually free? No hidden fees?",
53
+ answer:
54
+ "Yes. The Free plan is genuinely free with no credit card required. You get full access to core capture and qualification features up to the plan limits. We will always have a free tier.",
55
+ },
56
+ {
57
+ question: "When will Growth and Enterprise plans be available?",
58
+ answer:
59
+ "We're in active development. Join the waitlist below and you'll be among the first to know — and receive early-access pricing when we launch.",
60
+ },
61
+ {
62
+ question: "Can I upgrade from Free to a paid plan later?",
63
+ answer:
64
+ "Absolutely. Your data and configuration will carry over. Upgrading will unlock higher limits and additional features without any migration needed.",
65
+ },
66
+ {
67
+ question: "How does lead volume work on the Free plan?",
68
+ answer:
69
+ "The Free plan includes a reasonable monthly lead capture limit suitable for small teams getting started. We'll always be transparent about limits — no surprise overages.",
70
+ },
71
+ {
72
+ question: "Do you offer refunds?",
73
+ answer:
74
+ "Paid plans (when available) will include a 14-day money-back guarantee. We'd rather earn your business on value than lock you in.",
75
+ },
76
+ ];
77
+
78
+ // ── Sub-components ──────────────────────────────────────────────────
79
+
80
+ function WaitlistForm({ planNames }: { planNames: string[] }) {
81
+ const [email, setEmail] = useState("");
82
+ const [plan, setPlan] = useState(planNames[0] ?? "Growth");
83
+ const [state, setState] = useState<"idle" | "loading" | "success" | "error">("idle");
84
+ const [error, setError] = useState("");
85
+
86
+ async function handleSubmit(e: React.FormEvent) {
87
+ e.preventDefault();
88
+ if (!email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
89
+ setError("Please enter a valid email address.");
90
+ return;
91
+ }
92
+ setError("");
93
+ setState("loading");
94
+ // TODO: wire to real waitlist API
95
+ await new Promise((r) => setTimeout(r, 1200));
96
+ setState("success");
97
+ }
98
+
99
+ if (state === "success") {
100
+ return (
101
+ <div className="flex flex-col items-center gap-3 py-8 text-center" role="alert">
102
+ <CheckCircle className="w-10 h-10" style={{ color: "#14B8A6" }} aria-hidden="true" />
103
+ <p className="text-base font-semibold text-white">You&apos;re on the list.</p>
104
+ <p className="text-sm" style={{ color: "#94A3B8" }}>
105
+ We&apos;ll notify you as soon as {plan} launches — including early-access pricing.
106
+ </p>
107
+ </div>
108
+ );
109
+ }
110
+
111
+ return (
112
+ <form onSubmit={handleSubmit} aria-label="Waitlist signup form">
113
+ <div className="flex flex-col sm:flex-row gap-3">
114
+ <div className="flex-1">
115
+ <label htmlFor="waitlist-email" className="sr-only">Work email</label>
116
+ <input
117
+ id="waitlist-email"
118
+ type="email"
119
+ value={email}
120
+ onChange={(e) => { setEmail(e.target.value); setError(""); }}
121
+ placeholder="your@company.com"
122
+ required
123
+ aria-required="true"
124
+ aria-describedby={error ? "waitlist-error" : undefined}
125
+ className="w-full px-4 py-3 rounded-xl text-sm placeholder:text-slate-600"
126
+ style={{
127
+ background: "rgba(255,255,255,0.06)",
128
+ border: error ? "1px solid rgba(239,68,68,0.5)" : "1px solid rgba(255,255,255,0.1)",
129
+ color: "#F1F5F9",
130
+ outline: "none",
131
+ }}
132
+ onFocus={(e) => {
133
+ e.currentTarget.style.borderColor = "#0F766E";
134
+ e.currentTarget.style.boxShadow = "0 0 0 3px rgba(15,118,110,0.2)";
135
+ }}
136
+ onBlur={(e) => {
137
+ e.currentTarget.style.borderColor = error ? "rgba(239,68,68,0.5)" : "rgba(255,255,255,0.1)";
138
+ e.currentTarget.style.boxShadow = "none";
139
+ }}
140
+ />
141
+ </div>
142
+ <div>
143
+ <label htmlFor="waitlist-plan" className="sr-only">Plan interest</label>
144
+ <select
145
+ id="waitlist-plan"
146
+ value={plan}
147
+ onChange={(e) => setPlan(e.target.value)}
148
+ className="w-full sm:w-36 px-4 py-3 rounded-xl text-sm"
149
+ style={{
150
+ background: "rgba(255,255,255,0.06)",
151
+ border: "1px solid rgba(255,255,255,0.1)",
152
+ color: "#F1F5F9",
153
+ outline: "none",
154
+ }}
155
+ >
156
+ {planNames.map((name) => (
157
+ <option key={name} value={name}>{name}</option>
158
+ ))}
159
+ </select>
160
+ </div>
161
+ <button
162
+ type="submit"
163
+ disabled={state === "loading"}
164
+ className="px-6 py-3 rounded-xl font-semibold text-white text-sm flex items-center justify-center gap-2 transition-all duration-200"
165
+ style={{
166
+ background: state === "loading"
167
+ ? "rgba(15,118,110,0.5)"
168
+ : "linear-gradient(135deg, #0F766E, #14B8A6)",
169
+ cursor: state === "loading" ? "not-allowed" : "pointer",
170
+ }}
171
+ onMouseEnter={(e) => {
172
+ if (state !== "loading") e.currentTarget.style.boxShadow = "0 0 20px rgba(15,118,110,0.4)";
173
+ }}
174
+ onMouseLeave={(e) => { e.currentTarget.style.boxShadow = "none"; }}
175
+ >
176
+ {state === "loading" && <Loader2 className="w-4 h-4 animate-spin" aria-hidden="true" />}
177
+ {state === "loading" ? "Joining…" : "Join Waitlist"}
178
+ </button>
179
+ </div>
180
+ {error && (
181
+ <p id="waitlist-error" className="mt-2 text-xs flex items-center gap-1.5" style={{ color: "#FCA5A5" }} role="alert">
182
+ <AlertCircle className="w-3.5 h-3.5" aria-hidden="true" /> {error}
183
+ </p>
184
+ )}
185
+ </form>
186
+ );
187
+ }
188
+
189
+ function FAQItem({ question, answer }: { question: string; answer: string }) {
190
+ const [open, setOpen] = useState(false);
191
+ return (
192
+ <div className="rounded-xl overflow-hidden" style={{ border: "1px solid rgba(255,255,255,0.08)" }}>
193
+ <button
194
+ onClick={() => setOpen(!open)}
195
+ className="w-full flex items-center justify-between px-6 py-5 text-left"
196
+ aria-expanded={open}
197
+ style={{ background: "rgba(255,255,255,0.03)" }}
198
+ onMouseEnter={(e) => { e.currentTarget.style.background = "rgba(15,118,110,0.06)"; }}
199
+ onMouseLeave={(e) => { e.currentTarget.style.background = "rgba(255,255,255,0.03)"; }}
200
+ >
201
+ <span className="text-sm font-semibold text-white pr-4">{question}</span>
202
+ {open
203
+ ? <ChevronUp className="w-4 h-4 shrink-0" style={{ color: "#14B8A6" }} aria-hidden="true" />
204
+ : <ChevronDown className="w-4 h-4 shrink-0" style={{ color: "#475569" }} aria-hidden="true" />
205
+ }
206
+ </button>
207
+ {open && (
208
+ <div
209
+ className="px-6 pb-5 pt-2"
210
+ style={{ background: "rgba(255,255,255,0.02)", borderTop: "1px solid rgba(255,255,255,0.06)" }}
211
+ >
212
+ <p className="text-sm leading-relaxed" style={{ color: "#94A3B8" }}>{answer}</p>
213
+ </div>
214
+ )}
215
+ </div>
216
+ );
217
+ }
218
+
219
+ // ── Main Component ──────────────────────────────────────────────────
220
+
221
+ export default function PlansClientContent({ plans }: { plans: CatalogPlan[] }) {
222
+ // Unavailable plan names for waitlist selector (exclude free)
223
+ const waitlistPlanNames = plans
224
+ .filter((p) => !PLAN_PRICING[p.name]?.available)
225
+ .map((p) => p.display_name);
226
+
227
+ return (
228
+ <>
229
+ {/* ─── HERO ─────────────────────────────────────────────────── */}
230
+ <section
231
+ className="relative py-28 overflow-hidden"
232
+ style={{ background: "#0B1320" }}
233
+ aria-labelledby="plans-hero-heading"
234
+ >
235
+ <div className="absolute inset-0 cockpit-grid opacity-40" aria-hidden="true" />
236
+ <div
237
+ className="absolute inset-0 pointer-events-none"
238
+ aria-hidden="true"
239
+ style={{
240
+ background: "radial-gradient(ellipse 60% 50% at 50% 0%, rgba(15,118,110,0.25) 0%, transparent 70%)",
241
+ }}
242
+ />
243
+ <div className="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
244
+ <p className="text-xs font-bold uppercase tracking-widest mb-4" style={{ color: "#14B8A6" }}>
245
+ Pricing
246
+ </p>
247
+ <h1
248
+ id="plans-hero-heading"
249
+ className="text-4xl sm:text-5xl md:text-6xl font-bold text-white mb-6 tracking-tight"
250
+ >
251
+ Start free.
252
+ <br />
253
+ <span className="gradient-text">Scale when you&apos;re ready.</span>
254
+ </h1>
255
+ <p className="text-lg max-w-xl mx-auto leading-relaxed" style={{ color: "rgba(148,163,184,0.9)" }}>
256
+ The Free plan is available now with no credit card required.
257
+ {waitlistPlanNames.length > 0 && (
258
+ <> {waitlistPlanNames.join(" and ")} plans are coming soon — join the waitlist for early access.</>
259
+ )}
260
+ </p>
261
+ </div>
262
+ </section>
263
+
264
+ {/* ─── PLANS GRID ───────────────────────────────────────────── */}
265
+ <section
266
+ className="py-24"
267
+ style={{ background: "#0B1320" }}
268
+ aria-labelledby="plans-grid-heading"
269
+ >
270
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
271
+ <h2 id="plans-grid-heading" className="sr-only">Available plans</h2>
272
+
273
+ {plans.length === 0 ? (
274
+ <p className="text-center text-lg" style={{ color: "#64748B" }}>
275
+ Plans are loading. Check back soon.
276
+ </p>
277
+ ) : (
278
+ <div className={`grid grid-cols-1 ${plans.length === 3 ? "md:grid-cols-3" : "md:grid-cols-2 xl:grid-cols-4"} gap-6 max-w-5xl mx-auto`}>
279
+ {plans.map((plan) => {
280
+ const pricing = PLAN_PRICING[plan.name] ?? { price: "Contact Us", priceNote: "", tagline: plan.description ?? "", available: false };
281
+ const highlighted = plan.name === "free";
282
+
283
+ // Convert entitlements to marketing strings
284
+ const featureLines = plan.entitlements
285
+ .map((e) => entitlementToString(e.module_key, e.hard_limit))
286
+ .filter((s): s is string => s !== null);
287
+
288
+ return (
289
+ <div
290
+ key={plan.id}
291
+ className="relative rounded-2xl p-8 flex flex-col transition-all duration-300"
292
+ style={
293
+ highlighted
294
+ ? {
295
+ background: "rgba(15,118,110,0.08)",
296
+ border: "1px solid rgba(15,118,110,0.4)",
297
+ boxShadow: "0 0 50px rgba(15,118,110,0.2)",
298
+ }
299
+ : pricing.available
300
+ ? {
301
+ background: "rgba(255,255,255,0.04)",
302
+ border: "1px solid rgba(255,255,255,0.1)",
303
+ }
304
+ : {
305
+ background: "rgba(255,255,255,0.02)",
306
+ border: "1px solid rgba(255,255,255,0.06)",
307
+ }
308
+ }
309
+ >
310
+ {/* Badges */}
311
+ {highlighted && (
312
+ <div
313
+ className="absolute -top-3.5 left-1/2 -translate-x-1/2 px-4 py-1 rounded-full text-xs font-bold text-white whitespace-nowrap"
314
+ style={{ background: "linear-gradient(135deg, #0F766E, #14B8A6)" }}
315
+ >
316
+ Available Now
317
+ </div>
318
+ )}
319
+ {!pricing.available && (
320
+ <div
321
+ className="absolute top-4 right-4 flex items-center gap-1.5 px-2.5 py-1 rounded-full"
322
+ style={{ background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.08)" }}
323
+ >
324
+ <Clock className="w-3 h-3" style={{ color: "#64748B" }} aria-hidden="true" />
325
+ <span className="text-xs font-medium" style={{ color: "#64748B" }}>Coming Soon</span>
326
+ </div>
327
+ )}
328
+
329
+ {/* Header */}
330
+ <div className="mb-6">
331
+ <h3
332
+ className="text-lg font-bold mb-1"
333
+ style={{ color: pricing.available ? "#F1F5F9" : "#64748B" }}
334
+ >
335
+ {plan.display_name}
336
+ </h3>
337
+ <p className="text-sm" style={{ color: pricing.available ? "#94A3B8" : "#475569" }}>
338
+ {pricing.tagline}
339
+ </p>
340
+ </div>
341
+
342
+ {/* Price */}
343
+ <div className="mb-8">
344
+ <span
345
+ className="text-3xl font-bold"
346
+ style={{ color: pricing.available ? "#FFFFFF" : "#475569" }}
347
+ >
348
+ {pricing.price}
349
+ </span>
350
+ {pricing.priceNote && (
351
+ <span className="text-sm ml-2" style={{ color: "#64748B" }}>{pricing.priceNote}</span>
352
+ )}
353
+ </div>
354
+
355
+ {/* Features from DB entitlements */}
356
+ <ul className="space-y-3 mb-8 flex-1" role="list">
357
+ {featureLines.map((feat) => (
358
+ <li key={feat} className="flex items-start gap-2.5">
359
+ <Check
360
+ className="w-4 h-4 mt-0.5 shrink-0"
361
+ style={{ color: pricing.available ? "#14B8A6" : "#334155" }}
362
+ aria-hidden="true"
363
+ />
364
+ <span className="text-sm" style={{ color: pricing.available ? "#CBD5E1" : "#475569" }}>
365
+ {feat}
366
+ </span>
367
+ </li>
368
+ ))}
369
+ </ul>
370
+
371
+ {/* CTA */}
372
+ {pricing.available ? (
373
+ <Link
374
+ href="/contact"
375
+ className="block text-center px-6 py-3 rounded-xl font-semibold text-white text-sm transition-all duration-200"
376
+ style={{ background: "linear-gradient(135deg, #0F766E, #14B8A6)" }}
377
+ onMouseEnter={(e) => { e.currentTarget.style.boxShadow = "0 0 25px rgba(15,118,110,0.45)"; }}
378
+ onMouseLeave={(e) => { e.currentTarget.style.boxShadow = "none"; }}
379
+ >
380
+ Get Started Free
381
+ </Link>
382
+ ) : (
383
+ <a
384
+ href="#waitlist"
385
+ className="block text-center px-6 py-3 rounded-xl font-semibold text-sm transition-all duration-200"
386
+ style={{
387
+ background: "rgba(255,255,255,0.05)",
388
+ border: "1px solid rgba(255,255,255,0.08)",
389
+ color: "#475569",
390
+ }}
391
+ onMouseEnter={(e) => {
392
+ e.currentTarget.style.color = "#94A3B8";
393
+ e.currentTarget.style.borderColor = "rgba(255,255,255,0.15)";
394
+ }}
395
+ onMouseLeave={(e) => {
396
+ e.currentTarget.style.color = "#475569";
397
+ e.currentTarget.style.borderColor = "rgba(255,255,255,0.08)";
398
+ }}
399
+ >
400
+ Join Waitlist
401
+ </a>
402
+ )}
403
+ </div>
404
+ );
405
+ })}
406
+ </div>
407
+ )}
408
+
409
+ <p className="text-center mt-10 text-sm" style={{ color: "#475569" }}>
410
+ All plans include SSL, data privacy, and a secure hosting environment.{" "}
411
+ <Link href="/contact" className="underline underline-offset-2" style={{ color: "#14B8A6" }}>
412
+ Contact us
413
+ </Link>{" "}
414
+ with questions about pricing or custom arrangements.
415
+ </p>
416
+ </div>
417
+ </section>
418
+
419
+ {/* ─── WAITLIST ─────────────────────────────────────────────── */}
420
+ {waitlistPlanNames.length > 0 && (
421
+ <section
422
+ id="waitlist"
423
+ className="py-24 border-t"
424
+ style={{ background: "#0E1826", borderColor: "rgba(255,255,255,0.07)" }}
425
+ aria-labelledby="waitlist-heading"
426
+ >
427
+ <div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
428
+ <p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: "#14B8A6" }}>
429
+ Early Access
430
+ </p>
431
+ <h2 id="waitlist-heading" className="text-3xl font-bold text-white mb-4 tracking-tight">
432
+ Join the waitlist for paid plans.
433
+ </h2>
434
+ <p className="text-base mb-8" style={{ color: "#94A3B8" }}>
435
+ Be first in line when {waitlistPlanNames.join(" and ")} launch — with early-access pricing.
436
+ </p>
437
+ <WaitlistForm planNames={waitlistPlanNames} />
438
+ </div>
439
+ </section>
440
+ )}
441
+
442
+ {/* ─── FAQ ──────────────────────────────────────────────────── */}
443
+ <section
444
+ className="py-24 border-t"
445
+ style={{ background: "#0B1320", borderColor: "rgba(255,255,255,0.06)" }}
446
+ aria-labelledby="faq-heading"
447
+ >
448
+ <div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
449
+ <h2 id="faq-heading" className="text-3xl font-bold text-white mb-10 tracking-tight text-center">
450
+ Pricing FAQ
451
+ </h2>
452
+ <div className="space-y-3">
453
+ {faqs.map((faq) => (
454
+ <FAQItem key={faq.question} {...faq} />
455
+ ))}
456
+ </div>
457
+ <p className="mt-10 text-center text-sm" style={{ color: "#475569" }}>
458
+ Still have questions?{" "}
459
+ <Link href="/contact" className="underline underline-offset-2" style={{ color: "#14B8A6" }}>
460
+ Contact us
461
+ </Link>
462
+ </p>
463
+ </div>
464
+ </section>
465
+ </>
466
+ );
467
+ }
Website/src/app/plans/page.tsx CHANGED
@@ -1,573 +1,14 @@
1
- "use client";
2
-
3
- import { useState } from "react";
4
- import {
5
- Check,
6
- Clock,
7
- ChevronDown,
8
- ChevronUp,
9
- Loader2,
10
- CheckCircle,
11
- AlertCircle,
12
- } from "lucide-react";
13
- import Link from "next/link";
14
-
15
- const tiers = [
16
- {
17
- name: "Free",
18
- tagline: "Start capturing and qualifying leads today",
19
- price: "$0",
20
- priceNote: "/ month, forever",
21
- available: true,
22
- highlighted: true,
23
- ctaLabel: "Get Started Free",
24
- ctaHref: "/contact",
25
- features: [
26
- "Lead capture (limited monthly volume)",
27
- "AI qualification prompts (basic)",
28
- "Manual routing to team members",
29
- "Email notifications on new leads",
30
- "Basic analytics dashboard",
31
- "1 team seat",
32
- "Community support",
33
- ],
34
- notIncluded: [
35
- "Automated routing rules",
36
- "CRM integrations",
37
- "Advanced AI playbooks",
38
- "Priority support",
39
- ],
40
- },
41
- {
42
- name: "Growth",
43
- tagline: "For growing sales teams with higher volume",
44
- price: "Coming Soon",
45
- priceNote: "",
46
- available: false,
47
- highlighted: false,
48
- ctaLabel: "Coming Soon",
49
- ctaHref: "#waitlist",
50
- features: [
51
- "Everything in Free",
52
- "Unlimited lead capture",
53
- "Advanced AI qualification & scoring",
54
- "Automated routing rules",
55
- "CRM sync & integrations",
56
- "Up to 5 team seats",
57
- "Email & chat support",
58
- ],
59
- notIncluded: [],
60
- },
61
- {
62
- name: "Pro",
63
- tagline: "For high-volume pipelines and larger teams",
64
- price: "Coming Soon",
65
- priceNote: "",
66
- available: false,
67
- highlighted: false,
68
- ctaLabel: "Coming Soon",
69
- ctaHref: "#waitlist",
70
- features: [
71
- "Everything in Growth",
72
- "Multi-channel capture",
73
- "Custom AI playbooks & templates",
74
- "Team inbox & human handover",
75
- "Audit trail & activity log",
76
- "Role-based access control",
77
- "Priority support",
78
- "Unlimited seats",
79
- ],
80
- notIncluded: [],
81
- },
82
- {
83
- name: "Enterprise",
84
- tagline: "Custom deployment for large organisations",
85
- price: "Coming Soon",
86
- priceNote: "",
87
- available: false,
88
- highlighted: false,
89
- ctaLabel: "Coming Soon",
90
- ctaHref: "#waitlist",
91
- features: [
92
- "Everything in Pro",
93
- "Dedicated infrastructure option",
94
- "Custom integrations & API access",
95
- "SSO & advanced security controls",
96
- "SLA-backed uptime guarantees",
97
- "Dedicated account manager",
98
- "Custom onboarding & training",
99
- ],
100
- notIncluded: [],
101
- },
102
- ];
103
-
104
- const faqs = [
105
- {
106
- question: "Is the Free plan actually free? No hidden fees?",
107
- answer:
108
- "Yes. The Free plan is genuinely free with no credit card required. You get full access to core capture and qualification features up to the plan limits. We will always have a free tier.",
109
- },
110
- {
111
- question: "When will Growth and Pro plans be available?",
112
- answer:
113
- "We're in active development. Join the waitlist below and you'll be among the first to know — and receive early-access pricing when we launch.",
114
- },
115
- {
116
- question: "Can I upgrade from Free to a paid plan later?",
117
- answer:
118
- "Absolutely. Your data and configuration will carry over. Upgrading will unlock higher limits and additional features without any migration needed.",
119
- },
120
- {
121
- question: "How does lead volume work on the Free plan?",
122
- answer:
123
- "The Free plan includes a reasonable monthly lead capture limit suitable for small teams getting started. We'll always be transparent about limits — no surprise overages.",
124
- },
125
- {
126
- question: "Do you offer refunds?",
127
- answer:
128
- "Paid plans (when available) will include a 14-day money-back guarantee. We'd rather earn your business on value than lock you in.",
129
- },
130
- ];
131
-
132
- function WaitlistForm() {
133
- const [email, setEmail] = useState("");
134
- const [plan, setPlan] = useState("Growth");
135
- const [state, setState] = useState<"idle" | "loading" | "success" | "error">("idle");
136
- const [error, setError] = useState("");
137
-
138
- async function handleSubmit(e: React.FormEvent) {
139
- e.preventDefault();
140
- if (!email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
141
- setError("Please enter a valid email address.");
142
- return;
143
- }
144
- setError("");
145
- setState("loading");
146
- // TODO: wire to real waitlist API
147
- await new Promise((r) => setTimeout(r, 1200));
148
- setState("success");
149
- }
150
-
151
- if (state === "success") {
152
- return (
153
- <div
154
- className="flex flex-col items-center gap-3 py-8 text-center"
155
- role="alert"
156
- >
157
- <CheckCircle className="w-10 h-10" style={{ color: "#14B8A6" }} aria-hidden="true" />
158
- <p className="text-base font-semibold text-white">You&apos;re on the list.</p>
159
- <p className="text-sm" style={{ color: "#94A3B8" }}>
160
- We&apos;ll notify you as soon as {plan} launches — including early-access pricing.
161
- </p>
162
- </div>
163
- );
164
- }
165
-
166
- return (
167
- <form onSubmit={handleSubmit} aria-label="Waitlist signup form">
168
- <div className="flex flex-col sm:flex-row gap-3">
169
- <div className="flex-1">
170
- <label htmlFor="waitlist-email" className="sr-only">
171
- Work email
172
- </label>
173
- <input
174
- id="waitlist-email"
175
- type="email"
176
- value={email}
177
- onChange={(e) => { setEmail(e.target.value); setError(""); }}
178
- placeholder="your@company.com"
179
- required
180
- aria-required="true"
181
- aria-describedby={error ? "waitlist-error" : undefined}
182
- className="w-full px-4 py-3 rounded-xl text-sm placeholder:text-slate-600"
183
- style={{
184
- background: "rgba(255,255,255,0.06)",
185
- border: error ? "1px solid rgba(239,68,68,0.5)" : "1px solid rgba(255,255,255,0.1)",
186
- color: "#F1F5F9",
187
- outline: "none",
188
- }}
189
- onFocus={(e) => {
190
- (e.currentTarget as HTMLElement).style.borderColor = "#0F766E";
191
- (e.currentTarget as HTMLElement).style.boxShadow = "0 0 0 3px rgba(15,118,110,0.2)";
192
- }}
193
- onBlur={(e) => {
194
- (e.currentTarget as HTMLElement).style.borderColor = error
195
- ? "rgba(239,68,68,0.5)"
196
- : "rgba(255,255,255,0.1)";
197
- (e.currentTarget as HTMLElement).style.boxShadow = "none";
198
- }}
199
- />
200
- </div>
201
-
202
- <div>
203
- <label htmlFor="waitlist-plan" className="sr-only">
204
- Plan interest
205
- </label>
206
- <select
207
- id="waitlist-plan"
208
- value={plan}
209
- onChange={(e) => setPlan(e.target.value)}
210
- className="w-full sm:w-36 px-4 py-3 rounded-xl text-sm"
211
- style={{
212
- background: "rgba(255,255,255,0.06)",
213
- border: "1px solid rgba(255,255,255,0.1)",
214
- color: "#F1F5F9",
215
- outline: "none",
216
- }}
217
- >
218
- <option value="Growth">Growth</option>
219
- <option value="Pro">Pro</option>
220
- <option value="Enterprise">Enterprise</option>
221
- </select>
222
- </div>
223
-
224
- <button
225
- type="submit"
226
- disabled={state === "loading"}
227
- className="px-6 py-3 rounded-xl font-semibold text-white text-sm flex items-center justify-center gap-2 transition-all duration-200"
228
- style={{
229
- background: state === "loading"
230
- ? "rgba(15,118,110,0.5)"
231
- : "linear-gradient(135deg, #0F766E, #14B8A6)",
232
- cursor: state === "loading" ? "not-allowed" : "pointer",
233
- }}
234
- onMouseEnter={(e) => {
235
- if (state !== "loading") {
236
- (e.currentTarget as HTMLElement).style.boxShadow = "0 0 20px rgba(15,118,110,0.4)";
237
- }
238
- }}
239
- onMouseLeave={(e) => {
240
- (e.currentTarget as HTMLElement).style.boxShadow = "none";
241
- }}
242
- >
243
- {state === "loading" && <Loader2 className="w-4 h-4 animate-spin" aria-hidden="true" />}
244
- {state === "loading" ? "Joining…" : "Join Waitlist"}
245
- </button>
246
- </div>
247
- {error && (
248
- <p id="waitlist-error" className="mt-2 text-xs flex items-center gap-1.5" style={{ color: "#FCA5A5" }} role="alert">
249
- <AlertCircle className="w-3.5 h-3.5" aria-hidden="true" /> {error}
250
- </p>
251
- )}
252
- </form>
253
- );
254
- }
255
-
256
- function FAQItem({ question, answer }: { question: string; answer: string }) {
257
- const [open, setOpen] = useState(false);
258
- return (
259
- <div
260
- className="rounded-xl overflow-hidden"
261
- style={{ border: "1px solid rgba(255,255,255,0.08)" }}
262
- >
263
- <button
264
- onClick={() => setOpen(!open)}
265
- className="w-full flex items-center justify-between px-6 py-5 text-left"
266
- aria-expanded={open}
267
- style={{ background: "rgba(255,255,255,0.03)" }}
268
- onMouseEnter={(e) => {
269
- (e.currentTarget as HTMLElement).style.background = "rgba(15,118,110,0.06)";
270
- }}
271
- onMouseLeave={(e) => {
272
- (e.currentTarget as HTMLElement).style.background = "rgba(255,255,255,0.03)";
273
- }}
274
- >
275
- <span className="text-sm font-semibold text-white pr-4">{question}</span>
276
- {open
277
- ? <ChevronUp className="w-4 h-4 shrink-0" style={{ color: "#14B8A6" }} aria-hidden="true" />
278
- : <ChevronDown className="w-4 h-4 shrink-0" style={{ color: "#475569" }} aria-hidden="true" />
279
- }
280
- </button>
281
- {open && (
282
- <div
283
- className="px-6 pb-5 pt-2"
284
- style={{ background: "rgba(255,255,255,0.02)", borderTop: "1px solid rgba(255,255,255,0.06)" }}
285
- >
286
- <p className="text-sm leading-relaxed" style={{ color: "#94A3B8" }}>
287
- {answer}
288
- </p>
289
- </div>
290
- )}
291
- </div>
292
- );
293
- }
294
-
295
- export default function PlansPage() {
296
- return (
297
- <>
298
- {/* ─── HERO ───────────────────────────────────────���─────────── */}
299
- <section
300
- className="relative py-28 overflow-hidden"
301
- style={{ background: "#0B1320" }}
302
- aria-labelledby="plans-hero-heading"
303
- >
304
- <div className="absolute inset-0 cockpit-grid opacity-40" aria-hidden="true" />
305
- <div
306
- className="absolute inset-0 pointer-events-none"
307
- aria-hidden="true"
308
- style={{
309
- background:
310
- "radial-gradient(ellipse 60% 50% at 50% 0%, rgba(15,118,110,0.25) 0%, transparent 70%)",
311
- }}
312
- />
313
-
314
- <div className="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
315
- <p
316
- className="text-xs font-bold uppercase tracking-widest mb-4"
317
- style={{ color: "#14B8A6" }}
318
- >
319
- Pricing
320
- </p>
321
- <h1
322
- id="plans-hero-heading"
323
- className="text-4xl sm:text-5xl md:text-6xl font-bold text-white mb-6 tracking-tight"
324
- >
325
- Start free.
326
- <br />
327
- <span className="gradient-text">Scale when you&apos;re ready.</span>
328
- </h1>
329
- <p
330
- className="text-lg max-w-xl mx-auto leading-relaxed"
331
- style={{ color: "rgba(148,163,184,0.9)" }}
332
- >
333
- The Free plan is available now with no credit card required.
334
- Growth, Pro, and Enterprise plans are coming soon — join the waitlist for early access.
335
- </p>
336
- </div>
337
- </section>
338
-
339
- {/* ─── PLANS GRID ───────────────────────────────────────────── */}
340
- <section
341
- className="py-24"
342
- style={{ background: "#0B1320" }}
343
- aria-labelledby="plans-grid-heading"
344
- >
345
- <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
346
- <h2 id="plans-grid-heading" className="sr-only">Available plans</h2>
347
-
348
- <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
349
- {tiers.map((tier) => (
350
- <div
351
- key={tier.name}
352
- className="relative rounded-2xl p-8 flex flex-col transition-all duration-300"
353
- style={
354
- tier.highlighted
355
- ? {
356
- background: "rgba(15,118,110,0.08)",
357
- border: "1px solid rgba(15,118,110,0.4)",
358
- boxShadow: "0 0 50px rgba(15,118,110,0.2)",
359
- }
360
- : tier.available
361
- ? {
362
- background: "rgba(255,255,255,0.04)",
363
- border: "1px solid rgba(255,255,255,0.1)",
364
- }
365
- : {
366
- background: "rgba(255,255,255,0.02)",
367
- border: "1px solid rgba(255,255,255,0.06)",
368
- }
369
- }
370
- >
371
- {/* Badges */}
372
- {tier.highlighted && (
373
- <div
374
- className="absolute -top-3.5 left-1/2 -translate-x-1/2 px-4 py-1 rounded-full text-xs font-bold text-white whitespace-nowrap"
375
- style={{ background: "linear-gradient(135deg, #0F766E, #14B8A6)" }}
376
- >
377
- Available Now
378
- </div>
379
- )}
380
- {!tier.available && (
381
- <div
382
- className="absolute top-4 right-4 flex items-center gap-1.5 px-2.5 py-1 rounded-full"
383
- style={{
384
- background: "rgba(255,255,255,0.05)",
385
- border: "1px solid rgba(255,255,255,0.08)",
386
- }}
387
- >
388
- <Clock className="w-3 h-3" style={{ color: "#64748B" }} aria-hidden="true" />
389
- <span className="text-xs font-medium" style={{ color: "#64748B" }}>
390
- Coming Soon
391
- </span>
392
- </div>
393
- )}
394
-
395
- {/* Header */}
396
- <div className="mb-6">
397
- <h3
398
- className="text-lg font-bold mb-1"
399
- style={{ color: tier.available ? "#F1F5F9" : "#64748B" }}
400
- >
401
- {tier.name}
402
- </h3>
403
- <p className="text-sm" style={{ color: tier.available ? "#94A3B8" : "#475569" }}>
404
- {tier.tagline}
405
- </p>
406
- </div>
407
-
408
- {/* Price */}
409
- <div className="mb-8">
410
- <span
411
- className="text-3xl font-bold"
412
- style={{ color: tier.available ? "#FFFFFF" : "#475569" }}
413
- >
414
- {tier.price}
415
- </span>
416
- {tier.priceNote && (
417
- <span className="text-sm ml-2" style={{ color: "#64748B" }}>
418
- {tier.priceNote}
419
- </span>
420
- )}
421
- </div>
422
-
423
- {/* Features */}
424
- <ul className="space-y-3 mb-8 flex-1" role="list">
425
- {tier.features.map((f) => (
426
- <li key={f} className="flex items-start gap-2.5">
427
- <Check
428
- className="w-4 h-4 mt-0.5 shrink-0"
429
- style={{ color: tier.available ? "#14B8A6" : "#334155" }}
430
- aria-hidden="true"
431
- />
432
- <span
433
- className="text-sm"
434
- style={{ color: tier.available ? "#CBD5E1" : "#475569" }}
435
- >
436
- {f}
437
- </span>
438
- </li>
439
- ))}
440
- {tier.notIncluded.map((f) => (
441
- <li key={f} className="flex items-start gap-2.5 opacity-40">
442
- <span
443
- className="w-4 h-4 mt-0.5 shrink-0 flex items-center justify-center text-xs"
444
- aria-hidden="true"
445
- style={{ color: "#475569" }}
446
- >
447
-
448
- </span>
449
- <span className="text-sm line-through" style={{ color: "#475569" }}>
450
- {f}
451
- </span>
452
- </li>
453
- ))}
454
- </ul>
455
-
456
- {/* CTA */}
457
- {tier.available ? (
458
- <Link
459
- href={tier.ctaHref}
460
- className="block text-center px-6 py-3 rounded-xl font-semibold text-white text-sm transition-all duration-200"
461
- style={{ background: "linear-gradient(135deg, #0F766E, #14B8A6)" }}
462
- onMouseEnter={(e) => {
463
- (e.currentTarget as HTMLElement).style.boxShadow =
464
- "0 0 25px rgba(15,118,110,0.45)";
465
- }}
466
- onMouseLeave={(e) => {
467
- (e.currentTarget as HTMLElement).style.boxShadow = "none";
468
- }}
469
- >
470
- {tier.ctaLabel}
471
- </Link>
472
- ) : (
473
- <a
474
- href="#waitlist"
475
- className="block text-center px-6 py-3 rounded-xl font-semibold text-sm transition-all duration-200"
476
- style={{
477
- background: "rgba(255,255,255,0.05)",
478
- border: "1px solid rgba(255,255,255,0.08)",
479
- color: "#475569",
480
- }}
481
- onMouseEnter={(e) => {
482
- (e.currentTarget as HTMLElement).style.color = "#94A3B8";
483
- (e.currentTarget as HTMLElement).style.borderColor = "rgba(255,255,255,0.15)";
484
- }}
485
- onMouseLeave={(e) => {
486
- (e.currentTarget as HTMLElement).style.color = "#475569";
487
- (e.currentTarget as HTMLElement).style.borderColor = "rgba(255,255,255,0.08)";
488
- }}
489
- >
490
- Join Waitlist
491
- </a>
492
- )}
493
- </div>
494
- ))}
495
- </div>
496
-
497
- {/* Comparison note */}
498
- <p className="text-center mt-10 text-sm" style={{ color: "#475569" }}>
499
- All plans include SSL, data privacy, and a secure hosting environment.{" "}
500
- <Link
501
- href="/contact"
502
- className="underline underline-offset-2"
503
- style={{ color: "#14B8A6" }}
504
- >
505
- Contact us
506
- </Link>{" "}
507
- with questions about pricing or custom arrangements.
508
- </p>
509
- </div>
510
- </section>
511
-
512
- {/* ─── WAITLIST ─────────────────────────────────────────────── */}
513
- <section
514
- id="waitlist"
515
- className="py-24 border-t"
516
- style={{ background: "#0E1826", borderColor: "rgba(255,255,255,0.07)" }}
517
- aria-labelledby="waitlist-heading"
518
- >
519
- <div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
520
- <p
521
- className="text-xs font-bold uppercase tracking-widest mb-3"
522
- style={{ color: "#14B8A6" }}
523
- >
524
- Early Access
525
- </p>
526
- <h2
527
- id="waitlist-heading"
528
- className="text-3xl font-bold text-white mb-4 tracking-tight"
529
- >
530
- Join the waitlist for paid plans.
531
- </h2>
532
- <p className="text-base mb-8" style={{ color: "#94A3B8" }}>
533
- Be first in line when Growth, Pro, and Enterprise launch — with early-access pricing.
534
- </p>
535
-
536
- <WaitlistForm />
537
- </div>
538
- </section>
539
-
540
- {/* ─── FAQ ───────────────────────────────────��──────────────── */}
541
- <section
542
- className="py-24 border-t"
543
- style={{ background: "#0B1320", borderColor: "rgba(255,255,255,0.06)" }}
544
- aria-labelledby="faq-heading"
545
- >
546
- <div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
547
- <h2
548
- id="faq-heading"
549
- className="text-3xl font-bold text-white mb-10 tracking-tight text-center"
550
- >
551
- Pricing FAQ
552
- </h2>
553
- <div className="space-y-3">
554
- {faqs.map((faq) => (
555
- <FAQItem key={faq.question} {...faq} />
556
- ))}
557
- </div>
558
-
559
- <p className="mt-10 text-center text-sm" style={{ color: "#475569" }}>
560
- Still have questions?{" "}
561
- <Link
562
- href="/contact"
563
- className="underline underline-offset-2"
564
- style={{ color: "#14B8A6" }}
565
- >
566
- Contact us
567
- </Link>
568
- </p>
569
- </div>
570
- </section>
571
- </>
572
- );
573
  }
 
1
+ import type { Metadata } from "next";
2
+ import { getPlans } from "@/lib/api";
3
+ import PlansClientContent from "./PlansClientContent";
4
+
5
+ export const metadata: Metadata = {
6
+ title: "Plans — Start Free, Scale When Ready",
7
+ description:
8
+ "LeadPilot pricing: a free plan available now, plus Growth and Enterprise tiers coming soon. Join the waitlist for early access.",
9
+ };
10
+
11
+ export default async function PlansPage() {
12
+ const plans = await getPlans();
13
+ return <PlansClientContent plans={plans} />;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  }
Website/src/app/templates/CategoryFilter.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import type { CatalogTemplate, CatalogEnum } from "@/lib/api";
5
+ import TemplateCard from "./TemplateCard";
6
+
7
+ interface Props {
8
+ templates: CatalogTemplate[];
9
+ categories: CatalogEnum[];
10
+ }
11
+
12
+ export default function CategoryFilter({ templates, categories }: Props) {
13
+ const [active, setActive] = useState<string | null>(null);
14
+
15
+ const filtered = active
16
+ ? templates.filter((t) => t.category === active)
17
+ : templates;
18
+
19
+ return (
20
+ <>
21
+ {/* Filter tabs */}
22
+ <div className="flex flex-wrap items-center gap-2 justify-center mb-12">
23
+ <button
24
+ onClick={() => setActive(null)}
25
+ className={`px-3 py-1.5 rounded-lg text-xs font-medium ${
26
+ active === null ? "category-link-active" : "category-link"
27
+ }`}
28
+ >
29
+ All
30
+ </button>
31
+ {categories.map((cat) => (
32
+ <button
33
+ key={cat.key}
34
+ onClick={() => setActive(cat.key)}
35
+ className={`px-3 py-1.5 rounded-lg text-xs font-medium ${
36
+ active === cat.key ? "category-link-active" : "category-link"
37
+ }`}
38
+ >
39
+ {cat.label}
40
+ </button>
41
+ ))}
42
+ </div>
43
+
44
+ {/* Template grid */}
45
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
46
+ {filtered.map((t) => (
47
+ <TemplateCard key={t.id} template={t} />
48
+ ))}
49
+ </div>
50
+
51
+ {filtered.length === 0 && (
52
+ <p className="text-center text-sm py-12" style={{ color: "#64748B" }}>
53
+ No templates in this category yet. Check back soon.
54
+ </p>
55
+ )}
56
+ </>
57
+ );
58
+ }
Website/src/app/templates/TemplateCard.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { CatalogTemplate } from "@/lib/api";
2
+ import { Star } from "lucide-react";
3
+
4
+ interface Props {
5
+ template: CatalogTemplate;
6
+ }
7
+
8
+ export default function TemplateCard({ template: t }: Props) {
9
+ return (
10
+ <div className="catalog-card p-6 flex flex-col">
11
+ {/* Top row: category + featured */}
12
+ <div className="flex items-center gap-2 mb-3">
13
+ <span className="badge-category" data-category={t.category}>
14
+ {t.category.replace(/_/g, " ")}
15
+ </span>
16
+ {t.is_featured && (
17
+ <span className="inline-flex items-center gap-1 text-[10px] font-semibold" style={{ color: "#FBBF24" }}>
18
+ <Star className="w-3 h-3 fill-current" aria-hidden="true" />
19
+ Featured
20
+ </span>
21
+ )}
22
+ </div>
23
+
24
+ {/* Name + description */}
25
+ <h3 className="text-white font-semibold text-base mb-1">{t.name}</h3>
26
+ <p
27
+ className="text-sm leading-relaxed line-clamp-2 flex-1 mb-4"
28
+ style={{ color: "rgba(148,163,184,0.85)" }}
29
+ >
30
+ {t.description}
31
+ </p>
32
+
33
+ {/* Platform + integration badges */}
34
+ <div className="flex flex-wrap gap-1.5 mb-3">
35
+ {t.platforms.map((p) => (
36
+ <span key={p} className="badge-platform">{p}</span>
37
+ ))}
38
+ </div>
39
+
40
+ {/* Required integrations */}
41
+ {t.required_integrations.length > 0 && (
42
+ <p className="text-[10px] mb-2" style={{ color: "#475569" }}>
43
+ Requires: {t.required_integrations.join(", ")}
44
+ </p>
45
+ )}
46
+
47
+ {/* Clone count */}
48
+ {t.clone_count > 0 && (
49
+ <p className="text-[10px]" style={{ color: "#475569" }}>
50
+ {t.clone_count.toLocaleString()} clones
51
+ </p>
52
+ )}
53
+ </div>
54
+ );
55
+ }
Website/src/app/templates/page.tsx ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { getPublicTemplates, getTemplateCategories } from "@/lib/api";
3
+ import CTASection from "@/components/CTASection";
4
+ import CategoryFilter from "./CategoryFilter";
5
+ import TemplateCard from "./TemplateCard";
6
+
7
+ export const metadata: Metadata = {
8
+ title: "Templates — Ready-Made Automation Flows",
9
+ description:
10
+ "Browse LeadPilot's library of pre-built automation templates for WhatsApp, Instagram, Messenger, and more. Clone and customise in minutes.",
11
+ };
12
+
13
+ export default async function TemplatesPage() {
14
+ const [templates, categories] = await Promise.all([
15
+ getPublicTemplates(),
16
+ getTemplateCategories(),
17
+ ]);
18
+
19
+ const featuredTemplates = templates.filter((t) => t.is_featured);
20
+
21
+ return (
22
+ <>
23
+ {/* ─── HERO ─────────────────────────────────────────────────── */}
24
+ <section
25
+ className="relative py-28 overflow-hidden"
26
+ style={{ background: "#0B1320" }}
27
+ aria-labelledby="templates-hero-heading"
28
+ >
29
+ <div className="absolute inset-0 cockpit-grid opacity-40" aria-hidden="true" />
30
+ <div
31
+ className="absolute inset-0 pointer-events-none"
32
+ aria-hidden="true"
33
+ style={{
34
+ background:
35
+ "radial-gradient(ellipse 60% 50% at 50% 0%, rgba(15,118,110,0.25) 0%, transparent 70%)",
36
+ }}
37
+ />
38
+ <div className="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
39
+ <p
40
+ className="text-xs font-bold uppercase tracking-widest mb-4"
41
+ style={{ color: "#14B8A6" }}
42
+ >
43
+ Template Catalog
44
+ </p>
45
+ <h1
46
+ id="templates-hero-heading"
47
+ className="text-4xl sm:text-5xl md:text-6xl font-bold text-white mb-6 tracking-tight"
48
+ >
49
+ Ready-made automation flows.
50
+ <br />
51
+ <span className="gradient-text">Clone. Customise. Launch.</span>
52
+ </h1>
53
+ <p
54
+ className="text-lg max-w-2xl mx-auto leading-relaxed"
55
+ style={{ color: "rgba(148,163,184,0.9)" }}
56
+ >
57
+ Pre-built workflows for lead qualification, appointment booking, support intake,
58
+ and more. Each template includes AI-powered nodes that you can tailor to your business.
59
+ </p>
60
+ </div>
61
+ </section>
62
+
63
+ {/* ─── FEATURED TEMPLATES ───────────────────────────────────── */}
64
+ {featuredTemplates.length > 0 && (
65
+ <section
66
+ className="py-16 border-b"
67
+ style={{ background: "#0E1826", borderColor: "rgba(255,255,255,0.06)" }}
68
+ aria-labelledby="featured-heading"
69
+ >
70
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
71
+ <h2
72
+ id="featured-heading"
73
+ className="text-xs font-bold uppercase tracking-widest mb-8 text-center"
74
+ style={{ color: "#14B8A6" }}
75
+ >
76
+ Featured Templates
77
+ </h2>
78
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4">
79
+ {featuredTemplates.map((t) => (
80
+ <TemplateCard key={t.id} template={t} />
81
+ ))}
82
+ </div>
83
+ </div>
84
+ </section>
85
+ )}
86
+
87
+ {/* ─── ALL TEMPLATES (filterable) ───────────────────────────── */}
88
+ <section
89
+ className="py-24"
90
+ style={{ background: "#0B1320" }}
91
+ aria-labelledby="all-templates-heading"
92
+ >
93
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
94
+ <div className="text-center mb-10">
95
+ <h2
96
+ id="all-templates-heading"
97
+ className="text-2xl font-bold text-white tracking-tight mb-2"
98
+ >
99
+ All Templates
100
+ </h2>
101
+ <p className="text-sm" style={{ color: "#64748B" }}>
102
+ {templates.length} templates across {categories.length} categories
103
+ </p>
104
+ </div>
105
+
106
+ <CategoryFilter templates={templates} categories={categories} />
107
+ </div>
108
+ </section>
109
+
110
+ {/* ─── CTA ──────────────────────────────────────────────────── */}
111
+ <CTASection
112
+ headline="Ready to automate your pipeline?"
113
+ subheadline="Clone a template, customise it for your business, and go live in minutes."
114
+ primaryLabel="Book a Demo"
115
+ primaryHref="/contact"
116
+ secondaryLabel="Start Free"
117
+ secondaryHref="/plans"
118
+ />
119
+ </>
120
+ );
121
+ }
Website/src/components/FeatureCard.tsx CHANGED
@@ -1,23 +1,52 @@
1
- import { type LucideIcon } from "lucide-react";
 
2
 
3
  interface FeatureCardProps {
4
- icon: LucideIcon;
 
5
  title: string;
6
  benefit: string;
7
  description: string;
8
  dark?: boolean;
 
 
 
 
 
 
 
 
 
9
  }
10
 
11
  export default function FeatureCard({
12
- icon: Icon,
 
13
  title,
14
  benefit,
15
  description,
16
  dark = false,
 
17
  }: FeatureCardProps) {
 
 
18
  if (dark) {
19
  return (
20
- <div className="rounded-xl p-6 feature-card-dark">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  <div
22
  className="w-10 h-10 rounded-lg flex items-center justify-center mb-4"
23
  style={{ background: "rgba(15,118,110,0.2)", border: "1px solid rgba(20,184,166,0.25)" }}
@@ -39,7 +68,21 @@ export default function FeatureCard({
39
  }
40
 
41
  return (
42
- <div className="rounded-xl p-6 feature-card-light">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  <div
44
  className="w-10 h-10 rounded-lg flex items-center justify-center mb-4"
45
  style={{ background: "rgba(15,118,110,0.08)", border: "1px solid rgba(15,118,110,0.15)" }}
 
1
+ import * as Icons from "lucide-react";
2
+ import { type LucideIcon, Clock } from "lucide-react";
3
 
4
  interface FeatureCardProps {
5
+ icon?: LucideIcon;
6
+ iconName?: string;
7
  title: string;
8
  benefit: string;
9
  description: string;
10
  dark?: boolean;
11
+ comingSoon?: boolean;
12
+ }
13
+
14
+ function resolveIcon(iconName?: string, icon?: LucideIcon): LucideIcon {
15
+ if (icon) return icon;
16
+ if (iconName && iconName in Icons) {
17
+ return (Icons as unknown as Record<string, LucideIcon>)[iconName];
18
+ }
19
+ return Icons.Zap; // fallback
20
  }
21
 
22
  export default function FeatureCard({
23
+ icon,
24
+ iconName,
25
  title,
26
  benefit,
27
  description,
28
  dark = false,
29
+ comingSoon = false,
30
  }: FeatureCardProps) {
31
+ const Icon = resolveIcon(iconName, icon);
32
+
33
  if (dark) {
34
  return (
35
+ <div className={`relative rounded-xl p-6 ${comingSoon ? "catalog-card-muted" : "feature-card-dark"}`}>
36
+ {comingSoon && (
37
+ <div
38
+ className="absolute top-4 right-4 flex items-center gap-1.5 px-2 py-0.5 rounded-full"
39
+ style={{
40
+ background: "rgba(255,255,255,0.06)",
41
+ border: "1px solid rgba(255,255,255,0.1)",
42
+ }}
43
+ >
44
+ <Clock className="w-3 h-3" style={{ color: "#64748B" }} aria-hidden="true" />
45
+ <span className="text-[10px] font-medium" style={{ color: "#64748B" }}>
46
+ Coming Soon
47
+ </span>
48
+ </div>
49
+ )}
50
  <div
51
  className="w-10 h-10 rounded-lg flex items-center justify-center mb-4"
52
  style={{ background: "rgba(15,118,110,0.2)", border: "1px solid rgba(20,184,166,0.25)" }}
 
68
  }
69
 
70
  return (
71
+ <div className={`relative rounded-xl p-6 ${comingSoon ? "opacity-50" : "feature-card-light"}`}>
72
+ {comingSoon && (
73
+ <div
74
+ className="absolute top-4 right-4 flex items-center gap-1.5 px-2 py-0.5 rounded-full"
75
+ style={{
76
+ background: "rgba(15,118,110,0.08)",
77
+ border: "1px solid rgba(15,118,110,0.2)",
78
+ }}
79
+ >
80
+ <Clock className="w-3 h-3" style={{ color: "#64748B" }} aria-hidden="true" />
81
+ <span className="text-[10px] font-medium" style={{ color: "#64748B" }}>
82
+ Coming Soon
83
+ </span>
84
+ </div>
85
+ )}
86
  <div
87
  className="w-10 h-10 rounded-lg flex items-center justify-center mb-4"
88
  style={{ background: "rgba(15,118,110,0.08)", border: "1px solid rgba(15,118,110,0.15)" }}
Website/src/components/Footer.tsx CHANGED
@@ -5,6 +5,8 @@ const footerNav = {
5
  Product: [
6
  { label: "How it Works", href: "/product" },
7
  { label: "Features", href: "/features" },
 
 
8
  { label: "Use Cases", href: "/use-cases" },
9
  { label: "Plans", href: "/plans" },
10
  ],
 
5
  Product: [
6
  { label: "How it Works", href: "/product" },
7
  { label: "Features", href: "/features" },
8
+ { label: "Templates", href: "/templates" },
9
+ { label: "Integrations", href: "/integrations" },
10
  { label: "Use Cases", href: "/use-cases" },
11
  { label: "Plans", href: "/plans" },
12
  ],
Website/src/components/Header.tsx CHANGED
@@ -8,6 +8,8 @@ import { Menu, X, Zap } from "lucide-react";
8
  const navLinks = [
9
  { label: "Product", href: "/product" },
10
  { label: "Features", href: "/features" },
 
 
11
  { label: "Use Cases", href: "/use-cases" },
12
  { label: "Plans", href: "/plans" },
13
  { label: "About", href: "/about" },
 
8
  const navLinks = [
9
  { label: "Product", href: "/product" },
10
  { label: "Features", href: "/features" },
11
+ { label: "Templates", href: "/templates" },
12
+ { label: "Integrations", href: "/integrations" },
13
  { label: "Use Cases", href: "/use-cases" },
14
  { label: "Plans", href: "/plans" },
15
  { label: "About", href: "/about" },
Website/src/hooks/useIntersectionObserver.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useRef, type RefObject } from "react";
4
+
5
+ /**
6
+ * Scroll-reveal hook: adds a CSS class when the element enters the viewport.
7
+ * Fires once, then disconnects the observer.
8
+ */
9
+ export function useReveal<T extends HTMLElement>(
10
+ className = "reveal-visible",
11
+ ): RefObject<T | null> {
12
+ const ref = useRef<T>(null);
13
+
14
+ useEffect(() => {
15
+ const el = ref.current;
16
+ if (!el) return;
17
+
18
+ const observer = new IntersectionObserver(
19
+ ([entry]) => {
20
+ if (entry.isIntersecting) {
21
+ el.classList.add(className);
22
+ observer.unobserve(el);
23
+ }
24
+ },
25
+ { threshold: 0.1 },
26
+ );
27
+
28
+ observer.observe(el);
29
+ return () => observer.disconnect();
30
+ }, [className]);
31
+
32
+ return ref;
33
+ }
Website/src/lib/api.ts ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Catalog API client — fetches public data from the LeadPilot backend.
3
+ * Used by Server Components with ISR (revalidate every 60s).
4
+ */
5
+
6
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000";
7
+
8
+ // ── Types ────────────────────────────────────────────────────────────
9
+
10
+ export interface CatalogPlan {
11
+ id: string;
12
+ name: string;
13
+ display_name: string;
14
+ description: string | null;
15
+ sort_order: number;
16
+ entitlements: CatalogEntitlement[];
17
+ }
18
+
19
+ export interface CatalogEntitlement {
20
+ module_key: string;
21
+ hard_limit: number | null; // null = unlimited
22
+ }
23
+
24
+ export interface CatalogModule {
25
+ key: string;
26
+ label: string;
27
+ is_enabled: boolean;
28
+ }
29
+
30
+ export interface CatalogProvider {
31
+ key: string;
32
+ label: string;
33
+ description: string;
34
+ icon_hint: string;
35
+ fields: { name: string; label: string; type: string }[];
36
+ }
37
+
38
+ export interface CatalogTemplate {
39
+ id: string;
40
+ slug: string;
41
+ name: string;
42
+ description: string;
43
+ category: string;
44
+ industry_tags: string[];
45
+ platforms: string[];
46
+ required_integrations: string[];
47
+ is_featured: boolean;
48
+ clone_count: number;
49
+ }
50
+
51
+ export interface CatalogEnum {
52
+ key: string;
53
+ label: string;
54
+ }
55
+
56
+ // ── Generic fetcher ──────────────────────────────────────────────────
57
+
58
+ interface Envelope<T> {
59
+ success: boolean;
60
+ data: T;
61
+ error: string | null;
62
+ }
63
+
64
+ async function fetchCatalog<T>(path: string): Promise<T> {
65
+ try {
66
+ const res = await fetch(`${API_BASE}/api/v1/catalog${path}`, {
67
+ next: { revalidate: 60 },
68
+ });
69
+ if (!res.ok) return [] as unknown as T;
70
+ const json: Envelope<T> = await res.json();
71
+ return json.success ? json.data : ([] as unknown as T);
72
+ } catch {
73
+ return [] as unknown as T;
74
+ }
75
+ }
76
+
77
+ // ── Typed fetchers ───────────────────────────────────────────────────
78
+
79
+ export async function getPlans(): Promise<CatalogPlan[]> {
80
+ return fetchCatalog<CatalogPlan[]>("/plans");
81
+ }
82
+
83
+ export async function getModules(): Promise<CatalogModule[]> {
84
+ return fetchCatalog<CatalogModule[]>("/modules");
85
+ }
86
+
87
+ export async function getIntegrationProviders(): Promise<CatalogProvider[]> {
88
+ return fetchCatalog<CatalogProvider[]>("/integration-providers");
89
+ }
90
+
91
+ export async function getPublicTemplates(category?: string): Promise<CatalogTemplate[]> {
92
+ const params = category ? `?category=${encodeURIComponent(category)}` : "";
93
+ return fetchCatalog<CatalogTemplate[]>(`/templates${params}`);
94
+ }
95
+
96
+ export async function getTemplateCategories(): Promise<CatalogEnum[]> {
97
+ return fetchCatalog<CatalogEnum[]>("/template-categories");
98
+ }
99
+
100
+ export async function getTemplatePlatforms(): Promise<CatalogEnum[]> {
101
+ return fetchCatalog<CatalogEnum[]>("/template-platforms");
102
+ }
Website/src/lib/moduleMap.ts ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Maps DB module keys to marketing-friendly content for the website.
3
+ * Only modules present here appear on the public site.
4
+ * A new DB module without an entry is silently excluded — no fake claims.
5
+ */
6
+
7
+ export interface ModuleMarketingInfo {
8
+ marketingTitle: string;
9
+ benefit: string;
10
+ description: string;
11
+ iconName: string; // lucide-react icon name
12
+ category: "capture" | "intelligence" | "operations" | "visibility" | "platform";
13
+ }
14
+
15
+ export const MODULE_MARKETING_MAP: Record<string, ModuleMarketingInfo> = {
16
+ prompt_studio: {
17
+ marketingTitle: "AI Prompt Studio",
18
+ benefit: "Qualify at scale",
19
+ description:
20
+ "Custom AI prompts engage every lead the moment they arrive. Scores are assigned based on intent, fit, and urgency — so your reps only work leads worth their time.",
21
+ iconName: "Brain",
22
+ category: "intelligence",
23
+ },
24
+ runtime_engine: {
25
+ marketingTitle: "Runtime Engine",
26
+ benefit: "Always-on execution",
27
+ description:
28
+ "Process and route leads 24/7 with a high-throughput engine that handles qualification, scoring, and dispatch without downtime.",
29
+ iconName: "Zap",
30
+ category: "operations",
31
+ },
32
+ inbox: {
33
+ marketingTitle: "Team Inbox & Handover",
34
+ benefit: "Stay in the loop",
35
+ description:
36
+ "All conversations flow into a shared team inbox. Reps can pick up context instantly, send messages, and mark leads without switching tools.",
37
+ iconName: "Inbox",
38
+ category: "operations",
39
+ },
40
+ analytics: {
41
+ marketingTitle: "Analytics & Conversion Tracking",
42
+ benefit: "Measure what matters",
43
+ description:
44
+ "Track lead volume, qualification rate, response time, conversion by channel and rep, and pipeline velocity — all in real time from a single dashboard.",
45
+ iconName: "BarChart3",
46
+ category: "visibility",
47
+ },
48
+ integrations_hub: {
49
+ marketingTitle: "Integrations Hub",
50
+ benefit: "Connect your stack",
51
+ description:
52
+ "Manage all your connected platforms — WhatsApp, Meta, Zoho CRM — from a single dashboard. Configure, monitor, and troubleshoot in one place.",
53
+ iconName: "Globe",
54
+ category: "platform",
55
+ },
56
+ integrations_connect: {
57
+ marketingTitle: "Integration Connect",
58
+ benefit: "Zero leakage",
59
+ description:
60
+ "Capture leads from web forms, chat widgets, WhatsApp, and more — all routed into one unified pipeline with native integrations.",
61
+ iconName: "Cable",
62
+ category: "capture",
63
+ },
64
+ dispatch_engine: {
65
+ marketingTitle: "Smart Routing & Dispatch",
66
+ benefit: "Right rep, right time",
67
+ description:
68
+ "Route leads by territory, product line, availability, or any custom logic you define. Automated dispatch with instant assignment.",
69
+ iconName: "GitMerge",
70
+ category: "operations",
71
+ },
72
+ knowledge_files: {
73
+ marketingTitle: "Knowledge Base",
74
+ benefit: "AI with context",
75
+ description:
76
+ "Upload product docs, FAQs, and playbooks. Your AI agents reference real business knowledge to give accurate, on-brand answers.",
77
+ iconName: "BookOpen",
78
+ category: "intelligence",
79
+ },
80
+ automations: {
81
+ marketingTitle: "Workflow Automation",
82
+ benefit: "Your rules",
83
+ description:
84
+ "Build trigger-based workflows that act on lead status changes, score thresholds, or time-based events. Automate follow-ups, escalations, and re-routing.",
85
+ iconName: "Settings",
86
+ category: "intelligence",
87
+ },
88
+ webhooks_ingestion: {
89
+ marketingTitle: "Webhook Ingestion",
90
+ benefit: "Any source, any format",
91
+ description:
92
+ "Accept inbound data from any external system via webhooks. Parse, validate, and route payloads directly into your lead pipeline.",
93
+ iconName: "Webhook",
94
+ category: "capture",
95
+ },
96
+ zoho_sync: {
97
+ marketingTitle: "CRM Sync (Zoho)",
98
+ benefit: "Stay connected",
99
+ description:
100
+ "Push qualified leads directly to Zoho CRM. Map fields, set sync rules, and keep your sales stack in perfect alignment without manual exports.",
101
+ iconName: "Database",
102
+ category: "platform",
103
+ },
104
+ email_engine: {
105
+ marketingTitle: "Email Engine",
106
+ benefit: "Reach every inbox",
107
+ description:
108
+ "Send transactional and follow-up emails from your lead pipeline. Templated messages, delivery tracking, and automatic retry on failure.",
109
+ iconName: "Mail",
110
+ category: "operations",
111
+ },
112
+ email_verification: {
113
+ marketingTitle: "Email Verification",
114
+ benefit: "Clean data",
115
+ description:
116
+ "Validate email addresses at capture time to keep your pipeline clean. Reduce bounce rates and improve deliverability automatically.",
117
+ iconName: "ShieldCheck",
118
+ category: "capture",
119
+ },
120
+ diagnostics: {
121
+ marketingTitle: "System Diagnostics",
122
+ benefit: "Full transparency",
123
+ description:
124
+ "Monitor system health, message queues, and integration statuses. Real-time diagnostics help you catch issues before they affect leads.",
125
+ iconName: "Activity",
126
+ category: "visibility",
127
+ },
128
+ admin_portal: {
129
+ marketingTitle: "Admin Portal",
130
+ benefit: "Stay in control",
131
+ description:
132
+ "Role-based access control, user management, and workspace configuration. Know who touched what, and when, with a full audit trail.",
133
+ iconName: "Shield",
134
+ category: "platform",
135
+ },
136
+ };
137
+
138
+ /** Category groupings for the features page */
139
+ export const FEATURE_CATEGORIES: {
140
+ key: ModuleMarketingInfo["category"];
141
+ label: string;
142
+ iconName: string;
143
+ }[] = [
144
+ { key: "capture", label: "Capture & Intake", iconName: "Zap" },
145
+ { key: "intelligence", label: "Intelligence", iconName: "Brain" },
146
+ { key: "operations", label: "Operations", iconName: "GitMerge" },
147
+ { key: "visibility", label: "Visibility", iconName: "BarChart3" },
148
+ { key: "platform", label: "Platform", iconName: "Shield" },
149
+ ];
backend/app/api/v1/catalog.py CHANGED
@@ -1,16 +1,19 @@
1
  """
2
- Catalog API — Mission 19
3
  Read-only endpoints for all enumerated reference data.
4
  No authentication required. Safe for 60s client caching.
5
  """
6
- from typing import Any
7
 
8
- from fastapi import APIRouter, Depends, Response
9
  from sqlalchemy.ext.asyncio import AsyncSession
10
  from sqlmodel import select
11
 
12
  from app.core.db import get_db
13
- from app.models.models import Plan, PlanEntitlement, SystemModuleConfig
 
 
 
14
  from app.schemas.envelope import wrap_data
15
  from app.core.catalog_registry import (
16
  INTEGRATION_PROVIDERS,
@@ -86,6 +89,47 @@ async def get_tiers(
86
  return await get_plans(response, db)
87
 
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  @router.get("/modules")
90
  async def get_modules(
91
  response: Response,
 
1
  """
2
+ Catalog API — Mission 19 + Mission 33
3
  Read-only endpoints for all enumerated reference data.
4
  No authentication required. Safe for 60s client caching.
5
  """
6
+ from typing import Any, Optional
7
 
8
+ from fastapi import APIRouter, Depends, Query, Response
9
  from sqlalchemy.ext.asyncio import AsyncSession
10
  from sqlmodel import select
11
 
12
  from app.core.db import get_db
13
+ from app.models.models import (
14
+ Plan, PlanEntitlement, SystemModuleConfig,
15
+ AutomationTemplate, TemplateUsageStat,
16
+ )
17
  from app.schemas.envelope import wrap_data
18
  from app.core.catalog_registry import (
19
  INTEGRATION_PROVIDERS,
 
89
  return await get_plans(response, db)
90
 
91
 
92
+ @router.get("/templates")
93
+ async def get_public_templates(
94
+ response: Response,
95
+ db: AsyncSession = Depends(get_db),
96
+ category: Optional[str] = Query(None),
97
+ featured: Optional[bool] = Query(None),
98
+ ) -> Any:
99
+ """Return public template metadata (no builder_graph_json or variables)."""
100
+ _set_cache(response)
101
+ query = (
102
+ select(AutomationTemplate, TemplateUsageStat)
103
+ .outerjoin(TemplateUsageStat, TemplateUsageStat.template_id == AutomationTemplate.id)
104
+ .where(AutomationTemplate.is_active == True)
105
+ )
106
+ if category:
107
+ query = query.where(AutomationTemplate.category == category)
108
+ if featured is not None:
109
+ query = query.where(AutomationTemplate.is_featured == featured)
110
+ query = query.order_by(
111
+ AutomationTemplate.is_featured.desc(),
112
+ AutomationTemplate.name.asc(),
113
+ )
114
+ result = await db.execute(query)
115
+ rows = result.all()
116
+ return wrap_data([
117
+ {
118
+ "id": str(t.id),
119
+ "slug": t.slug,
120
+ "name": t.name,
121
+ "description": t.description,
122
+ "category": t.category,
123
+ "industry_tags": t.industry_tags or [],
124
+ "platforms": t.platforms or [],
125
+ "required_integrations": t.required_integrations or [],
126
+ "is_featured": t.is_featured,
127
+ "clone_count": s.clone_count if s else 0,
128
+ }
129
+ for t, s in rows
130
+ ])
131
+
132
+
133
  @router.get("/modules")
134
  async def get_modules(
135
  response: Response,
backend/main.py CHANGED
@@ -58,6 +58,7 @@ app.add_middleware(
58
  allow_origins=[
59
  "http://localhost:3000",
60
  "http://127.0.0.1:3000",
 
61
  "https://ashrafkassem-leadpilot.hf.space",
62
  ],
63
  allow_credentials=True,
 
58
  allow_origins=[
59
  "http://localhost:3000",
60
  "http://127.0.0.1:3000",
61
+ "http://localhost:3001",
62
  "https://ashrafkassem-leadpilot.hf.space",
63
  ],
64
  allow_credentials=True,
backend/tests/test_catalog.py CHANGED
@@ -7,7 +7,11 @@ import pytest_asyncio
7
  from httpx import AsyncClient
8
  from sqlalchemy.ext.asyncio import AsyncSession
9
 
10
- from app.models.models import Plan, PlanEntitlement, SystemModuleConfig
 
 
 
 
11
 
12
 
13
  # ── Seed helpers ─────────────────────────────────────────────────────
@@ -143,3 +147,91 @@ async def test_catalog_integration_providers_have_fields(async_client: AsyncClie
143
  assert "name" in field
144
  assert "label" in field
145
  assert "type" in field
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  from httpx import AsyncClient
8
  from sqlalchemy.ext.asyncio import AsyncSession
9
 
10
+ from app.models.models import (
11
+ Plan, PlanEntitlement, SystemModuleConfig,
12
+ AutomationTemplate, AutomationTemplateVersion, TemplateUsageStat,
13
+ )
14
+ from datetime import datetime
15
 
16
 
17
  # ── Seed helpers ─────────────────────────────────────────────────────
 
147
  assert "name" in field
148
  assert "label" in field
149
  assert "type" in field
150
+
151
+
152
+ # ── Public templates endpoint (Mission 33) ────────────────────────────
153
+
154
+
155
+ async def _seed_template(db: AsyncSession, slug: str = "test-tmpl", category: str = "lead_generation", is_featured: bool = False):
156
+ """Create a minimal template + published version + usage stat for testing."""
157
+ now = datetime.utcnow()
158
+ tmpl = AutomationTemplate(
159
+ slug=slug,
160
+ name=f"Test Template {slug}",
161
+ description=f"Description for {slug}",
162
+ category=category,
163
+ platforms=["whatsapp"],
164
+ required_integrations=["whatsapp"],
165
+ industry_tags=["test"],
166
+ is_active=True,
167
+ is_featured=is_featured,
168
+ created_at=now,
169
+ updated_at=now,
170
+ )
171
+ db.add(tmpl)
172
+ await db.flush()
173
+
174
+ ver = AutomationTemplateVersion(
175
+ template_id=tmpl.id,
176
+ version_number=1,
177
+ builder_graph_json={"nodes": [], "edges": []},
178
+ changelog="Seed",
179
+ is_published=True,
180
+ published_at=now,
181
+ created_at=now,
182
+ updated_at=now,
183
+ )
184
+ db.add(ver)
185
+
186
+ stat = TemplateUsageStat(
187
+ template_id=tmpl.id,
188
+ clone_count=0,
189
+ publish_count=0,
190
+ active_flows_count=0,
191
+ created_at=now,
192
+ updated_at=now,
193
+ )
194
+ db.add(stat)
195
+ await db.flush()
196
+ return tmpl
197
+
198
+
199
+ @pytest.mark.asyncio
200
+ async def test_catalog_templates_returns_list(async_client: AsyncClient, db_session: AsyncSession):
201
+ await _seed_template(db_session, slug="cat-tmpl-1")
202
+ response = await async_client.get("/api/v1/catalog/templates")
203
+ assert response.status_code == 200
204
+ data = response.json()
205
+ assert data["success"] is True
206
+ assert isinstance(data["data"], list)
207
+ assert len(data["data"]) >= 1
208
+ item = data["data"][0]
209
+ assert "slug" in item
210
+ assert "name" in item
211
+ assert "category" in item
212
+ assert "clone_count" in item
213
+ assert "Cache-Control" in response.headers
214
+ assert "max-age=60" in response.headers["Cache-Control"]
215
+
216
+
217
+ @pytest.mark.asyncio
218
+ async def test_catalog_templates_category_filter(async_client: AsyncClient, db_session: AsyncSession):
219
+ await _seed_template(db_session, slug="cat-lg-1", category="lead_generation")
220
+ await _seed_template(db_session, slug="cat-sales-1", category="sales")
221
+ response = await async_client.get("/api/v1/catalog/templates?category=lead_generation")
222
+ assert response.status_code == 200
223
+ data = response.json()
224
+ assert data["success"] is True
225
+ for item in data["data"]:
226
+ assert item["category"] == "lead_generation"
227
+
228
+
229
+ @pytest.mark.asyncio
230
+ async def test_catalog_templates_excludes_graph(async_client: AsyncClient, db_session: AsyncSession):
231
+ await _seed_template(db_session, slug="cat-no-graph")
232
+ response = await async_client.get("/api/v1/catalog/templates")
233
+ assert response.status_code == 200
234
+ data = response.json()
235
+ for item in data["data"]:
236
+ assert "builder_graph_json" not in item
237
+ assert "variables" not in item