feat(marketing): full public landing page at /

Replaces the root redirect to /login with a real marketing landing so
first-time visitors can learn about WaCRM and convert. Authed users
still see it and get a "Go to Dashboard" CTA instead of "Sign in" /
"Get started".

Sections, in order:
- Sticky nav with smooth scroll to Features / How it works / FAQ.
  Auth-aware CTA: pending/signed-in/signed-out states render the
  right buttons without layout shift.
- Hero with clear one-liner, benefit subhead, primary + secondary
  CTA, trust cues, and a miniature inbox preview (pure CSS/SVG
  recreation of the real UI — no screenshot to age out).
- Features grid: 6 benefit-first cards.
- 4x feature spotlights (alternating): shared inbox, automations,
  pipelines, analytics. Each with a bespoke product miniature.
- How It Works: 3 steps framed around "live in 30 minutes".
- FAQ: 6 questions answering the real objections (BSP access, team
  sharing, setup speed, data ownership, templates, broadcasts).
- Final CTA banner + footer with nav + product/account link columns.

Implementation notes:
- All visuals are CSS/SVG miniatures in src/components/landing/mock —
  intentionally not real screenshots so the page doesn't drift from
  the product over time.
- Dark theme throughout to preserve the app's brand (per the repo's
  dark-only convention). No light sections.
- Per-viewport verified: desktop (1440x900) two-column hero with
  mock; mobile (375x812) stacks hero and reveals a hamburger menu.
- Middleware untouched — the page itself did the /login redirect;
  replacing page.tsx is enough.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arnas Donauskas 2026-04-18 14:51:30 +03:00
parent 221afcefc4
commit d5d406fe8e
No known key found for this signature in database
14 changed files with 1193 additions and 3 deletions

View File

@ -1,5 +1,101 @@
import { redirect } from "next/navigation"; import type { Metadata } from 'next'
export default function Home() { import { LandingNav } from '@/components/landing/nav'
redirect("/login"); 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 (
<div className="bg-slate-950 text-slate-100">
<LandingNav />
<main>
<Hero />
<FeaturesGrid />
<FeatureSpotlight
anchorId="inbox"
eyebrow="Shared inbox"
title="Never drop a WhatsApp conversation again"
body="Your whole team works from one inbox. Conversations can be assigned, tagged, and handed off without losing context. Real-time updates so two agents never reply to the same thread at the same time."
bullets={[
'Assign threads to specific agents or round-robin across the team',
'Internal notes that only your team sees',
'Unread indicators so urgent replies never slip through',
'Deep-link into any conversation from the dashboard',
]}
visual={<InboxMock />}
/>
<HowItWorks />
<FeatureSpotlight
anchorId="automations"
eyebrow="No-code automations"
title="Automate the repetitive, focus on the humans"
body="Build flows that react to WhatsApp events: welcome new contacts, chase unanswered replies, route leads by keyword. Conditions, waits, tags, deals — all with a visual builder that feels like Figma for workflows."
bullets={[
'Triggers for new messages, new contacts, tag changes, keywords, schedules',
'Actions: send message / template, add tag, create deal, webhook, and more',
'Conditional branches and wait steps for human-time delays',
'Per-run logs so you always know what ran and why',
]}
reverse
visual={<AutomationMock />}
/>
<FeatureSpotlight
anchorId="pipelines"
eyebrow="Sales pipelines"
title="Turn conversations into revenue"
body="Drag deals through custom stages, link them to contacts, and see exactly where revenue is getting stuck. Every deal keeps its WhatsApp thread one click away — so context never gets lost on a handoff."
bullets={[
'Unlimited pipelines and stages',
'Kanban board with drag-and-drop',
'Deal value totals per stage and pipeline-wide',
'Linked contacts, conversations, and notes per deal',
]}
visual={<PipelineMock />}
/>
<FeatureSpotlight
anchorId="analytics"
eyebrow="Real-time analytics"
title="See what is actually working"
body="Response times, daily volume, pipeline value, and a cross-module activity feed. The dashboard tells you where attention is needed without you building a single chart."
bullets={[
'Active conversations, new contacts, open deal value — live',
'Conversations over time for 7, 30, or 90 days',
'Average first-response time by weekday against your target',
'Activity feed merged across messages, deals, broadcasts, automations',
]}
reverse
visual={<AnalyticsMock />}
/>
<FAQ />
<CtaBanner />
</main>
<Footer />
</div>
)
} }

