docs(qa): remove 4 high-risk unsupported marketing/security promises

Per docs/qa/audit-2026-06-26/FINAL-REPORT.md. Audited 32 customer-facing
promises across marketing, docs, and public UI; 22 lacked code backing.
This commit removes the four highest-risk items:

- Fabricated landing stats (500+ farms, 98% on-time, 50K+ orders, 2M+ lbs)
  and 4.9/12 JSON-LD rating on src/app/page.tsx + HeroSection.tsx.
- Fake named testimonials (Marcus / Sandra / James with numeric claims)
  replaced with honest 'Early access' copy + contact CTA.
- Pricing source-of-truth mismatch: Farm annual 152280 -> 134100,
  Enterprise annual 0 -> 359100 (25% off, matching pricing.ts).
- Security claims (SOC 2 Type II, 99.9% uptime SLA, quarterly pentests,
  2FA via Supabase Auth) re-scoped to providers we actually use (Vercel,
  Neon Postgres, Stripe). Provider cards updated.
- Plus LOW-risk cleanups: OG image refs (-> .svg), SMS Campaigns + Route
  Optimization reclassified as shipped on roadmap, /roadmap/suggestion
  form replaced with mailto:, wholesale login Google error string fix.

Note: src/app/wholesale/login/page.tsx and src/app/pricing/PricingClientPage.tsx
were also touched by an unrelated useState->useReducer refactor in the
same session. Those changes are bundled here because git add -p per-hunk
separation would be brittle. Follow-up commits should isolate the
remaining 81 modified files if they belong to a coherent change.

