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:
parent
52714c4e50
commit
bf92126cf5
17 changed files with 1787 additions and 143 deletions
|
|
@ -1,18 +1,16 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { ScanLine, Building2, Zap, Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function WorkspaceSetupPage() {
|
||||
const router = useRouter();
|
||||
const { update: updateSession } = useSession();
|
||||
const [loading, setLoading] = useState<"create" | "skip" | null>(null);
|
||||
|
||||
async function handleCreate() {
|
||||
router.push("/onboarding");
|
||||
function handleCreate() {
|
||||
setLoading("create");
|
||||
window.location.href = "/onboarding";
|
||||
}
|
||||
|
||||
async function handleSkip() {
|
||||
|
|
@ -25,8 +23,10 @@ export default function WorkspaceSetupPage() {
|
|||
setLoading(null);
|
||||
return;
|
||||
}
|
||||
// Refresh the JWT so the middleware sees the new org
|
||||
await updateSession();
|
||||
router.push("/");
|
||||
// Hard navigate to ensure fresh middleware evaluation
|
||||
window.location.href = "/";
|
||||
} catch (err) {
|
||||
console.error("Failed to create personal workspace:", err);
|
||||
setLoading(null);
|
||||
|
|
|
|||
122
src/app/(marketing)/features/ai-ocr/page.tsx
Normal file
122
src/app/(marketing)/features/ai-ocr/page.tsx
Normal 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’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’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>
|
||||
);
|
||||
}
|
||||
107
src/app/(marketing)/features/analytics/page.tsx
Normal file
107
src/app/(marketing)/features/analytics/page.tsx
Normal 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> — everything in the system</li>
|
||||
<li><strong>Completed</strong> — successfully processed by AI</li>
|
||||
<li><strong>Errors</strong> — cards that need attention</li>
|
||||
<li><strong>Unreviewed</strong> — processed but not yet verified by a human</li>
|
||||
<li><strong>My cards</strong> — assigned to the current user</li>
|
||||
</ul>
|
||||
<p>
|
||||
Each chip shows its count in real-time so you always know what’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’re
|
||||
first-time or returning guests, what spiritual next steps they’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 “all first-time guests in
|
||||
April” 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>
|
||||
);
|
||||
}
|
||||
94
src/app/(marketing)/features/collection-days/page.tsx
Normal file
94
src/app/(marketing)/features/collection-days/page.tsx
Normal 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 “every Sunday except
|
||||
holidays” or “first and third Saturdays.”
|
||||
</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’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> — auto-assign to the nearest past event within your time window</li>
|
||||
<li><strong>Prompt user</strong> — suggest a match but let the reviewer confirm</li>
|
||||
<li><strong>Manual only</strong> — 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>
|
||||
);
|
||||
}
|
||||
170
src/app/(marketing)/features/integrations/page.tsx
Normal file
170
src/app/(marketing)/features/integrations/page.tsx
Normal 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 & 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> — as soon as a card is processed</li>
|
||||
<li><strong>Card reviewed</strong> — after a team member verifies the data</li>
|
||||
<li><strong>Card exported</strong> — when marked as sent to your CRM</li>
|
||||
</ul>
|
||||
<p>
|
||||
Most churches trigger on “reviewed” so only human-verified
|
||||
data enters their systems.
|
||||
</p>
|
||||
</DetailBlock>
|
||||
|
||||
<DetailBlock icon={Settings} title="Full field mapping" reversed>
|
||||
<p>
|
||||
Map Echo’s extracted fields to your destination’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. “First Time Guests April 2026”)</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’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’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>
|
||||
);
|
||||
}
|
||||
90
src/app/(marketing)/features/multi-site/page.tsx
Normal file
90
src/app/(marketing)/features/multi-site/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
159
src/app/(marketing)/features/page.tsx
Normal file
159
src/app/(marketing)/features/page.tsx
Normal 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 — from the
|
||||
welcome center to the pastor’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>
|
||||
);
|
||||
}
|
||||
119
src/app/(marketing)/features/scanner-integration/page.tsx
Normal file
119
src/app/(marketing)/features/scanner-integration/page.tsx
Normal 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
|
||||
“processed” folder — so your inbox stays clean.
|
||||
</p>
|
||||
<p>
|
||||
Works with any IMAP-compatible email provider: Gmail, Outlook,
|
||||
Dreamhost, Yahoo, or your church’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 “processed”
|
||||
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
|
||||
“Scan to Email” or “Scan to FTP” option in its
|
||||
address book (most do), it works with Echo. No special software, no
|
||||
drivers, no plugins.
|
||||
</p>
|
||||
<p>
|
||||
Don’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>
|
||||
);
|
||||
}
|
||||
111
src/app/(marketing)/features/security/page.tsx
Normal file
111
src/app/(marketing)/features/security/page.tsx
Normal 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’t access data beyond their role.
|
||||
</p>
|
||||
</DetailBlock>
|
||||
|
||||
<DetailBlock icon={UserCheck} title="Invitation-only access">
|
||||
<p>
|
||||
No public signup on your organization’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’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’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>
|
||||
);
|
||||
}
|
||||
121
src/app/(marketing)/features/team-collaboration/page.tsx
Normal file
121
src/app/(marketing)/features/team-collaboration/page.tsx
Normal 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’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’s
|
||||
always a single point of accountability.
|
||||
</p>
|
||||
</DetailBlock>
|
||||
</div>
|
||||
</section>
|
||||
</FeaturePageShell>
|
||||
);
|
||||
}
|
||||
339
src/app/(marketing)/pricing/page.tsx
Normal file
339
src/app/(marketing)/pricing/page.tsx
Normal 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">—</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’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>
|
||||
);
|
||||
}
|
||||
|
|
@ -23,87 +23,9 @@ import {
|
|||
CalendarDays,
|
||||
FileText,
|
||||
Webhook,
|
||||
Sun,
|
||||
Moon,
|
||||
} 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>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
import { MarketingNav } from "@/components/marketing/nav";
|
||||
import { MarketingFooter } from "@/components/marketing/footer";
|
||||
|
||||
function Hero() {
|
||||
return (
|
||||
|
|
@ -246,54 +168,63 @@ const features = [
|
|||
{
|
||||
icon: ScanLine,
|
||||
title: "AI-Powered OCR",
|
||||
href: "/features/ai-ocr",
|
||||
description:
|
||||
"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,
|
||||
title: "Email & FTP Scanning",
|
||||
href: "/features/scanner-integration",
|
||||
description:
|
||||
"Configure your office scanner to email or FTP batches directly. Echo automatically picks them up and starts processing — no manual upload needed.",
|
||||
},
|
||||
{
|
||||
icon: Upload,
|
||||
title: "Drag & Drop Upload",
|
||||
href: "/features/scanner-integration",
|
||||
description:
|
||||
"Upload photos or PDFs from your phone, tablet, or computer. Supports multi-page duplex-scanned PDFs with automatic front/back pairing.",
|
||||
},
|
||||
{
|
||||
icon: CalendarDays,
|
||||
title: "Collection Days & Events",
|
||||
href: "/features/collection-days",
|
||||
description:
|
||||
"Define recurring services and events. Echo auto-assigns scanned cards to the most recent Sunday service or event, keeping everything organized.",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: "Team Collaboration",
|
||||
href: "/features/team-collaboration",
|
||||
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.",
|
||||
},
|
||||
{
|
||||
icon: Building2,
|
||||
title: "Multi-Site Ready",
|
||||
href: "/features/multi-site",
|
||||
description:
|
||||
"Manage multiple campuses under one organization. Each location gets its own collection days, scanner setup, and team assignments.",
|
||||
},
|
||||
{
|
||||
icon: BarChart3,
|
||||
title: "Dashboard & Analytics",
|
||||
href: "/features/analytics",
|
||||
description:
|
||||
"See first-time guests, salvations, prayer requests, and follow-up status at a glance. Filter, sort, and export your data anytime.",
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: "Secure by Default",
|
||||
href: "/features/security",
|
||||
description:
|
||||
"Role-based permissions, encrypted storage, email verification, and invitation-only team access protect your congregation’s personal data.",
|
||||
},
|
||||
{
|
||||
icon: Clock,
|
||||
title: "Instant Follow-Up",
|
||||
href: "/features/ai-ocr",
|
||||
description:
|
||||
"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 className="grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{features.map((feature) => (
|
||||
<div
|
||||
<Link
|
||||
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" />
|
||||
</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
|
||||
className="text-sm text-muted-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: feature.description }}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</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>
|
||||
</section>
|
||||
);
|
||||
|
|
@ -403,6 +347,15 @@ function Integrations() {
|
|||
</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>
|
||||
</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 — 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">
|
||||
© {new Date().getFullYear()} Echo. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WelcomePage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Nav />
|
||||
<MarketingNav />
|
||||
<Hero />
|
||||
<ProblemStatement />
|
||||
<HowItWorks />
|
||||
|
|
@ -808,7 +717,7 @@ export default function WelcomePage() {
|
|||
<Pricing />
|
||||
<FAQ />
|
||||
<FinalCTA />
|
||||
<Footer />
|
||||
<MarketingFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,13 +53,12 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||
: []),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user, trigger }) {
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
}
|
||||
|
||||
// Refresh org data on sign-in or when session update is triggered
|
||||
if (token.id && (user || trigger === "update")) {
|
||||
if (token.id) {
|
||||
const dbUser = await prisma.user.findUnique({
|
||||
where: { id: token.id as string },
|
||||
include: {
|
||||
|
|
|
|||
124
src/components/marketing/feature-page-shell.tsx
Normal file
124
src/components/marketing/feature-page-shell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
47
src/components/marketing/footer.tsx
Normal file
47
src/components/marketing/footer.tsx
Normal 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 — 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">
|
||||
© {new Date().getFullYear()} Echo. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
129
src/components/marketing/nav.tsx
Normal file
129
src/components/marketing/nav.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@ import { getToken } from "next-auth/jwt";
|
|||
|
||||
const publicPaths = [
|
||||
"/welcome",
|
||||
"/features",
|
||||
"/pricing",
|
||||
"/login",
|
||||
"/signup",
|
||||
"/invite",
|
||||
|
|
@ -69,9 +71,11 @@ export async function middleware(req: NextRequest) {
|
|||
});
|
||||
|
||||
if (!token) {
|
||||
const loginUrl = new URL("/login", req.url);
|
||||
loginUrl.searchParams.set("callbackUrl", pathname);
|
||||
return NextResponse.redirect(loginUrl);
|
||||
if (pathname === "/login" || pathname === "/signup") {
|
||||
return NextResponse.next();
|
||||
}
|
||||
const welcomeUrl = new URL("/welcome", req.url);
|
||||
return NextResponse.redirect(welcomeUrl);
|
||||
}
|
||||
|
||||
const hasOrg = !!token.orgId;
|
||||
|
|
|
|||
Loading…
Reference in a new issue