View File

@ -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 (
<Section className="py-16 sm:py-20">
<div
className="relative overflow-hidden rounded-2xl border border-emerald-500/20 bg-slate-900/70 px-6 py-16 text-center sm:px-12 sm:py-20"
style={{
background:
'radial-gradient(800px circle at 50% -40%, rgb(16 185 129 / 0.18), transparent 60%), linear-gradient(to bottom, rgb(15 23 42 / 0.9), rgb(15 23 42 / 0.7))',
}}
>
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">
Ready to stop switching between tools?
</h2>
<p className="mt-4 text-base text-slate-400">
Bring your WhatsApp conversations, contacts, and deals into one place.
</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
<Link
href="/signup"
className="inline-flex items-center gap-2 rounded-lg bg-emerald-500 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-emerald-400"
>
Get started free
<ArrowRight className="h-4 w-4" />
</Link>
<Link
href="/login"
className="inline-flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-900 px-6 py-3 text-sm font-semibold text-slate-200 transition-colors hover:border-slate-600 hover:text-white"
>
Sign in
</Link>
</div>
</div>
</Section>
)
}

View File

@ -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<number | null>(0)
return (
<Section id="faq">
<SectionHeader
eyebrow="FAQ"
title="Questions, answered"
description="If you cannot find what you are looking for, reach out and we will get back to you."
/>
<div className="mx-auto max-w-3xl divide-y divide-slate-800 rounded-xl border border-slate-800 bg-slate-900/40">
{QA.map((item, i) => {
const isOpen = openIdx === i
return (
<div key={item.q}>
<button
type="button"
onClick={() => setOpenIdx(isOpen ? null : i)}
className="flex w-full items-center justify-between gap-4 px-6 py-4 text-left transition-colors hover:bg-slate-900/70"
aria-expanded={isOpen}
>
<span className="text-sm font-medium text-white">{item.q}</span>
<ChevronDown
className={cn(
'h-4 w-4 flex-shrink-0 text-slate-500 transition-transform',
isOpen && 'rotate-180 text-emerald-400',
)}
/>
</button>
{isOpen && (
<div className="px-6 pb-5 text-sm leading-relaxed text-slate-400">
{item.a}
</div>
)}
</div>
)
})}
</div>
</Section>
)
}

View File

@ -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 (
<Section id={anchorId} className="py-16 sm:py-20">
<div
className={cn(
'grid grid-cols-1 items-center gap-12 lg:grid-cols-2 lg:gap-16',
reverse && 'lg:[&>*:first-child]:order-2',
)}
>
<div>
<p className="text-xs font-semibold uppercase tracking-wider text-emerald-400">
{eyebrow}
</p>
<h3 className="mt-3 text-2xl font-bold tracking-tight text-white sm:text-3xl">
{title}
</h3>
<p className="mt-4 text-base leading-relaxed text-slate-400">
{body}
</p>
{bullets && bullets.length > 0 && (
<ul className="mt-5 space-y-2.5">
{bullets.map((b) => (
<li key={b} className="flex items-start gap-2.5 text-sm text-slate-300">
<Check className="mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-400" />
<span>{b}</span>
</li>
))}
</ul>
)}
</div>
<div>{visual}</div>
</div>
</Section>
)
}

View File

@ -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 (
<Section id="features">
<SectionHeader
eyebrow="Everything you need"
title="One toolkit for your WhatsApp business"
description="Stop stitching together an inbox, a spreadsheet, and a broadcast tool. WaCRM brings them together — and talks to WhatsApp natively."
/>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
{FEATURES.map((f) => {
const Icon = f.icon
return (
<div
key={f.title}
className="rounded-xl border border-slate-800 bg-slate-900/40 p-6 transition-colors hover:border-slate-700 hover:bg-slate-900/70"
>
<div className={`flex h-10 w-10 items-center justify-center rounded-lg ${f.tint}`}>
<Icon className="h-5 w-5" />
</div>
<h3 className="mt-4 text-base font-semibold text-white">
{f.title}
</h3>
<p className="mt-1.5 text-sm leading-relaxed text-slate-400">
{f.description}
</p>
</div>
)
})}
</div>
</Section>
)
}

