Add feature pages, pricing page, and redirect unauthenticated users to landing page

- Middleware now redirects unauthenticated users to /welcome instead of /login
- Added /features and /pricing to public paths
- Created 8 dedicated feature pages: AI OCR, Scanner Integration, Integrations,
  Team Collaboration, Collection Days, Multi-Site, Analytics, Security
- Created features index page at /features with links to all detail pages
- Created standalone pricing page at /pricing with comparison table
- Extracted shared MarketingNav (with features dropdown) and MarketingFooter
- Created FeaturePageShell for consistent feature page layout
- Updated landing page to use shared components and link to feature pages

Made-with: Cursor
This commit is contained in:
Randall Stillwell 2026-04-16 19:09:52 -05:00
parent 52714c4e50
commit bf92126cf5
17 changed files with 1787 additions and 143 deletions

View file

@ -1,18 +1,16 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { ScanLine, Building2, Zap, Loader2 } from "lucide-react"; import { ScanLine, Building2, Zap, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
export default function WorkspaceSetupPage() { export default function WorkspaceSetupPage() {
const router = useRouter();
const { update: updateSession } = useSession(); const { update: updateSession } = useSession();
const [loading, setLoading] = useState<"create" | "skip" | null>(null); const [loading, setLoading] = useState<"create" | "skip" | null>(null);
async function handleCreate() { function handleCreate() {
router.push("/onboarding"); setLoading("create");
window.location.href = "/onboarding";
} }
async function handleSkip() { async function handleSkip() {
@ -25,8 +23,10 @@ export default function WorkspaceSetupPage() {
setLoading(null); setLoading(null);
return; return;
} }
// Refresh the JWT so the middleware sees the new org
await updateSession(); await updateSession();
router.push("/"); // Hard navigate to ensure fresh middleware evaluation
window.location.href = "/";
} catch (err) { } catch (err) {
console.error("Failed to create personal workspace:", err); console.error("Failed to create personal workspace:", err);
setLoading(null); setLoading(null);

View file

@ -0,0 +1,122 @@
"use client";
import {
ScanLine,
Eye,
FileCheck,
RefreshCw,
Brain,
Layers,
CheckCircle,
Sparkles,
} from "lucide-react";
import { FeaturePageShell, DetailBlock, StatRow } from "@/components/marketing/feature-page-shell";
const extractedFields = [
"Full name",
"Gender",
"Date of birth",
"Marital status",
"Cell & home phone",
"Email address",
"Mailing address",
"Visit type (first-time, returning, etc.)",
"Prayer requests",
"Confidential prayer flags",
"Spiritual decisions (baptism, salvation)",
"Service attended",
"Campus preference",
"How they heard about you",
"Message topics of interest",
"Next-step commitments",
];
export default function AiOcrPage() {
return (
<FeaturePageShell
badge="Core Technology"
badgeIcon={ScanLine}
title="AI that reads handwriting"
titleAccent="like your best volunteer."
subtitle="Echo's vision AI extracts 20+ fields from every response card — names, phone numbers, prayer requests, spiritual decisions — even when the handwriting is messy."
>
<section className="py-12 sm:py-16">
<div className="mx-auto max-w-4xl px-6 space-y-6">
<StatRow
stats={[
{ value: "20+", label: "Fields extracted per card" },
{ value: "95%", label: "Accuracy on printed text" },
{ value: "85-95%", label: "Accuracy on handwriting" },
{ value: "<30s", label: "Average processing time" },
]}
/>
<DetailBlock icon={Eye} title="Vision AI, not template matching">
<p>
Unlike traditional OCR that relies on fixed zones and templates, Echo
uses multimodal vision AI to understand the entire card. It reads
handwritten text, identifies checked boxes, interprets context, and
extracts structured data even from cards it has never seen before.
</p>
<p>
This means you don&rsquo;t need to redesign your cards or train the
system. Whether you use pre-printed forms, custom cards, or a mix of
both, Echo figures it out.
</p>
</DetailBlock>
<DetailBlock icon={Layers} title="Duplex PDF support with automatic page pairing" reversed>
<p>
Scan a full stack of double-sided cards through your office scanner. Echo
automatically pairs front and back pages, rotates them for readability,
and extracts data from both sides as a single record.
</p>
<p>
Supports PDF (multi-page), JPEG, PNG, and WebP. Files up to 50 MB each.
</p>
</DetailBlock>
<DetailBlock icon={Brain} title="Smart field extraction">
<p>
Every card is analyzed for all of the following fields. Echo only fills
in what it can confidently read and flags the rest for human review.
</p>
<div className="grid sm:grid-cols-2 gap-x-6 gap-y-1.5 mt-3">
{extractedFields.map((field) => (
<div key={field} className="flex items-center gap-2">
<CheckCircle className="h-3.5 w-3.5 text-primary shrink-0" />
<span>{field}</span>
</div>
))}
</div>
</DetailBlock>
<DetailBlock icon={Sparkles} title="Confidence scoring & human review" reversed>
<p>
Every extraction includes a confidence score. High-confidence fields are
accepted automatically, while low-confidence results are highlighted so
your team can quickly verify and correct them. Nothing syncs to your CRM
until a human approves it.
</p>
</DetailBlock>
<DetailBlock icon={FileCheck} title="Automatic date and event assignment">
<p>
Echo intelligently assigns first-time guest dates and salvation dates
based on card context and your service schedule. A card scanned on Monday
morning? It knows it came from last Sunday&rsquo;s service.
</p>
</DetailBlock>
<DetailBlock icon={RefreshCw} title="One-click reprocessing" reversed>
<p>
Made changes to a card image? Want to try again with different AI
settings? Reprocess individual cards, entire batches, or specific jobs
with one click. The original images are always preserved.
</p>
</DetailBlock>
</div>
</section>
</FeaturePageShell>
);
}

View file

@ -0,0 +1,107 @@
"use client";
import {
BarChart3,
LayoutDashboard,
Filter,
Download,
Table,
Activity,
TrendingUp,
} from "lucide-react";
import { FeaturePageShell, DetailBlock } from "@/components/marketing/feature-page-shell";
export default function AnalyticsPage() {
return (
<FeaturePageShell
badge="Analytics"
badgeIcon={BarChart3}
title="Know your guests."
titleAccent="See the trends."
subtitle="A real-time dashboard shows total cards, processing status, review progress, and follow-up metrics at a glance. Filter, sort, and export your data anytime."
>
<section className="py-12 sm:py-16">
<div className="mx-auto max-w-4xl px-6 space-y-6">
<DetailBlock icon={LayoutDashboard} title="Dashboard at a glance">
<p>
The moment you log in, you see the numbers that matter: total cards
processed, cards awaiting review, OCR success rate, and cards assigned
to you. Quick-action buttons let you jump straight to uploading,
reviewing, or managing your team.
</p>
<p>
A getting-started guide walks new users through setup, then
dismisses itself so the dashboard stays clean.
</p>
</DetailBlock>
<DetailBlock icon={Table} title="Powerful card table with advanced filtering" reversed>
<p>
The response card grid is built on TanStack Table the same engine
behind enterprise data tools. You get:
</p>
<ul className="list-disc list-inside space-y-1 mt-2">
<li>Full-text search across names, emails, phone numbers, and notes</li>
<li>Filter by OCR status, review status, visit type, date range, and more</li>
<li>Sort by any column click once for ascending, twice for descending</li>
<li>Column visibility toggles to show only what matters to you</li>
<li>Pagination with configurable page sizes</li>
<li>Row selection for bulk operations</li>
</ul>
</DetailBlock>
<DetailBlock icon={Activity} title="Status-based stat cards">
<p>
Quick-filter chips at the top of the cards view let you jump between
views with one click:
</p>
<ul className="list-disc list-inside space-y-1 mt-2">
<li><strong>All cards</strong> &mdash; everything in the system</li>
<li><strong>Completed</strong> &mdash; successfully processed by AI</li>
<li><strong>Errors</strong> &mdash; cards that need attention</li>
<li><strong>Unreviewed</strong> &mdash; processed but not yet verified by a human</li>
<li><strong>My cards</strong> &mdash; assigned to the current user</li>
</ul>
<p>
Each chip shows its count in real-time so you always know what&rsquo;s
waiting for you.
</p>
</DetailBlock>
<DetailBlock icon={TrendingUp} title="Actionable guest insights" reversed>
<p>
Every card captures rich context about your visitors: whether they&rsquo;re
first-time or returning guests, what spiritual next steps they&rsquo;re
considering, prayer requests, how they heard about your church, and
which service they attended.
</p>
<p>
This data powers your follow-up strategy. Filter for all first-time
guests from last Sunday, see who requested prayer, and identify new
believers then route them to the right team member.
</p>
</DetailBlock>
<DetailBlock icon={Filter} title="Card detail view with full history">
<p>
Click any card to see the complete record: front and back images,
every extracted field, confidence scores, assignment history, review
status, and a timestamped activity log. Edit any field directly,
add notes, and trigger reprocessing if needed.
</p>
</DetailBlock>
<DetailBlock icon={Download} title="CSV export" reversed>
<p>
Export the current view with all active filters applied as a CSV
file. This means you can filter for &ldquo;all first-time guests in
April&rdquo; and export just that subset. The CSV includes all visible
columns, ready to import into Excel, Google Sheets, or any reporting
tool.
</p>
</DetailBlock>
</div>
</section>
</FeaturePageShell>
);
}

View file

@ -0,0 +1,94 @@
"use client";
import {
CalendarDays,
Repeat,
Wand2,
ListChecks,
Calendar,
MapPin,
Settings,
} from "lucide-react";
import { FeaturePageShell, DetailBlock } from "@/components/marketing/feature-page-shell";
export default function CollectionDaysPage() {
return (
<FeaturePageShell
badge="Event Management"
badgeIcon={CalendarDays}
title="Organize cards by"
titleAccent="service, not just date."
subtitle="Define your recurring services and events. Echo auto-assigns scanned cards to the right event so you always know which Sunday, which campus, and which service a card came from."
>
<section className="py-12 sm:py-16">
<div className="mx-auto max-w-4xl px-6 space-y-6">
<DetailBlock icon={Repeat} title="Recurring events with flexible scheduling">
<p>
Create collection days that repeat on a schedule: every Sunday at
9 AM, the first Wednesday of the month, bi-weekly small groups whatever
fits your church calendar.
</p>
<p>
Echo uses the RFC 5545 recurrence standard (the same one Google Calendar
uses) so it can handle complex patterns like &ldquo;every Sunday except
holidays&rdquo; or &ldquo;first and third Saturdays.&rdquo;
</p>
</DetailBlock>
<DetailBlock icon={Wand2} title="Automatic card-to-event assignment" reversed>
<p>
When cards are processed, Echo looks at the timestamp, your location,
and your event schedule to automatically assign each card to the most
recent matching service. A batch scanned Monday morning? Echo knows it
came from last Sunday&rsquo;s service.
</p>
<p>
Three assignment strategies, configurable per organization:
</p>
<ul className="list-disc list-inside space-y-1 mt-2">
<li><strong>Most recent</strong> &mdash; auto-assign to the nearest past event within your time window</li>
<li><strong>Prompt user</strong> &mdash; suggest a match but let the reviewer confirm</li>
<li><strong>Manual only</strong> &mdash; leave assignment to team members</li>
</ul>
</DetailBlock>
<DetailBlock icon={Calendar} title="One-off events too">
<p>
Not everything is recurring. Create one-off collection days for special
events like Easter services, VBS, community outreach days, or
conferences. These work exactly like recurring events but with a single
fixed date.
</p>
</DetailBlock>
<DetailBlock icon={MapPin} title="Tied to locations" reversed>
<p>
Each collection day belongs to a specific location. This means your
downtown campus can have different service times than your north campus,
and cards scanned at each location are assigned to the correct event
automatically.
</p>
</DetailBlock>
<DetailBlock icon={ListChecks} title="See cards by event">
<p>
Browse all cards collected during a specific service. Filter your card
table by event to see exactly who visited on Easter Sunday at your
Main Street campus vs. your Northside campus. Great for targeted
follow-up.
</p>
</DetailBlock>
<DetailBlock icon={Settings} title="Configurable assignment window" reversed>
<p>
The auto-assignment engine looks backward in time for a matching event.
The default window is 48 hours, but you can adjust this per organization.
A 72-hour window works well for churches that scan cards on Monday or
Tuesday.
</p>
</DetailBlock>
</div>
</section>
</FeaturePageShell>
);
}

View file

@ -0,0 +1,170 @@
"use client";
import {
Heart,
Globe,
FileText,
BarChart3,
Webhook,
ArrowRight,
Link2,
RefreshCw,
Settings,
Zap,
} from "lucide-react";
import Link from "next/link";
import { FeaturePageShell, DetailBlock } from "@/components/marketing/feature-page-shell";
const integrationCards = [
{
name: "Planning Center",
tag: "Most Popular",
icon: Heart,
description:
"OAuth-connected. Match or create People records, sync contact info, add to lists. Full field mapping with custom field support.",
plan: "Growth",
},
{
name: "Google Sheets",
icon: FileText,
description:
"OAuth-connected. Append rows to any shared spreadsheet. Great for custom reporting or bridging to other tools.",
plan: "Starter",
},
{
name: "Monday.com",
icon: Globe,
description:
"API-connected. Create board items with mapped columns for project-style follow-up tracking and automation.",
plan: "Growth",
},
{
name: "Airtable",
icon: BarChart3,
description:
"API-connected. Push records with full field mapping into any base. Perfect for flexible data modeling.",
plan: "Growth",
},
{
name: "Webhooks",
icon: Webhook,
description:
"Send signed JSON payloads to any URL on card events. Connect to Zapier, Make, n8n, or your custom backend.",
plan: "Starter",
},
{
name: "CSV Export",
icon: FileText,
description:
"Export your full card table or filtered views as CSV. Import into any system or archive for records.",
plan: "Free",
},
];
export default function IntegrationsPage() {
return (
<FeaturePageShell
badge="Integrations"
badgeIcon={Link2}
title="Connects to"
titleAccent="where your team already works."
subtitle="Echo doesn't replace your church management system — it feeds it. Scanned card data flows directly into Planning Center, Google Sheets, Monday.com, Airtable, or any system via webhooks."
>
<section className="py-12 sm:py-16">
<div className="mx-auto max-w-5xl px-6 space-y-12">
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
{integrationCards.map((integration) => (
<div key={integration.name} className="glass-card rounded-xl p-6 relative">
{integration.tag && (
<span className="absolute top-4 right-4 text-[10px] font-bold uppercase tracking-wider bg-primary/10 text-primary px-2 py-0.5 rounded-full">
{integration.tag}
</span>
)}
<div className="mb-4 h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center">
<integration.icon className="h-5 w-5 text-primary" />
</div>
<h3 className="font-semibold mb-2">{integration.name}</h3>
<p className="text-sm text-muted-foreground leading-relaxed mb-3">
{integration.description}
</p>
<span className="text-[10px] font-medium text-muted-foreground/70 uppercase tracking-wider">
{integration.plan} plan &amp; above
</span>
</div>
))}
</div>
<div className="space-y-6">
<DetailBlock icon={Zap} title="Event-driven triggers">
<p>
Every integration fires on the events you choose. Configure each
connection to trigger on:
</p>
<ul className="list-disc list-inside space-y-1 mt-2">
<li><strong>OCR complete</strong> &mdash; as soon as a card is processed</li>
<li><strong>Card reviewed</strong> &mdash; after a team member verifies the data</li>
<li><strong>Card exported</strong> &mdash; when marked as sent to your CRM</li>
</ul>
<p>
Most churches trigger on &ldquo;reviewed&rdquo; so only human-verified
data enters their systems.
</p>
</DetailBlock>
<DetailBlock icon={Settings} title="Full field mapping" reversed>
<p>
Map Echo&rsquo;s extracted fields to your destination&rsquo;s fields
with a visual mapper. For Planning Center, Echo can even pull your
existing custom field definitions so everything lines up perfectly.
</p>
<p>
Change your card layout? Just update the field mapping no code required.
</p>
</DetailBlock>
<DetailBlock icon={Heart} title="Planning Center deep integration">
<p>
The Planning Center integration goes beyond basic syncing:
</p>
<ul className="list-disc list-inside space-y-1 mt-2">
<li>OAuth connection no API keys to manage, tokens refresh automatically</li>
<li>Smart person matching by email address or name</li>
<li>Creates new People records for first-time guests</li>
<li>Updates existing records with new contact info</li>
<li>Adds people to PCO Lists (e.g. &ldquo;First Time Guests April 2026&rdquo;)</li>
<li>Syncs phone numbers, emails, and addresses as typed sub-records</li>
</ul>
</DetailBlock>
<DetailBlock icon={RefreshCw} title="Webhook signatures for security" reversed>
<p>
Every webhook payload is signed with your secret key using HMAC-SHA256.
Your receiving endpoint can verify the signature to ensure the data
came from Echo and wasn&rsquo;t tampered with. Full payload includes
card data, event type, and timestamp.
</p>
</DetailBlock>
</div>
<div className="glass-card rounded-2xl p-8 sm:p-10 text-center">
<h3 className="text-lg font-semibold mb-3">
Don&rsquo;t see your tool?
</h3>
<p className="text-sm text-muted-foreground mb-5 max-w-lg mx-auto">
Use webhooks to connect Echo to Zapier, Make, n8n, or any platform
that accepts HTTP callbacks. Or reach out about a custom integration
on our Enterprise plan.
</p>
<Link
href="mailto:sales@echoocr.com"
className="inline-flex items-center gap-2 text-sm font-semibold text-primary hover:underline"
>
Request an integration
<ArrowRight className="h-4 w-4" />
</Link>
</div>
</div>
</section>
</FeaturePageShell>
);
}

View file

@ -0,0 +1,90 @@
"use client";
import {
Building2,
MapPin,
Users,
ArrowLeftRight,
Settings,
Globe,
} from "lucide-react";
import { FeaturePageShell, DetailBlock, StatRow } from "@/components/marketing/feature-page-shell";
export default function MultiSitePage() {
return (
<FeaturePageShell
badge="Multi-Site"
badgeIcon={Building2}
title="One organization."
titleAccent="Every campus."
subtitle="Run multiple locations under a single Echo account. Each campus gets its own collection days, scanner setup, and team assignments — all feeding into one unified dashboard."
>
<section className="py-12 sm:py-16">
<div className="mx-auto max-w-4xl px-6 space-y-6">
<StatRow
stats={[
{ value: "1", label: "Unified dashboard" },
{ value: "N", label: "Locations per org" },
{ value: "Per-site", label: "Event schedules" },
]}
/>
<DetailBlock icon={MapPin} title="Locations with independent schedules">
<p>
Each location in your organization has its own name, address, timezone
override, and set of collection days. Your downtown campus might have
services at 9 AM and 11 AM on Sundays, while your satellite campus
meets at 10 AM Echo tracks both separately.
</p>
</DetailBlock>
<DetailBlock icon={ArrowLeftRight} title="Separate scanners, unified pipeline" reversed>
<p>
Each campus can have its own scanner configuration. Northside emails
scans to one inbox, Downtown uses FTP, and the mobile campus uploads
from phones. All three feed into the same processing pipeline and
appear in the same dashboard.
</p>
</DetailBlock>
<DetailBlock icon={Users} title="Per-location team assignments">
<p>
Assign team members to review cards from specific locations. Your
downtown volunteer coordinator only sees downtown cards, while the
executive pastor sees everything. Role-based permissions work
alongside location context.
</p>
</DetailBlock>
<DetailBlock icon={Globe} title="Organization-wide reporting" reversed>
<p>
See aggregate stats across all locations or drill down by campus.
How many first-time guests did we have across all sites this month?
Which campus had the most prayer requests? The dashboard gives you
both the big picture and the details.
</p>
</DetailBlock>
<DetailBlock icon={Settings} title="Centralized settings, local overrides">
<p>
Organization-level settings (AI provider, integration connections,
team permissions) apply everywhere. Location-specific settings
(timezone, scanner config, collection day schedules) can be
customized per campus. Change your Planning Center connection once
and it applies to all locations.
</p>
</DetailBlock>
<DetailBlock icon={Building2} title="Organization switcher for multi-org users" reversed>
<p>
Consultants, denominational leaders, or staff who serve multiple
churches can switch between organizations from the sidebar. Each
org is fully isolated separate data, separate teams, separate
billing.
</p>
</DetailBlock>
</div>
</section>
</FeaturePageShell>
);
}

View file

@ -0,0 +1,159 @@
"use client";
import Link from "next/link";
import {
ScanLine,
Mail,
Link2,
Users,
CalendarDays,
Building2,
BarChart3,
Shield,
ArrowRight,
} from "lucide-react";
import { MarketingNav } from "@/components/marketing/nav";
import { MarketingFooter } from "@/components/marketing/footer";
const features = [
{
href: "/features/ai-ocr",
icon: ScanLine,
title: "AI-Powered OCR",
description:
"Vision AI extracts 20+ fields from every card — names, contact info, prayer requests, spiritual decisions — even messy handwriting.",
},
{
href: "/features/scanner-integration",
icon: Mail,
title: "Scanner Integration",
description:
"Upload from your phone, scan to email, or send batches via FTP. Echo picks up files automatically and starts processing.",
},
{
href: "/features/integrations",
icon: Link2,
title: "Integrations",
description:
"Sync directly to Planning Center, Google Sheets, Monday.com, Airtable, or any system via webhooks and CSV export.",
},
{
href: "/features/team-collaboration",
icon: Users,
title: "Team Collaboration",
description:
"Invite your guest services team with five distinct roles. Assign cards for review and track follow-up across your team.",
},
{
href: "/features/collection-days",
icon: CalendarDays,
title: "Collection Days & Events",
description:
"Define recurring services and events. Echo auto-assigns scanned cards to the correct service based on your schedule.",
},
{
href: "/features/multi-site",
icon: Building2,
title: "Multi-Site Management",
description:
"Run multiple campuses under one organization. Independent schedules, scanners, and teams — one unified dashboard.",
},
{
href: "/features/analytics",
icon: BarChart3,
title: "Dashboard & Analytics",
description:
"Real-time stats, powerful filtering, card detail views with full history, and CSV export for custom reporting.",
},
{
href: "/features/security",
icon: Shield,
title: "Security & Privacy",
description:
"Encryption, role-based access, invitation-only teams, email verification, SSO support, and full audit trails.",
},
];
export default function FeaturesIndexPage() {
return (
<div className="min-h-screen bg-background">
<MarketingNav />
<section className="relative pt-32 pb-16 sm:pt-40 sm:pb-20 overflow-hidden">
<div className="absolute inset-0 gradient-mesh pointer-events-none" />
<div className="relative mx-auto max-w-4xl px-6 text-center">
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight leading-[1.1] mb-6">
Everything your guest services team{" "}
<span className="bg-gradient-to-r from-primary via-foreground to-primary bg-clip-text text-transparent">
needs.
</span>
</h1>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto leading-relaxed">
Purpose-built for the way churches actually work &mdash; from the
welcome center to the pastor&rsquo;s desk.
</p>
</div>
</section>
<section className="py-8 sm:py-12">
<div className="mx-auto max-w-5xl px-6">
<div className="grid sm:grid-cols-2 gap-5">
{features.map((feature) => (
<Link
key={feature.href}
href={feature.href}
className="glass-card rounded-xl p-6 group hover:scale-[1.01] transition-transform"
>
<div className="flex items-start gap-4">
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0 group-hover:bg-primary/20 transition-colors">
<feature.icon className="h-5 w-5 text-primary" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className="font-semibold">{feature.title}</h3>
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-0.5 transition-all" />
</div>
<p className="text-sm text-muted-foreground leading-relaxed mt-1.5">
{feature.description}
</p>
</div>
</div>
</Link>
))}
</div>
</div>
</section>
<section className="py-16 sm:py-24 relative">
<div className="absolute inset-0 gradient-mesh pointer-events-none" />
<div className="relative mx-auto max-w-3xl px-6 text-center">
<div className="glass-card rounded-2xl p-10 sm:p-14">
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight mb-4">
Ready to see it in action?
</h2>
<p className="text-muted-foreground text-lg mb-8 max-w-xl mx-auto">
Start free with 50 cards per month. No credit card required.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link
href="/signup"
className="inline-flex items-center gap-2 rounded-xl bg-primary px-8 py-3.5 text-base font-semibold text-primary-foreground shadow-lg hover:opacity-90 transition-opacity"
>
Get Started Free
<ArrowRight className="h-4 w-4" />
</Link>
<Link
href="/pricing"
className="inline-flex items-center gap-2 rounded-xl border border-border/60 bg-card/60 px-8 py-3.5 text-base font-medium text-foreground hover:bg-accent/40 transition-colors glass"
>
View Pricing
</Link>
</div>
</div>
</div>
</section>
<MarketingFooter />
</div>
);
}

View file

@ -0,0 +1,119 @@
"use client";
import {
Mail,
Printer,
Upload,
Smartphone,
Server,
Inbox,
FolderSync,
Clock,
} from "lucide-react";
import { FeaturePageShell, DetailBlock, StatRow } from "@/components/marketing/feature-page-shell";
export default function ScannerIntegrationPage() {
return (
<FeaturePageShell
badge="Ingestion"
badgeIcon={Printer}
title="Three ways in."
titleAccent="Zero manual steps."
subtitle="Upload from your phone, scan to email, or send batches via FTP. Echo picks up files automatically and starts processing — no one has to sit at a computer."
>
<section className="py-12 sm:py-16">
<div className="mx-auto max-w-4xl px-6 space-y-6">
<StatRow
stats={[
{ value: "3", label: "Ingestion methods" },
{ value: "2 min", label: "Auto-poll interval" },
{ value: "50 MB", label: "Max file size" },
]}
/>
<DetailBlock icon={Upload} title="Drag & drop upload">
<p>
The simplest path: open Echo on your phone, tablet, or computer, and
drag files onto the card grid. Or click the upload button in the
header. Supports PDF, JPEG, PNG, and WebP.
</p>
<p>
Perfect for volunteers who snap photos of cards with their phone right
after the service. Multi-file upload means you can select an entire
folder at once.
</p>
</DetailBlock>
<DetailBlock icon={Mail} title="Scan to Email" reversed>
<p>
Configure your office multi-function printer to email scanned documents
to a dedicated inbox. Echo polls that inbox every two minutes, downloads
PDF and image attachments, and queues them for processing automatically.
</p>
<p>
After processing, Echo can mark emails as read or move them to a
&ldquo;processed&rdquo; folder so your inbox stays clean.
</p>
<p>
Works with any IMAP-compatible email provider: Gmail, Outlook,
Dreamhost, Yahoo, or your church&rsquo;s hosted email.
</p>
</DetailBlock>
<DetailBlock icon={Server} title="Scan to FTP">
<p>
For high-volume environments, configure your scanner to send batches to
an FTP server. Echo connects via FTP/FTPS, downloads new files from the
incoming directory, processes them, and moves them to a &ldquo;processed&rdquo;
folder.
</p>
<p>
This is ideal for churches that run multiple scanners at different
campuses each scanner sends to the same FTP endpoint and Echo handles
all of them.
</p>
</DetailBlock>
<DetailBlock icon={Smartphone} title="Works with any scanner" reversed>
<p>
Ricoh, Xerox, Canon, HP, Brother, Epson if your printer has a
&ldquo;Scan to Email&rdquo; or &ldquo;Scan to FTP&rdquo; option in its
address book (most do), it works with Echo. No special software, no
drivers, no plugins.
</p>
<p>
Don&rsquo;t have a dedicated scanner? A smartphone camera works too.
Take a photo, upload it, and Echo reads it just the same.
</p>
</DetailBlock>
<DetailBlock icon={Inbox} title="Smart attachment handling">
<p>
Echo filters email attachments by type and size it only processes
PDFs and images, ignoring signatures, logos, and other irrelevant
attachments. Multi-attachment emails are supported: each valid file
becomes its own processing job.
</p>
</DetailBlock>
<DetailBlock icon={Clock} title="Automatic polling on a schedule" reversed>
<p>
Both email and FTP watchers run on a two-minute cron cycle. Cards
scanned and emailed during the Sunday service start appearing in your
dashboard within minutes without anyone touching a computer.
</p>
</DetailBlock>
<DetailBlock icon={FolderSync} title="Full audit trail">
<p>
Every import is tracked as a processing job with source metadata:
where the file came from (upload, email, FTP), when it arrived, how
many cards were extracted, and the processing status. You always know
what happened and when.
</p>
</DetailBlock>
</div>
</section>
</FeaturePageShell>
);
}

View file

@ -0,0 +1,111 @@
"use client";
import {
Shield,
Lock,
KeyRound,
UserCheck,
MailCheck,
Server,
ShieldCheck,
Eye,
} from "lucide-react";
import { FeaturePageShell, DetailBlock } from "@/components/marketing/feature-page-shell";
export default function SecurityPage() {
return (
<FeaturePageShell
badge="Security & Privacy"
badgeIcon={Shield}
title="Your congregation's data,"
titleAccent="protected."
subtitle="Echo is built with the same security standards expected of healthcare and financial applications. Because your members' personal information and prayer requests deserve nothing less."
>
<section className="py-12 sm:py-16">
<div className="mx-auto max-w-4xl px-6 space-y-6">
<DetailBlock icon={Lock} title="Encryption everywhere">
<p>
All data is encrypted in transit using TLS 1.3 and at rest using
AES-256 encryption. This applies to your database (Supabase
PostgreSQL), file storage (Supabase Storage), and every API
connection between Echo and third-party services.
</p>
<p>
Passwords are hashed with bcrypt they are never stored or
transmitted in plain text.
</p>
</DetailBlock>
<DetailBlock icon={ShieldCheck} title="Role-based access control" reversed>
<p>
Five distinct roles (Owner, Admin, Editor, Reviewer, Viewer) control
exactly what each team member can see and do. Permissions are enforced
on every API route not just in the UI. Even if someone bookmarks a
URL, they can&rsquo;t access data beyond their role.
</p>
</DetailBlock>
<DetailBlock icon={UserCheck} title="Invitation-only access">
<p>
No public signup on your organization&rsquo;s instance. New team members
must be invited by an Admin or Owner. Invitations include a pre-assigned
role and expire after 7 days. This prevents unauthorized access and
keeps your member data private.
</p>
</DetailBlock>
<DetailBlock icon={MailCheck} title="Email verification" reversed>
<p>
Every account requires a verified email address. Users who haven&rsquo;t
verified see a persistent banner and can request a new verification
email at any time. This ensures that account recovery and
notifications reach the right person.
</p>
</DetailBlock>
<DetailBlock icon={KeyRound} title="SSO & enterprise authentication">
<p>
Enterprise plans support OIDC/SSO integration with identity providers
like Authentik, Okta, Azure AD, and Google Workspace. This lets large
churches and denominations use their existing identity infrastructure
single sign-on, centralized user management, and automatic
deprovisioning when staff leave.
</p>
</DetailBlock>
<DetailBlock icon={Eye} title="Audit trail" reversed>
<p>
Every significant action is logged: who created a card, who reviewed
it, who changed a setting, when integrations fired, and what data was
synced. The activity log is timestamped and user-attributed, giving
you a complete chain of custody for sensitive data like prayer
requests and personal decisions.
</p>
</DetailBlock>
<DetailBlock icon={Server} title="Enterprise-grade infrastructure">
<p>
Echo runs on Vercel&rsquo;s global edge network with Supabase (built
on AWS) for database and storage. Both platforms maintain SOC 2
Type II compliance, regular security audits, and automatic backups.
</p>
<p>
For organizations with strict data residency requirements, the
Enterprise plan includes self-hosted deployment via Docker
your data never leaves your own infrastructure.
</p>
</DetailBlock>
<DetailBlock icon={Lock} title="Webhook signature verification" reversed>
<p>
All outbound webhooks are signed with HMAC-SHA256 using your secret
key. Receiving systems can verify that payloads originated from Echo
and were not modified in transit. This prevents spoofed data from
entering your CRM or other connected systems.
</p>
</DetailBlock>
</div>
</section>
</FeaturePageShell>
);
}

View file

@ -0,0 +1,121 @@
"use client";
import {
Users,
ShieldCheck,
UserPlus,
Eye,
Pencil,
Crown,
ClipboardCheck,
Bell,
} from "lucide-react";
import { FeaturePageShell, DetailBlock } from "@/components/marketing/feature-page-shell";
const roles = [
{
icon: Crown,
name: "Owner",
description: "Full control. Manages billing, org settings, and all users. Created during setup.",
},
{
icon: ShieldCheck,
name: "Admin",
description: "Manages team members, all settings, integrations, and every card operation.",
},
{
icon: Pencil,
name: "Editor",
description: "Creates, edits, deletes cards. Runs OCR, manages imports and collection days.",
},
{
icon: ClipboardCheck,
name: "Reviewer",
description: "Edits cards assigned to them. Marks cards as reviewed for syncing.",
},
{
icon: Eye,
name: "Viewer",
description: "Read-only access to cards, events, and stats. Perfect for senior pastors who just want visibility.",
},
];
export default function TeamCollaborationPage() {
return (
<FeaturePageShell
badge="Team Management"
badgeIcon={Users}
title="Your whole team,"
titleAccent="with the right access."
subtitle="Invite volunteers, staff, and pastors with role-based permissions. Everyone sees exactly what they need — nothing more, nothing less."
>
<section className="py-12 sm:py-16">
<div className="mx-auto max-w-4xl px-6 space-y-6">
<div className="glass-card rounded-2xl p-8 sm:p-10">
<h3 className="text-lg font-semibold mb-6 text-center">Five roles, clear boundaries</h3>
<div className="space-y-4">
{roles.map((role) => (
<div key={role.name} className="flex items-start gap-4 p-4 rounded-xl hover:bg-accent/20 transition-colors">
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
<role.icon className="h-5 w-5 text-primary" />
</div>
<div>
<h4 className="font-semibold text-sm">{role.name}</h4>
<p className="text-sm text-muted-foreground mt-0.5">{role.description}</p>
</div>
</div>
))}
</div>
</div>
<DetailBlock icon={UserPlus} title="Invitation-based onboarding">
<p>
No self-registration on your instance. Admins send email invitations
with a pre-assigned role. The invitee clicks a link, creates their
account, and is immediately placed on your team with the right permissions.
</p>
<p>
Invitations expire after 7 days and can be revoked at any time from
the team management page.
</p>
</DetailBlock>
<DetailBlock icon={ClipboardCheck} title="Card assignment & review workflow" reversed>
<p>
After cards are scanned, assign them to specific team members for
review. Reviewers see only their assigned cards, verify the AI&rsquo;s
extraction, make corrections, and mark them as reviewed.
</p>
<p>
Once reviewed, cards can be synced to Planning Center or your CRM
with confidence that the data is accurate. The full assignment and
review history is tracked in the activity log.
</p>
</DetailBlock>
<DetailBlock icon={Bell} title="In-app notifications">
<p>
Team members receive notifications when cards are assigned to them,
when processing completes, and when errors need attention. A
notification center in the header keeps everyone informed without
email overload.
</p>
</DetailBlock>
<DetailBlock icon={Users} title="Built for church teams" reversed>
<p>
Most churches have a mix of paid staff and volunteers on their guest
services team. Echo makes it easy to give volunteers limited access
(Reviewer or Viewer) while staff have broader control (Editor or Admin).
</p>
<p>
The Owner role is reserved for the person who set up the account
typically the executive pastor or admin director ensuring there&rsquo;s
always a single point of accountability.
</p>
</DetailBlock>
</div>
</section>
</FeaturePageShell>
);
}

View file

@ -0,0 +1,339 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { Check, ArrowRight, HelpCircle } from "lucide-react";
import { MarketingNav } from "@/components/marketing/nav";
import { MarketingFooter } from "@/components/marketing/footer";
const tiers = [
{
name: "Free",
price: "$0",
period: "forever",
description: "Perfect for trying Echo with your team.",
cards: "50 cards/month",
features: [
"AI-powered OCR extraction",
"Web upload (PDF, JPEG, PNG)",
"2 team members",
"1 location",
"CSV export",
"Community support",
],
cta: "Get Started Free",
ctaHref: "/signup",
highlighted: false,
},
{
name: "Starter",
price: "$29",
period: "/month",
annualPrice: "$24",
description: "For churches ready to automate their guest workflow.",
cards: "250 cards/month",
features: [
"Everything in Free, plus:",
"Email & FTP scanner integration",
"5 team members",
"Collection day management",
"Google Sheets integration",
"Webhook support",
"Email support",
],
cta: "Start 14-Day Trial",
ctaHref: "/signup?plan=starter",
highlighted: false,
},
{
name: "Growth",
price: "$79",
period: "/month",
annualPrice: "$66",
description: "Full integration power for growing congregations.",
cards: "1,000 cards/month",
features: [
"Everything in Starter, plus:",
"Planning Center sync",
"Monday.com & Airtable",
"15 team members",
"3 locations",
"Auto-assign to events",
"API access",
"Priority support",
],
cta: "Start 14-Day Trial",
ctaHref: "/signup?plan=growth",
highlighted: true,
},
{
name: "Enterprise",
price: "Custom",
period: "",
description: "For multi-site churches and denominations.",
cards: "Unlimited cards",
features: [
"Everything in Growth, plus:",
"Unlimited team members",
"Unlimited locations",
"SSO / SAML authentication",
"Custom integrations",
"Dedicated account manager",
"99.9% uptime SLA",
"Self-hosted deployment option",
],
cta: "Contact Sales",
ctaHref: "mailto:sales@echoocr.com",
highlighted: false,
},
];
const comparisonRows = [
{ feature: "Cards per month", free: "50", starter: "250", growth: "1,000", enterprise: "Unlimited" },
{ feature: "Team members", free: "2", starter: "5", growth: "15", enterprise: "Unlimited" },
{ feature: "Locations", free: "1", starter: "1", growth: "3", enterprise: "Unlimited" },
{ feature: "AI-powered OCR", free: true, starter: true, growth: true, enterprise: true },
{ feature: "Web upload", free: true, starter: true, growth: true, enterprise: true },
{ feature: "Email scanning", free: false, starter: true, growth: true, enterprise: true },
{ feature: "FTP scanning", free: false, starter: true, growth: true, enterprise: true },
{ feature: "Collection days", free: false, starter: true, growth: true, enterprise: true },
{ feature: "CSV export", free: true, starter: true, growth: true, enterprise: true },
{ feature: "Google Sheets", free: false, starter: true, growth: true, enterprise: true },
{ feature: "Webhooks", free: false, starter: true, growth: true, enterprise: true },
{ feature: "Planning Center", free: false, starter: false, growth: true, enterprise: true },
{ feature: "Monday.com", free: false, starter: false, growth: true, enterprise: true },
{ feature: "Airtable", free: false, starter: false, growth: true, enterprise: true },
{ feature: "Auto-assign to events", free: false, starter: false, growth: true, enterprise: true },
{ feature: "API access", free: false, starter: false, growth: true, enterprise: true },
{ feature: "SSO / SAML", free: false, starter: false, growth: false, enterprise: true },
{ feature: "Custom integrations", free: false, starter: false, growth: false, enterprise: true },
{ feature: "Self-hosted option", free: false, starter: false, growth: false, enterprise: true },
{ feature: "Dedicated account manager", free: false, starter: false, growth: false, enterprise: true },
{ feature: "Uptime SLA", free: false, starter: false, growth: false, enterprise: true },
];
function CellValue({ value }: { value: boolean | string }) {
if (typeof value === "string") {
return <span className="text-sm font-medium">{value}</span>;
}
return value ? (
<Check className="h-4 w-4 text-primary mx-auto" />
) : (
<span className="text-muted-foreground/30">&mdash;</span>
);
}
export default function PricingPage() {
const [annual, setAnnual] = useState(false);
return (
<div className="min-h-screen bg-background">
<MarketingNav />
<section className="relative pt-32 pb-16 sm:pt-40 sm:pb-20 overflow-hidden">
<div className="absolute inset-0 gradient-mesh pointer-events-none" />
<div className="relative mx-auto max-w-4xl px-6 text-center">
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight leading-[1.1] mb-6">
Simple, transparent{" "}
<span className="bg-gradient-to-r from-primary via-foreground to-primary bg-clip-text text-transparent">
pricing.
</span>
</h1>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto leading-relaxed mb-8">
Start free. Upgrade when you&rsquo;re ready. Every plan saves your team
hours of manual data entry every single week.
</p>
<div className="inline-flex items-center gap-3 rounded-full border border-border/60 bg-card/60 p-1 glass">
<button
onClick={() => setAnnual(false)}
className={`rounded-full px-5 py-2 text-sm font-medium transition-all ${
!annual
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
Monthly
</button>
<button
onClick={() => setAnnual(true)}
className={`rounded-full px-5 py-2 text-sm font-medium transition-all ${
annual
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
Annual
<span className="ml-1.5 text-xs opacity-80">Save 17%</span>
</button>
</div>
</div>
</section>
<section className="pb-16">
<div className="mx-auto max-w-6xl px-6">
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-5">
{tiers.map((tier) => (
<div
key={tier.name}
className={`rounded-2xl p-6 flex flex-col ${
tier.highlighted
? "glass-card ring-2 ring-primary/30 shadow-xl scale-[1.02]"
: "glass-card"
}`}
>
{tier.highlighted && (
<div className="text-[10px] font-bold uppercase tracking-wider text-primary mb-4">
Most Popular
</div>
)}
<h3 className="text-lg font-semibold">{tier.name}</h3>
<div className="mt-3 mb-1">
<span className="text-3xl font-bold">
{annual && tier.annualPrice ? tier.annualPrice : tier.price}
</span>
{tier.period && (
<span className="text-sm text-muted-foreground ml-1">
{tier.period}
</span>
)}
</div>
{annual && tier.annualPrice && (
<div className="text-xs text-muted-foreground mb-3">
Billed annually ({tier.annualPrice === "$24" ? "$288" : "$792"}/year)
</div>
)}
<p className="text-sm text-muted-foreground mb-4">
{tier.description}
</p>
<div className="text-sm font-semibold text-foreground mb-4 pb-4 border-b border-border/40">
{tier.cards}
</div>
<ul className="space-y-3 mb-8 flex-1">
{tier.features.map((feature) => (
<li key={feature} className="flex items-start gap-2.5 text-sm">
<Check className="h-4 w-4 text-primary mt-0.5 shrink-0" />
<span className="text-muted-foreground">{feature}</span>
</li>
))}
</ul>
<Link
href={tier.ctaHref}
className={`block text-center rounded-lg py-2.5 text-sm font-semibold transition-all ${
tier.highlighted
? "bg-primary text-primary-foreground shadow-sm hover:opacity-90"
: "border border-border/60 bg-card/60 text-foreground hover:bg-accent/40"
}`}
>
{tier.cta}
</Link>
</div>
))}
</div>
<p className="text-center text-xs text-muted-foreground mt-8">
All paid plans include a 14-day free trial. No credit card required to
start. Need more cards? Overage billed at $0.08/card.
</p>
</div>
</section>
<section className="py-16">
<div className="mx-auto max-w-5xl px-6">
<h2 className="text-2xl font-bold tracking-tight text-center mb-10">
Compare plans
</h2>
<div className="glass-card rounded-2xl overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border/40">
<th className="text-left p-4 font-semibold w-[200px]">Feature</th>
<th className="text-center p-4 font-semibold">Free</th>
<th className="text-center p-4 font-semibold">Starter</th>
<th className="text-center p-4 font-semibold bg-primary/5">Growth</th>
<th className="text-center p-4 font-semibold">Enterprise</th>
</tr>
</thead>
<tbody>
{comparisonRows.map((row) => (
<tr key={row.feature} className="border-b border-border/20 last:border-0">
<td className="p-4 text-muted-foreground">{row.feature}</td>
<td className="p-4 text-center"><CellValue value={row.free} /></td>
<td className="p-4 text-center"><CellValue value={row.starter} /></td>
<td className="p-4 text-center bg-primary/5"><CellValue value={row.growth} /></td>
<td className="p-4 text-center"><CellValue value={row.enterprise} /></td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</section>
<section className="py-16">
<div className="mx-auto max-w-3xl px-6">
<h2 className="text-2xl font-bold tracking-tight text-center mb-10">
Common questions about pricing
</h2>
<div className="space-y-6">
{[
{
q: "What counts as a \"card\"?",
a: "Each individual response card extracted from your upload counts as one card. A 10-page duplex PDF with 5 card pairs = 5 cards. A single photo of one card = 1 card.",
},
{
q: "What happens if I exceed my monthly limit?",
a: "We'll never cut you off mid-month. Overage is billed at $0.08 per card at the end of the billing cycle. If you're consistently over, we'll suggest upgrading to save money.",
},
{
q: "Can I change plans at any time?",
a: "Yes. Upgrade instantly and we'll prorate the difference. Downgrade at the end of your current billing period. No penalties, no contracts.",
},
{
q: "Do you offer nonprofit discounts?",
a: "Our pricing is already designed to be accessible for churches and nonprofits. If you're a small church with budget constraints, contact us — we're happy to work something out.",
},
{
q: "Is there really no credit card required?",
a: "Correct. The Free plan is fully functional with no credit card. Paid plan trials also start without payment — we only ask for billing info when you're ready to continue after the trial.",
},
].map((faq) => (
<div key={faq.q} className="glass-card rounded-xl p-6">
<div className="flex items-start gap-3">
<HelpCircle className="h-5 w-5 text-primary mt-0.5 shrink-0" />
<div>
<h3 className="font-semibold text-sm mb-2">{faq.q}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">{faq.a}</p>
</div>
</div>
</div>
))}
</div>
</div>
</section>
<section className="py-16 sm:py-24 relative">
<div className="absolute inset-0 gradient-mesh pointer-events-none" />
<div className="relative mx-auto max-w-3xl px-6 text-center">
<div className="glass-card rounded-2xl p-10 sm:p-14">
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight mb-4">
Ready to give your team Sunday evenings back?
</h2>
<p className="text-muted-foreground text-lg mb-8 max-w-xl mx-auto">
Start free. No credit card. No commitment.
</p>
<Link
href="/signup"
className="inline-flex items-center gap-2 rounded-xl bg-primary px-8 py-3.5 text-base font-semibold text-primary-foreground shadow-lg hover:opacity-90 transition-opacity"
>
Get Started Free
<ArrowRight className="h-4 w-4" />
</Link>
</div>
</div>
</section>
<MarketingFooter />
</div>
);
}

View file

@ -23,87 +23,9 @@ import {
CalendarDays, CalendarDays,
FileText, FileText,
Webhook, Webhook,
Sun,
Moon,
} from "lucide-react"; } from "lucide-react";
import { useTheme } from "next-themes"; import { MarketingNav } from "@/components/marketing/nav";
import { MarketingFooter } from "@/components/marketing/footer";
function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="rounded-full p-2 text-muted-foreground hover:text-foreground transition-colors"
aria-label="Toggle theme"
>
<Sun className="h-4 w-4 hidden dark:block" />
<Moon className="h-4 w-4 block dark:hidden" />
</button>
);
}
function Nav() {
const [mobileOpen, setMobileOpen] = useState(false);
return (
<nav className="fixed top-0 inset-x-0 z-50 glass-panel">
<div className="mx-auto max-w-6xl flex items-center justify-between px-6 h-16">
<Link href="/welcome" className="flex items-center gap-2.5">
<div className="h-8 w-8 rounded-lg bg-primary flex items-center justify-center">
<ScanLine className="h-4 w-4 text-primary-foreground" />
</div>
<span className="text-lg font-semibold tracking-tight">Echo</span>
</Link>
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-muted-foreground">
<a href="#features" className="hover:text-foreground transition-colors">Features</a>
<a href="#how-it-works" className="hover:text-foreground transition-colors">How It Works</a>
<a href="#integrations" className="hover:text-foreground transition-colors">Integrations</a>
<a href="#pricing" className="hover:text-foreground transition-colors">Pricing</a>
<a href="#faq" className="hover:text-foreground transition-colors">FAQ</a>
</div>
<div className="flex items-center gap-3">
<ThemeToggle />
<Link
href="/login"
className="hidden sm:inline-flex text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
Sign in
</Link>
<Link
href="/signup"
className="inline-flex items-center gap-1.5 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground shadow-sm hover:opacity-90 transition-opacity"
>
Get Started Free
</Link>
<button
className="md:hidden p-2 text-muted-foreground"
onClick={() => setMobileOpen(!mobileOpen)}
aria-label="Toggle menu"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{mobileOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</div>
</div>
{mobileOpen && (
<div className="md:hidden border-t border-border/50 px-6 py-4 space-y-3 text-sm font-medium glass">
<a href="#features" className="block text-muted-foreground hover:text-foreground" onClick={() => setMobileOpen(false)}>Features</a>
<a href="#how-it-works" className="block text-muted-foreground hover:text-foreground" onClick={() => setMobileOpen(false)}>How It Works</a>
<a href="#integrations" className="block text-muted-foreground hover:text-foreground" onClick={() => setMobileOpen(false)}>Integrations</a>
<a href="#pricing" className="block text-muted-foreground hover:text-foreground" onClick={() => setMobileOpen(false)}>Pricing</a>
<a href="#faq" className="block text-muted-foreground hover:text-foreground" onClick={() => setMobileOpen(false)}>FAQ</a>
<Link href="/login" className="block text-muted-foreground hover:text-foreground" onClick={() => setMobileOpen(false)}>Sign in</Link>
</div>
)}
</nav>
);
}
function Hero() { function Hero() {
return ( return (
@ -246,54 +168,63 @@ const features = [
{ {
icon: ScanLine, icon: ScanLine,
title: "AI-Powered OCR", title: "AI-Powered OCR",
href: "/features/ai-ocr",
description: description:
"Advanced vision AI reads handwriting, checkboxes, and printed text. Extracts 20+ fields per card including names, contact info, prayer requests, and spiritual decisions.", "Advanced vision AI reads handwriting, checkboxes, and printed text. Extracts 20+ fields per card including names, contact info, prayer requests, and spiritual decisions.",
}, },
{ {
icon: Mail, icon: Mail,
title: "Email & FTP Scanning", title: "Email & FTP Scanning",
href: "/features/scanner-integration",
description: description:
"Configure your office scanner to email or FTP batches directly. Echo automatically picks them up and starts processing &mdash; no manual upload needed.", "Configure your office scanner to email or FTP batches directly. Echo automatically picks them up and starts processing &mdash; no manual upload needed.",
}, },
{ {
icon: Upload, icon: Upload,
title: "Drag & Drop Upload", title: "Drag & Drop Upload",
href: "/features/scanner-integration",
description: description:
"Upload photos or PDFs from your phone, tablet, or computer. Supports multi-page duplex-scanned PDFs with automatic front/back pairing.", "Upload photos or PDFs from your phone, tablet, or computer. Supports multi-page duplex-scanned PDFs with automatic front/back pairing.",
}, },
{ {
icon: CalendarDays, icon: CalendarDays,
title: "Collection Days & Events", title: "Collection Days & Events",
href: "/features/collection-days",
description: description:
"Define recurring services and events. Echo auto-assigns scanned cards to the most recent Sunday service or event, keeping everything organized.", "Define recurring services and events. Echo auto-assigns scanned cards to the most recent Sunday service or event, keeping everything organized.",
}, },
{ {
icon: Users, icon: Users,
title: "Team Collaboration", title: "Team Collaboration",
href: "/features/team-collaboration",
description: description:
"Invite your pastoral care team with role-based access. Assign cards for follow-up, track review status, and keep everyone on the same page.", "Invite your pastoral care team with role-based access. Assign cards for follow-up, track review status, and keep everyone on the same page.",
}, },
{ {
icon: Building2, icon: Building2,
title: "Multi-Site Ready", title: "Multi-Site Ready",
href: "/features/multi-site",
description: description:
"Manage multiple campuses under one organization. Each location gets its own collection days, scanner setup, and team assignments.", "Manage multiple campuses under one organization. Each location gets its own collection days, scanner setup, and team assignments.",
}, },
{ {
icon: BarChart3, icon: BarChart3,
title: "Dashboard & Analytics", title: "Dashboard & Analytics",
href: "/features/analytics",
description: description:
"See first-time guests, salvations, prayer requests, and follow-up status at a glance. Filter, sort, and export your data anytime.", "See first-time guests, salvations, prayer requests, and follow-up status at a glance. Filter, sort, and export your data anytime.",
}, },
{ {
icon: Shield, icon: Shield,
title: "Secure by Default", title: "Secure by Default",
href: "/features/security",
description: description:
"Role-based permissions, encrypted storage, email verification, and invitation-only team access protect your congregation&rsquo;s personal data.", "Role-based permissions, encrypted storage, email verification, and invitation-only team access protect your congregation&rsquo;s personal data.",
}, },
{ {
icon: Clock, icon: Clock,
title: "Instant Follow-Up", title: "Instant Follow-Up",
href: "/features/ai-ocr",
description: description:
"Stop waiting until Tuesday to start reaching out. Cards scanned Sunday afternoon are in your system by Sunday evening.", "Stop waiting until Tuesday to start reaching out. Cards scanned Sunday afternoon are in your system by Sunday evening.",
}, },
@ -315,21 +246,34 @@ function Features() {
</div> </div>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-5"> <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
{features.map((feature) => ( {features.map((feature) => (
<div <Link
key={feature.title} key={feature.title}
className="glass-card rounded-xl p-6 hover:scale-[1.01] transition-transform" href={feature.href}
className="glass-card rounded-xl p-6 hover:scale-[1.01] transition-transform group"
> >
<div className="mb-4 h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center"> <div className="mb-4 h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
<feature.icon className="h-5 w-5 text-primary" /> <feature.icon className="h-5 w-5 text-primary" />
</div> </div>
<h3 className="font-semibold mb-2">{feature.title}</h3> <div className="flex items-center gap-2 mb-2">
<h3 className="font-semibold">{feature.title}</h3>
<ArrowRight className="h-3.5 w-3.5 text-muted-foreground opacity-0 group-hover:opacity-100 group-hover:translate-x-0.5 transition-all" />
</div>
<p <p
className="text-sm text-muted-foreground leading-relaxed" className="text-sm text-muted-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: feature.description }} dangerouslySetInnerHTML={{ __html: feature.description }}
/> />
</div> </Link>
))} ))}
</div> </div>
<div className="text-center mt-10">
<Link
href="/features"
className="inline-flex items-center gap-2 text-sm font-semibold text-primary hover:underline"
>
Explore all features
<ArrowRight className="h-4 w-4" />
</Link>
</div>
</div> </div>
</section> </section>
); );
@ -403,6 +347,15 @@ function Integrations() {
</div> </div>
))} ))}
</div> </div>
<div className="text-center mt-10">
<Link
href="/features/integrations"
className="inline-flex items-center gap-2 text-sm font-semibold text-primary hover:underline"
>
See all integrations in detail
<ArrowRight className="h-4 w-4" />
</Link>
</div>
</div> </div>
</section> </section>
); );
@ -751,54 +704,10 @@ function FinalCTA() {
); );
} }
function Footer() {
return (
<footer className="border-t border-border/40 py-12">
<div className="mx-auto max-w-6xl px-6">
<div className="grid sm:grid-cols-4 gap-8">
<div className="sm:col-span-2">
<div className="flex items-center gap-2.5 mb-4">
<div className="h-7 w-7 rounded-lg bg-primary flex items-center justify-center">
<ScanLine className="h-3.5 w-3.5 text-primary-foreground" />
</div>
<span className="text-base font-semibold tracking-tight">Echo</span>
</div>
<p className="text-sm text-muted-foreground max-w-xs leading-relaxed">
AI-powered response card scanning for churches, ministries, and
nonprofits. Turn paper into people &mdash; faster.
</p>
</div>
<div>
<h4 className="text-sm font-semibold mb-4">Product</h4>
<ul className="space-y-2.5 text-sm text-muted-foreground">
<li><a href="#features" className="hover:text-foreground transition-colors">Features</a></li>
<li><a href="#integrations" className="hover:text-foreground transition-colors">Integrations</a></li>
<li><a href="#pricing" className="hover:text-foreground transition-colors">Pricing</a></li>
<li><a href="#faq" className="hover:text-foreground transition-colors">FAQ</a></li>
</ul>
</div>
<div>
<h4 className="text-sm font-semibold mb-4">Company</h4>
<ul className="space-y-2.5 text-sm text-muted-foreground">
<li><a href="mailto:support@echoocr.com" className="hover:text-foreground transition-colors">Support</a></li>
<li><a href="mailto:sales@echoocr.com" className="hover:text-foreground transition-colors">Sales</a></li>
<li><a href="#" className="hover:text-foreground transition-colors">Privacy Policy</a></li>
<li><a href="#" className="hover:text-foreground transition-colors">Terms of Service</a></li>
</ul>
</div>
</div>
<div className="mt-10 pt-6 border-t border-border/30 text-center text-xs text-muted-foreground">
&copy; {new Date().getFullYear()} Echo. All rights reserved.
</div>
</div>
</footer>
);
}
export default function WelcomePage() { export default function WelcomePage() {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<Nav /> <MarketingNav />
<Hero /> <Hero />
<ProblemStatement /> <ProblemStatement />
<HowItWorks /> <HowItWorks />
@ -808,7 +717,7 @@ export default function WelcomePage() {
<Pricing /> <Pricing />
<FAQ /> <FAQ />
<FinalCTA /> <FinalCTA />
<Footer /> <MarketingFooter />
</div> </div>
); );
} }

