mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 11:24:57 -05:00
docs: migrate products page from query params to sub-routes (#8299)
This commit is contained in:
@@ -66,7 +66,7 @@ function EnterpriseHero() {
|
||||
{/* CTA */}
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
<Link
|
||||
href="/products?tab=infrastructure"
|
||||
href="/products/infrastructure"
|
||||
className="inline-flex items-center gap-1.5 text-[12px] text-foreground/60 hover:text-foreground/80 font-mono uppercase tracking-wider transition-colors"
|
||||
>
|
||||
View Products
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
|
||||
export function FrameworkContent() {
|
||||
const frameworkFeatures = [
|
||||
{
|
||||
category: "Authentication",
|
||||
items: [
|
||||
"Email & password",
|
||||
"Social sign-in (OAuth)",
|
||||
"Magic link / OTP",
|
||||
"Passkeys (WebAuthn)",
|
||||
"Multi-factor authentication",
|
||||
"Anonymous sessions",
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Authorization",
|
||||
items: [
|
||||
"Role-based access control",
|
||||
"Organization / teams",
|
||||
"Admin management",
|
||||
"Session management",
|
||||
"Account linking",
|
||||
"Impersonation",
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Platform",
|
||||
items: [
|
||||
"Framework agnostic",
|
||||
"20+ database adapters",
|
||||
"TypeScript-first",
|
||||
"Edge runtime support",
|
||||
"Rate limiting",
|
||||
"CSRF protection",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="px-5 sm:px-6 lg:px-8 pb-32 space-y-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.05 }}
|
||||
>
|
||||
<p className="text-[13px] text-foreground/55 dark:text-foreground/45 max-w-lg mb-5">
|
||||
Everything you need to build production-ready auth. Free and open
|
||||
source under the MIT license.
|
||||
</p>
|
||||
|
||||
<div className="relative border border-dashed border-foreground/[0.12] bg-foreground/[0.02] overflow-hidden">
|
||||
<div className="px-5 py-5">
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
<div className="space-y-3 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-[11px] font-mono uppercase tracking-widest text-foreground/85">
|
||||
Community
|
||||
</h3>
|
||||
<span className="text-[9px] font-mono uppercase tracking-widest text-foreground/75 dark:text-foreground/60 border border-dashed border-foreground/15 px-1.5 py-0.5 leading-none">
|
||||
free forever
|
||||
</span>
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{[
|
||||
"Unlimited users",
|
||||
"All auth features",
|
||||
"50+ plugins",
|
||||
"Self-hosted",
|
||||
"MIT license",
|
||||
].map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="flex items-start gap-2 text-[13px] text-foreground/75 dark:text-foreground/60"
|
||||
>
|
||||
<span className="text-foreground/50 dark:text-foreground/55 mt-0.5 font-mono text-[10px] leading-none select-none shrink-0">
|
||||
+
|
||||
</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="text-right shrink-0 flex flex-col items-end gap-3">
|
||||
<Link
|
||||
href="/docs"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 border border-foreground/[0.12] text-foreground/70 dark:text-foreground/50 hover:text-foreground/80 hover:border-foreground/25 hover:bg-foreground/[0.02] transition-all"
|
||||
>
|
||||
<span className="font-mono text-[11px] uppercase tracking-widest">
|
||||
Read Docs
|
||||
</span>
|
||||
<svg
|
||||
className="h-2.5 w-2.5 opacity-50"
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M1 9L9 1M9 1H3M9 1V7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.2"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.15 }}
|
||||
>
|
||||
<p className="text-[11px] uppercase tracking-widest text-foreground/55 dark:text-foreground/40 font-mono mb-5">
|
||||
# what's included
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-0">
|
||||
{frameworkFeatures.map((group, gi) => (
|
||||
<motion.div
|
||||
key={group.category}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
delay: 0.2 + gi * 0.08,
|
||||
}}
|
||||
className="border border-dashed border-foreground/[0.08] -mt-px -ml-px p-5"
|
||||
>
|
||||
<h4 className="text-[10px] font-mono uppercase tracking-widest text-foreground/70 dark:text-foreground/50 mb-4">
|
||||
{group.category}
|
||||
</h4>
|
||||
<ul className="space-y-2">
|
||||
{group.items.map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="flex items-start gap-2 text-[13px] text-foreground/75 dark:text-foreground/60"
|
||||
>
|
||||
<span className="text-foreground/50 dark:text-foreground/55 mt-0.5 font-mono text-[10px] leading-none select-none shrink-0">
|
||||
+
|
||||
</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+2
-445
@@ -1,16 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { motion } from "framer-motion";
|
||||
import { Check, Minus } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { HalftoneBackground } from "@/components/landing/halftone-bg";
|
||||
|
||||
const ctaHref = `https://dash.better-auth.com/sign-in?callbackUrl=${encodeURIComponent("/?redirectTo=/settings/billing")}`;
|
||||
|
||||
type Tab = "framework" | "infrastructure";
|
||||
|
||||
const pricingTiers = [
|
||||
{
|
||||
name: "Starter",
|
||||
@@ -260,193 +254,6 @@ function ComparisonCell({ value }: { value: string | boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
function FrameworkHero() {
|
||||
const highlights = [{ label: "License", value: "MIT" }];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
className="relative w-full pt-6 md:pt-10 pb-6 lg:pb-0 flex flex-col justify-center lg:h-full"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-lg md:text-xl lg:text-2xl text-neutral-800 dark:text-neutral-200 tracking-tight leading-tight">
|
||||
<span className="underline underline-offset-4 decoration-foreground/30">
|
||||
Open Source
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-[11px] text-foreground/70 dark:text-foreground/50 leading-relaxed max-w-[260px]">
|
||||
A comprehensive, framework-agnostic authentication library for
|
||||
TypeScript. Free and open source under the MIT license.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-foreground/10 pt-4 space-y-0">
|
||||
{highlights.map((item, i) => (
|
||||
<motion.div
|
||||
key={item.label}
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{
|
||||
duration: 0.25,
|
||||
delay: 0.3 + i * 0.06,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
className="flex items-baseline justify-between py-1.5 border-b border-dashed border-foreground/[0.06] last:border-0"
|
||||
>
|
||||
<span className="text-[11px] text-foreground/70 dark:text-foreground/50 uppercase tracking-wider">
|
||||
{item.label}
|
||||
</span>
|
||||
<span className="text-[11px] text-foreground/85 dark:text-foreground/75 font-mono">
|
||||
{item.value}
|
||||
</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
<Link
|
||||
href="/docs"
|
||||
className="inline-flex items-center gap-1.5 px-5 py-2 bg-foreground text-background text-sm font-medium hover:opacity-90 transition-colors"
|
||||
>
|
||||
Read Docs
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/better-auth/better-auth"
|
||||
className="inline-flex items-center gap-1.5 text-[12px] text-foreground/70 dark:text-foreground/50 hover:text-foreground/80 font-mono uppercase tracking-wider transition-colors"
|
||||
>
|
||||
GitHub
|
||||
<svg
|
||||
className="h-2.5 w-2.5 opacity-50"
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M1 9L9 1M9 1H3M9 1V7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.2"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function InfrastructureHero() {
|
||||
const principles = [
|
||||
{ label: "Framework", value: "Open source" },
|
||||
{ label: "Users", value: "Unlimited" },
|
||||
{ label: "Audit logs", value: "From 10k/mo" },
|
||||
{ label: "Security", value: "$0.0001/event overage" },
|
||||
{ label: "SSO", value: "Business+" },
|
||||
];
|
||||
|
||||
const tiers = [
|
||||
{ name: "Starter", price: "$0", note: "/mo" },
|
||||
{ name: "Pro", price: "$20", note: "/mo", highlight: true },
|
||||
{ name: "Business", price: "$299", note: "/mo" },
|
||||
{ name: "Enterprise", price: "Custom", note: null },
|
||||
];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
className="relative w-full pt-6 md:pt-10 pb-6 lg:pb-0 flex flex-col justify-center lg:h-full"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-lg md:text-xl lg:text-2xl text-neutral-800 dark:text-neutral-100 tracking-tight leading-tight">
|
||||
<span className="underline underline-offset-4 decoration-foreground/40">
|
||||
Infrastructure
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-[11px] text-foreground/75 dark:text-foreground/65 leading-relaxed max-w-[260px]">
|
||||
Dashboard, security, audit logs, and more for your auth.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-stretch gap-0 border border-foreground/[0.12]">
|
||||
{tiers.map((tier) => (
|
||||
<div
|
||||
key={tier.name}
|
||||
className={`flex-1 px-3 py-2.5 text-center border-r border-foreground/[0.12] last:border-r-0 ${
|
||||
tier.highlight ? "bg-foreground/[0.04]" : ""
|
||||
}`}
|
||||
>
|
||||
<p className="text-[8px] font-mono uppercase tracking-widest text-foreground/65 dark:text-foreground/55 mb-1">
|
||||
{tier.name}
|
||||
</p>
|
||||
<p className="text-sm font-light text-foreground/85 dark:text-foreground/80 tabular-nums flex items-baseline justify-center gap-0.5">
|
||||
<span>{tier.price}</span>
|
||||
{tier.note && (
|
||||
<span className="text-[9px] text-foreground/55 dark:text-foreground/60 font-mono">
|
||||
{tier.note}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-foreground/[0.12] pt-4 space-y-0">
|
||||
{principles.map((item, i) => (
|
||||
<motion.div
|
||||
key={item.label}
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{
|
||||
duration: 0.25,
|
||||
delay: 0.3 + i * 0.06,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
className="flex items-baseline justify-between py-1.5 border-b border-dashed border-foreground/[0.08] last:border-0"
|
||||
>
|
||||
<span className="text-[11px] text-foreground/75 dark:text-foreground/60 uppercase tracking-wider">
|
||||
{item.label}
|
||||
</span>
|
||||
<span className="text-[11px] text-foreground/90 dark:text-foreground/80 font-mono">
|
||||
{item.value}
|
||||
</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
<Link
|
||||
href="https://dash.better-auth.com/sign-in"
|
||||
className="inline-flex items-center gap-1.5 px-5 py-2 bg-foreground text-background text-sm font-medium hover:opacity-90 transition-colors"
|
||||
>
|
||||
Get Started
|
||||
</Link>
|
||||
<Link
|
||||
href="/enterprise"
|
||||
className="inline-flex items-center gap-1.5 text-[12px] text-foreground/70 dark:text-foreground/50 hover:text-foreground/80 font-mono uppercase tracking-wider transition-colors"
|
||||
>
|
||||
Contact Sales
|
||||
<svg
|
||||
className="h-2.5 w-2.5 opacity-50"
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M1 9L9 1M9 1H3M9 1V7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.2"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function PricingCard({
|
||||
tier,
|
||||
index,
|
||||
@@ -541,159 +348,7 @@ function PricingCard({
|
||||
);
|
||||
}
|
||||
|
||||
function FrameworkContent() {
|
||||
const frameworkFeatures = [
|
||||
{
|
||||
category: "Authentication",
|
||||
items: [
|
||||
"Email & password",
|
||||
"Social sign-in (OAuth)",
|
||||
"Magic link / OTP",
|
||||
"Passkeys (WebAuthn)",
|
||||
"Multi-factor authentication",
|
||||
"Anonymous sessions",
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Authorization",
|
||||
items: [
|
||||
"Role-based access control",
|
||||
"Organization / teams",
|
||||
"Admin management",
|
||||
"Session management",
|
||||
"Account linking",
|
||||
"Impersonation",
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Platform",
|
||||
items: [
|
||||
"Framework agnostic",
|
||||
"20+ database adapters",
|
||||
"TypeScript-first",
|
||||
"Edge runtime support",
|
||||
"Rate limiting",
|
||||
"CSRF protection",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="px-5 sm:px-6 lg:px-8 pb-32 space-y-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.05 }}
|
||||
>
|
||||
<p className="text-[13px] text-foreground/55 dark:text-foreground/45 max-w-lg mb-5">
|
||||
Everything you need to build production-ready auth. Free and open
|
||||
source under the MIT license.
|
||||
</p>
|
||||
|
||||
<div className="relative border border-dashed border-foreground/[0.12] bg-foreground/[0.02] overflow-hidden">
|
||||
<div className="px-5 py-5">
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
<div className="space-y-3 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-[11px] font-mono uppercase tracking-widest text-foreground/85">
|
||||
Community
|
||||
</h3>
|
||||
<span className="text-[9px] font-mono uppercase tracking-widest text-foreground/75 dark:text-foreground/60 border border-dashed border-foreground/15 px-1.5 py-0.5 leading-none">
|
||||
free forever
|
||||
</span>
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{[
|
||||
"Unlimited users",
|
||||
"All auth features",
|
||||
"50+ plugins",
|
||||
"Self-hosted",
|
||||
"MIT license",
|
||||
].map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="flex items-start gap-2 text-[13px] text-foreground/75 dark:text-foreground/60"
|
||||
>
|
||||
<span className="text-foreground/50 dark:text-foreground/55 mt-0.5 font-mono text-[10px] leading-none select-none shrink-0">
|
||||
+
|
||||
</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="text-right shrink-0 flex flex-col items-end gap-3">
|
||||
<Link
|
||||
href="/docs"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 border border-foreground/[0.12] text-foreground/70 dark:text-foreground/50 hover:text-foreground/80 hover:border-foreground/25 hover:bg-foreground/[0.02] transition-all"
|
||||
>
|
||||
<span className="font-mono text-[11px] uppercase tracking-widest">
|
||||
Read Docs
|
||||
</span>
|
||||
<svg
|
||||
className="h-2.5 w-2.5 opacity-50"
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M1 9L9 1M9 1H3M9 1V7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.2"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.15 }}
|
||||
>
|
||||
<p className="text-[11px] uppercase tracking-widest text-foreground/55 dark:text-foreground/40 font-mono mb-5">
|
||||
# what's included
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-0">
|
||||
{frameworkFeatures.map((group, gi) => (
|
||||
<motion.div
|
||||
key={group.category}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
delay: 0.2 + gi * 0.08,
|
||||
}}
|
||||
className="border border-dashed border-foreground/[0.08] -mt-px -ml-px p-5"
|
||||
>
|
||||
<h4 className="text-[10px] font-mono uppercase tracking-widest text-foreground/70 dark:text-foreground/50 mb-4">
|
||||
{group.category}
|
||||
</h4>
|
||||
<ul className="space-y-2">
|
||||
{group.items.map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="flex items-start gap-2 text-[13px] text-foreground/75 dark:text-foreground/60"
|
||||
>
|
||||
<span className="text-foreground/50 dark:text-foreground/55 mt-0.5 font-mono text-[10px] leading-none select-none shrink-0">
|
||||
+
|
||||
</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InfrastructureContent() {
|
||||
export function InfrastructureContent() {
|
||||
const tierKeys = ["starter", "pro", "business", "enterprise"] as const;
|
||||
const tierLabels = ["Starter", "Pro", "Business", "Enterprise"];
|
||||
|
||||
@@ -933,101 +588,3 @@ function InfrastructureContent() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProductsPageClient() {
|
||||
const searchParams = useSearchParams();
|
||||
const tabParam = searchParams.get("tab");
|
||||
const initialTab: Tab =
|
||||
tabParam === "infrastructure" ? "infrastructure" : "framework";
|
||||
const [activeTab, setActiveTab] = useState<Tab>(initialTab);
|
||||
|
||||
useEffect(() => {
|
||||
const next: Tab =
|
||||
tabParam === "infrastructure" ? "infrastructure" : "framework";
|
||||
setActiveTab(next);
|
||||
}, [tabParam]);
|
||||
|
||||
return (
|
||||
<div className="relative h-full overflow-x-hidden">
|
||||
<div className="relative text-foreground h-full">
|
||||
<div className="flex flex-col lg:flex-row h-full">
|
||||
{/* Left side — Hero */}
|
||||
<div className="hidden lg:block relative w-full lg:w-[30%] border-b lg:border-b-0 lg:border-r border-foreground/[0.06] overflow-hidden px-5 sm:px-6 lg:px-10">
|
||||
<div className="hidden lg:block">
|
||||
<HalftoneBackground />
|
||||
</div>
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab === "framework" ? (
|
||||
<motion.div
|
||||
key="framework-hero"
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 10 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="h-full"
|
||||
>
|
||||
<FrameworkHero />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="infrastructure-hero"
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 10 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="h-full"
|
||||
>
|
||||
<InfrastructureHero />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Right side — Content */}
|
||||
<div className="relative w-full lg:w-[70%] overflow-y-auto overflow-x-hidden no-scrollbar">
|
||||
<div className="px-5 sm:px-6 lg:px-8 pt-16 lg:pt-16 pb-4">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.h2
|
||||
key={activeTab}
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -4 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="text-base text-foreground/90 tracking-tight"
|
||||
>
|
||||
{activeTab === "framework"
|
||||
? "Better Auth Framework"
|
||||
: "Infrastructure"}
|
||||
</motion.h2>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab === "framework" ? (
|
||||
<motion.div
|
||||
key="framework-content"
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<FrameworkContent />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="infrastructure-content"
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<InfrastructureContent />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { HalftoneBackground } from "@/components/landing/halftone-bg";
|
||||
|
||||
function FrameworkHero() {
|
||||
const highlights = [{ label: "License", value: "MIT" }];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
className="relative w-full pt-6 md:pt-10 pb-6 lg:pb-0 flex flex-col justify-center lg:h-full"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-lg md:text-xl lg:text-2xl text-neutral-800 dark:text-neutral-200 tracking-tight leading-tight">
|
||||
<span className="underline underline-offset-4 decoration-foreground/30">
|
||||
Open Source
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-[11px] text-foreground/70 dark:text-foreground/50 leading-relaxed max-w-[260px]">
|
||||
A comprehensive, framework-agnostic authentication library for
|
||||
TypeScript. Free and open source under the MIT license.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-foreground/10 pt-4 space-y-0">
|
||||
{highlights.map((item, i) => (
|
||||
<motion.div
|
||||
key={item.label}
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{
|
||||
duration: 0.25,
|
||||
delay: 0.3 + i * 0.06,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
className="flex items-baseline justify-between py-1.5 border-b border-dashed border-foreground/[0.06] last:border-0"
|
||||
>
|
||||
<span className="text-[11px] text-foreground/70 dark:text-foreground/50 uppercase tracking-wider">
|
||||
{item.label}
|
||||
</span>
|
||||
<span className="text-[11px] text-foreground/85 dark:text-foreground/75 font-mono">
|
||||
{item.value}
|
||||
</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
<Link
|
||||
href="/docs"
|
||||
className="inline-flex items-center gap-1.5 px-5 py-2 bg-foreground text-background text-sm font-medium hover:opacity-90 transition-colors"
|
||||
>
|
||||
Read Docs
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/better-auth/better-auth"
|
||||
className="inline-flex items-center gap-1.5 text-[12px] text-foreground/70 dark:text-foreground/50 hover:text-foreground/80 font-mono uppercase tracking-wider transition-colors"
|
||||
>
|
||||
GitHub
|
||||
<svg
|
||||
className="h-2.5 w-2.5 opacity-50"
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M1 9L9 1M9 1H3M9 1V7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.2"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function InfrastructureHero() {
|
||||
const principles = [
|
||||
{ label: "Framework", value: "Open source" },
|
||||
{ label: "Users", value: "Unlimited" },
|
||||
{ label: "Audit logs", value: "From 10k/mo" },
|
||||
{ label: "Security", value: "$0.0001/event overage" },
|
||||
{ label: "SSO", value: "Business+" },
|
||||
];
|
||||
|
||||
const tiers = [
|
||||
{ name: "Starter", price: "$0", note: "/mo" },
|
||||
{ name: "Pro", price: "$20", note: "/mo", highlight: true },
|
||||
{ name: "Business", price: "$299", note: "/mo" },
|
||||
{ name: "Enterprise", price: "Custom", note: null },
|
||||
];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
className="relative w-full pt-6 md:pt-10 pb-6 lg:pb-0 flex flex-col justify-center lg:h-full"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-lg md:text-xl lg:text-2xl text-neutral-800 dark:text-neutral-100 tracking-tight leading-tight">
|
||||
<span className="underline underline-offset-4 decoration-foreground/40">
|
||||
Infrastructure
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-[11px] text-foreground/75 dark:text-foreground/65 leading-relaxed max-w-[260px]">
|
||||
Dashboard, security, audit logs, and more for your auth.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-stretch gap-0 border border-foreground/[0.12]">
|
||||
{tiers.map((tier) => (
|
||||
<div
|
||||
key={tier.name}
|
||||
className={`flex-1 px-3 py-2.5 text-center border-r border-foreground/[0.12] last:border-r-0 ${
|
||||
tier.highlight ? "bg-foreground/[0.04]" : ""
|
||||
}`}
|
||||
>
|
||||
<p className="text-[8px] font-mono uppercase tracking-widest text-foreground/65 dark:text-foreground/55 mb-1">
|
||||
{tier.name}
|
||||
</p>
|
||||
<p className="text-sm font-light text-foreground/85 dark:text-foreground/80 tabular-nums flex items-baseline justify-center gap-0.5">
|
||||
<span>{tier.price}</span>
|
||||
{tier.note && (
|
||||
<span className="text-[9px] text-foreground/55 dark:text-foreground/60 font-mono">
|
||||
{tier.note}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-foreground/[0.12] pt-4 space-y-0">
|
||||
{principles.map((item, i) => (
|
||||
<motion.div
|
||||
key={item.label}
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{
|
||||
duration: 0.25,
|
||||
delay: 0.3 + i * 0.06,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
className="flex items-baseline justify-between py-1.5 border-b border-dashed border-foreground/[0.08] last:border-0"
|
||||
>
|
||||
<span className="text-[11px] text-foreground/75 dark:text-foreground/60 uppercase tracking-wider">
|
||||
{item.label}
|
||||
</span>
|
||||
<span className="text-[11px] text-foreground/90 dark:text-foreground/80 font-mono">
|
||||
{item.value}
|
||||
</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="inline-flex items-center gap-1.5 px-5 py-2 bg-foreground text-background text-sm font-medium hover:opacity-90 transition-colors"
|
||||
>
|
||||
Get Started
|
||||
</Link>
|
||||
<Link
|
||||
href="/enterprise"
|
||||
className="inline-flex items-center gap-1.5 text-[12px] text-foreground/70 dark:text-foreground/50 hover:text-foreground/80 font-mono uppercase tracking-wider transition-colors"
|
||||
>
|
||||
Contact Sales
|
||||
<svg
|
||||
className="h-2.5 w-2.5 opacity-50"
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M1 9L9 1M9 1H3M9 1V7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.2"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProductsShell({
|
||||
tab,
|
||||
children,
|
||||
}: {
|
||||
tab: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const isInfrastructure = tab === "infrastructure";
|
||||
|
||||
return (
|
||||
<div className="relative h-full overflow-x-hidden">
|
||||
<div className="relative text-foreground h-full">
|
||||
<div className="flex flex-col lg:flex-row h-full">
|
||||
{/* Left side — Hero */}
|
||||
<div className="hidden lg:block relative w-full lg:w-[30%] border-b lg:border-b-0 lg:border-r border-foreground/[0.06] overflow-hidden px-5 sm:px-6 lg:px-10">
|
||||
<div className="hidden lg:block">
|
||||
<HalftoneBackground />
|
||||
</div>
|
||||
<AnimatePresence mode="wait">
|
||||
{isInfrastructure ? (
|
||||
<motion.div
|
||||
key="infrastructure-hero"
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 10 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="h-full"
|
||||
>
|
||||
<InfrastructureHero />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="framework-hero"
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 10 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="h-full"
|
||||
>
|
||||
<FrameworkHero />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Right side — Content */}
|
||||
<div className="relative w-full lg:w-[70%] overflow-y-auto overflow-x-hidden no-scrollbar">
|
||||
<div className="px-5 sm:px-6 lg:px-8 pt-16 lg:pt-16 pb-4">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.h2
|
||||
key={tab}
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -4 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="text-base text-foreground/90 tracking-tight"
|
||||
>
|
||||
{isInfrastructure
|
||||
? "Infrastructure"
|
||||
: "Better Auth Framework"}
|
||||
</motion.h2>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ProductsShell } from "./_components/products-shell";
|
||||
|
||||
export default async function TabLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ tab: string }>;
|
||||
}) {
|
||||
const { tab } = await params;
|
||||
|
||||
return <ProductsShell tab={tab}>{children}</ProductsShell>;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { Metadata } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
import { FrameworkContent } from "./_components/framework-content";
|
||||
import { InfrastructureContent } from "./_components/infrastructure-content";
|
||||
|
||||
const tabs = {
|
||||
framework: {
|
||||
title: "Framework — Products",
|
||||
description:
|
||||
"Better Auth — a comprehensive, framework-agnostic authentication library for TypeScript. Free and open source under the MIT license.",
|
||||
},
|
||||
infrastructure: {
|
||||
title: "Infrastructure — Products",
|
||||
description:
|
||||
"Better Auth Infrastructure — dashboard, audit logs, security detection, and more for your auth.",
|
||||
},
|
||||
} as const;
|
||||
|
||||
type Tab = keyof typeof tabs;
|
||||
|
||||
export function generateStaticParams() {
|
||||
return Object.keys(tabs).map((tab) => ({ tab }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ tab: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { tab } = await params;
|
||||
const meta = tabs[tab as Tab];
|
||||
if (!meta) return {};
|
||||
return { title: meta.title, description: meta.description };
|
||||
}
|
||||
|
||||
export default async function TabPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ tab: string }>;
|
||||
}) {
|
||||
const { tab } = await params;
|
||||
|
||||
if (tab === "framework") return <FrameworkContent />;
|
||||
if (tab === "infrastructure") return <InfrastructureContent />;
|
||||
|
||||
redirect("/products/framework");
|
||||
}
|
||||
@@ -1,17 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Suspense } from "react";
|
||||
import { ProductsPageClient } from "./products-client";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Products",
|
||||
description:
|
||||
"Better Auth — free open-source framework with optional managed infrastructure.",
|
||||
};
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function ProductsPage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<ProductsPageClient />
|
||||
</Suspense>
|
||||
);
|
||||
redirect("/products/framework");
|
||||
}
|
||||
|
||||
@@ -1621,7 +1621,7 @@ export function HeroReadMe({
|
||||
headline: "Security & observability.",
|
||||
desc: "Bot detection, real-time behavior analysis, IP blocking, email validation, and more.",
|
||||
security: true,
|
||||
href: "/products?tab=infrastructure",
|
||||
href: "/products/infrastructure",
|
||||
managed: true,
|
||||
},
|
||||
{
|
||||
@@ -1629,7 +1629,7 @@ export function HeroReadMe({
|
||||
headline: "User management.",
|
||||
desc: "Manage users, sessions, and organizations. Track sign-ups, active users, and growth.",
|
||||
dashboard: true,
|
||||
href: "/products?tab=infrastructure",
|
||||
href: "/products/infrastructure",
|
||||
managed: true,
|
||||
},
|
||||
].map((feature, i) => (
|
||||
@@ -2499,7 +2499,7 @@ export function HeroReadMe({
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/products?tab=infrastructure"
|
||||
href="/products/infrastructure"
|
||||
className="inline-flex items-center gap-1.5 shrink-0 ml-4 px-4 py-2 border border-dashed border-foreground/[0.14] text-foreground dark:text-foreground/80 hover:text-foreground hover:border-foreground/25 hover:bg-foreground/[0.02] transition-all"
|
||||
>
|
||||
<span className="font-mono text-[11px] uppercase tracking-widest">
|
||||
|
||||
@@ -26,8 +26,8 @@ const navFiles: NavFileItem[] = [
|
||||
];
|
||||
|
||||
const productFiles: NavFileItem[] = [
|
||||
{ name: "framework", href: "/products?tab=framework" },
|
||||
{ name: "infrastructure", href: "/products?tab=infrastructure" },
|
||||
{ name: "framework", href: "/products/framework" },
|
||||
{ name: "infrastructure", href: "/products/infrastructure" },
|
||||
];
|
||||
|
||||
const resourceFiles: NavFileItem[] = [
|
||||
|
||||
@@ -38,6 +38,19 @@ const nextConfig = {
|
||||
destination: "/docs/introduction",
|
||||
permanent: false,
|
||||
},
|
||||
// Legacy query string based redirects
|
||||
{
|
||||
source: "/products",
|
||||
has: [{ type: "query", key: "tab", value: "framework" }],
|
||||
destination: "/products/framework",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/products",
|
||||
has: [{ type: "query", key: "tab", value: "infrastructure" }],
|
||||
destination: "/products/infrastructure",
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user