View File

@ -0,0 +1,77 @@
import Link from 'next/link'
import { MessageSquare } from 'lucide-react'
export function Footer() {
const year = new Date().getFullYear()
return (
<footer className="border-t border-slate-800 bg-slate-950">
<div className="mx-auto grid w-full max-w-7xl grid-cols-2 gap-8 px-6 py-12 sm:grid-cols-4">
<div className="col-span-2 sm:col-span-2">
<Link href="/" className="flex items-center gap-2" aria-label="WaCRM home">
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500">
<MessageSquare className="h-4 w-4 text-white" />
</span>
<span className="text-lg font-semibold text-white">WaCRM</span>
</Link>
<p className="mt-3 max-w-sm text-sm text-slate-500">
One toolkit for your WhatsApp business — shared inbox, pipelines,
broadcasts, and automations.
</p>
</div>
<FooterColumn
title="Product"
links={[
{ href: '#features', label: 'Features' },
{ href: '#how-it-works', label: 'How it works' },
{ href: '#faq', label: 'FAQ' },
]}
/>
<FooterColumn
title="Account"
links={[
{ href: '/signup', label: 'Get started' },
{ href: '/login', label: 'Sign in' },
{ href: '/forgot-password', label: 'Forgot password' },
]}
/>
</div>
<div className="border-t border-slate-900">
<div className="mx-auto flex w-full max-w-7xl flex-col items-start justify-between gap-3 px-6 py-5 text-xs text-slate-500 sm:flex-row sm:items-center">
<span>© {year} WaCRM. All rights reserved.</span>
<span>Built on the official WhatsApp Business API.</span>
</div>
</div>
</footer>
)
}
function FooterColumn({
title,
links,
}: {
title: string
links: { href: string; label: string }[]
}) {
return (
<div>
<h4 className="text-xs font-semibold uppercase tracking-wider text-slate-400">
{title}
</h4>
<ul className="mt-4 space-y-2.5">
{links.map((l) => (
<li key={l.href}>
<Link
href={l.href}
className="text-sm text-slate-400 transition-colors hover:text-white"
>
{l.label}
</Link>
</li>
))}
</ul>
</div>
)
}

View File

@ -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 (
<div className="relative overflow-hidden">
{/* Soft radial glow behind the hero. Pure CSS so it doesn't
count against any image budget. */}
<div
aria-hidden
className="pointer-events-none absolute inset-0 -z-10 opacity-60"
style={{
background:
'radial-gradient(800px circle at 70% -10%, rgb(16 185 129 / 0.18), transparent 60%), radial-gradient(600px circle at 10% 30%, rgb(59 130 246 / 0.08), transparent 60%)',
}}
/>
<div className="mx-auto grid w-full max-w-7xl grid-cols-1 items-center gap-12 px-6 py-20 sm:py-28 lg:grid-cols-2 lg:gap-16">
<div>
<div className="inline-flex items-center gap-2 rounded-full border border-slate-800 bg-slate-900/60 px-3 py-1 text-xs text-slate-300">
<ShieldCheck className="h-3.5 w-3.5 text-emerald-400" />
Built on the official WhatsApp Business API
</div>
<h1 className="mt-5 text-4xl font-bold tracking-tight text-white sm:text-5xl lg:text-6xl">
Run your WhatsApp business from{' '}
<span className="text-emerald-400">one inbox.</span>
</h1>
<p className="mt-5 max-w-xl text-base text-slate-400 sm:text-lg">
Shared inbox, contact hub, sales pipelines, broadcasts, and
no-code automations — in one place. Get your team on the same
page without switching tools.
</p>
<div className="mt-8 flex flex-wrap items-center gap-3">
<Link
href="/signup"
className="inline-flex items-center gap-2 rounded-lg bg-emerald-500 px-5 py-3 text-sm font-semibold text-white transition-colors hover:bg-emerald-400"
>
Get started
<ArrowRight className="h-4 w-4" />
</Link>
<Link
href="/login"
className="inline-flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-900 px-5 py-3 text-sm font-semibold text-slate-200 transition-colors hover:border-slate-600 hover:text-white"
>
Sign in
</Link>
</div>
<div className="mt-8 flex flex-wrap items-center gap-x-6 gap-y-2 text-xs text-slate-500">
<span className="flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
No credit card required
</span>
<span className="flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
Live in 30 minutes
</span>
<span className="flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
Your data, your Supabase
</span>
</div>
</div>
<div className="lg:justify-self-end">
<InboxMock />
</div>
</div>
</div>
)
}