View file

@ -53,13 +53,12 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
: []), : []),
], ],
callbacks: { callbacks: {
async jwt({ token, user, trigger }) { async jwt({ token, user }) {
if (user) { if (user) {
token.id = user.id; token.id = user.id;
} }
// Refresh org data on sign-in or when session update is triggered if (token.id) {
if (token.id && (user || trigger === "update")) {
const dbUser = await prisma.user.findUnique({ const dbUser = await prisma.user.findUnique({
where: { id: token.id as string }, where: { id: token.id as string },
include: { include: {

View file

@ -0,0 +1,124 @@
"use client";
import Link from "next/link";
import { ArrowRight, type LucideIcon } from "lucide-react";
import { MarketingNav } from "./nav";
import { MarketingFooter } from "./footer";
interface FeaturePageShellProps {
badge: string;
badgeIcon: LucideIcon;
title: string;
titleAccent: string;
subtitle: string;
children: React.ReactNode;
}
export function FeaturePageShell({
badge,
badgeIcon: BadgeIcon,
title,
titleAccent,
subtitle,
children,
}: FeaturePageShellProps) {
return (
<div className="min-h-screen bg-background">
<MarketingNav />
<section className="relative pt-32 pb-16 sm:pt-40 sm:pb-20 overflow-hidden">
<div className="absolute inset-0 gradient-mesh pointer-events-none" />
<div className="relative mx-auto max-w-4xl px-6 text-center">
<div className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-card/60 px-4 py-1.5 text-xs font-medium text-muted-foreground mb-8 glass">
<BadgeIcon className="h-3.5 w-3.5" />
{badge}
</div>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight leading-[1.1] mb-6">
{title}{" "}
<span className="bg-gradient-to-r from-primary via-foreground to-primary bg-clip-text text-transparent">
{titleAccent}
</span>
</h1>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto leading-relaxed">
{subtitle}
</p>
</div>
</section>
{children}
<section className="py-16 sm:py-24 relative">
<div className="absolute inset-0 gradient-mesh pointer-events-none" />
<div className="relative mx-auto max-w-3xl px-6 text-center">
<div className="glass-card rounded-2xl p-10 sm:p-14">
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight mb-4">
Ready to see it in action?
</h2>
<p className="text-muted-foreground text-lg mb-8 max-w-xl mx-auto">
Start free with 50 cards per month. No credit card required.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link
href="/signup"
className="inline-flex items-center gap-2 rounded-xl bg-primary px-8 py-3.5 text-base font-semibold text-primary-foreground shadow-lg hover:opacity-90 transition-opacity"
>
Get Started Free
<ArrowRight className="h-4 w-4" />
</Link>
<Link
href="/pricing"
className="inline-flex items-center gap-2 rounded-xl border border-border/60 bg-card/60 px-8 py-3.5 text-base font-medium text-foreground hover:bg-accent/40 transition-colors glass"
>
View Pricing
</Link>
</div>
</div>
</div>
</section>
<MarketingFooter />
</div>
);
}
interface DetailBlockProps {
icon: LucideIcon;
title: string;
children: React.ReactNode;
reversed?: boolean;
}
export function DetailBlock({ icon: Icon, title, children, reversed }: DetailBlockProps) {
return (
<div className={`glass-card rounded-2xl p-8 sm:p-10 flex flex-col ${reversed ? "sm:flex-row-reverse" : "sm:flex-row"} gap-8 items-start`}>
<div className="shrink-0 h-14 w-14 rounded-xl bg-primary/10 flex items-center justify-center">
<Icon className="h-7 w-7 text-primary" />
</div>
<div>
<h3 className="text-lg font-semibold mb-3">{title}</h3>
<div className="text-sm text-muted-foreground leading-relaxed space-y-3">
{children}
</div>
</div>
</div>
);
}
interface StatRowProps {
stats: { value: string; label: string }[];
}
export function StatRow({ stats }: StatRowProps) {
return (
<div className="glass-card rounded-2xl p-8 sm:p-10">
<div className={`grid grid-cols-2 ${stats.length > 2 ? "md:grid-cols-" + stats.length : ""} gap-8 text-center`}>
{stats.map((stat) => (
<div key={stat.label}>
<div className="text-3xl sm:text-4xl font-bold text-primary mb-2">{stat.value}</div>
<div className="text-sm text-muted-foreground">{stat.label}</div>
</div>
))}
</div>
</div>
);
}

View file

@ -0,0 +1,47 @@
import Link from "next/link";
import { ScanLine } from "lucide-react";
export function MarketingFooter() {
return (
<footer className="border-t border-border/40 py-12">
<div className="mx-auto max-w-6xl px-6">
<div className="grid sm:grid-cols-4 gap-8">
<div className="sm:col-span-2">
<Link href="/welcome" className="flex items-center gap-2.5 mb-4">
<div className="h-7 w-7 rounded-lg bg-primary flex items-center justify-center">
<ScanLine className="h-3.5 w-3.5 text-primary-foreground" />
</div>
<span className="text-base font-semibold tracking-tight">Echo</span>
</Link>
<p className="text-sm text-muted-foreground max-w-xs leading-relaxed">
AI-powered response card scanning for churches, ministries, and
nonprofits. Turn paper into people &mdash; faster.
</p>
</div>
<div>
<h4 className="text-sm font-semibold mb-4">Product</h4>
<ul className="space-y-2.5 text-sm text-muted-foreground">
<li><Link href="/features/ai-ocr" className="hover:text-foreground transition-colors">AI-Powered OCR</Link></li>
<li><Link href="/features/integrations" className="hover:text-foreground transition-colors">Integrations</Link></li>
<li><Link href="/features/scanner-integration" className="hover:text-foreground transition-colors">Scanner Integration</Link></li>
<li><Link href="/pricing" className="hover:text-foreground transition-colors">Pricing</Link></li>
<li><Link href="/welcome#faq" className="hover:text-foreground transition-colors">FAQ</Link></li>
</ul>
</div>
<div>
<h4 className="text-sm font-semibold mb-4">Company</h4>
<ul className="space-y-2.5 text-sm text-muted-foreground">
<li><a href="mailto:support@echoocr.com" className="hover:text-foreground transition-colors">Support</a></li>
<li><a href="mailto:sales@echoocr.com" className="hover:text-foreground transition-colors">Sales</a></li>
<li><a href="#" className="hover:text-foreground transition-colors">Privacy Policy</a></li>
<li><a href="#" className="hover:text-foreground transition-colors">Terms of Service</a></li>
</ul>
</div>
</div>
<div className="mt-10 pt-6 border-t border-border/30 text-center text-xs text-muted-foreground">
&copy; {new Date().getFullYear()} Echo. All rights reserved.
</div>
</div>
</footer>
);
}

View file

@ -0,0 +1,129 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { ScanLine, Sun, Moon, ChevronDown } from "lucide-react";
import { useTheme } from "next-themes";
function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="rounded-full p-2 text-muted-foreground hover:text-foreground transition-colors"
aria-label="Toggle theme"
>
<Sun className="h-4 w-4 hidden dark:block" />
<Moon className="h-4 w-4 block dark:hidden" />
</button>
);
}
const featureLinks = [
{ href: "/features/ai-ocr", label: "AI-Powered OCR" },
{ href: "/features/scanner-integration", label: "Scanner Integration" },
{ href: "/features/integrations", label: "Integrations" },
{ href: "/features/team-collaboration", label: "Team Collaboration" },
{ href: "/features/collection-days", label: "Collection Days" },
{ href: "/features/multi-site", label: "Multi-Site Management" },
{ href: "/features/analytics", label: "Dashboard & Analytics" },
{ href: "/features/security", label: "Security & Privacy" },
];
export function MarketingNav() {
const [mobileOpen, setMobileOpen] = useState(false);
const [featuresOpen, setFeaturesOpen] = useState(false);
return (
<nav className="fixed top-0 inset-x-0 z-50 glass-panel">
<div className="mx-auto max-w-6xl flex items-center justify-between px-6 h-16">
<Link href="/welcome" className="flex items-center gap-2.5">
<div className="h-8 w-8 rounded-lg bg-primary flex items-center justify-center">
<ScanLine className="h-4 w-4 text-primary-foreground" />
</div>
<span className="text-lg font-semibold tracking-tight">Echo</span>
</Link>
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-muted-foreground">
<div
className="relative group"
onMouseEnter={() => setFeaturesOpen(true)}
onMouseLeave={() => setFeaturesOpen(false)}
>
<button className="flex items-center gap-1 hover:text-foreground transition-colors">
Features
<ChevronDown className="h-3.5 w-3.5" />
</button>
{featuresOpen && (
<div className="absolute top-full left-1/2 -translate-x-1/2 pt-2">
<div className="glass-card rounded-xl p-2 w-56 shadow-xl">
{featureLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="block px-3 py-2 rounded-lg text-sm text-muted-foreground hover:text-foreground hover:bg-accent/40 transition-colors"
onClick={() => setFeaturesOpen(false)}
>
{link.label}
</Link>
))}
</div>
</div>
)}
</div>
<Link href="/pricing" className="hover:text-foreground transition-colors">Pricing</Link>
<Link href="/welcome#faq" className="hover:text-foreground transition-colors">FAQ</Link>
</div>
<div className="flex items-center gap-3">
<ThemeToggle />
<Link
href="/login"
className="hidden sm:inline-flex text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
Sign in
</Link>
<Link
href="/signup"
className="inline-flex items-center gap-1.5 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground shadow-sm hover:opacity-90 transition-opacity"
>
Get Started Free
</Link>
<button
className="md:hidden p-2 text-muted-foreground"
onClick={() => setMobileOpen(!mobileOpen)}
aria-label="Toggle menu"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{mobileOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</div>
</div>
{mobileOpen && (
<div className="md:hidden border-t border-border/50 px-6 py-4 space-y-3 text-sm font-medium glass">
<div className="text-xs font-bold text-muted-foreground/60 uppercase tracking-wider mb-2">Features</div>
{featureLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="block pl-2 text-muted-foreground hover:text-foreground"
onClick={() => setMobileOpen(false)}
>
{link.label}
</Link>
))}
<div className="pt-2 border-t border-border/30">
<Link href="/pricing" className="block text-muted-foreground hover:text-foreground" onClick={() => setMobileOpen(false)}>Pricing</Link>
</div>
<Link href="/welcome#faq" className="block text-muted-foreground hover:text-foreground" onClick={() => setMobileOpen(false)}>FAQ</Link>
<Link href="/login" className="block text-muted-foreground hover:text-foreground" onClick={() => setMobileOpen(false)}>Sign in</Link>
</div>
)}
</nav>
);
}

View file

@ -3,6 +3,8 @@ import { getToken } from "next-auth/jwt";
const publicPaths = [ const publicPaths = [
"/welcome", "/welcome",
"/features",
"/pricing",
"/login", "/login",
"/signup", "/signup",
"/invite", "/invite",
@ -69,9 +71,11 @@ export async function middleware(req: NextRequest) {
}); });
if (!token) { if (!token) {
const loginUrl = new URL("/login", req.url); if (pathname === "/login" || pathname === "/signup") {
loginUrl.searchParams.set("callbackUrl", pathname); return NextResponse.next();
return NextResponse.redirect(loginUrl); }
const welcomeUrl = new URL("/welcome", req.url);
return NextResponse.redirect(welcomeUrl);
} }
const hasOrg = !!token.orgId; const hasOrg = !!token.orgId;