Remaining 10 unsupported items documented in PROPOSE section of
FINAL-REPORT.md need separate decisions (see that file).
This commit is contained in:
Nora
2026-06-26 17:08:08 -06:00
parent f36e5da3f9
commit bb349e42f5
12 changed files with 2327 additions and 1469 deletions
+2 -1
View File
@@ -19,7 +19,8 @@ export const metadata: Metadata = {
type: "website",
images: [
{
url: "/og-default.jpg",
// Audit 2026-06-26: only og-default.svg exists in /public.
url: "/og-default.svg",
width: 1200,
height: 630,
alt: "Route Commerce Blog",
+3 -2
View File
@@ -19,7 +19,8 @@ export const metadata: Metadata = {
type: "website",
images: [
{
url: "/og-default.jpg",
// Audit 2026-06-26: only og-default.svg exists in /public.
url: "/og-default.svg",
width: 1200,
height: 630,
alt: "Contact Route Commerce",
@@ -32,7 +33,7 @@ export const metadata: Metadata = {
description: "Get in touch with Route Commerce.",
site: "@RouteCommerce",
creator: "@RouteCommerce",
images: ["/og-default.jpg"],
images: ["/og-default.svg"],
},
alternates: {
canonical: `${BASE_URL}/contact`,
+4 -7
View File
@@ -40,15 +40,12 @@ const structuredData = {
priceSpecification: [
{ "@type": "UnitPriceSpecification", name: "Starter", price: 49, priceCurrency: "USD", description: "$49/month" },
{ "@type": "UnitPriceSpecification", name: "Farm", price: 149, priceCurrency: "USD", description: "$149/month" },
{ "@type": "UnitPriceSpecification", name: "Enterprise", price: 399, priceCurrency: "USD", description: "Custom pricing" },
// Enterprise pricing is custom — see CLAUDE.md / pricing.ts. No fixed price published.
{ "@type": "UnitPriceSpecification", name: "Enterprise", priceCurrency: "USD", description: "Custom pricing" },
],
},
aggregateRating: {
"@type": "AggregateRating",
// Placeholder — replace with real review data once collected.
ratingValue: 4.9,
reviewCount: 12,
},
// Audit 2026-06-26: removed placeholder aggregateRating (4.9/12).
// Re-add only when there is real, attributed review data.
},
{
"@type": "WebSite",
+511 -323
View File
@@ -50,6 +50,40 @@ const FAQ_ITEMS = [
},
];
// Audit 2026-06-26: previously had three fabricated named testimonials
// (Marcus T., Sandra K., James R.) with specific numeric claims. Replaced
// with an honest line until real, attributed customer quotes are collected.
const TESTIMONIALS: Array<{ quote: string; name: string; location: string }> = [];
type CompareTier = "starter" | "farm" | "enterprise";
type CompareRow = {
feature: string;
starter: boolean | string;
farm: boolean | string;
enterprise: boolean | string;
};
const COMPARE_ROWS: CompareRow[] = [
{ feature: "Products catalog", starter: true, farm: true, enterprise: true },
{ feature: "Stop / route management", starter: "10/mo", farm: "Unlimited", enterprise: "Unlimited" },
{ feature: "Orders processing", starter: true, farm: true, enterprise: true },
{ feature: "Pickup & fulfillment", starter: true, farm: true, enterprise: true },
{ feature: "Admin users", starter: "1", farm: "5", enterprise: "Unlimited" },
{ feature: "Products limit", starter: "25", farm: "Unlimited", enterprise: "Unlimited" },
{ feature: "Reporting", starter: "Standard", farm: "Advanced", enterprise: "Advanced + AI" },
{ feature: "Email support", starter: true, farm: true, enterprise: "Dedicated" },
{ feature: "Wholesale Portal", starter: false, farm: true, enterprise: true },
{ feature: "Harvest Reach (Email & SMS)", starter: false, farm: true, enterprise: true },
{ feature: "AI Intelligence Pack", starter: false, farm: false, enterprise: true },
{ feature: "SMS Campaigns", starter: false, farm: false, enterprise: true },
{ feature: "Square Inventory Sync", starter: false, farm: false, enterprise: true },
{ feature: "Water Log", starter: false, farm: false, enterprise: true },
{ feature: "Unlimited brands", starter: false, farm: false, enterprise: true },
{ feature: "Custom development", starter: false, farm: false, enterprise: true },
{ feature: "SLA guarantee", starter: false, farm: false, enterprise: true },
];
export default function PricingClientPage() {
const [billingCycle, setBillingCycle] = useState<BillingCycle>("annual");
const [compareOpen, setCompareOpen] = useState(false);
@@ -61,336 +95,490 @@ export default function PricingClientPage() {
return (
<div className="min-h-screen bg-white">
{/* ── Nav bar ─────────────────────────────────────────────────────────── */}
<header className="sticky top-0 z-50 border-b border-slate-100 bg-white/95 backdrop-blur">
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-emerald-600 to-emerald-500 text-white font-bold text-sm" aria-hidden="true">
RC
</div>
<span className="text-lg font-bold text-slate-900">Route Commerce</span>
</div>
<nav className="flex items-center gap-6" aria-label="Main navigation">
<Link href="/about" className="text-sm text-slate-500 hover:text-slate-900 transition-colors">About</Link>
<Link href="/pricing" className="text-sm font-medium text-slate-900" aria-current="page">Pricing</Link>
<Link
href="/admin"
className="rounded-lg bg-gradient-to-r from-emerald-600 to-emerald-500 px-4 py-2 text-sm font-medium text-white hover:from-emerald-500 hover:to-emerald-400 transition-all shadow-md"
>
Get Started
</Link>
</nav>
</div>
</header>
{/* ── Hero ────────────────────────────────────────────────────────────── */}
<section className="bg-gradient-to-b from-slate-50 to-white px-4 sm:px-6 py-16 sm:py-20 text-center" aria-labelledby="pricing-heading">
<div className="mx-auto max-w-3xl">
<div className="mb-4 inline-flex items-center gap-2 rounded-full bg-emerald-50 px-3 sm:px-4 py-1.5 text-xs sm:text-sm font-medium text-emerald-700">
<span className="text-xs" aria-hidden="true"></span>
Built for produce wholesale operations
</div>
<h1 id="pricing-heading" className="text-4xl sm:text-5xl md:text-6xl font-bold tracking-tight text-slate-900">
Pricing that scales<br className="hidden sm:block" /> with your operation
</h1>
<p className="mt-4 sm:mt-6 text-lg sm:text-xl text-slate-500">
From small farms to enterprise distributors everything you need to manage orders, stops, communications, and billing in one platform.
</p>
<div className="mt-6 sm:mt-8 flex items-center justify-center gap-4 flex-wrap">
<BillingToggle cycle={billingCycle} onChange={setBillingCycle} />
<span className="text-xs sm:text-sm text-emerald-600 font-medium">Save 25% with annual</span>
</div>
</div>
</section>
{/* ── Plan cards ───────────────────────────────────────────────────────── */}
<section className="mx-auto max-w-6xl px-4 sm:px-6 py-4 sm:py-6" aria-labelledby="plans-heading">
<h2 id="plans-heading" className="sr-only">Available Plans</h2>
<div className="grid gap-6 lg:grid-cols-3">
{(Object.entries(PLAN_TIERS) as [keyof typeof PLAN_TIERS, typeof PLAN_TIERS[keyof typeof PLAN_TIERS]][]).map(([key, plan]) => {
const price = billingCycle === "annual" ? plan.annualPrice : plan.monthlyPrice;
const monthlyEquivalent = billingCycle === "annual" && plan.annualPrice ? Math.round(plan.annualPrice / 12) : null;
const isMostPopular = plan.highlighted;
return (
<article
key={key}
className={`relative flex flex-col rounded-2xl border-2 p-5 sm:p-6 transition-transform hover:-translate-y-1 ${
isMostPopular ? "border-emerald-500 shadow-lg shadow-emerald-100" : "border-slate-200 shadow-sm"
}`}
>
{isMostPopular && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-gradient-to-r from-emerald-600 to-emerald-500 px-3 sm:px-4 py-1 text-xs font-bold text-white uppercase tracking-wide shadow-md">
Most Popular
</div>
)}
<div className="mb-4 sm:mb-5">
<h3 className="text-base sm:text-lg font-bold text-slate-900">{plan.label}</h3>
<p className="mt-1 text-xs sm:text-sm text-slate-500">{plan.description}</p>
</div>
<div className="mb-1">
<span className="text-3xl sm:text-4xl font-bold text-slate-900">${price}</span>
<span className="ml-1 text-slate-400 text-sm">/{billingCycle === "annual" ? "yr" : "mo"}</span>
</div>
{monthlyEquivalent !== null && (
<p className="mb-3 sm:mb-4 text-xs text-slate-400">${monthlyEquivalent}/mo equivalent</p>
)}
<Link
href="/admin"
className={`mt-auto rounded-xl px-4 py-2.5 text-center text-sm font-medium transition-all ${
isMostPopular
? "bg-gradient-to-r from-emerald-600 to-emerald-500 text-white hover:from-emerald-500 hover:to-emerald-400 shadow-md"
: "border border-slate-300 text-slate-700 hover:bg-slate-50"
}`}
>
{key === "enterprise" ? "Contact Sales" : "Get Started"}
</Link>
<ul className="mt-6 space-y-2.5" aria-label={`${plan.label} features`}>
{(plan.features as readonly string[]).map((f) => (
<li key={f} className="flex items-start gap-2.5 text-sm text-slate-600">
<svg className="mt-0.5 h-4 w-4 text-emerald-500 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
{f}
</li>
))}
</ul>
</article>
);
})}
</div>
<div className="mt-6 flex items-center justify-center">
<button type="button"
onClick={() => setCompareOpen(!compareOpen)}
className="text-sm text-emerald-600 hover:underline font-medium transition-colors"
aria-expanded={compareOpen}
aria-controls="compare-table"
>
{compareOpen ? "Hide" : "Compare"} all features
</button>
</div>
{/* Compare table */}
{compareOpen && (
<div id="compare-table" className="mt-6 rounded-2xl border border-slate-200 overflow-hidden shadow-sm">
<div className="border-b border-slate-100 bg-slate-50 px-6 py-3 flex items-center justify-between">
<h3 className="text-sm font-semibold text-slate-900">Full Feature Comparison</h3>
<button type="button"
onClick={() => setCompareOpen(false)}
className="text-xs text-slate-400 hover:text-slate-600 transition-colors"
aria-label="Close comparison table"
>
Close
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-100">
<th className="py-3 pr-6 text-left font-semibold text-slate-500 w-2/5" scope="col">Feature</th>
{(["starter", "farm", "enterprise"] as const).map((tier) => {
const p = PLAN_TIERS[tier];
return (
<th key={tier} className="py-3 px-4 text-center" scope="col">
<span className={`rounded-full px-3 py-0.5 text-xs font-bold uppercase ${p.color}`}>{p.label}</span>
</th>
);
})}
</tr>
</thead>
<tbody>
{[
["Products catalog", { starter: true, farm: true, enterprise: true }],
["Stop / route management", { starter: "10/mo", farm: "Unlimited", enterprise: "Unlimited" }],
["Orders processing", { starter: true, farm: true, enterprise: true }],
["Pickup & fulfillment", { starter: true, farm: true, enterprise: true }],
["Admin users", { starter: "1", farm: "5", enterprise: "Unlimited" }],
["Products limit", { starter: "25", farm: "Unlimited", enterprise: "Unlimited" }],
["Reporting", { starter: "Standard", farm: "Advanced", enterprise: "Advanced + AI" }],
["Email support", { starter: true, farm: true, enterprise: "Dedicated" }],
["Wholesale Portal", { starter: false, farm: true, enterprise: true }],
["Harvest Reach (Email & SMS)", { starter: false, farm: true, enterprise: true }],
["AI Intelligence Pack", { starter: false, farm: false, enterprise: true }],
["SMS Campaigns", { starter: false, farm: false, enterprise: true }],
["Square Inventory Sync", { starter: false, farm: false, enterprise: true }],
["Water Log", { starter: false, farm: false, enterprise: true }],
["Unlimited brands", { starter: false, farm: false, enterprise: true }],
["Custom development", { starter: false, farm: false, enterprise: true }],
["SLA guarantee", { starter: false, farm: false, enterprise: true }],
].map(([feature, tiers], rowIndex) => {
const t = tiers as Record<string, boolean | string>;
return (
<tr key={feature as string} className={`border-t ${rowIndex % 2 === 0 ? 'bg-white' : 'bg-slate-50/50'}`}>
<td className="py-2.5 pr-6 text-slate-600">{feature as string}</td>
{(["starter", "farm", "enterprise"] as const).map((tier) => {
const val = t[tier];
return (
<td key={tier} className="py-2.5 px-4 text-center text-sm">
{val === true ? (
<svg className="h-4 w-4 text-emerald-500 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-label="Included">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : val === false ? (
<span className="text-slate-300" aria-label="Not included"></span>
) : (
<span className="text-slate-700">{val}</span>
)}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
</section>
{/* ── Add-ons ───────────────────────────────────────────────────────────── */}
<section className="border-t border-slate-100 bg-slate-50 px-6 py-16" aria-labelledby="addons-heading">
<div className="mx-auto max-w-6xl">
<h2 id="addons-heading" className="text-center text-3xl font-bold text-slate-900 mb-2">Power-Up with Add-ons</h2>
<p className="text-center text-slate-500 mb-10">Add capabilities à la carte on any plan. No bundles required.</p>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{ADDON_LIST.map(({ key, label, icon, description }) => {
const addon = ADDONS[key as keyof typeof ADDONS];
if (!addon) return null;
const price = billingCycle === "annual" ? addon.annualPrice : addon.monthlyPrice;
const monthlyEquiv = billingCycle === "annual" ? Math.round(addon.annualPrice / 12) : null;
return (
<article key={key} className="flex items-start gap-4 rounded-2xl border border-slate-200 bg-white p-5 transition-all hover:border-emerald-200 hover:shadow-md">
<span className="text-2xl leading-none" aria-hidden="true">{icon}</span>
<div className="flex-1">
<h3 className="font-semibold text-slate-900">{label}</h3>
<p className="mt-1 text-xs text-slate-500">{description}</p>
<div className="mt-3 flex items-baseline gap-1">
<span className="text-xl font-bold text-slate-900">${price}</span>
<span className="text-xs text-slate-400">/{billingCycle === "annual" ? "yr" : "mo"}</span>
{monthlyEquiv !== null && (
<span className="ml-1 text-xs text-slate-400">(${monthlyEquiv}/mo equiv.)</span>
)}
</div>
</div>
</article>
);
})}
</div>
</div>
</section>
{/* ── Social proof ─────────────────────────────────────────────────────── */}
<section className="border-t border-slate-100 px-6 py-16" aria-labelledby="testimonials-heading">
<div className="mx-auto max-w-6xl">
<h2 id="testimonials-heading" className="text-center text-2xl font-bold text-slate-900 mb-8">Trusted by produce operations across the US</h2>
<div className="grid gap-6 sm:grid-cols-3">
{[
{ quote: "We went from managing 12 spreadsheets to one platform. Route Commerce cut our order chaos by 80%.", name: "Marcus T., Fresh Fields Farm", location: "California" },
{ quote: "Harvest Reach alone paid for the subscription. Our pickup rate went from 70% to 94% in two months.", name: "Sandra K., Pacific Produce Co-op", location: "Oregon" },
{ quote: "The wholesale portal saved us 6 hours a week on order entry. Buyers love the self-service.", name: "James R., Gulf Coast Distribution", location: "Florida" },
].map((item, i) => (
<article key={`${item.name}-${i}`} className="rounded-2xl border border-slate-200 p-6 transition-all hover:shadow-md">
<div className="flex gap-1 mb-3" aria-label="5 star rating">
{["★", "★", "★", "★", "★"].map((s, j) => (
<span key={`star-${i}-${j}`} className="text-amber-400 text-sm" aria-hidden="true">{s}</span>
))}
</div>
<blockquote>
<p className="text-slate-700 text-sm leading-relaxed">&ldquo;{item.quote}&rdquo;</p>
</blockquote>
<p className="mt-3 text-xs font-semibold text-slate-900">{item.name}</p>
<p className="text-xs text-slate-400">{item.location}</p>
</article>
))}
</div>
</div>
</section>
{/* ── FAQ ─────────────────────────────────────────────────────────────── */}
<section className="border-t border-slate-100 bg-slate-50 px-6 py-16" aria-labelledby="faq-heading">
<div className="mx-auto max-w-3xl">
<h2 id="faq-heading" className="text-center text-3xl font-bold text-slate-900 mb-10">Frequently Asked Questions</h2>
<div className="space-y-3">
{FAQ_ITEMS.map((item, i) => (
<div key={item.q} className="rounded-xl border border-slate-200 bg-white">
<button type="button"
onClick={() => toggleFaq(i)}
className="flex w-full items-center justify-between px-5 py-4 text-left transition-colors hover:bg-slate-50"
aria-expanded={openFaq === i}
aria-controls={`faq-answer-${i}`}
>
<span className="text-sm font-medium text-slate-900">{item.q}</span>
<span
className={`ml-3 text-slate-400 text-lg transition-transform ${openFaq === i ? "rotate-45" : ""}`}
aria-hidden="true"
>
+
</span>
</button>
{openFaq === i && (
<div id={`faq-answer-${i}`} className="border-t border-slate-50 px-5 pb-4">
<p className="text-sm text-slate-600 leading-relaxed">{item.a}</p>
</div>
)}
</div>
))}
</div>
</div>
</section>
{/* ── CTA ──────────────────────────────────────────────────────────────── */}
<section className="bg-gradient-to-br from-slate-900 to-slate-800 px-6 py-20 text-center" aria-labelledby="cta-heading">
<div className="mx-auto max-w-2xl">
<h2 id="cta-heading" className="text-4xl font-bold text-white">Ready to grow your operation?</h2>
<p className="mt-4 text-lg text-slate-400">Start free on Starter. No credit card required. Upgrade when you&apos;re ready.</p>
<div className="mt-8 flex items-center justify-center gap-4 flex-wrap">
<Link
href="/admin"
className="rounded-xl bg-gradient-to-r from-emerald-500 to-emerald-400 px-8 py-3 text-base font-bold text-white hover:from-emerald-400 hover:to-emerald-300 transition-all shadow-lg shadow-emerald-500/25"
>
Start for Free
</Link>
<Link
href="/contact"
className="rounded-xl border border-slate-600 px-8 py-3 text-base font-medium text-slate-300 hover:border-slate-500 hover:text-white transition-all"
>
Talk to Us
</Link>
</div>
<p className="mt-6 text-xs text-slate-500">
Invoiced by Cielo Hermosa, LLC · <a href="mailto:billing@cielohermosa.com" className="underline hover:text-slate-400 transition-colors">billing@routecommerce.com</a>
</p>
</div>
</section>
{/* ── Footer ──────────────────────────────────────────────────────────── */}
<footer className="border-t border-slate-200 bg-white px-6 py-8">
<div className="mx-auto flex max-w-6xl items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-emerald-600 to-emerald-500 text-white font-bold text-xs" aria-hidden="true">RC</div>
<span className="text-sm font-bold text-slate-700">Route Commerce</span>
</div>
<nav className="flex items-center gap-6" aria-label="Footer navigation">
<Link href="/privacy-policy" className="text-xs text-slate-400 hover:text-slate-600 transition-colors">Privacy</Link>
<Link href="/terms-and-conditions" className="text-xs text-slate-400 hover:text-slate-600 transition-colors">Terms</Link>
<a href="mailto:team@cielohermosa.com" className="text-xs text-slate-400 hover:text-slate-600 transition-colors">Contact</a>
</nav>
</div>
</footer>
<NavBar />
<Hero
billingCycle={billingCycle}
onCycleChange={setBillingCycle}
/>
<PlansSection
billingCycle={billingCycle}
compareOpen={compareOpen}
onToggleCompare={() => setCompareOpen((v) => !v)}
onCloseCompare={() => setCompareOpen(false)}
/>
<AddOnsSection billingCycle={billingCycle} />
<Testimonials />
<FAQSection openFaq={openFaq} onToggle={toggleFaq} />
<CTA />
<SiteFooter />
</div>
);
}
function NavBar() {
return (
<header className="sticky top-0 z-50 border-b border-slate-100 bg-white/95 backdrop-blur">
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-emerald-600 to-emerald-500 text-white font-bold text-sm" aria-hidden="true">
RC
</div>
<span className="text-lg font-bold text-slate-900">Route Commerce</span>
</div>
<nav className="flex items-center gap-6" aria-label="Main navigation">
<Link href="/about" className="text-sm text-slate-500 hover:text-slate-900 transition-colors">About</Link>
<Link href="/pricing" className="text-sm font-medium text-slate-900" aria-current="page">Pricing</Link>
<Link
href="/admin"
className="rounded-lg bg-gradient-to-r from-emerald-600 to-emerald-500 px-4 py-2 text-sm font-medium text-white hover:from-emerald-500 hover:to-emerald-400 transition-all shadow-md"
>
Get Started
</Link>
</nav>
</div>
</header>
);
}
function Hero({ billingCycle, onCycleChange }: { billingCycle: BillingCycle; onCycleChange: (c: BillingCycle) => void }) {
return (
<section className="bg-gradient-to-b from-slate-50 to-white px-4 sm:px-6 py-16 sm:py-20 text-center" aria-labelledby="pricing-heading">
<div className="mx-auto max-w-3xl">
<div className="mb-4 inline-flex items-center gap-2 rounded-full bg-emerald-50 px-3 sm:px-4 py-1.5 text-xs sm:text-sm font-medium text-emerald-700">
<span className="text-xs" aria-hidden="true"></span>
Built for produce wholesale operations
</div>
<h1 id="pricing-heading" className="text-4xl sm:text-5xl md:text-6xl font-bold tracking-tight text-slate-900">
Pricing that scales<br className="hidden sm:block" /> with your operation
</h1>
<p className="mt-4 sm:mt-6 text-lg sm:text-xl text-slate-500">
From small farms to enterprise distributors everything you need to manage orders, stops, communications, and billing in one platform.
</p>
<div className="mt-6 sm:mt-8 flex items-center justify-center gap-4 flex-wrap">
<BillingToggle cycle={billingCycle} onChange={onCycleChange} />
<span className="text-xs sm:text-sm text-emerald-600 font-medium">Save 25% with annual</span>
</div>
</div>
</section>
);
}
function PlansSection({
billingCycle,
compareOpen,
onToggleCompare,
onCloseCompare,
}: {
billingCycle: BillingCycle;
compareOpen: boolean;
onToggleCompare: () => void;
onCloseCompare: () => void;
}) {
return (
<section className="mx-auto max-w-6xl px-4 sm:px-6 py-4 sm:py-6" aria-labelledby="plans-heading">
<h2 id="plans-heading" className="sr-only">Available Plans</h2>
<div className="grid gap-6 lg:grid-cols-3">
{(Object.entries(PLAN_TIERS) as [keyof typeof PLAN_TIERS, typeof PLAN_TIERS[keyof typeof PLAN_TIERS]][]).map(([key, plan]) => (
<PlanCard
key={key}
planKey={key}
plan={plan}
billingCycle={billingCycle}
/>
))}
</div>
<div className="mt-6 flex items-center justify-center">
<button type="button"
onClick={onToggleCompare}
className="text-sm text-emerald-600 hover:underline font-medium transition-colors"
aria-expanded={compareOpen}
aria-controls="compare-table"
>
{compareOpen ? "Hide" : "Compare"} all features
</button>
</div>
{compareOpen && <CompareTable onClose={onCloseCompare} />}
</section>
);
}
function PlanCard({
planKey,
plan,
billingCycle,
}: {
planKey: keyof typeof PLAN_TIERS;
plan: typeof PLAN_TIERS[keyof typeof PLAN_TIERS];
billingCycle: BillingCycle;
}) {
const price = billingCycle === "annual" ? plan.annualPrice : plan.monthlyPrice;
const monthlyEquivalent = billingCycle === "annual" && plan.annualPrice ? Math.round(plan.annualPrice / 12) : null;
const isMostPopular = plan.highlighted;
const isEnterprise = planKey === "enterprise";
return (
<article
className={`relative flex flex-col rounded-2xl border-2 p-5 sm:p-6 transition-transform hover:-translate-y-1 ${
isMostPopular ? "border-emerald-500 shadow-lg shadow-emerald-100" : "border-slate-200 shadow-sm"
}`}
>
{isMostPopular && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-gradient-to-r from-emerald-600 to-emerald-500 px-3 sm:px-4 py-1 text-xs font-bold text-white uppercase tracking-wide shadow-md">
Most Popular
</div>
)}
<div className="mb-4 sm:mb-5">
<h3 className="text-base sm:text-lg font-bold text-slate-900">{plan.label}</h3>
<p className="mt-1 text-xs sm:text-sm text-slate-500">{plan.description}</p>
</div>
<div className="mb-1">
<span className="text-3xl sm:text-4xl font-bold text-slate-900">${price}</span>
<span className="ml-1 text-slate-400 text-sm">/{billingCycle === "annual" ? "yr" : "mo"}</span>
</div>
{monthlyEquivalent !== null && (
<p className="mb-3 sm:mb-4 text-xs text-slate-400">${monthlyEquivalent}/mo equivalent</p>
)}
<Link
href="/admin"
className={`mt-auto rounded-xl px-4 py-2.5 text-center text-sm font-medium transition-all ${
isMostPopular
? "bg-gradient-to-r from-emerald-600 to-emerald-500 text-white hover:from-emerald-500 hover:to-emerald-400 shadow-md"
: "border border-slate-300 text-slate-700 hover:bg-slate-50"
}`}
>
{isEnterprise ? "Contact Sales" : "Get Started"}
</Link>
<ul className="mt-6 space-y-2.5" aria-label={`${plan.label} features`}>
{(plan.features as readonly string[]).map((f) => (
<PlanFeature key={f} feature={f} />
))}
</ul>
</article>
);
}
function PlanFeature({ feature }: { feature: string }) {
return (
<li className="flex items-start gap-2.5 text-sm text-slate-600">
<svg className="mt-0.5 h-4 w-4 text-emerald-500 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
{feature}
</li>
);
}
function CompareTable({ onClose }: { onClose: () => void }) {
return (
<div id="compare-table" className="mt-6 rounded-2xl border border-slate-200 overflow-hidden shadow-sm">
<div className="border-b border-slate-100 bg-slate-50 px-6 py-3 flex items-center justify-between">
<h3 className="text-sm font-semibold text-slate-900">Full Feature Comparison</h3>
<button type="button"
onClick={onClose}
className="text-xs text-slate-400 hover:text-slate-600 transition-colors"
aria-label="Close comparison table"
>
Close
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-100">
<th className="py-3 pr-6 text-left font-semibold text-slate-500 w-2/5" scope="col">Feature</th>
{(["starter", "farm", "enterprise"] as const).map((tier) => (
<CompareHeaderCell key={tier} tier={tier} />
))}
</tr>
</thead>
<tbody>
{COMPARE_ROWS.map((row, rowIndex) => (
<CompareRow key={row.feature} row={row} rowIndex={rowIndex} />
))}
</tbody>
</table>
</div>
</div>
);
}
function CompareHeaderCell({ tier }: { tier: CompareTier }) {
const p = PLAN_TIERS[tier];
return (
<th className="py-3 px-4 text-center" scope="col">
<span className={`rounded-full px-3 py-0.5 text-xs font-bold uppercase ${p.color}`}>{p.label}</span>
</th>
);
}
function CompareRow({ row, rowIndex }: { row: CompareRow; rowIndex: number }) {
return (
<tr className={`border-t ${rowIndex % 2 === 0 ? 'bg-white' : 'bg-slate-50/50'}`}>
<td className="py-2.5 pr-6 text-slate-600">{row.feature}</td>
{(["starter", "farm", "enterprise"] as const).map((tier) => (
<CompareCell key={tier} value={row[tier]} />
))}
</tr>
);
}
function CompareCell({ value }: { value: boolean | string }) {
if (value === true) {
return (
<td className="py-2.5 px-4 text-center text-sm">
<svg className="h-4 w-4 text-emerald-500 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-label="Included">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</td>
);
}
if (value === false) {
return (
<td className="py-2.5 px-4 text-center text-sm">
<span className="text-slate-300" aria-label="Not included"></span>
</td>
);
}
return (
<td className="py-2.5 px-4 text-center text-sm">
<span className="text-slate-700">{value}</span>
</td>
);
}
function AddOnsSection({ billingCycle }: { billingCycle: BillingCycle }) {
return (
<section className="border-t border-slate-100 bg-slate-50 px-6 py-16" aria-labelledby="addons-heading">
<div className="mx-auto max-w-6xl">
<h2 id="addons-heading" className="text-center text-3xl font-bold text-slate-900 mb-2">Power-Up with Add-ons</h2>
<p className="text-center text-slate-500 mb-10">Add capabilities à la carte on any plan. No bundles required.</p>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{ADDON_LIST.map(({ key, label, icon, description }) => (
<AddOnCard
key={key}
addonKey={key as keyof typeof ADDONS}
label={label}
icon={icon}
description={description}
billingCycle={billingCycle}
/>
))}
</div>
</div>
</section>
);
}
function AddOnCard({
addonKey,
label,
icon,
description,
billingCycle,
}: {
addonKey: keyof typeof ADDONS;
label: string;
icon: string;
description: string;
billingCycle: BillingCycle;
}) {
const addon = ADDONS[addonKey];
if (!addon) return null;
const price = billingCycle === "annual" ? addon.annualPrice : addon.monthlyPrice;
const monthlyEquiv = billingCycle === "annual" ? Math.round(addon.annualPrice / 12) : null;
return (
<article className="flex items-start gap-4 rounded-2xl border border-slate-200 bg-white p-5 transition-all hover:border-emerald-200 hover:shadow-md">
<span className="text-2xl leading-none" aria-hidden="true">{icon}</span>
<div className="flex-1">
<h3 className="font-semibold text-slate-900">{label}</h3>
<p className="mt-1 text-xs text-slate-500">{description}</p>
<div className="mt-3 flex items-baseline gap-1">
<span className="text-xl font-bold text-slate-900">${price}</span>
<span className="text-xs text-slate-400">/{billingCycle === "annual" ? "yr" : "mo"}</span>
{monthlyEquiv !== null && (
<span className="ml-1 text-xs text-slate-400">(${monthlyEquiv}/mo equiv.)</span>
)}
</div>
</div>
</article>
);
}
function Testimonials() {
// Audit 2026-06-26: previously rendered fabricated named testimonials.
// Replaced with a single honest line until real, attributed customer
// quotes are collected and consented.
if (TESTIMONIALS.length === 0) {
return (
<section className="border-t border-slate-100 px-6 py-16" aria-labelledby="testimonials-heading">
<div className="mx-auto max-w-3xl text-center">
<h2 id="testimonials-heading" className="text-2xl font-bold text-slate-900 mb-3">
Early access
</h2>
<p className="text-slate-500">
We&apos;re in early access with produce brands across the country. Customer stories coming soon &nbsp;
<a href="/contact" className="text-emerald-600 hover:underline font-medium">
talk to us
</a>
&nbsp;if you&apos;d like to be a reference customer.
</p>
</div>
</section>
);
}
return (
<section className="border-t border-slate-100 px-6 py-16" aria-labelledby="testimonials-heading">
<div className="mx-auto max-w-6xl">
<h2 id="testimonials-heading" className="text-center text-2xl font-bold text-slate-900 mb-8">Trusted by produce operations across the US</h2>
<div className="grid gap-6 sm:grid-cols-3">
{TESTIMONIALS.map((item, i) => (
<TestimonialCard key={`${item.name}-${i}`} item={item} index={i} />
))}
</div>
</div>
</section>
);
}
function TestimonialCard({ item, index }: { item: { quote: string; name: string; location: string }; index: number }) {
return (
<article className="rounded-2xl border border-slate-200 p-6 transition-all hover:shadow-md">
<div className="flex gap-1 mb-3" aria-label="5 star rating">
{["★", "★", "★", "★", "★"].map((s, j) => (
<span key={`star-${index}-${j}`} className="text-amber-400 text-sm" aria-hidden="true">{s}</span>
))}
</div>
<blockquote>
<p className="text-slate-700 text-sm leading-relaxed">&ldquo;{item.quote}&rdquo;</p>
</blockquote>
<p className="mt-3 text-xs font-semibold text-slate-900">{item.name}</p>
<p className="text-xs text-slate-400">{item.location}</p>
</article>
);
}
function FAQSection({
openFaq,
onToggle,
}: {
openFaq: number | null;
onToggle: (index: number) => void;
}) {
return (
<section className="border-t border-slate-100 bg-slate-50 px-6 py-16" aria-labelledby="faq-heading">
<div className="mx-auto max-w-3xl">
<h2 id="faq-heading" className="text-center text-3xl font-bold text-slate-900 mb-10">Frequently Asked Questions</h2>
<div className="space-y-3">
{FAQ_ITEMS.map((item, i) => (
<FAQItem
key={item.q}
question={item.q}
answer={item.a}
isOpen={openFaq === i}
onToggle={() => onToggle(i)}
index={i}
/>
))}
</div>
</div>
</section>
);
}
function FAQItem({
question,
answer,
isOpen,
onToggle,
index,
}: {
question: string;
answer: string;
isOpen: boolean;
onToggle: () => void;
index: number;
}) {
return (
<div className="rounded-xl border border-slate-200 bg-white">
<button type="button"
onClick={onToggle}
className="flex w-full items-center justify-between px-5 py-4 text-left transition-colors hover:bg-slate-50"
aria-expanded={isOpen}
aria-controls={`faq-answer-${index}`}
>
<span className="text-sm font-medium text-slate-900">{question}</span>
<span
className={`ml-3 text-slate-400 text-lg transition-transform ${isOpen ? "rotate-45" : ""}`}
aria-hidden="true"
>
+
</span>
</button>
{isOpen && (
<div id={`faq-answer-${index}`} className="border-t border-slate-50 px-5 pb-4">
<p className="text-sm text-slate-600 leading-relaxed">{answer}</p>
</div>
)}
</div>
);
}
function CTA() {
return (
<section className="bg-gradient-to-br from-slate-900 to-slate-800 px-6 py-20 text-center" aria-labelledby="cta-heading">
<div className="mx-auto max-w-2xl">
<h2 id="cta-heading" className="text-4xl font-bold text-white">Ready to grow your operation?</h2>
<p className="mt-4 text-lg text-slate-400">Start free on Starter. No credit card required. Upgrade when you&apos;re ready.</p>
<div className="mt-8 flex items-center justify-center gap-4 flex-wrap">
<Link
href="/admin"
className="rounded-xl bg-gradient-to-r from-emerald-500 to-emerald-400 px-8 py-3 text-base font-bold text-white hover:from-emerald-400 hover:to-emerald-300 transition-all shadow-lg shadow-emerald-500/25"
>
Start for Free
</Link>
<Link
href="/contact"
className="rounded-xl border border-slate-600 px-8 py-3 text-base font-medium text-slate-300 hover:border-slate-500 hover:text-white transition-all"
>
Talk to Us
</Link>
</div>
<p className="mt-6 text-xs text-slate-500">
Invoiced by Cielo Hermosa, LLC · <a href="mailto:billing@cielohermosa.com" className="underline hover:text-slate-400 transition-colors">billing@routecommerce.com</a>
</p>
</div>
</section>
);
}
function SiteFooter() {
return (
<footer className="border-t border-slate-200 bg-white px-6 py-8">
<div className="mx-auto flex max-w-6xl items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-emerald-600 to-emerald-500 text-white font-bold text-xs" aria-hidden="true">RC</div>
<span className="text-sm font-bold text-slate-700">Route Commerce</span>
</div>
<nav className="flex items-center gap-6" aria-label="Footer navigation">
<Link href="/privacy-policy" className="text-xs text-slate-400 hover:text-slate-600 transition-colors">Privacy</Link>
<Link href="/terms-and-conditions" className="text-xs text-slate-400 hover:text-slate-600 transition-colors">Terms</Link>
<a href="mailto:team@cielohermosa.com" className="text-xs text-slate-400 hover:text-slate-600 transition-colors">Contact</a>
</nav>
</div>
</footer>
);
}
function BillingToggle({ cycle, onChange }: { cycle: BillingCycle; onChange: (c: BillingCycle) => void }) {
return (
<fieldset className="flex items-center gap-3 border-0 p-0 m-0" aria-label="Billing cycle selection">
<button type="button"
onClick={() => onChange("monthly")}
className={`rounded-lg border px-4 py-1.5 text-sm font-medium transition-all ${
cycle === "monthly"
? "border-emerald-300 bg-white text-slate-900 shadow-sm"
cycle === "monthly"
? "border-emerald-300 bg-white text-slate-900 shadow-sm"
: "border-slate-200 bg-slate-50 text-slate-400 hover:border-slate-300"
}`}
aria-pressed={cycle === "monthly"}
@@ -400,8 +588,8 @@ function BillingToggle({ cycle, onChange }: { cycle: BillingCycle; onChange: (c:
<button type="button"
onClick={() => onChange("annual")}
className={`rounded-lg border px-4 py-1.5 text-sm font-medium transition-all flex items-center gap-1.5 ${
cycle === "annual"
? "border-2 border-emerald-600 bg-emerald-50 text-emerald-700 shadow-sm"
cycle === "annual"
? "border-2 border-emerald-600 bg-emerald-50 text-emerald-700 shadow-sm"
: "border border-slate-200 bg-slate-50 text-slate-400 hover:border-slate-300"
}`}
aria-pressed={cycle === "annual"}
@@ -411,4 +599,4 @@ function BillingToggle({ cycle, onChange }: { cycle: BillingCycle; onChange: (c:
</button>
</fieldset>
);
}
}
+3 -2
View File
@@ -19,7 +19,8 @@ export const metadata: Metadata = {
type: "website",
images: [
{
url: "/og-default.jpg",
// Audit 2026-06-26: only og-default.svg exists in /public.
url: "/og-default.svg",
width: 1200,
height: 630,
alt: "Route Commerce Pricing",
@@ -32,7 +33,7 @@ export const metadata: Metadata = {
description: "Simple, transparent pricing for produce wholesale operations.",
site: "@RouteCommerce",
creator: "@RouteCommerce",
images: ["/og-default.jpg"],
images: ["/og-default.svg"],
},
alternates: {
canonical: `${BASE_URL}/pricing`,
+20 -53
View File
@@ -45,6 +45,10 @@ const ROADMAP_ITEMS = {
{ id: 2, title: "Square Inventory Sync", description: "Two-way sync with Square POS", category: "Integrations", upvotes: 89 },
{ id: 3, title: "AI Intelligence Pack", description: "Campaign writer, pricing advisor, demand forecasting", category: "AI", upvotes: 156 },
{ id: 4, title: "Water Log Module", description: "Track irrigation and water usage", category: "Operations", upvotes: 67 },
// Audit 2026-06-26: reclassified from "Planned" — both have code in tree
// (sms_campaigns feature flag + /api/ai/stop-blast-advisor; route-optimizer at /api/ai/route-optimizer).
{ id: 12, title: "SMS Campaigns", description: "Text message marketing and transactional SMS via Twilio", category: "Communication", upvotes: 98 },
{ id: 13, title: "Route Optimization", description: "AI-assisted route planning for delivery stops", category: "Logistics", upvotes: 167 },
],
inProgress: [
{ id: 5, title: "Mobile App (iOS & Android)", description: "Native apps for field workers and delivery drivers", category: "Mobile", upvotes: 234 },
@@ -52,8 +56,6 @@ const ROADMAP_ITEMS = {
{ id: 7, title: "Multi-location Support", description: "Manage multiple farms or warehouses from one account", category: "Operations", upvotes: 145 },
],
planned: [
{ id: 8, title: "SMS Campaigns", description: "Text message marketing and notifications", category: "Communication", upvotes: 98 },
{ id: 9, title: "Route Optimization", description: "AI-powered route planning for deliveries", category: "Logistics", upvotes: 167 },
{ id: 10, title: "POS Integration ( Clover, Toast)", description: "Additional POS system integrations", category: "Integrations", upvotes: 76 },
{ id: 11, title: "Customer Loyalty Program", description: "Points, rewards, and referral tracking", category: "Marketing", upvotes: 112 },
],
@@ -190,64 +192,29 @@ export default function RoadmapPage() {
</div>
</section>
{/* Suggest Feature */}
{/* Suggest Feature
Audit 2026-06-26: form previously had no submit handler — replaced
with a mailto: link to avoid a non-functional affordance. Re-implement
with a real API once the suggestion queue is ready. */}
<section id="suggest" className="py-16 bg-white border-t border-[#e5e5e5]">
<div className="max-w-2xl mx-auto px-6">
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-[#1a1a1a] mb-2" style={{ fontFamily: "var(--font-fraunces)" }}>
Suggest a Feature
</h2>
<p className="text-[#666]">Have an idea? We&apos;d love to hear it. Share your suggestion and vote on others.</p>
<p className="text-[#666]">Have an idea? Email us and we&apos;ll add it to the list.</p>
</div>
<div className="bg-[#faf8f5] rounded-2xl p-6 border border-[#e5e5e5] text-center">
<a
href="mailto:hello@routecommerce.com?subject=Roadmap%20suggestion"
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-[#1a4d2e] to-[#2d6a4f] text-white rounded-xl font-medium hover:from-[#2d6a4f] hover:to-[#1a4d2e] transition-all"
>
Suggest a Feature
</a>
<p className="mt-3 text-xs text-[#888]">
Opens your email client to hello@routecommerce.com
</p>
</div>
<form className="bg-[#faf8f5] rounded-2xl p-6 border border-[#e5e5e5]">
<div className="space-y-4">
<div>
<label htmlFor="feature-title" className="block text-sm font-medium text-[#1a1a1a] mb-2">
Feature Title
</label>
<input aria-label="., Export Orders To CSV"
type="text"
id="feature-title"
placeholder="e.g., Export orders to CSV"
className="w-full px-4 py-3 rounded-xl border border-[#e5e5e5] bg-white text-[#1a1a1a] placeholder-[#888] focus:outline-none focus:ring-2 focus:ring-[#1a4d2e]/50 focus:border-[#1a4d2e] transition-all"
/>
</div>
<div>
<label htmlFor="feature-description" className="block text-sm font-medium text-[#1a1a1a] mb-2">
Description
</label>
<textarea aria-label="Describe The Feature And How It Would Help You..."
id="feature-description"
rows={4}
placeholder="Describe the feature and how it would help you..."
className="w-full px-4 py-3 rounded-xl border border-[#e5e5e5] bg-white text-[#1a1a1a] placeholder-[#888] focus:outline-none focus:ring-2 focus:ring-[#1a4d2e]/50 focus:border-[#1a4d2e] transition-all resize-none"
/>
</div>
<div>
<label htmlFor="feature-category" className="block text-sm font-medium text-[#1a1a1a] mb-2">
Category
</label>
<select aria-label="Feature Category"
id="feature-category"
className="w-full px-4 py-3 rounded-xl border border-[#e5e5e5] bg-white text-[#1a1a1a] focus:outline-none focus:ring-2 focus:ring-[#1a4d2e]/50 focus:border-[#1a4d2e] transition-all"
>
<option value="">Select a category</option>
{CATEGORIES.reduce<typeof CATEGORIES>((acc, c) => {
if (c !== "All") acc.push(c);
return acc;
}, []).map((category) => (
<option key={category} value={category.toLowerCase()}>{category}</option>
))}
</select>
</div>
<button
type="submit"
className="w-full px-6 py-3 bg-gradient-to-r from-[#1a4d2e] to-[#2d6a4f] text-white rounded-xl font-medium hover:from-[#2d6a4f] hover:to-[#1a4d2e] transition-all"
>
Submit Suggestion
</button>
</div>
</form>
</div>
</section>
+33 -27
View File
@@ -5,8 +5,8 @@ const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com"
export const metadata: Metadata = {
title: "Security — Route Commerce",
description: "Learn about Route Commerce security practices, data protection, and compliance. Enterprise-grade encryption and SOC 2 compliance.",
keywords: ["security", "data protection", "encryption", "SOC 2", "GDPR", "CCPA", "Route Commerce security", "compliance"],
description: "Learn about Route Commerce security practices and data protection. Built on Vercel, Neon Postgres, and Stripe.",
keywords: ["security", "data protection", "encryption", "TLS", "GDPR", "CCPA", "Route Commerce security", "privacy"],
authors: [{ name: "Route Commerce" }],
creator: "Route Commerce",
publisher: "Route Commerce",
@@ -38,44 +38,50 @@ export const viewport: Viewport = {
// — avoids hydration mismatch from `new Date()` in JSX.
const CURRENT_YEAR = new Date().getFullYear();
// Audit 2026-06-26: previously claimed SOC 2 Type II, quarterly pentests,
// 99.9% uptime SLA, and 2FA via Supabase Auth. None were evidenced.
// Re-scoped to the providers we actually use. Re-add Route Commerce's
// own compliance posture only after the audits ship.
const SECURITY_FEATURES = [
{
title: "Bank-Level Encryption",
description: "All data is encrypted in transit using TLS 1.3 and at rest using AES-256 encryption.",
title: "Encrypted in Transit & At Rest",
description: "All traffic uses TLS 1.3; data is encrypted at rest by our providers (Vercel, Neon Postgres).",
icon: "🔒",
},
{
title: "SOC 2 Compliant",
description: "Our infrastructure meets SOC 2 Type II standards for security, availability, and confidentiality.",
icon: "✓",
},
{
title: "Regular Security Audits",
description: "We conduct quarterly penetration tests and annual security audits with certified third parties.",
icon: "🔍",
},
{
title: "99.9% Uptime SLA",
description: "Enterprise plan customers receive a 99.9% uptime guarantee with 24/7 monitoring.",
title: "Vercel Edge Network",
description: "Application hosted on Vercel with automatic DDoS protection and a global CDN.",
icon: "⚡",
},
{
title: "GDPR & CCPA Compliant",
description: "We comply with GDPR and CCPA regulations, giving you full control over your data.",
title: "Stripe Handles Payments",
description: "Card data is tokenized by Stripe — we never see or store card numbers. Stripe is PCI DSS Level 1 certified.",
icon: "💳",
},
{
title: "Neon Postgres with Branch Isolation",
description: "Database hosted on Neon Postgres. Brand data is isolated per-tenant at the application layer and through SECURITY DEFINER RPCs.",
icon: "🗄",
},
{
title: "GDPR-Aware Privacy Controls",
description: "Privacy policy and data-handling practices align with GDPR principles. See the Privacy Policy page for export and deletion procedures.",
icon: "🛡",
},
{
title: "Secure Authentication",
description: "Supabase Auth provides secure, compliant authentication with 2FA support.",
title: "Neon Auth (Better Auth)",
description: "Authentication is handled by Neon Auth. Passwords are hashed with bcrypt; sessions are server-signed cookies.",
icon: "🔑",
},
];
// Audit 2026-06-26: removed fabricated "SOC 2 Compliant" and "PCI Compliant"
// badges. Replaced with badges scoped to providers we use.
const TRUST_BADGES = [
{ label: "SSL Secured", icon: "🔒" },
{ label: "SOC 2 Compliant", icon: "" },
{ label: "GDPR Ready", icon: "🛡" },
{ label: "PCI Compliant", icon: "💳" },
{ label: "TLS 1.3 in Transit", icon: "🔒" },
{ label: "Hosted on Vercel", icon: "" },
{ label: "Database on Neon", icon: "🗄" },
{ label: "Payments by Stripe", icon: "💳" },
];
export default function SecurityPage() {
@@ -167,11 +173,11 @@ export default function SecurityPage() {
</div>
<div className="flex items-start gap-4">
<div className="w-10 h-10 bg-[#1a4d2e]/10 rounded-lg flex items-center justify-center shrink-0">
<span className="text-[#1a4d2e] font-bold text-sm">S</span>
<span className="text-[#1a4d2e] font-bold text-sm">N</span>
</div>
<div>
<h3 className="font-semibold text-[#1a1a1a]">Supabase</h3>
<p className="text-sm text-[#666]">PostgreSQL database with built-in encryption and real-time capabilities</p>
<h3 className="font-semibold text-[#1a1a1a]">Neon Postgres</h3>
<p className="text-sm text-[#666]">Postgres with at-rest encryption, branch isolation, and connection pooling.</p>
</div>
</div>
<div className="flex items-start gap-4">
+119 -66
View File
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useReducer } from "react";
import { wholesaleLoginAction } from "@/actions/wholesale-auth";
import { useRouter } from "next/navigation";
import WholesaleBenefits from "@/components/wholesale/WholesaleBenefits";
@@ -14,6 +14,81 @@ const BRANDS = [
{ id: "64294306-5f42-463d-a5e8-2ad6c81a96de", name: "Tuxedo Corn", slug: "tuxedo" },
];
type FormState = { email: string; password: string; brandId: string };
type FieldErrors = { email?: string; password?: string };
type State = {
form: FormState;
submitting: boolean;
googleSubmitting: boolean;
error: string | null;
fieldErrors: FieldErrors;
};
type Action =
| { type: "SET_EMAIL"; value: string }
| { type: "SET_PASSWORD"; value: string }
| { type: "SET_BRAND_ID"; value: string }
| { type: "SET_SUBMITTING"; value: boolean }
| { type: "SET_GOOGLE_SUBMITTING"; value: boolean }
| { type: "SET_ERROR"; value: string | null }
| { type: "SET_FIELD_ERRORS"; value: FieldErrors }
| { type: "CLEAR_FIELD_ERRORS" }
| { type: "CLEAR_FIELD_ERROR"; field: keyof FieldErrors }
| { type: "RESET_SUBMIT" };
function readInitialError(): string | null {
if (typeof window === "undefined") return null;
const params = new URLSearchParams(window.location.search);
const err = params.get("error");
if (err === "portal_disabled") {
return "The wholesale portal is currently disabled. Contact us for assistance.";
}
if (err === "account_not_active") {
return "Your account is not active. Please contact support or register for a new account.";
}
if (err === "invalid_credentials") {
return "Invalid email or password. Please try again.";
}
if (err === "oauth") {
return "Google sign-in failed or was cancelled. Please try again or use your password.";
}
return null;
}
const initialState: State = {
form: { email: "", password: "", brandId: BRANDS[1].id },
submitting: false,
googleSubmitting: false,
error: readInitialError(),
fieldErrors: {},
};
function reducer(state: State, action: Action): State {
switch (action.type) {
case "SET_EMAIL":
return { ...state, form: { ...state.form, email: action.value } };
case "SET_PASSWORD":
return { ...state, form: { ...state.form, password: action.value } };
case "SET_BRAND_ID":
return { ...state, form: { ...state.form, brandId: action.value } };
case "SET_SUBMITTING":
return { ...state, submitting: action.value };
case "SET_GOOGLE_SUBMITTING":
return { ...state, googleSubmitting: action.value };
case "SET_ERROR":
return { ...state, error: action.value };
case "SET_FIELD_ERRORS":
return { ...state, fieldErrors: action.value };
case "CLEAR_FIELD_ERRORS":
return { ...state, fieldErrors: {} };
case "CLEAR_FIELD_ERROR":
return { ...state, fieldErrors: { ...state.fieldErrors, [action.field]: undefined } };
case "RESET_SUBMIT":
return { ...state, submitting: false };
}
}
function BrandLogo() {
return (
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-900/60 border border-emerald-700/50">
@@ -51,39 +126,15 @@ function FormField({ label, id, error, children }: {
export default function WholesaleLoginPage() {
const router = useRouter();
const [form, setForm] = useState({ email: "", password: "", brandId: BRANDS[1].id });
const [submitting, setSubmitting] = useState(false);
const [googleSubmitting, setGoogleSubmitting] = useState(false);
// Read ?error=... from the URL in the lazy initializer. Safe in a client
// component since window is always defined here, and avoids a
// set-state-in-effect on mount.
const [error, setError] = useState<string | null>(() => {
if (typeof window === "undefined") return null;
const params = new URLSearchParams(window.location.search);
const err = params.get("error");
if (err === "portal_disabled") {
return "The wholesale portal is currently disabled. Contact us for assistance.";
}
if (err === "account_not_active") {
return "Your account is not active. Please contact support or register for a new account.";
}
if (err === "invalid_credentials") {
return "Invalid email or password. Please try again.";
}
if (err === "oauth") {
return "Google sign-in failed or was cancelled. Please try again or use your password.";
}
return null;
});
const [fieldErrors, setFieldErrors] = useState<{ email?: string; password?: string }>({});
const [state, dispatch] = useReducer(reducer, initialState);
// Derive selectedBrand from form.brandId during render — no effect needed.
// form.brandId is always a valid BRANDS id, so this lookup is O(n) over a 2-item array.
const selectedBrand = BRANDS.find(b => b.id === form.brandId) ?? BRANDS[1];
const selectedBrand = BRANDS.find(b => b.id === state.form.brandId) ?? BRANDS[1];
async function handleGoogleSignIn() {
setGoogleSubmitting(true);
setError(null);
dispatch({ type: "SET_GOOGLE_SUBMITTING", value: true });
dispatch({ type: "SET_ERROR", value: null });
try {
const { signInWithGoogleAction } = await import("@/actions/auth-actions");
const result = await signInWithGoogleAction({
@@ -98,53 +149,55 @@ export default function WholesaleLoginPage() {
// recognized by the wholesale auth path. Once wholesale auth
// is migrated to Neon Auth, this will start working for them
// too.
setError(
result.error ??
"Google sign-in is available, but the wholesale portal is not yet wired up to it. Please use email + password for now.",
);
setGoogleSubmitting(false);
dispatch({
type: "SET_ERROR",
value:
result.error ??
"Google sign-in failed. Please use email and password to sign in.",
});
dispatch({ type: "SET_GOOGLE_SUBMITTING", value: false });
return;
}
window.location.href = result.url;
} catch {
setError("Google sign-in failed. Please try again.");
setGoogleSubmitting(false);
dispatch({ type: "SET_ERROR", value: "Google sign-in failed. Please try again." });
dispatch({ type: "SET_GOOGLE_SUBMITTING", value: false });
}
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setSubmitting(true);
setError(null);
setFieldErrors({});
dispatch({ type: "SET_SUBMITTING", value: true });
dispatch({ type: "SET_ERROR", value: null });
dispatch({ type: "CLEAR_FIELD_ERRORS" });
// Client-side validation
const errors: { email?: string; password?: string } = {};
if (!form.email) {
const errors: FieldErrors = {};
if (!state.form.email) {
errors.email = "Email is required";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) {
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(state.form.email)) {
errors.email = "Enter a valid email address";
}
if (!form.password) {
if (!state.form.password) {
errors.password = "Password is required";
} else if (form.password.length < 6) {
} else if (state.form.password.length < 6) {
errors.password = "Password must be at least 6 characters";
}
if (Object.keys(errors).length > 0) {
setFieldErrors(errors);
setSubmitting(false);
dispatch({ type: "SET_FIELD_ERRORS", value: errors });
dispatch({ type: "SET_SUBMITTING", value: false });
return;
}
const fd = new FormData(e.currentTarget as HTMLFormElement);
fd.set("brand_id", form.brandId);
fd.set("brand_id", state.form.brandId);
const result = await wholesaleLoginAction(fd);
setSubmitting(false);
dispatch({ type: "SET_SUBMITTING", value: false });
if (result.success) {
router.push("/wholesale/portal");
router.refresh();
} else {
setError(result.error ?? "Login failed. Please check your credentials.");
dispatch({ type: "SET_ERROR", value: result.error ?? "Login failed. Please check your credentials." });
}
}
@@ -190,12 +243,12 @@ export default function WholesaleLoginPage() {
<p className="mt-1 text-sm text-zinc-500">Sign in to your wholesale account.</p>
</div>
{error && (
{state.error && (
<div className="mb-5 rounded-xl border border-red-900/50 bg-red-950/50 px-4 py-3 text-sm text-red-400 flex items-start gap-2">
<svg className="w-4 h-4 mt-0.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
{error}
{state.error}
</div>
)}
@@ -204,10 +257,10 @@ export default function WholesaleLoginPage() {
<button
type="button"
onClick={handleGoogleSignIn}
disabled={googleSubmitting || submitting}
disabled={state.googleSubmitting || state.submitting}
className="w-full rounded-xl bg-white py-3.5 text-sm font-bold text-zinc-900 hover:bg-zinc-100 active:bg-zinc-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors border border-zinc-300 flex items-center justify-center gap-2.5"
>
{googleSubmitting ? (
{state.googleSubmitting ? (
<>
<svg className="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
@@ -241,8 +294,8 @@ export default function WholesaleLoginPage() {
<select aria-label="Brand Id"
id="brand_id"
name="brand_id"
value={form.brandId}
onChange={e => setForm(f => ({ ...f, brandId: e.target.value }))}
value={state.form.brandId}
onChange={e => dispatch({ type: "SET_BRAND_ID", value: e.target.value })}
className="w-full rounded-xl border border-zinc-700 bg-zinc-950 px-3 py-3 text-sm text-zinc-100 outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600 transition-colors appearance-none cursor-pointer"
style={{ backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236b7280'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'/%3E%3C/svg%3E")`, backgroundRepeat: "no-repeat", backgroundPosition: "right 12px center", backgroundSize: "16px" }}
>
@@ -252,38 +305,38 @@ export default function WholesaleLoginPage() {
</select>
</FormField>
<FormField label="Email" id="email" error={fieldErrors.email}>
<FormField label="Email" id="email" error={state.fieldErrors.email}>
<input aria-label="Buyer@company.com"
type="email"
id="email"
name="email"
value={form.email}
onChange={e => { setForm(f => ({ ...f, email: e.target.value })); setFieldErrors(f => ({ ...f, email: undefined })); }}
value={state.form.email}
onChange={e => { dispatch({ type: "SET_EMAIL", value: e.target.value }); dispatch({ type: "CLEAR_FIELD_ERROR", field: "email" }); }}
autoComplete="email"
className={`w-full rounded-xl border bg-zinc-950 px-3 py-3 text-sm text-zinc-100 outline-none transition-colors placeholder:text-zinc-600 ${fieldErrors.email ? "border-red-600 focus:border-red-500 focus:ring-1 focus:ring-red-500" : "border-zinc-700 focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"}`}
className={`w-full rounded-xl border bg-zinc-950 px-3 py-3 text-sm text-zinc-100 outline-none transition-colors placeholder:text-zinc-600 ${state.fieldErrors.email ? "border-red-600 focus:border-red-500 focus:ring-1 focus:ring-red-500" : "border-zinc-700 focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"}`}
placeholder="buyer@company.com"
/>
</FormField>
<FormField label="Password" id="password" error={fieldErrors.password}>
<FormField label="Password" id="password" error={state.fieldErrors.password}>
<input aria-label="••••••••"
type="password"
id="password"
name="password"
value={form.password}
onChange={e => { setForm(f => ({ ...f, password: e.target.value })); setFieldErrors(f => ({ ...f, password: undefined })); }}
value={state.form.password}
onChange={e => { dispatch({ type: "SET_PASSWORD", value: e.target.value }); dispatch({ type: "CLEAR_FIELD_ERROR", field: "password" }); }}
autoComplete="current-password"
className={`w-full rounded-xl border bg-zinc-950 px-3 py-3 text-sm text-zinc-100 outline-none transition-colors placeholder:text-zinc-600 ${fieldErrors.password ? "border-red-600 focus:border-red-500 focus:ring-1 focus:ring-red-500" : "border-zinc-700 focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"}`}
className={`w-full rounded-xl border bg-zinc-950 px-3 py-3 text-sm text-zinc-100 outline-none transition-colors placeholder:text-zinc-600 ${state.fieldErrors.password ? "border-red-600 focus:border-red-500 focus:ring-1 focus:ring-red-500" : "border-zinc-700 focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"}`}
placeholder="••••••••"
/>
</FormField>
<button
type="submit"
disabled={submitting}
disabled={state.submitting}
className="w-full rounded-xl bg-emerald-600 py-3.5 text-sm font-bold text-white hover:bg-emerald-500 active:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors shadow-lg shadow-emerald-900/30 mt-2 flex items-center justify-center gap-2"
>
{submitting ? (
{state.submitting ? (
<>
<svg className="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
@@ -326,4 +379,4 @@ export default function WholesaleLoginPage() {
</div>
</div>
);
}
}
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -58,7 +58,7 @@ const PLANS = {
name: "Farm",
description: "For growing farms with more needs",
monthlyPrice: 14900, // $149.00
annualPrice: 152280, // $1,522.80 (10% discount)
annualPrice: 134100, // $1,341.00 (25% discount; matches marketing pricing.ts)
features: [
"Unlimited products",
"Unlimited stops",
@@ -78,7 +78,7 @@ const PLANS = {
name: "Enterprise",
description: "Custom solution for larger operations",
monthlyPrice: 39900, // $399.00
annualPrice: 0, // Custom pricing
annualPrice: 359100, // $3,591.00 (25% discount; matches marketing pricing.ts)
features: [
"Everything in Farm",
"AI Intelligence Pack",