Spaces:
Running
feat: Mission 33 — Marketing Website Alignment + DB-Driven Content
Browse filesConvert 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 +0 -0
- Website/package.json +3 -1
- Website/scripts/parity-audit.ts +155 -0
- Website/src/app/features/page.tsx +63 -151
- Website/src/app/globals.css +118 -0
- Website/src/app/integrations/page.tsx +231 -0
- Website/src/app/page.tsx +241 -321
- Website/src/app/plans/PlansClientContent.tsx +467 -0
- Website/src/app/plans/page.tsx +13 -572
- Website/src/app/templates/CategoryFilter.tsx +58 -0
- Website/src/app/templates/TemplateCard.tsx +55 -0
- Website/src/app/templates/page.tsx +121 -0
- Website/src/components/FeatureCard.tsx +48 -5
- Website/src/components/Footer.tsx +2 -0
- Website/src/components/Header.tsx +2 -0
- Website/src/hooks/useIntersectionObserver.ts +33 -0
- Website/src/lib/api.ts +102 -0
- Website/src/lib/moduleMap.ts +149 -0
- backend/app/api/v1/catalog.py +48 -4
- backend/main.py +1 -0
- backend/tests/test_catalog.py +93 -1
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -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 |
}
|
|
@@ -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 |
+
});
|
|
@@ -1,20 +1,10 @@
|
|
| 1 |
import type { Metadata } from "next";
|
| 2 |
-
import {
|
| 3 |
-
|
| 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 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 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 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 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 |
-
{
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 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 |
-
{
|
| 222 |
-
<FeatureCard
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
))}
|
| 224 |
</div>
|
| 225 |
</div>
|
| 226 |
</section>
|
| 227 |
|
| 228 |
{/* ─── CATEGORIES DEEP DIVE ─────────────────────────────────── */}
|
| 229 |
-
{
|
| 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 |
-
<
|
| 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 |
-
{
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 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>
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 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 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 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 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
]
|
| 135 |
-
|
| 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 |
-
|
| 197 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
{
|
| 500 |
-
<FeatureCard
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
{/* ───
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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=
|
| 547 |
-
{plans.map((plan) =>
|
| 548 |
-
|
| 549 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 550 |
</div>
|
| 551 |
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
href="/plans"
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
>
|
| 559 |
-
|
| 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 |
|
|
@@ -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're on the list.</p>
|
| 104 |
+
<p className="text-sm" style={{ color: "#94A3B8" }}>
|
| 105 |
+
We'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'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 |
+
}
|
|
@@ -1,573 +1,14 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
import
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 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're on the list.</p>
|
| 159 |
-
<p className="text-sm" style={{ color: "#94A3B8" }}>
|
| 160 |
-
We'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'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 |
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -1,23 +1,52 @@
|
|
| 1 |
-
import
|
|
|
|
| 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
|
|
|
|
| 13 |
title,
|
| 14 |
benefit,
|
| 15 |
description,
|
| 16 |
dark = false,
|
|
|
|
| 17 |
}: FeatureCardProps) {
|
|
|
|
|
|
|
| 18 |
if (dark) {
|
| 19 |
return (
|
| 20 |
-
<div className=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)" }}
|
|
@@ -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 |
],
|
|
@@ -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" },
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
];
|
|
@@ -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
|
|
|
|
|
|
|
|
|
|
| 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,
|
|
@@ -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,
|
|
@@ -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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|