View File

@ -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 (
<Section id="how-it-works">
<SectionHeader
eyebrow="How it works"
title="Live in under 30 minutes"
description="Most teams are up and running before their first coffee refill. No onboarding calls required."
/>
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
{STEPS.map((s) => {
const Icon = s.icon
return (
<div
key={s.num}
className="relative rounded-xl border border-slate-800 bg-slate-900/40 p-6"
>
<div className="flex items-start justify-between">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-emerald-500/10 text-emerald-400">
<Icon className="h-5 w-5" />
</div>
<span
className="text-xl font-bold tracking-tight text-slate-800 tabular-nums"
aria-hidden
>
{s.num}
</span>
</div>
<h3 className="mt-4 text-base font-semibold text-white">
{s.title}
</h3>
<p className="mt-1.5 text-sm leading-relaxed text-slate-400">
{s.body}
</p>
</div>
)
})}
</div>
</Section>
)
}

View File

@ -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 (
<div className="w-full overflow-hidden rounded-2xl border border-slate-800 bg-slate-950 shadow-2xl">
<div className="flex items-center gap-1.5 border-b border-slate-800 bg-slate-900 px-3 py-2">
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="ml-3 text-[10px] text-slate-500">Dashboard — WaCRM</span>
</div>
<div className="space-y-3 p-4">
<div className="grid grid-cols-3 gap-2">
{[
{ 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) => (
<div
key={s.label}
className="rounded-lg border border-slate-800 bg-slate-900 px-3 py-2"
>
<div className="text-[10px] text-slate-500">{s.label}</div>
<div className="mt-0.5 text-lg font-bold text-white tabular-nums">
{s.value}
</div>
<div
className={`text-[10px] tabular-nums ${
s.positive ? 'text-emerald-400' : 'text-red-400'
}`}
>
{s.delta}
</div>
</div>
))}
</div>
<div className="rounded-lg border border-slate-800 bg-slate-900 p-3">
<div className="mb-2 flex items-center justify-between">
<span className="text-[11px] font-semibold text-white">
Conversations over time
</span>
<div className="flex items-center gap-3">
<span className="inline-flex items-center gap-1 text-[9px] text-slate-500">
<span className="h-1.5 w-1.5 rounded-full bg-blue-500" />
Incoming
</span>
<span className="inline-flex items-center gap-1 text-[9px] text-slate-500">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
Outgoing
</span>
</div>
</div>
<svg
viewBox={`0 0 ${VB_W} ${VB_H}`}
className="h-28 w-full"
role="img"
aria-label="Sample conversations chart"
>
{[0, 0.5, 1].map((t) => (
<line
key={t}
x1={PAD.left}
x2={VB_W - PAD.right}
y1={PAD.top + chartH * t}
y2={PAD.top + chartH * t}
stroke="rgb(30 41 59)"
strokeDasharray="3 3"
/>
))}
<path d={path(outgoing)} fill="none" stroke="#10b981" strokeWidth={1.5} />
<path d={path(incoming)} fill="none" stroke="#3b82f6" strokeWidth={1.5} />
</svg>
</div>
</div>
</div>
)
}

View File

