diff --git a/src/app/page.tsx b/src/app/page.tsx index 9f85280..4e4e79b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,101 @@ -import { redirect } from "next/navigation"; +import type { Metadata } from 'next' -export default function Home() { - redirect("/login"); +import { LandingNav } from '@/components/landing/nav' +import { Hero } from '@/components/landing/hero' +import { FeaturesGrid } from '@/components/landing/features-grid' +import { HowItWorks } from '@/components/landing/how-it-works' +import { FeatureSpotlight } from '@/components/landing/feature-spotlight' +import { FAQ } from '@/components/landing/faq' +import { CtaBanner } from '@/components/landing/cta-banner' +import { Footer } from '@/components/landing/footer' +import { InboxMock } from '@/components/landing/mock/inbox-mock' +import { PipelineMock } from '@/components/landing/mock/pipeline-mock' +import { AutomationMock } from '@/components/landing/mock/automation-mock' +import { AnalyticsMock } from '@/components/landing/mock/analytics-mock' + +export const metadata: Metadata = { + title: 'WaCRM — Run your WhatsApp business from one inbox', + description: + 'Shared inbox, contact hub, sales pipelines, broadcasts, and no-code automations for WhatsApp. Built on the official WhatsApp Business API.', +} + +// Marketing landing. Visible at / for everyone (auth redirect that +// used to send anonymous visitors to /login was removed). Authed users +// still see this page; the nav swaps its CTA to "Go to Dashboard". +export default function LandingPage() { + return ( +
+ +
+ + + + + } + /> + + + + } + /> + + } + /> + + } + /> + + + + +
+
+ ) } diff --git a/src/components/landing/cta-banner.tsx b/src/components/landing/cta-banner.tsx new file mode 100644 index 0000000..c0c0a83 --- /dev/null +++ b/src/components/landing/cta-banner.tsx @@ -0,0 +1,43 @@ +import Link from 'next/link' +import { ArrowRight } from 'lucide-react' +import { Section } from './section' + +/** + * Last push before the footer. Visually distinct (border, gradient + * background) so it doesn't read as "just another section". + */ +export function CtaBanner() { + return ( +
+
+

+ Ready to stop switching between tools? +

+

+ Bring your WhatsApp conversations, contacts, and deals into one place. +

+
+ + Get started free + + + + Sign in + +
+
+
+ ) +} diff --git a/src/components/landing/faq.tsx b/src/components/landing/faq.tsx new file mode 100644 index 0000000..c58ca00 --- /dev/null +++ b/src/components/landing/faq.tsx @@ -0,0 +1,78 @@ +"use client" + +import { useState } from 'react' +import { ChevronDown } from 'lucide-react' +import { Section, SectionHeader } from './section' +import { cn } from '@/lib/utils' + +// Answering real objections a small-business owner has before signing +// up. Order matters — setup friction is usually the #1 concern, cost +// #2, data ownership #3. +const QA = [ + { + q: 'Do I need my own WhatsApp Business API access?', + a: "Yes. WaCRM plugs into your existing Meta WhatsApp Business setup — you bring the phone number and access token, we provide the CRM tooling around it. Any Meta-approved BSP (Business Solution Provider) works.", + }, + { + q: 'Can my whole team share one WhatsApp number?', + a: 'Yes. Assign conversations to specific agents, track who is responding to what, and hand off threads without losing context. All your agents work from a single shared inbox.', + }, + { + q: 'How fast is setup?', + a: 'Most teams are live in under 30 minutes once their WhatsApp Business number has been approved by Meta. Paste your credentials in Settings, import contacts if you have them, and start replying.', + }, + { + q: 'Who owns the data?', + a: 'You do. Everything lives in your own Supabase project — contacts, conversations, deals, automation logs. Export it anytime; there is no lock-in on the data layer.', + }, + { + q: 'Can I send bulk messages and automated replies?', + a: 'Yes. Broadcasts send Meta-approved templates to segmented contact lists with delivery tracking. Automations run no-code flows triggered by new contacts, keywords, tag changes, and more.', + }, + { + q: 'What about message templates?', + a: 'Templates you create in Meta are synced automatically. Use them from the inbox, broadcasts, or inside an automation step.', + }, +] + +export function FAQ() { + const [openIdx, setOpenIdx] = useState(0) + return ( +
+ + +
+ {QA.map((item, i) => { + const isOpen = openIdx === i + return ( +
+ + {isOpen && ( +
+ {item.a} +
+ )} +
+ ) + })} +
+
+ ) +} diff --git a/src/components/landing/feature-spotlight.tsx b/src/components/landing/feature-spotlight.tsx new file mode 100644 index 0000000..7cb2b51 --- /dev/null +++ b/src/components/landing/feature-spotlight.tsx @@ -0,0 +1,64 @@ +import type { ReactNode } from 'react' +import { Check } from 'lucide-react' +import { Section } from './section' +import { cn } from '@/lib/utils' + +interface FeatureSpotlightProps { + eyebrow: string + title: string + body: string + bullets?: string[] + /** Right-side by default; flip to put the visual on the left. */ + reverse?: boolean + visual: ReactNode + anchorId?: string +} + +/** + * Reusable alternating feature section (copy on one side, product + * miniature on the other). Three instances on the page so we use the + * same component to keep spacing + typography consistent. + */ +export function FeatureSpotlight({ + eyebrow, + title, + body, + bullets, + reverse, + visual, + anchorId, +}: FeatureSpotlightProps) { + return ( +
+
*:first-child]:order-2', + )} + > +
+

+ {eyebrow} +

+

+ {title} +

+

+ {body} +

+ {bullets && bullets.length > 0 && ( +
    + {bullets.map((b) => ( +
  • + + {b} +
  • + ))} +
+ )} +
+
{visual}
+
+
+ ) +} diff --git a/src/components/landing/features-grid.tsx b/src/components/landing/features-grid.tsx new file mode 100644 index 0000000..8c25195 --- /dev/null +++ b/src/components/landing/features-grid.tsx @@ -0,0 +1,99 @@ +import { + MessageSquare, + Users, + GitBranch, + Radio, + Zap, + LineChart, +} from 'lucide-react' +import type { ComponentType } from 'react' +import { Section, SectionHeader } from './section' + +interface Feature { + title: string + description: string + icon: ComponentType<{ className?: string }> + tint: string +} + +// Intentionally six — more than that becomes a feature list, less +// feels thin. Copy is benefit-first ("never miss a lead") rather +// than feature-first ("notifications"). +const FEATURES: Feature[] = [ + { + title: 'Shared inbox', + description: + 'Every WhatsApp conversation in one place. Assign threads, reply as a team, never drop a lead.', + icon: MessageSquare, + tint: 'text-blue-400 bg-blue-500/10', + }, + { + title: 'Contact hub', + description: + 'Tags, custom fields, notes, and automatic deduplication. Import existing contacts from CSV.', + icon: Users, + tint: 'text-emerald-400 bg-emerald-500/10', + }, + { + title: 'Sales pipelines', + description: + 'Drag deals through stages. See what is won, what is slipping, and where revenue is stuck.', + icon: GitBranch, + tint: 'text-violet-400 bg-violet-500/10', + }, + { + title: 'Broadcast campaigns', + description: + 'Send Meta-approved templates to segmented lists. Track delivery, reads, and replies in real time.', + icon: Radio, + tint: 'text-amber-400 bg-amber-500/10', + }, + { + title: 'No-code automations', + description: + 'Welcome new contacts, chase unanswered replies, route leads by keyword. Visual flow builder.', + icon: Zap, + tint: 'text-rose-400 bg-rose-500/10', + }, + { + title: 'Real-time analytics', + description: + 'Response times, daily volume, pipeline value. See what is working without building dashboards.', + icon: LineChart, + tint: 'text-cyan-400 bg-cyan-500/10', + }, +] + +export function FeaturesGrid() { + return ( +
+ + +
+ {FEATURES.map((f) => { + const Icon = f.icon + return ( +
+
+ +
+

+ {f.title} +

+

+ {f.description} +

+
+ ) + })} +
+
+ ) +} diff --git a/src/components/landing/footer.tsx b/src/components/landing/footer.tsx new file mode 100644 index 0000000..17c13ac --- /dev/null +++ b/src/components/landing/footer.tsx @@ -0,0 +1,77 @@ +import Link from 'next/link' +import { MessageSquare } from 'lucide-react' + +export function Footer() { + const year = new Date().getFullYear() + return ( + + ) +} + +function FooterColumn({ + title, + links, +}: { + title: string + links: { href: string; label: string }[] +}) { + return ( +
+

+ {title} +

+ +
+ ) +} diff --git a/src/components/landing/hero.tsx b/src/components/landing/hero.tsx new file mode 100644 index 0000000..bf59b01 --- /dev/null +++ b/src/components/landing/hero.tsx @@ -0,0 +1,80 @@ +import Link from 'next/link' +import { ShieldCheck, ArrowRight } from 'lucide-react' +import { InboxMock } from './mock/inbox-mock' + +/** + * Above-the-fold hero. Two-column on desktop (copy + product visual), + * stacks on mobile. The product miniature doubles as proof of what + * the app actually looks like — better than a generic illustration. + */ +export function Hero() { + return ( +
+ {/* Soft radial glow behind the hero. Pure CSS so it doesn't + count against any image budget. */} +
+ +
+
+
+ + Built on the official WhatsApp Business API +
+ +

+ Run your WhatsApp business from{' '} + one inbox. +

+ +

+ Shared inbox, contact hub, sales pipelines, broadcasts, and + no-code automations — in one place. Get your team on the same + page without switching tools. +

+ +
+ + Get started + + + + Sign in + +
+ +
+ + + No credit card required + + + + Live in 30 minutes + + + + Your data, your Supabase + +
+
+ +
+ +
+
+
+ ) +} diff --git a/src/components/landing/how-it-works.tsx b/src/components/landing/how-it-works.tsx new file mode 100644 index 0000000..4b9c675 --- /dev/null +++ b/src/components/landing/how-it-works.tsx @@ -0,0 +1,68 @@ +import { Plug, Users, Zap } from 'lucide-react' +import { Section, SectionHeader } from './section' + +const STEPS = [ + { + num: '01', + icon: Plug, + title: 'Connect your WhatsApp number', + body: + 'Paste your phone number ID and access token from Meta. Works with any Meta-approved WhatsApp Business API provider.', + }, + { + num: '02', + icon: Users, + title: 'Bring in your contacts', + body: + 'Import a CSV, or let incoming messages build your contact list automatically. Tags and custom fields are ready from day one.', + }, + { + num: '03', + icon: Zap, + title: 'Reply, automate, measure', + body: + 'Use the shared inbox with your team, set up flows for repeat work, and track what is actually moving the needle in your analytics.', + }, +] + +export function HowItWorks() { + return ( +
+ + +
+ {STEPS.map((s) => { + const Icon = s.icon + return ( +
+
+
+ +
+ + {s.num} + +
+

+ {s.title} +

+

+ {s.body} +

+
+ ) + })} +
+
+ ) +} diff --git a/src/components/landing/mock/analytics-mock.tsx b/src/components/landing/mock/analytics-mock.tsx new file mode 100644 index 0000000..6adbfd1 --- /dev/null +++ b/src/components/landing/mock/analytics-mock.tsx @@ -0,0 +1,99 @@ +/** + * Miniature analytics panel with a line chart + 3 stat tiles. + * Shown alongside the dashboard feature spotlight. SVG rendered + * inline so there's no image to load. + */ +export function AnalyticsMock() { + // Pre-sampled line data so the curve looks "realistic" without + // being random (we want the same pleasing shape on every render). + const incoming = [4, 6, 3, 7, 9, 6, 11, 14, 10, 13, 16, 18, 15, 22] + const outgoing = [3, 5, 2, 6, 7, 5, 9, 12, 9, 11, 14, 15, 13, 19] + + const VB_W = 320 + const VB_H = 120 + const PAD = { top: 8, right: 10, bottom: 10, left: 24 } + const chartW = VB_W - PAD.left - PAD.right + const chartH = VB_H - PAD.top - PAD.bottom + const maxY = Math.max(...incoming, ...outgoing) + const stepX = chartW / (incoming.length - 1) + const y = (v: number) => PAD.top + chartH - (v / maxY) * chartH + const x = (i: number) => PAD.left + i * stepX + const path = (arr: number[]) => + arr.map((v, i) => `${i === 0 ? 'M' : 'L'}${x(i)},${y(v)}`).join(' ') + + return ( +
+
+ + + + Dashboard — WaCRM +
+ +
+
+ {[ + { label: 'Open convos', value: '42', delta: '+5', positive: true }, + { label: 'New today', value: '18', delta: '+3', positive: true }, + { label: 'Avg reply', value: '3.2m', delta: '-0.4m', positive: true }, + ].map((s) => ( +
+
{s.label}
+
+ {s.value} +
+
+ {s.delta} +
+
+ ))} +
+ +
+
+ + Conversations over time + +
+ + + Incoming + + + + Outgoing + +
+
+ + {[0, 0.5, 1].map((t) => ( + + ))} + + + +
+
+
+ ) +} diff --git a/src/components/landing/mock/automation-mock.tsx b/src/components/landing/mock/automation-mock.tsx new file mode 100644 index 0000000..0d90678 --- /dev/null +++ b/src/components/landing/mock/automation-mock.tsx @@ -0,0 +1,91 @@ +import { MessageSquare, Zap, GitBranch, Hourglass } from 'lucide-react' + +/** + * Miniature flow-builder for the Automations feature spotlight. + * Mirrors the real builder's look (trigger + stepped cards + arrows) + * but is pure presentation — no interactivity. + */ +export function AutomationMock() { + const steps = [ + { + kind: 'trigger', + label: 'First Message from Contact', + icon: Zap, + accent: 'border-l-blue-500', + badgeClass: 'bg-blue-500/10 text-blue-400', + kindLabel: 'TRIGGER', + }, + { + kind: 'action', + label: 'Send "Hi! 👋 Thanks for reaching out."', + icon: MessageSquare, + accent: 'border-l-emerald-500', + badgeClass: 'bg-emerald-500/10 text-emerald-400', + kindLabel: 'ACTION', + }, + { + kind: 'wait', + label: 'Wait 10 minutes', + icon: Hourglass, + accent: 'border-l-slate-500', + badgeClass: 'bg-slate-500/10 text-slate-400', + kindLabel: 'WAIT', + }, + { + kind: 'condition', + label: 'If tag = "lead" → assign to sales', + icon: GitBranch, + accent: 'border-l-amber-500', + badgeClass: 'bg-amber-500/10 text-amber-400', + kindLabel: 'CONDITION', + }, + ] + + return ( +
+
+ + + + Automations — WaCRM +
+ + {/* Dotted canvas — matches the real flow builder's background. */} +
+ {steps.map((s, i) => { + const Icon = s.icon + return ( +
+ {i > 0 &&
} +
+
+ + + +
+
+ {s.kindLabel} +
+
+ {s.label} +
+
+
+
+
+ ) + })} +
+
+ ) +} diff --git a/src/components/landing/mock/inbox-mock.tsx b/src/components/landing/mock/inbox-mock.tsx new file mode 100644 index 0000000..f9df75a --- /dev/null +++ b/src/components/landing/mock/inbox-mock.tsx @@ -0,0 +1,97 @@ +import { MessageSquare, Search } from 'lucide-react' + +/** + * Miniature of the app's shared inbox. Pure CSS — no real data, no + * supabase calls. Used in the hero and the inbox feature spotlight. + * Kept stateless and presentational so the page renders fast. + */ +export function InboxMock() { + return ( +
+ {/* Fake window chrome — makes the mock read as "a product", not + an abstract component. */} +
+ + + + Inbox — WaCRM +
+ +
+ {/* Conversation list */} +
+
+ + Search +
+ {[ + { name: 'Aisha', preview: 'Thanks! Received it.', unread: false, active: true }, + { name: 'Diego', preview: 'Do you ship to Brazil?', unread: true, active: false }, + { name: 'Yuki', preview: 'Price sheet attached.', unread: false, active: false }, + { name: 'Luca', preview: 'Got it, will test.', unread: false, active: false }, + ].map((c, i) => ( +
+ + {c.name[0]} + +
+
+ {c.name} + {c.unread && } +
+

{c.preview}

+
+
+ ))} +
+ + {/* Thread */} +
+
+ + A + +
+
Aisha
+
+44 7700 900123
+
+
+ Open +
+
+ +
+
+ Hi! Is the kit available in large? +
+
+ Yes — shipping today 📦 +
+
+ Thanks! Received it. +
+
+ +
+
+ Type a message… +
+ +
+
+
+
+ ) +} diff --git a/src/components/landing/mock/pipeline-mock.tsx b/src/components/landing/mock/pipeline-mock.tsx new file mode 100644 index 0000000..d9bd575 --- /dev/null +++ b/src/components/landing/mock/pipeline-mock.tsx @@ -0,0 +1,68 @@ +/** + * Miniature kanban pipeline with three stages and a handful of fake + * deal cards. Feature-spotlight for the Pipelines section. + */ +export function PipelineMock() { + const stages = [ + { + name: 'Lead', + color: 'bg-blue-500', + deals: [ + { title: 'Acme Co', value: '$1,200' }, + { title: 'Bakery on 3rd', value: '$450' }, + ], + }, + { + name: 'Proposal', + color: 'bg-amber-500', + deals: [ + { title: 'Nova Studios', value: '$4,800' }, + { title: 'Riveria Hotel', value: '$2,100' }, + { title: 'Pine & Co', value: '$960' }, + ], + }, + { + name: 'Won', + color: 'bg-emerald-500', + deals: [{ title: 'Lagoon Spa', value: '$3,200' }], + }, + ] + + return ( +
+
+ + + + Pipelines — WaCRM +
+ +
+ {stages.map((s) => ( +
+
+ + + {s.name} + + + {s.deals.length} + +
+ {s.deals.map((d, i) => ( +
+
{d.title}
+
+ {d.value} +
+
+ ))} +
+ ))} +
+
+ ) +} diff --git a/src/components/landing/nav.tsx b/src/components/landing/nav.tsx new file mode 100644 index 0000000..4ea9e10 --- /dev/null +++ b/src/components/landing/nav.tsx @@ -0,0 +1,161 @@ +"use client" + +import Link from 'next/link' +import { useEffect, useState } from 'react' +import { MessageSquare, Menu, X } from 'lucide-react' +import { createClient } from '@/lib/supabase/client' +import { cn } from '@/lib/utils' + +/** + * Landing-page top nav. Client component because we need to flip the + * primary CTA depending on whether the visitor is already signed in. + * "Auth pending" renders a placeholder (a pair of muted pill shapes) + * so the CTA doesn't pop in jarringly after hydration. + */ +type AuthState = 'pending' | 'signed-in' | 'signed-out' + +const LINKS = [ + { href: '#features', label: 'Features' }, + { href: '#how-it-works', label: 'How it works' }, + { href: '#faq', label: 'FAQ' }, +] + +export function LandingNav() { + const [auth, setAuth] = useState('pending') + const [scrolled, setScrolled] = useState(false) + const [mobileOpen, setMobileOpen] = useState(false) + + useEffect(() => { + // Quick auth check — no realtime needed, just the initial state. + const supabase = createClient() + let cancelled = false + supabase.auth.getSession().then(({ data }) => { + if (cancelled) return + setAuth(data.session?.user ? 'signed-in' : 'signed-out') + }) + return () => { + cancelled = true + } + }, []) + + useEffect(() => { + const onScroll = () => setScrolled(window.scrollY > 8) + onScroll() + window.addEventListener('scroll', onScroll, { passive: true }) + return () => window.removeEventListener('scroll', onScroll) + }, []) + + return ( +
+
+ + + + + WaCRM + + + + +
+ +
+ + +
+ + {mobileOpen && ( +
+
+ {LINKS.map((l) => ( + setMobileOpen(false)} + className="rounded-md px-3 py-2 text-sm text-slate-300 hover:bg-slate-800 hover:text-white" + > + {l.label} + + ))} +
+ +
+
+
+ )} +
+ ) +} + +function NavCtas({ auth, mobile = false }: { auth: AuthState; mobile?: boolean }) { + const btnBase = + 'inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-colors' + const secondary = cn( + btnBase, + 'text-slate-300 hover:bg-slate-800 hover:text-white', + mobile && 'justify-center', + ) + const primary = cn( + btnBase, + 'bg-emerald-500 text-white hover:bg-emerald-400', + mobile && 'justify-center', + ) + + if (auth === 'pending') { + // Neutral placeholder that matches the eventual button sizes so + // nothing shifts once auth resolves. + return ( + <> + · + + · + + + ) + } + + if (auth === 'signed-in') { + return ( + + Go to Dashboard + + ) + } + + return ( + <> + + Sign in + + + Get started + + + ) +} diff --git a/src/components/landing/section.tsx b/src/components/landing/section.tsx new file mode 100644 index 0000000..2340739 --- /dev/null +++ b/src/components/landing/section.tsx @@ -0,0 +1,69 @@ +import { cn } from '@/lib/utils' +import type { ReactNode } from 'react' + +/** + * Shared section wrapper — consistent horizontal padding, max-width, + * and vertical rhythm across every landing section. Keeping it in one + * place means tweaking spacing once vs. in every section component. + */ +export function Section({ + id, + className, + children, + tight, +}: { + id?: string + className?: string + children: ReactNode + /** Compact vertical padding — useful for nav strips / dense sections. */ + tight?: boolean +}) { + return ( +
+ {children} +
+ ) +} + +/** + * Pair of small label + big headline used atop every section. The + * eyebrow establishes context in 1-2 words before the user commits + * to reading the headline. + */ +export function SectionHeader({ + eyebrow, + title, + description, + align = 'center', +}: { + eyebrow?: string + title: string + description?: string + align?: 'center' | 'left' +}) { + const base = + align === 'center' ? 'text-center' : 'text-left' + const max = align === 'center' ? 'max-w-2xl mx-auto' : 'max-w-2xl' + return ( +
+ {eyebrow && ( +

+ {eyebrow} +

+ )} +

+ {title} +

+ {description && ( +

{description}

+ )} +
+ ) +}