@ -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 (
<div className="relative w-full overflow-hidden rounded-2xl border border-slate-800 bg-slate-950 shadow-2xl">
<div className="flex items-center gap-1.5 border-b border-slate-800 bg-slate-900 px-3 py-2">
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="ml-3 text-[10px] text-slate-500">Automations — WaCRM</span>
</div>
{/* Dotted canvas — matches the real flow builder's background. */}
<div
className="relative flex flex-col items-center gap-0 px-6 py-6"
style={{
backgroundImage: 'radial-gradient(circle, rgb(30 41 59) 1px, transparent 1px)',
backgroundSize: '14px 14px',
}}
>
{steps.map((s, i) => {
const Icon = s.icon
return (
<div key={i} className="flex w-full max-w-xs flex-col items-center">
{i > 0 && <div className="h-4 w-px bg-slate-700" aria-hidden />}
<div
className={`w-full rounded-lg border border-slate-800 border-l-4 bg-slate-900 px-3 py-2 ${s.accent}`}
>
<div className="flex items-center gap-2">
<span
className={`flex h-6 w-6 items-center justify-center rounded-md ${s.badgeClass}`}
>
<Icon className="h-3 w-3" />
</span>
<div className="min-w-0">
<div className="text-[9px] font-semibold uppercase tracking-wider text-slate-500">
{s.kindLabel}
</div>
<div className="truncate text-[11px] font-medium text-white">
{s.label}
</div>
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
)
}

View File

@ -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 (
<div className="w-full overflow-hidden rounded-2xl border border-slate-800 bg-slate-950 shadow-2xl">
{/* Fake window chrome — makes the mock read as "a product", not
an abstract component. */}
<div className="flex items-center gap-1.5 border-b border-slate-800 bg-slate-900 px-3 py-2">
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="ml-3 text-[10px] text-slate-500">Inbox — WaCRM</span>
</div>
<div className="grid grid-cols-[180px_1fr] lg:grid-cols-[220px_1fr]">
{/* Conversation list */}
<div className="border-r border-slate-800 bg-slate-900/60">
<div className="flex items-center gap-2 border-b border-slate-800 px-3 py-2">
<Search className="h-3 w-3 text-slate-500" />
<span className="text-[10px] text-slate-500">Search</span>
</div>
{[
{ 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) => (
<div
key={i}
className={`flex items-start gap-2 px-3 py-2.5 ${
c.active ? 'border-l-2 border-emerald-500 bg-slate-800/60' : 'border-l-2 border-transparent'
}`}
>
<span className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full bg-emerald-500/10 text-[10px] font-medium text-emerald-400">
{c.name[0]}
</span>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">
<span className="truncate text-xs font-medium text-white">{c.name}</span>
{c.unread && <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />}
</div>
<p className="truncate text-[10px] text-slate-500">{c.preview}</p>
</div>
</div>
))}
</div>
{/* Thread */}
<div className="flex min-h-[280px] flex-col bg-slate-950">
<div className="flex items-center gap-2 border-b border-slate-800 px-4 py-2.5">
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-emerald-500/10 text-[10px] font-medium text-emerald-400">
A
</span>
<div>
<div className="text-xs font-medium text-white">Aisha</div>
<div className="text-[10px] text-slate-500">+44 7700 900123</div>
</div>
<div className="ml-auto inline-flex items-center gap-1 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2 py-0.5 text-[10px] text-emerald-300">
Open
</div>
</div>
<div className="flex flex-1 flex-col gap-2 px-4 py-4">
<div className="mr-auto max-w-[75%] rounded-2xl rounded-bl-sm bg-slate-800 px-3 py-2 text-xs text-slate-200">
Hi! Is the kit available in large?
</div>
<div className="ml-auto max-w-[75%] rounded-2xl rounded-br-sm bg-emerald-500 px-3 py-2 text-xs text-white">
Yes — shipping today 📦
</div>
<div className="mr-auto max-w-[75%] rounded-2xl rounded-bl-sm bg-slate-800 px-3 py-2 text-xs text-slate-200">
Thanks! Received it.
</div>
</div>
<div className="flex items-center gap-2 border-t border-slate-800 bg-slate-900/60 px-4 py-2.5">
<div className="flex h-8 flex-1 items-center rounded-lg border border-slate-700 bg-slate-900 px-3 text-[10px] text-slate-500">
Type a message…
</div>
<button
type="button"
tabIndex={-1}
aria-hidden
className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500 text-white"
>
<MessageSquare className="h-3.5 w-3.5" />
</button>
</div>
</div>
</div>
</div>
)
}

View File

@ -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 (
<div className="w-full overflow-hidden rounded-2xl border border-slate-800 bg-slate-950 shadow-2xl">
<div className="flex items-center gap-1.5 border-b border-slate-800 bg-slate-900 px-3 py-2">
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-700" />
<span className="ml-3 text-[10px] text-slate-500">Pipelines — WaCRM</span>
</div>
<div className="grid grid-cols-3 gap-3 p-4">
{stages.map((s) => (
<div key={s.name} className="flex flex-col gap-2 rounded-lg bg-slate-900/60 p-2">
<div className="flex items-center gap-2 px-1">
<span className={`h-1.5 w-1.5 rounded-full ${s.color}`} />
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-300">
{s.name}
</span>
<span className="ml-auto text-[10px] text-slate-500 tabular-nums">
{s.deals.length}
</span>
</div>
{s.deals.map((d, i) => (
<div
key={i}
className="rounded-md border border-slate-800 bg-slate-900 px-2.5 py-2"
>
<div className="text-[11px] font-medium text-white">{d.title}</div>
<div className="mt-1 text-[10px] text-slate-500 tabular-nums">
{d.value}
</div>
</div>
))}
</div>
))}
</div>
</div>
)
}

View File

@ -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<AuthState>('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 (
<header
className={cn(
'sticky top-0 z-40 w-full border-b transition-colors',
scrolled
? 'border-slate-800 bg-slate-950/80 backdrop-blur'
: 'border-transparent bg-transparent',
)}
>
<div className="mx-auto flex h-16 w-full max-w-7xl items-center justify-between px-6">
<Link href="/" className="flex items-center gap-2" aria-label="WaCRM home">
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500">
<MessageSquare className="h-4 w-4 text-white" />
</span>
<span className="text-lg font-semibold text-white">WaCRM</span>
</Link>
<nav className="hidden items-center gap-8 md:flex">
{LINKS.map((l) => (
<Link
key={l.href}
href={l.href}
className="text-sm text-slate-300 transition-colors hover:text-white"
>
{l.label}
</Link>
))}
</nav>
<div className="hidden items-center gap-2 md:flex">
<NavCtas auth={auth} />
</div>
<button
type="button"
className="flex h-9 w-9 items-center justify-center rounded-lg text-slate-300 transition-colors hover:bg-slate-800 hover:text-white md:hidden"
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
onClick={() => setMobileOpen((v) => !v)}
>
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
{mobileOpen && (
<div className="border-t border-slate-800 bg-slate-950/95 backdrop-blur md:hidden">
<div className="mx-auto flex max-w-7xl flex-col gap-1 px-6 py-4">
{LINKS.map((l) => (
<Link
key={l.href}
href={l.href}
onClick={() => setMobileOpen(false)}
className="rounded-md px-3 py-2 text-sm text-slate-300 hover:bg-slate-800 hover:text-white"
>
{l.label}
</Link>
))}
<div className="mt-2 flex flex-col gap-2 border-t border-slate-800 pt-3">
<NavCtas auth={auth} mobile />
</div>
</div>
</div>
)}
</header>
)
}
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 (
<>
<span className={cn(btnBase, 'w-20 bg-slate-800/60 text-transparent')}>·</span>
<span
className={cn(btnBase, 'w-28 bg-slate-800/60 text-transparent')}
>
·
</span>
</>
)
}
if (auth === 'signed-in') {
return (
<Link href="/dashboard" className={primary}>
Go to Dashboard
</Link>
)
}
return (
<>
<Link href="/login" className={secondary}>
Sign in
</Link>
<Link href="/signup" className={primary}>
Get started
</Link>
</>
)
}

View File

@ -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 (
<section
id={id}
className={cn(
'mx-auto w-full max-w-7xl px-6',
tight ? 'py-6' : 'py-20 sm:py-24',
className,
)}
>
{children}
</section>
)
}
/**
* 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 (
<div className={cn(base, max, 'mb-12')}>
{eyebrow && (
<p className="mb-3 text-xs font-semibold uppercase tracking-wider text-emerald-400">
{eyebrow}
</p>
)}
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">
{title}
</h2>
{description && (
<p className="mt-4 text-base text-slate-400">{description}</p>
)}
</div>
)
}