feat: migrate landing page to TypeScript and fix auth cookies

- Migrate all apps/website components from JS/JSX to TypeScript/TSX
- Add blog pages (layout, listing, slug) in TypeScript
- Add tsconfig.json and tailwind.config.mjs to website
- Fix cookie domain handling in vite/App.tsx (set .useautumn.com in prod)
- Update auth.ts, afterSessionCreated, afterSessionDeleted

Made-with: Cursor
This commit is contained in:
amianthus
2026-04-16 17:52:14 +01:00
parent 39fbd46d4f
commit d8a4979fe4
65 changed files with 5547 additions and 4672 deletions

View File

@@ -0,0 +1,114 @@
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import { MDXRemote } from "next-mdx-remote/rsc";
import { mdxComponents } from "@/components/blogComponents";
import { getAllPosts, getPostBySlug } from "@/lib/blogUtils";
import type { BlogParams } from "@/lib/types";
export function generateStaticParams() {
return getAllPosts().map((post) => ({ slug: post.slug }));
}
export async function generateMetadata({
params,
}: {
params: BlogParams;
}): Promise<Metadata> {
const { slug } = await params;
const post = getPostBySlug({ slug });
if (!post) return { title: "Post Not Found" };
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
type: "article",
...(post.date ? { publishedTime: post.date } : {}),
authors: [post.author],
...(post.image && {
images: [{ url: post.image }],
}),
},
};
}
function formatDate(dateString: string | null) {
if (!dateString) return "";
return new Date(dateString).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
}
export default async function BlogPostPage({ params }: { params: BlogParams }) {
const { slug } = await params;
const post = getPostBySlug({ slug });
if (!post) notFound();
return (
<div className="py-16 md:py-24 bg-[#0F0F0F]">
<div className="max-w-[720px] mx-auto px-4 xl:px-0">
<Link
href="/blog"
className="inline-flex items-center gap-2 font-mono text-[12px] md:text-[14px] uppercase tracking-[-2%] text-[#FFFFFF66] hover:text-white transition-colors duration-300 mb-10"
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
className="rotate-180"
>
<path
d="M6 3L11 8L6 13"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
Back to blog
</Link>
<header className="mb-12">
<div className="flex items-center gap-3 font-mono text-[12px] md:text-[14px] uppercase tracking-[-2%] text-[#FFFFFF66] mb-4">
<span>{formatDate(post.date)}</span>
<span className="w-1 h-1 bg-[#FFFFFF44] rounded-full" />
<span>{post.author}</span>
</div>
<h1 className="text-[30px] md:text-[40px] font-normal tracking-[-2%] leading-[1.1] font-sans text-white mb-4">
{post.title}
</h1>
{post.description && (
<p className="text-[14px] md:text-[16px] leading-5 text-[#FFFFFF99] font-light font-sans">
{post.description}
</p>
)}
</header>
{post.image && (
<div className="relative w-full aspect-[2/1] overflow-hidden border border-[#292929] mb-12">
<Image
src={post.image}
alt={post.title}
fill
className="object-cover"
priority
/>
</div>
)}
<hr className="border-[#292929] mb-12" />
<article className="prose prose-invert prose-lg max-w-none">
<MDXRemote source={post.source} components={mdxComponents} />
</article>
</div>
</div>
);
}

View File

@@ -1,30 +0,0 @@
import Navbar from "@/components/navbar";
import Footer from "@/components/footer";
export default function BlogLayout({ children }) {
return (
<div
className="w-full overflow-x-clip"
style={{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" }}
>
<div className="relative z-10 bg-[#0F0F0F] min-h-screen">
<div className="relative w-full px-4 md:px-(--page-pad) pt-5">
<div className="absolute pointer-events-none top-0 bottom-0 left-4 md:left-(--page-pad) border-l border-[#292929] z-50" />
<Navbar />
<div className="absolute pointer-events-none top-0 bottom-0 right-4 md:right-(--page-pad) border-r border-[#292929] z-50" />
<div className="flex flex-col gap-2.5 mt-2.5">
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full hidden md:block" />
<div className="border-t border-[#292929] w-full hidden md:block" />
<div className="border-t border-[#292929] w-full hidden md:block" />
</div>
{children}
<Footer />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,33 @@
import Footer from "@/components/footer";
import Navbar from "@/components/navbar";
import type { LayoutProps, PageStyle } from "@/lib/types";
export default function BlogLayout({ children }: LayoutProps) {
return (
<div
className="w-full overflow-x-clip"
style={
{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" } as PageStyle
}
>
<div className="relative z-10 bg-[#0F0F0F] min-h-screen">
<div className="relative w-full px-4 md:px-(--page-pad) pt-5">
<div className="absolute pointer-events-none top-0 bottom-0 left-4 md:left-(--page-pad) border-l border-[#292929] z-50" />
<Navbar />
<div className="absolute pointer-events-none top-0 bottom-0 right-4 md:right-(--page-pad) border-r border-[#292929] z-50" />
<div className="flex flex-col gap-2.5 mt-2.5">
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full hidden md:block" />
<div className="border-t border-[#292929] w-full hidden md:block" />
<div className="border-t border-[#292929] w-full hidden md:block" />
</div>
{children}
<Footer />
</div>
</div>
</div>
);
}

View File

@@ -1,79 +0,0 @@
import { getAllPosts } from "@/lib/blogUtils";
import Image from "next/image";
import Link from "next/link";
export const metadata = {
title: "Blog",
description:
"Thoughts on billing infrastructure, usage-based pricing, and building for AI startups.",
};
function formatDate(dateString) {
if (!dateString) return "";
return new Date(dateString).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
}
export default function BlogListingPage() {
const posts = getAllPosts();
return (
<div className="py-16 md:py-24 bg-[#0F0F0F]">
<div className="max-w-[800px] mx-auto px-4 xl:px-0">
<h1 className="text-[30px] md:text-[40px] font-normal tracking-[-2%] leading-[1.1] font-sans mb-4">
<span className="text-[#FFFFFF99] font-light">From the </span>
<span className="text-white">Blog</span>
</h1>
<p className="text-[14px] md:text-[16px] leading-5 text-[#FFFFFF99] font-light font-sans mb-16">
Thoughts on billing infrastructure, usage-based pricing, and building
for AI startups.
</p>
{posts.length === 0 && (
<p className="text-[#FFFFFF66] text-center py-16 font-light">
No posts yet. Check back soon.
</p>
)}
<div className="flex flex-col gap-1">
{posts.map((post) => (
<Link
key={post.slug}
href={`/blog/${post.slug}`}
className="group flex items-center gap-6 border border-[#292929] hover:border-[#3f3f3f] hover:bg-[#080808] transition-colors duration-300 p-6 md:p-8"
>
<div className="flex flex-col gap-3 flex-1 min-w-0">
<div className="flex items-center gap-3 font-mono text-[12px] md:text-[14px] uppercase tracking-[-2%] text-[#FFFFFF66]">
<span>{formatDate(post.date)}</span>
<span className="w-1 h-1 bg-[#FFFFFF44] rounded-full" />
<span>{post.author}</span>
</div>
<h2 className="font-sans text-[18px] md:text-[22px] tracking-[-2%] leading-[1.25] font-normal text-white group-hover:text-[#9564ff] transition-colors duration-300">
{post.title}
</h2>
{post.description && (
<p className="text-[14px] md:text-[16px] leading-5 text-[#FFFFFF99] font-light font-sans">
{post.description}
</p>
)}
</div>
{post.image && (
<div className="relative hidden sm:block w-[140px] md:w-[180px] aspect-[3/2] overflow-hidden shrink-0">
<Image
src={post.image}
alt={post.title}
fill
className="object-cover"
/>
</div>
)}
</Link>
))}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,80 @@
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { getAllPosts } from "@/lib/blogUtils";
export const metadata: Metadata = {
title: "Blog",
description:
"Thoughts on billing infrastructure, usage-based pricing, and building for AI startups.",
};
function formatDate(dateString: string | null) {
if (!dateString) return "";
return new Date(dateString).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
}
export default function BlogListingPage() {
const posts = getAllPosts();
return (
<div className="py-16 md:py-24 bg-[#0F0F0F]">
<div className="max-w-[800px] mx-auto px-4 xl:px-0">
<h1 className="text-[30px] md:text-[40px] font-normal tracking-[-2%] leading-[1.1] font-sans mb-4">
<span className="text-[#FFFFFF99] font-light">From the </span>
<span className="text-white">Blog</span>
</h1>
<p className="text-[14px] md:text-[16px] leading-5 text-[#FFFFFF99] font-light font-sans mb-16">
Thoughts on billing infrastructure, usage-based pricing, and building
for AI startups.
</p>
{posts.length === 0 && (
<p className="text-[#FFFFFF66] text-center py-16 font-light">
No posts yet. Check back soon.
</p>
)}
<div className="flex flex-col gap-1">
{posts.map((post) => (
<Link
key={post.slug}
href={`/blog/${post.slug}`}
className="group flex items-center gap-6 border border-[#292929] hover:border-[#3f3f3f] hover:bg-[#080808] transition-colors duration-300 p-6 md:p-8"
>
<div className="flex flex-col gap-3 flex-1 min-w-0">
<div className="flex items-center gap-3 font-mono text-[12px] md:text-[14px] uppercase tracking-[-2%] text-[#FFFFFF66]">
<span>{formatDate(post.date)}</span>
<span className="w-1 h-1 bg-[#FFFFFF44] rounded-full" />
<span>{post.author}</span>
</div>
<h2 className="font-sans text-[18px] md:text-[22px] tracking-[-2%] leading-[1.25] font-normal text-white group-hover:text-[#9564ff] transition-colors duration-300">
{post.title}
</h2>
{post.description && (
<p className="text-[14px] md:text-[16px] leading-5 text-[#FFFFFF99] font-light font-sans">
{post.description}
</p>
)}
</div>
{post.image && (
<div className="relative hidden sm:block w-[140px] md:w-[180px] aspect-[3/2] overflow-hidden shrink-0">
<Image
src={post.image}
alt={post.title}
fill
className="object-cover"
/>
</div>
)}
</Link>
))}
</div>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

1340
apps/website/app/constant.tsx Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -1,81 +0,0 @@
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata = {
title: {
default: "Autumn — Billing Infrastructure for AI Startups",
template: "%s | Autumn",
},
description:
"The drop-in billing layer for AI startups. Stop rebuilding usage limits, credit systems, and subscription logic. Autumn keeps webhooks, payments, and usage perfectly in-sync.",
keywords: [
"AI billing",
"usage-based billing",
"subscription management",
"AI startups",
"billing infrastructure",
"payment integration",
"usage limits",
"credit system",
],
authors: [{ name: "Autumn" }],
creator: "Autumn",
metadataBase: new URL("https://autumndev.vercel.app"),
openGraph: {
type: "website",
locale: "en_US",
url: "https://autumndev.vercel.app",
siteName: "Autumn",
title: "Autumn — Billing Infrastructure for AI Startups",
description:
"The drop-in billing layer for AI startups. Stop rebuilding usage limits, credit systems, and subscription logic.",
images: [
{
url: "/images/og-image.png",
width: 1200,
height: 630,
alt: "Autumn — Billing Infrastructure for AI Startups",
},
],
},
twitter: {
card: "summary_large_image",
title: "Autumn — Billing Infrastructure for AI Startups",
description:
"The drop-in billing layer for AI startups. Stop rebuilding usage limits, credit systems, and subscription logic.",
images: ["/images/og-image.png"],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
};
export default function RootLayout({ children }) {
return (
<html
lang="en"
suppressHydrationWarning
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased bg-black`}
>
<body className="min-h-full flex flex-col">{children}</body>
</html>
);
}

View File

@@ -0,0 +1,90 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import type { LayoutProps } from "@/lib/types";
import { cn } from "@/lib/utils";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
const url = "https://useautumn.com";
export const metadata: Metadata = {
title: {
default: "Autumn — Billing Infrastructure for AI Startups",
template: "%s | Autumn",
},
description:
"The drop-in billing layer for AI startups. Stop rebuilding usage limits, credit systems, and subscription logic. Autumn keeps webhooks, payments, and usage perfectly in-sync.",
keywords: [
"AI billing",
"usage-based billing",
"subscription management",
"AI startups",
"billing infrastructure",
"payment integration",
"usage limits",
"credit system",
],
authors: [{ name: "Autumn" }],
creator: "Autumn",
metadataBase: new URL(url),
openGraph: {
type: "website",
locale: "en_US",
url,
siteName: "Autumn",
title: "Autumn — Billing Infrastructure for AI Startups",
description:
"The drop-in billing layer for AI startups. Stop rebuilding usage limits, credit systems, and subscription logic.",
images: [
{
url: "/images/og-image.png",
width: 1200,
height: 630,
alt: "Autumn — Billing Infrastructure for AI Startups",
},
],
},
twitter: {
card: "summary_large_image",
title: "Autumn — Billing Infrastructure for AI Startups",
description:
"The drop-in billing layer for AI startups. Stop rebuilding usage limits, credit systems, and subscription logic.",
images: ["/images/og-image.png"],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
};
export default function RootLayout({ children }: LayoutProps) {
return (
<html
lang="en"
suppressHydrationWarning
className={cn(
geistSans.variable,
geistMono.variable,
"h-full bg-black antialiased",
)}
>
<body className="min-h-full flex flex-col">{children}</body>
</html>
);
}

View File

@@ -1,111 +0,0 @@
"use client";
import Navbar from "@/components/navbar";
import Image from "next/image";
import Link from "next/link";
import { IconArrowRightSmall } from "@/app/constant";
import { motion } from "motion/react";
export default function NotFound() {
return (
<div
className="w-full min-h-screen overflow-x-hidden overflow-y-auto bg-[#0f0f0f] flex flex-col"
style={{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" }}
>
<div className="relative w-full px-4 md:px-(--page-pad) pt-5 flex-1 block">
<div className="absolute pointer-events-none top-0 bottom-0 left-4 md:left-(--page-pad) border-l border-[#292929] z-50" />
<div className="absolute pointer-events-none top-0 bottom-0 right-4 md:right-(--page-pad) border-r border-[#292929] z-50" />
<Navbar animateIntro={false} />
<div className="relative flex-1 w-full min-h-[calc(100vh-100px)] flex flex-col items-center justify-center -mt-[1px]">
<div className="absolute inset-0 w-full h-full z-0 overflow-hidden">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1, ease: "easeOut" }}
className="absolute inset-0 w-full h-full"
>
<Image
src="/images/404.svg"
alt="404 Background City Base"
fill
className="object-cover object-center"
priority
/>
</motion.div>
<motion.div
initial={{ opacity: 0, display: "none" }}
animate={{
opacity: [0, 1, 0.8, 1, 0.6, 0],
display: ["block", "block", "block", "block", "block", "none"],
x: [0, -15, 20, -10, 15, 0],
y: [0, 8, -15, 5, -5, 0],
filter: [
"brightness(3) contrast(200%) hue-rotate(90deg)",
"brightness(2) contrast(300%) invert(30%)",
"brightness(1.5) contrast(150%) hue-rotate(-45deg)",
"brightness(3) contrast(200%) hue-rotate(180deg)",
"brightness(2) contrast(150%)",
"brightness(1)",
],
clipPath: [
"inset(10% 0% 60% 0%)",
"inset(20% 0% 30% 0%)",
"inset(80% 0% 5% 0%)",
"inset(40% 0% 40% 0%)",
"inset(5% 0% 80% 0%)",
"inset(0% 0% 0% 0%)",
],
}}
transition={{
duration: 0.45,
ease: "easeInOut",
delay: 0.1,
times: [0, 0.2, 0.6, 0.75, 0.9, 1],
}}
className="absolute inset-0 w-full h-full mix-blend-screen pointer-events-none z-10"
>
<Image
src="/images/404.svg"
alt="404 Background City Glitch"
fill
className="object-cover object-center scale-105"
priority
/>
</motion.div>
</div>
<div className="relative z-10 flex flex-col items-center justify-center w-full px-4 text-center mt-[-40px]">
<Image
src="/images/autumn-notfound.svg"
alt="Autumn Logo"
width={64}
height={64}
className="w-[56px] h-[56px] md:w-[64px] md:h-[64px] mb-6"
/>
<h1 className="text-white text-[48px] md:text-[64px] font-normal tracking-[-3%] mb-3 font-sans leading-[1.1]">
Page not found
</h1>
<p className="text-[#FFFFFF99] font-light text-[15px] md:text-[18px] mb-8 tracking-[-1%] text-center">
The page you are looking for doesn&apos;t exist or has been moved.
</p>
<Link href="/">
<button className="group relative flex items-stretch justify-between transition-colors duration-300 bg-[#8752FA] hover:bg-[#7641E8] w-[200px] md:w-[210px] h-[48px] md:h-[54px] border border-[#8752FA]">
<span className="text-white text-[14px] md:text-[15px] pl-6 flex items-center font-sans tracking-[-1%]">
Back to home
</span>
<div className="flex items-center justify-center w-[36px] md:w-[40px] transition-colors duration-300 bg-white text-[#8752FA] m-1 md:m-1.5 shrink-0">
<IconArrowRightSmall className="w-4 h-4" />
</div>
</button>
</Link>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,114 @@
"use client";
import { motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import { IconArrowRightSmall } from "@/app/constant";
import Navbar from "@/components/navbar";
import type { PageStyle } from "@/lib/types";
export default function NotFound() {
return (
<div
className="w-full min-h-screen overflow-x-hidden overflow-y-auto bg-[#0f0f0f] flex flex-col"
style={
{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" } as PageStyle
}
>
<div className="relative w-full px-4 md:px-(--page-pad) pt-5 flex-1 block">
<div className="absolute pointer-events-none top-0 bottom-0 left-4 md:left-(--page-pad) border-l border-[#292929] z-50" />
<div className="absolute pointer-events-none top-0 bottom-0 right-4 md:right-(--page-pad) border-r border-[#292929] z-50" />
<Navbar animateIntro={false} />
<div className="relative flex-1 w-full min-h-[calc(100vh-100px)] flex flex-col items-center justify-center -mt-[1px]">
<div className="absolute inset-0 w-full h-full z-0 overflow-hidden">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1, ease: "easeOut" }}
className="absolute inset-0 w-full h-full"
>
<Image
src="/images/404.svg"
alt="404 Background City Base"
fill
className="object-cover object-center"
priority
/>
</motion.div>
<motion.div
initial={{ opacity: 0, display: "none" }}
animate={{
opacity: [0, 1, 0.8, 1, 0.6, 0],
display: ["block", "block", "block", "block", "block", "none"],
x: [0, -15, 20, -10, 15, 0],
y: [0, 8, -15, 5, -5, 0],
filter: [
"brightness(3) contrast(200%) hue-rotate(90deg)",
"brightness(2) contrast(300%) invert(30%)",
"brightness(1.5) contrast(150%) hue-rotate(-45deg)",
"brightness(3) contrast(200%) hue-rotate(180deg)",
"brightness(2) contrast(150%)",
"brightness(1)",
],
clipPath: [
"inset(10% 0% 60% 0%)",
"inset(20% 0% 30% 0%)",
"inset(80% 0% 5% 0%)",
"inset(40% 0% 40% 0%)",
"inset(5% 0% 80% 0%)",
"inset(0% 0% 0% 0%)",
],
}}
transition={{
duration: 0.45,
ease: "easeInOut",
delay: 0.1,
times: [0, 0.2, 0.6, 0.75, 0.9, 1],
}}
className="absolute inset-0 w-full h-full mix-blend-screen pointer-events-none z-10"
>
<Image
src="/images/404.svg"
alt="404 Background City Glitch"
fill
className="object-cover object-center scale-105"
priority
/>
</motion.div>
</div>
<div className="relative z-10 flex flex-col items-center justify-center w-full px-4 text-center mt-[-40px]">
<Image
src="/images/autumn-notfound.svg"
alt="Autumn Logo"
width={64}
height={64}
className="w-[56px] h-[56px] md:w-[64px] md:h-[64px] mb-6"
/>
<h1 className="text-white text-[48px] md:text-[64px] font-normal tracking-[-3%] mb-3 font-sans leading-[1.1]">
Page not found
</h1>
<p className="text-[#FFFFFF99] font-light text-[15px] md:text-[18px] mb-8 tracking-[-1%] text-center">
The page you are looking for doesn&apos;t exist or has been moved.
</p>
<Link href="/">
<button className="group relative flex items-stretch justify-between transition-colors duration-300 bg-[#8752FA] hover:bg-[#7641E8] w-[200px] md:w-[210px] h-[48px] md:h-[54px] border border-[#8752FA]">
<span className="text-white text-[14px] md:text-[15px] pl-6 flex items-center font-sans tracking-[-1%]">
Back to home
</span>
<div className="flex items-center justify-center w-[36px] md:w-[40px] transition-colors duration-300 bg-white text-[#8752FA] m-1 md:m-1.5 shrink-0">
<IconArrowRightSmall className="w-4 h-4" />
</div>
</button>
</Link>
</div>
</div>
</div>
</div>
);
}

44
apps/website/app/page.tsx Normal file
View File

@@ -0,0 +1,44 @@
import ElasticRecoil from "@/components/elastic-footer";
import HomeSections from "@/components/home-sections";
import Navbar from "@/components/navbar";
import Preloader from "@/components/preloader";
import type { PageStyle } from "@/lib/types";
export default function Home() {
return (
<div
className="w-full overflow-x-clip"
style={
{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" } as PageStyle
}
>
<Preloader />
<ElasticRecoil>
<div className="relative z-10 bg-[#000000] min-h-screen">
<div className="relative w-full px-4 md:px-(--page-pad) pt-5">
<div className="absolute pointer-events-none top-0 bottom-0 left-4 md:left-(--page-pad) border-l border-[#292929] z-50" />
<Navbar />
<div className="absolute pointer-events-none top-0 bottom-0 right-4 md:right-(--page-pad) border-r border-[#292929] z-50" />
<div className="flex flex-col gap-2.5">
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full hidden md:block" />
<div className="border-t border-[#292929] w-full hidden md:block" />
<div className="border-t border-[#292929] w-full hidden md:block" />
<div className="border-t border-[#292929] w-full hidden md:block" />
</div>
<HomeSections />
<div className="w-full flex-col gap-2.5 mt-10.5 hidden md:flex">
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
</div>
</div>
</div>
</ElasticRecoil>
</div>
);
}

View File

@@ -1,191 +0,0 @@
import AnimatedFooterImage from "@/components/animated-footer-image";
import Footer from "@/components/footer";
import Navbar from "@/components/navbar";
const privacyTerms = [
{
title: "1. Acceptance of Terms",
content:
'By accessing or using the Autumn platform, APIs, SDKs, or any related services (collectively, the "Services") provided by Rebase, Inc., a Delaware corporation ("Autumn," "we," "us," or "our"), you agree to be bound by these Terms of Service ("Terms"). If you do not agree to these Terms, do not use our Services.',
isUppercase: false,
},
{
title: "2. Description of Services",
content:
"Autumn provides billing infrastructure, including usage-based billing, credits management, pricing configuration, and entitlements management for software applications. Our Services are designed for developers and businesses building products that require flexible billing and pricing capabilities.",
isUppercase: false,
},
{
title: "3. Account Registration",
content:
"To use certain features of our Services, you must create an account. You agree to provide accurate, current, and complete information during registration and to update such information as necessary. You are responsible for maintaining the confidentiality of your account credentials and for all activities that occur under your account.",
isUppercase: false,
},
{
title: "4. Acceptable Use",
content:
"You agree not to use the Services for any unlawful purpose or in violation of any applicable laws; (b) interfere with or disrupt the integrity or performance of the Services; (c) attempt to gain unauthorized access to the Services or related systems; (d) use the Services to transmit malicious code or harmful content; or (e) resell or redistribute the Services without our prior written consent.",
isUppercase: false,
},
{
title: "5. Payment Terms",
content:
"Fees for the Services are set forth on our pricing page or in a separate agreement. You agree to pay all applicable fees in accordance with the payment terms. All fees are non-refundable except as expressly stated otherwise. We reserve the right to modify our pricing with 30 days' notice.",
isUppercase: false,
},
{
title: "6. Data and Privacy",
content:
'Your use of the Services is subject to our Privacy Policy. You retain ownership of any data you submit to the Services ("Customer Data"). You grant us a limited license to process Customer Data solely to provide the Services. We implement reasonable security measures to protect Customer Data, but you acknowledge that no system is completely secure.',
isUppercase: false,
},
{
title: "7. Intellectual Property",
content:
"The Services, including all software, APIs, documentation, and related materials, are owned by Autumn and protected by intellectual property laws. We grant you a limited, non-exclusive, non-transferable license to use the Services in accordance with these Terms. You may not copy, modify, or create derivative works of the Services except as expressly permitted.",
isUppercase: false,
},
{
title: "8. Confidentiality",
content:
"Each party agrees to maintain the confidentiality of any non-public information disclosed by the other party and to use such information only for the purposes of these Terms. This obligation does not apply to information that is publicly available, independently developed, or rightfully received from a third party.",
isUppercase: false,
},
{
title: "9. Warranties and Disclaimers",
content:
'THE SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. WE DO NOT WARRANT THAT THE SERVICES WILL BE UNINTERRUPTED OR ERROR-FREE.',
isUppercase: true,
},
{
title: "10. Limitation of Liability",
content:
"TO THE MAXIMUM EXTENT PERMITTED BY LAW, AUTUMN SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS OR REVENUES. OUR TOTAL LIABILITY FOR ANY CLAIMS ARISING FROM THESE TERMS SHALL NOT EXCEED THE AMOUNTS PAID BY YOU TO AUTUMN IN THE TWELVE MONTHS PRECEDING THE CLAIM.",
isUppercase: true,
},
{
title: "11. Indemnification",
content:
"You agree to indemnify, defend, and hold harmless Autumn and its officers, directors, employees, and agents from any claims, liabilities, damages, losses, or expenses arising from your use of the Services, your violation of these Terms, or your infringement of any third-party rights.",
isUppercase: false,
},
{
title: "12. Term and Termination",
content:
"These Terms remain in effect until terminated. Either party may terminate for convenience with 30 days' written notice. We may suspend or terminate your access immediately if you breach these Terms. Upon termination, your right to use the Services ceases, and you must pay any outstanding fees.",
isUppercase: false,
},
{
title: "13. Modifications",
content:
"We may modify these Terms at any time by posting the revised Terms on our website. Material changes will be communicated with at least 30 days' notice. Your continued use of the Services after such changes constitutes acceptance of the modified Terms.",
isUppercase: false,
},
{
title: "14. Governing Law and Disputes",
content:
"These Terms are governed by the laws of the State of Delaware, without regard to conflict of law principles. Any disputes arising from these Terms shall be resolved through binding arbitration in accordance with the rules of the American Arbitration Association, except that either party may seek injunctive relief in any court of competent jurisdiction.",
isUppercase: false,
},
{
title: "15. General Provisions",
content:
"These Terms constitute the entire agreement between you and Autumn regarding the Services. If any provision is found unenforceable, the remaining provisions will continue in effect. Our failure to enforce any right or provision does not constitute a waiver. You may not assign these Terms without our prior written consent.",
isUppercase: false,
},
{
title: "16. Contact Information",
content: (
<>
For questions about these Terms, please contact us at
security@useautumn.com or at:
<br />
Rebase, Inc. (d/b/a Autumn)
<br />
Email: security@useautumn.com
<br />
Website: https://useautumn.com
</>
),
isUppercase: false,
},
];
export default function PrivacyPolicy() {
return (
<div
className="w-full overflow-x-hidden overflow-y-auto bg-[#000000]"
style={{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" }}
>
<div className="relative z-10 bg-[#000000] min-h-screen">
<div className="relative w-full px-4 md:px-(--page-pad) pt-5">
<div className="absolute pointer-events-none top-0 bottom-0 left-4 md:left-(--page-pad) border-l border-[#292929] z-50" />
<div className="absolute pointer-events-none left-0 border-t border-[#292929] w-full" />
<Navbar />
<div className="absolute pointer-events-none left-0 border-b border-[#292929] w-full" />
<div className="absolute pointer-events-none top-0 bottom-0 right-4 md:right-(--page-pad) border-r border-[#292929] z-50" />
<div className="flex flex-col gap-2.5">
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full hidden md:block" />
<div className="border-t border-[#292929] w-full hidden md:block" />
<div className="border-t border-[#292929] w-full hidden md:block" />
<div className="border-t border-[#292929] w-full hidden md:block" />
</div>
<div className="flex w-full flex-col border-b border-[#292929]">
<div className="flex w-full border-b border-[#292929]">
<div className="hidden md:block w-1/8 lg:w-1/6 border-r bg-[#0F0F0F] border-[#292929]" />
<div className="flex-1 bg-[#0F0F0F] px-4 sm:px-8 py-10 md:py-16">
<h1 className="text-white text-[40px] font-sans tracking-[-2%] uppercase">
Terms of Service
</h1>
</div>
<div className="hidden md:block w-1/8 lg:w-1/6 border-l bg-[#0F0F0F] border-[#292929]" />
</div>
<div className="flex w-full">
<div className="hidden md:block w-1/8 lg:w-1/6 border-r border-[#292929]" />
<div className="flex-1 px-4 sm:px-8 py-12 md:py-16 text-white text-[16px] font-light leading-[1.6] tracking-[-2%] font-sans pb-32">
<p className="mb-10">
Autumn (Rebase, Inc.)
<br />
Effective Date: February 1, 2025
</p>
{privacyTerms.map((term, index) => (
<div key={index}>
<h3
className={`text-white font-normal text-[24px] leading-[30px] tracking-[-2%] mb-2 mt-8`}
>
{term.title}
</h3>
<p
className={`mb-6 font-light text-[16px] tracking-[-2%] leading-[20px] md:leading-[24px] ${term.isUppercase ? "uppercase" : ""}`}
>
{term.content}
</p>
</div>
))}
</div>
<div className="hidden md:block w-1/8 lg:w-1/6 border-l border-[#292929]" />
</div>
</div>
<Footer />
</div>
<div className="w-full flex-col gap-2.5 mt-10.5 hidden md:flex mb-4">
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
</div>
</div>
<div className="w-full aspect-390/313 sm:aspect-1440/619" />
<AnimatedFooterImage />
</div>
);
}

View File

@@ -0,0 +1,201 @@
import type { ReactNode } from "react";
import AnimatedFooterImage from "@/components/animated-footer-image";
import Footer from "@/components/footer";
import Navbar from "@/components/navbar";
import type { PageStyle } from "@/lib/types";
import { cn } from "@/lib/utils";
const privacyTerms: Array<{
title: string;
content: ReactNode;
isUppercase: boolean;
}> = [
{
title: "1. Acceptance of Terms",
content:
'By accessing or using the Autumn platform, APIs, SDKs, or any related services (collectively, the "Services") provided by Rebase, Inc., a Delaware corporation ("Autumn," "we," "us," or "our"), you agree to be bound by these Terms of Service ("Terms"). If you do not agree to these Terms, do not use our Services.',
isUppercase: false,
},
{
title: "2. Description of Services",
content:
"Autumn provides billing infrastructure, including usage-based billing, credits management, pricing configuration, and entitlements management for software applications. Our Services are designed for developers and businesses building products that require flexible billing and pricing capabilities.",
isUppercase: false,
},
{
title: "3. Account Registration",
content:
"To use certain features of our Services, you must create an account. You agree to provide accurate, current, and complete information during registration and to update such information as necessary. You are responsible for maintaining the confidentiality of your account credentials and for all activities that occur under your account.",
isUppercase: false,
},
{
title: "4. Acceptable Use",
content:
"You agree not to use the Services for any unlawful purpose or in violation of any applicable laws; (b) interfere with or disrupt the integrity or performance of the Services; (c) attempt to gain unauthorized access to the Services or related systems; (d) use the Services to transmit malicious code or harmful content; or (e) resell or redistribute the Services without our prior written consent.",
isUppercase: false,
},
{
title: "5. Payment Terms",
content:
"Fees for the Services are set forth on our pricing page or in a separate agreement. You agree to pay all applicable fees in accordance with the payment terms. All fees are non-refundable except as expressly stated otherwise. We reserve the right to modify our pricing with 30 days' notice.",
isUppercase: false,
},
{
title: "6. Data and Privacy",
content:
'Your use of the Services is subject to our Privacy Policy. You retain ownership of any data you submit to the Services ("Customer Data"). You grant us a limited license to process Customer Data solely to provide the Services. We implement reasonable security measures to protect Customer Data, but you acknowledge that no system is completely secure.',
isUppercase: false,
},
{
title: "7. Intellectual Property",
content:
"The Services, including all software, APIs, documentation, and related materials, are owned by Autumn and protected by intellectual property laws. We grant you a limited, non-exclusive, non-transferable license to use the Services in accordance with these Terms. You may not copy, modify, or create derivative works of the Services except as expressly permitted.",
isUppercase: false,
},
{
title: "8. Confidentiality",
content:
"Each party agrees to maintain the confidentiality of any non-public information disclosed by the other party and to use such information only for the purposes of these Terms. This obligation does not apply to information that is publicly available, independently developed, or rightfully received from a third party.",
isUppercase: false,
},
{
title: "9. Warranties and Disclaimers",
content:
'THE SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. WE DO NOT WARRANT THAT THE SERVICES WILL BE UNINTERRUPTED OR ERROR-FREE.',
isUppercase: true,
},
{
title: "10. Limitation of Liability",
content:
"TO THE MAXIMUM EXTENT PERMITTED BY LAW, AUTUMN SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS OR REVENUES. OUR TOTAL LIABILITY FOR ANY CLAIMS ARISING FROM THESE TERMS SHALL NOT EXCEED THE AMOUNTS PAID BY YOU TO AUTUMN IN THE TWELVE MONTHS PRECEDING THE CLAIM.",
isUppercase: true,
},
{
title: "11. Indemnification",
content:
"You agree to indemnify, defend, and hold harmless Autumn and its officers, directors, employees, and agents from any claims, liabilities, damages, losses, or expenses arising from your use of the Services, your violation of these Terms, or your infringement of any third-party rights.",
isUppercase: false,
},
{
title: "12. Term and Termination",
content:
"These Terms remain in effect until terminated. Either party may terminate for convenience with 30 days' written notice. We may suspend or terminate your access immediately if you breach these Terms. Upon termination, your right to use the Services ceases, and you must pay any outstanding fees.",
isUppercase: false,
},
{
title: "13. Modifications",
content:
"We may modify these Terms at any time by posting the revised Terms on our website. Material changes will be communicated with at least 30 days' notice. Your continued use of the Services after such changes constitutes acceptance of the modified Terms.",
isUppercase: false,
},
{
title: "14. Governing Law and Disputes",
content:
"These Terms are governed by the laws of the State of Delaware, without regard to conflict of law principles. Any disputes arising from these Terms shall be resolved through binding arbitration in accordance with the rules of the American Arbitration Association, except that either party may seek injunctive relief in any court of competent jurisdiction.",
isUppercase: false,
},
{
title: "15. General Provisions",
content:
"These Terms constitute the entire agreement between you and Autumn regarding the Services. If any provision is found unenforceable, the remaining provisions will continue in effect. Our failure to enforce any right or provision does not constitute a waiver. You may not assign these Terms without our prior written consent.",
isUppercase: false,
},
{
title: "16. Contact Information",
content: (
<>
For questions about these Terms, please contact us at
security@useautumn.com or at:
<br />
Rebase, Inc. (d/b/a Autumn)
<br />
Email: security@useautumn.com
<br />
Website: https://useautumn.com
</>
),
isUppercase: false,
},
];
export default function PrivacyPolicy() {
return (
<div
className="w-full overflow-x-hidden overflow-y-auto bg-[#000000]"
style={
{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" } as PageStyle
}
>
<div className="relative z-10 bg-[#000000] min-h-screen">
<div className="relative w-full px-4 md:px-(--page-pad) pt-5">
<div className="absolute pointer-events-none top-0 bottom-0 left-4 md:left-(--page-pad) border-l border-[#292929] z-50" />
<div className="absolute pointer-events-none left-0 border-t border-[#292929] w-full" />
<Navbar />
<div className="absolute pointer-events-none left-0 border-b border-[#292929] w-full" />
<div className="absolute pointer-events-none top-0 bottom-0 right-4 md:right-(--page-pad) border-r border-[#292929] z-50" />
<div className="flex flex-col gap-2.5">
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full hidden md:block" />
<div className="border-t border-[#292929] w-full hidden md:block" />
<div className="border-t border-[#292929] w-full hidden md:block" />
<div className="border-t border-[#292929] w-full hidden md:block" />
</div>
<div className="flex w-full flex-col border-b border-[#292929]">
<div className="flex w-full border-b border-[#292929]">
<div className="hidden md:block w-1/8 lg:w-1/6 border-r bg-[#0F0F0F] border-[#292929]" />
<div className="flex-1 bg-[#0F0F0F] px-4 sm:px-8 py-10 md:py-16">
<h1 className="text-white text-[40px] font-sans tracking-[-2%] uppercase">
Terms of Service
</h1>
</div>
<div className="hidden md:block w-1/8 lg:w-1/6 border-l bg-[#0F0F0F] border-[#292929]" />
</div>
<div className="flex w-full">
<div className="hidden md:block w-1/8 lg:w-1/6 border-r border-[#292929]" />
<div className="flex-1 px-4 sm:px-8 py-12 md:py-16 text-white text-[16px] font-light leading-[1.6] tracking-[-2%] font-sans pb-32">
<p className="mb-10">
Autumn (Rebase, Inc.)
<br />
Effective Date: February 1, 2025
</p>
{privacyTerms.map((term, index) => (
<div key={index}>
<h3 className="mb-2 mt-8 text-[24px] leading-[30px] font-normal tracking-[-2%] text-white">
{term.title}
</h3>
<p
className={cn(
"mb-6 text-[16px] leading-[20px] font-light tracking-[-2%] md:leading-[24px]",
term.isUppercase && "uppercase",
)}
>
{term.content}
</p>
</div>
))}
</div>
<div className="hidden md:block w-1/8 lg:w-1/6 border-l border-[#292929]" />
</div>
</div>
<Footer />
</div>
<div className="w-full flex-col gap-2.5 mt-10.5 hidden md:flex mb-4">
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
</div>
</div>
<div className="w-full aspect-390/313 sm:aspect-1440/619" />
<AnimatedFooterImage />
</div>
);
}

View File

@@ -1,25 +0,0 @@
"use client";
import Image from "next/image";
import { forwardRef } from "react";
const AnimatedFooterImage = forwardRef(function AnimatedFooterImage(_, ref) {
return (
<div
ref={ref}
className="fixed bottom-0 left-0 w-full z-0 pointer-events-none overflow-hidden h-[420px] md:h-[580px]"
>
<div className="relative w-full h-full">
<Image
src="/images/footer/footer.webp"
alt="footer background"
fill
priority
sizes="100vw"
className="object-cover object-top"
/>
</div>
</div>
);
});
export default AnimatedFooterImage;

View File

@@ -0,0 +1,29 @@
"use client";
import Image from "next/image";
import { forwardRef } from "react";
const AnimatedFooterImage = forwardRef<HTMLDivElement>(
function AnimatedFooterImage(_props, ref) {
return (
<div
ref={ref}
className="fixed bottom-0 left-0 w-full z-0 pointer-events-none overflow-hidden h-[420px] md:h-[580px]"
>
<div className="relative w-full h-full">
<Image
src="/images/footer/footer.webp"
alt="footer background"
fill
priority
sizes="100vw"
className="object-cover object-top"
/>
</div>
</div>
);
},
);
AnimatedFooterImage.displayName = "AnimatedFooterImage";
export default AnimatedFooterImage;

View File

@@ -1,247 +0,0 @@
"use client";
import Image from "next/image";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
import js from "react-syntax-highlighter/dist/esm/languages/hljs/javascript";
// useLayoutEffect runs synchronously before browser paint on the client,
// eliminating the container-query font-size race that causes CLS on reload.
const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? useLayoutEffect : useEffect;
SyntaxHighlighter.registerLanguage("javascript", js);
const autumnTheme = {
hljs: {
display: "block",
background: "transparent",
color: "#BFBFBF",
padding: "0",
margin: "0",
},
"hljs-comment": { color: "#6B6B6B" },
"hljs-keyword": { color: "#9564ff" },
"hljs-built_in": { color: "#BFBFBF" },
"hljs-string": { color: "#2B8C3F" },
"hljs-number": { color: "#9564ff" },
"hljs-literal": { color: "#2B8C3F" },
"hljs-attr": { color: "#FF1F12" },
"hljs-property": { color: "#FF1F12" },
"hljs-variable": { color: "#0161B5" },
"hljs-title": { color: "#BFBFBF" },
"hljs-params": { color: "#0161B5" },
"hljs-punctuation": { color: "#BFBFBF" },
};
const TOTAL_LINES = 21;
const LINE_HEIGHT = 24;
const codeContent = `// Your entire billing integration
const { allowed, remaining } = await
autumn.check({
featureId: "ai_tokens"
});
if (allowed) {
await autumn.track({
featureId: "ai_tokens",
value: 1024
});
}`;
const TYPING_SPEED = 10;
export default function AutumnConfig({
lines = TOTAL_LINES,
initialDelay = 0,
awaitEvent = null,
}) {
const fullCode = (() => {
const lineCount = codeContent.split("\n").length;
return codeContent + "\n".repeat(Math.max(0, lines - lineCount));
})();
const containerRef = useRef(null);
const [fontSize, setFontSize] = useState(16);
const [displayed, setDisplayed] = useState("");
const [cursorVisible, setCursorVisible] = useState(true);
const [awaitDone, setAwaitDone] = useState(!awaitEvent);
const [started, setStarted] = useState(initialDelay === 0 && !awaitEvent);
const done = displayed.length >= fullCode.length;
// Wait for the signal event before starting the delay countdown
useEffect(() => {
if (!awaitEvent) return;
const handler = () => setAwaitDone(true);
window.addEventListener(awaitEvent, handler, { once: true });
return () => window.removeEventListener(awaitEvent, handler);
}, [awaitEvent]);
// Start typing after awaitDone, respecting initialDelay
useEffect(() => {
if (!awaitDone) return;
const t = setTimeout(() => setStarted(true), initialDelay);
return () => clearTimeout(t);
}, [awaitDone, initialDelay]);
useIsomorphicLayoutEffect(() => {
const el = containerRef.current;
if (!el) return;
const measure = () =>
setFontSize(Math.min(Math.max(el.offsetWidth * 0.0385, 10), 20));
measure();
const ro = new ResizeObserver(measure);
ro.observe(el);
return () => ro.disconnect();
}, []);
const displayedPadded =
displayed + "\n".repeat(Math.max(0, lines - displayed.split("\n").length));
useEffect(() => {
if (!started || done) return;
const timer = setTimeout(() => {
setDisplayed(fullCode.slice(0, displayed.length + 1));
}, TYPING_SPEED);
return () => clearTimeout(timer);
}, [displayed, done, started]);
// Blinking cursor after done
useEffect(() => {
if (!done) return;
const interval = setInterval(() => setCursorVisible((v) => !v), 530);
return () => clearInterval(interval);
}, [done]);
return (
<div
ref={containerRef}
className="@container w-full max-w-[520px] border border-[#2A2A2A] bg-[#000000]/90"
>
{/* Title bar */}
{/* <div className="flex items-center justify-between border-b border-[#2A2A2A] px-4 py-0.5 gap-3 w-full">
<div className="flex-1 min-w-0 overflow-hidden flex justify-end items-center h-[21px]">
<Image
src="/images/hero/box.svg"
width={186}
height={21}
alt="Box"
style={{ width: "auto", height: "auto" }}
className="max-w-none w-[186px] h-[21px] object-none object-right shrink-0"
/>
</div>
<span className="font-mono text-[17.5px] text-[#FFFFFF99] whitespace-nowrap pt-0.5 shrink-0 mx-1">
autumn.config.ts
</span>
<div className="flex-1 min-w-0 overflow-hidden flex justify-start items-center h-[21px]">
<Image
src="/images/hero/box.svg"
width={186}
height={21}
alt="Box"
style={{ width: "auto", height: "auto" }}
className="w-[186px] h-[21px] object-none object-left"
/>
</div>
<span className="ml-1 border border-[#292929] cursor-pointer select-none flex items-center justify-center shrink-0 p-1.5">
<img
src="/images/hero/cross.svg"
width={11}
height={11}
alt="cross"
/>
</span>
</div> */}
<div className="flex items-center justify-between border-b border-[#292929] px-2 py-1.5 w-full bg-[#000000]">
{/* Left Vent: flex-1 makes it stretch, min-w-0 allows it to shrink below its content if needed */}
<div className="flex-1 min-w-[10px] h-[22px] border border-[#292929] flex flex-col justify-evenly px-[2px]">
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
</div>
{/* Filename: shrink-0 ensures the text never gets squashed */}
<span className="font-mono text-[18px] text-[#FFFFFF99] whitespace-nowrap shrink-0 px-2">
billing.ts
</span>
{/* Right Vent: Matches the left one */}
<div className="flex-1 min-w-[10px] h-[22px] border border-[#2A2A2A] flex flex-col justify-evenly px-[2px]">
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
</div>
{/* Close Button Container */}
<div className="w-[24px] ml-2 h-[24px] border border-[#2A2A2A] flex items-center justify-center shrink-0 cursor-pointer hover:bg-white/5 transition-colors">
<Image
src="/images/hero/cross.svg"
width={11}
height={11}
alt="Box"
style={{ width: "auto", height: "auto" }}
className="w-full h-[11px] object-fill object-left"
/>
</div>
</div>
{/* Code area — fluidly driven by container query relative font sizes so height never distorts */}
<div className="relative px-4 py-3 font-mono text-sm overflow-hidden">
{/* Dynamic-height inner box: 20 lines × 1.25em */}
<div
style={{
height: `${lines * LINE_HEIGHT}px`,
overflow: "hidden",
fontSize: `${fontSize}px`,
}}
>
<SyntaxHighlighter
language="javascript"
style={autumnTheme}
showLineNumbers
lineNumberStyle={{
color: "#fff",
minWidth: "2rem",
paddingRight: "1rem",
userSelect: "none",
}}
customStyle={{
background: "transparent",
padding: 0,
margin: 0,
fontSize: "inherit",
lineHeight: `${LINE_HEIGHT}px`,
fontWeight: "300",
overflow: "hidden",
}}
>
{displayedPadded}
</SyntaxHighlighter>
</div>
<div style={{ height: 0, overflow: "visible", position: "relative" }}>
<span
className="absolute -top-3.5 left-14 w-0.5 h-3.5 bg-[#9564ff]"
style={{
opacity: cursorVisible ? 1 : 0,
transition: "opacity 0.1s",
}}
/>
</div>
<div className="pointer-events-none absolute bottom-0 left-0 right-0 h-28 bg-linear-to-t from-[#000000] via-[#000000]/70 to-transparent z-10" />
<div className="px-1 sm:px-2">
<div className="absolute left-2 right-2 sm:left-3 sm:right-3 bottom-[10px] flex items-center justify-between border border-[#9564ff] bg-[#20143C] px-2 sm:px-4 py-2 sm:py-2.5 font-mono text-[10px] sm:text-sm shadow-[0_4px_20px_rgba(149,100,255,0.1)] z-20">
<div className="flex items-center gap-1 sm:gap-2">
<span className="text-[#959494]">allowed:</span>
<span className="text-[#2B8C3F]">true</span>
<span className="text-[#959494] ml-0.5 sm:ml-0">remaining:</span>
<span className="text-[#9564ff]">8976</span>
</div>
<span className="text-[#9564ff] ml-1">92ms</span>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,261 @@
"use client";
import Image from "next/image";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { Light as RawSyntaxHighlighter } from "react-syntax-highlighter";
import js from "react-syntax-highlighter/dist/esm/languages/hljs/javascript";
const SyntaxHighlighter = RawSyntaxHighlighter as unknown as ((props: {
children: string;
customStyle?: Record<string, string | number>;
language: string;
lineNumberStyle?: Record<string, string | number>;
showLineNumbers?: boolean;
style?: Record<string, Record<string, string | number>>;
}) => JSX.Element) & {
registerLanguage: (name: string, language: unknown) => void;
};
// useLayoutEffect runs synchronously before browser paint on the client,
// eliminating the container-query font-size race that causes CLS on reload.
const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? useLayoutEffect : useEffect;
SyntaxHighlighter.registerLanguage("javascript", js);
const autumnTheme = {
hljs: {
display: "block",
background: "transparent",
color: "#BFBFBF",
padding: "0",
margin: "0",
},
"hljs-comment": { color: "#6B6B6B" },
"hljs-keyword": { color: "#9564ff" },
"hljs-built_in": { color: "#BFBFBF" },
"hljs-string": { color: "#2B8C3F" },
"hljs-number": { color: "#9564ff" },
"hljs-literal": { color: "#2B8C3F" },
"hljs-attr": { color: "#FF1F12" },
"hljs-property": { color: "#FF1F12" },
"hljs-variable": { color: "#0161B5" },
"hljs-title": { color: "#BFBFBF" },
"hljs-params": { color: "#0161B5" },
"hljs-punctuation": { color: "#BFBFBF" },
};
const TOTAL_LINES = 16;
const LINE_HEIGHT = 24;
const codeContent = `// Your entire billing integration
const { allowed } = await check({
featureId: "ai_tokens"
});
if (allowed) {
await track({
featureId: "ai_tokens",
value: 1024
});
}`;
const TYPING_SPEED = 10;
export default function AutumnConfig({
lines = TOTAL_LINES,
initialDelay = 0,
awaitEvent = null,
}: {
awaitEvent?: string | null;
initialDelay?: number;
lines?: number;
}) {
const fullCode = (() => {
const lineCount = codeContent.split("\n").length;
return codeContent + "\n".repeat(Math.max(0, lines - lineCount));
})();
const containerRef = useRef<HTMLDivElement | null>(null);
const [fontSize, setFontSize] = useState(16);
const [displayed, setDisplayed] = useState("");
const [cursorVisible, setCursorVisible] = useState(true);
const [awaitDone, setAwaitDone] = useState(!awaitEvent);
const [started, setStarted] = useState(initialDelay === 0 && !awaitEvent);
const done = displayed.length >= fullCode.length;
// Wait for the signal event before starting the delay countdown
useEffect(() => {
if (!awaitEvent) return;
const handler = () => setAwaitDone(true);
window.addEventListener(awaitEvent, handler, { once: true });
return () => window.removeEventListener(awaitEvent, handler);
}, [awaitEvent]);
// Start typing after awaitDone, respecting initialDelay
useEffect(() => {
if (!awaitDone) return;
const t = setTimeout(() => setStarted(true), initialDelay);
return () => clearTimeout(t);
}, [awaitDone, initialDelay]);
useIsomorphicLayoutEffect(() => {
const el = containerRef.current;
if (!el) return;
const measure = () =>
setFontSize(Math.min(Math.max(el.offsetWidth * 0.0385, 10), 20));
measure();
const ro = new ResizeObserver(measure);
ro.observe(el);
return () => ro.disconnect();
}, []);
const displayedPadded =
displayed + "\n".repeat(Math.max(0, lines - displayed.split("\n").length));
useEffect(() => {
if (!started || done) return;
const timer = setTimeout(() => {
setDisplayed(fullCode.slice(0, displayed.length + 1));
}, TYPING_SPEED);
return () => clearTimeout(timer);
}, [displayed, done, started, fullCode]);
// Blinking cursor after done
useEffect(() => {
if (!done) return;
const interval = setInterval(() => setCursorVisible((v) => !v), 530);
return () => clearInterval(interval);
}, [done]);
return (
<div
ref={containerRef}
className="@container w-full max-w-[520px] border border-[#2A2A2A] bg-[#000000]/90"
>
{/* Title bar */}
{/* <div className="flex items-center justify-between border-b border-[#2A2A2A] px-4 py-0.5 gap-3 w-full">
<div className="flex-1 min-w-0 overflow-hidden flex justify-end items-center h-[21px]">
<Image
src="/images/hero/box.svg"
width={186}
height={21}
alt="Box"
style={{ width: "auto", height: "auto" }}
className="max-w-none w-[186px] h-[21px] object-none object-right shrink-0"
/>
</div>
<span className="font-mono text-[17.5px] text-[#FFFFFF99] whitespace-nowrap pt-0.5 shrink-0 mx-1">
autumn.config.ts
</span>
<div className="flex-1 min-w-0 overflow-hidden flex justify-start items-center h-[21px]">
<Image
src="/images/hero/box.svg"
width={186}
height={21}
alt="Box"
style={{ width: "auto", height: "auto" }}
className="w-[186px] h-[21px] object-none object-left"
/>
</div>
<span className="ml-1 border border-[#292929] cursor-pointer select-none flex items-center justify-center shrink-0 p-1.5">
<img
src="/images/hero/cross.svg"
width={11}
height={11}
alt="cross"
/>
</span>
</div> */}
<div className="flex items-center justify-between border-b border-[#292929] px-2 py-1.5 w-full bg-[#000000]">
{/* Left Vent: flex-1 makes it stretch, min-w-0 allows it to shrink below its content if needed */}
<div className="flex-1 min-w-[10px] h-[22px] border border-[#292929] flex flex-col justify-evenly px-[2px]">
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
</div>
{/* Filename: shrink-0 ensures the text never gets squashed */}
<span className="font-mono text-[18px] text-[#FFFFFF99] whitespace-nowrap shrink-0 px-2">
billing.ts
</span>
{/* Right Vent: Matches the left one */}
<div className="flex-1 min-w-[10px] h-[22px] border border-[#2A2A2A] flex flex-col justify-evenly px-[2px]">
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
</div>
{/* Close Button Container */}
<div className="w-[24px] ml-2 h-[24px] border border-[#2A2A2A] flex items-center justify-center shrink-0 cursor-pointer hover:bg-white/5 transition-colors">
<Image
src="/images/hero/cross.svg"
width={11}
height={11}
alt="Box"
style={{ width: "auto", height: "auto" }}
className="w-full h-[11px] object-fill object-left"
/>
</div>
</div>
{/* Code area — fluidly driven by container query relative font sizes so height never distorts */}
<div className="relative px-4 py-3 font-mono text-sm overflow-hidden">
{/* Dynamic-height inner box: 20 lines × 1.25em */}
<div
style={{
height: `${lines * LINE_HEIGHT}px`,
overflow: "hidden",
fontSize: `${fontSize}px`,
}}
>
<SyntaxHighlighter
language="javascript"
style={autumnTheme}
showLineNumbers
lineNumberStyle={{
color: "#fff",
minWidth: "2rem",
paddingRight: "1rem",
userSelect: "none",
}}
customStyle={{
background: "transparent",
padding: 0,
margin: 0,
fontSize: "inherit",
lineHeight: `${LINE_HEIGHT}px`,
fontWeight: "300",
overflow: "hidden",
}}
>
{displayedPadded}
</SyntaxHighlighter>
</div>
<div style={{ height: 0, overflow: "visible", position: "relative" }}>
<span
className="absolute -top-3.5 left-14 w-0.5 h-3.5 bg-[#9564ff]"
style={{
opacity: cursorVisible ? 1 : 0,
transition: "opacity 0.1s",
}}
/>
</div>
<div className="pointer-events-none absolute bottom-0 left-0 right-0 h-28 bg-linear-to-t from-[#000000] via-[#000000]/70 to-transparent z-10" />
<div className="px-1 sm:px-2">
<div className="absolute left-2 right-2 sm:left-3 sm:right-3 bottom-[10px] flex items-center justify-between border border-[#9564ff] bg-[#20143C] px-2 sm:px-4 py-2 sm:py-2.5 font-mono text-[10px] sm:text-sm shadow-[0_4px_20px_rgba(149,100,255,0.1)] z-20">
<div className="flex items-center gap-1 sm:gap-2">
<span className="text-[#959494]">allowed:</span>
<span className="text-[#2B8C3F]">true</span>
<span className="text-[#959494] ml-0.5 sm:ml-0">remaining:</span>
<span className="text-[#9564ff]">8976</span>
</div>
<span className="text-[#9564ff] ml-1">92ms</span>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,89 +0,0 @@
import Link from "next/link";
function BlogHeading({ as: Tag, children, ...props }) {
return (
<Tag {...props} className="scroll-mt-24">
{children}
</Tag>
);
}
export const mdxComponents = {
h1: (props) => <BlogHeading as="h1" {...props} />,
h2: (props) => <BlogHeading as="h2" {...props} />,
h3: (props) => <BlogHeading as="h3" {...props} />,
h4: (props) => <BlogHeading as="h4" {...props} />,
a: ({ href, children, ...props }) => {
const isExternal = href?.startsWith("http");
if (isExternal) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-[#9564ff] hover:text-[#b08aff] underline underline-offset-2 transition-colors"
{...props}
>
{children}
</a>
);
}
return (
<Link
href={href || "#"}
className="text-[#9564ff] hover:text-[#b08aff] underline underline-offset-2 transition-colors"
{...props}
>
{children}
</Link>
);
},
pre: ({ children, ...props }) => (
<pre
className="rounded-lg border border-[#292929] bg-[#141414] p-4 overflow-x-auto text-sm leading-relaxed"
{...props}
>
{children}
</pre>
),
code: ({ children, ...props }) => {
const isInline = typeof children === "string";
if (isInline && !props.className) {
return (
<code className="rounded bg-[#1c1c1c] border border-[#292929] px-1.5 py-0.5 text-[0.875em] text-[#e0e0e0] font-mono">
{children}
</code>
);
}
return <code {...props}>{children}</code>;
},
blockquote: ({ children, ...props }) => (
<blockquote
className="border-l-2 border-[#9564ff] pl-4 italic text-[#FFFFFF99]"
{...props}
>
{children}
</blockquote>
),
hr: (props) => <hr className="border-[#292929] my-8" {...props} />,
table: ({ children, ...props }) => (
<div className="overflow-x-auto my-6">
<table className="w-full border-collapse text-sm" {...props}>
{children}
</table>
</div>
),
th: ({ children, ...props }) => (
<th
className="border border-[#292929] bg-[#141414] px-4 py-2 text-left font-medium text-white"
{...props}
>
{children}
</th>
),
td: ({ children, ...props }) => (
<td className="border border-[#292929] px-4 py-2" {...props}>
{children}
</td>
),
};

View File

@@ -0,0 +1,110 @@
import Link from "next/link";
import type { ComponentPropsWithoutRef, ElementType, ReactNode } from "react";
function BlogHeading({
as: Tag,
children,
...props
}: {
as: ElementType;
children?: ReactNode;
} & ComponentPropsWithoutRef<"h1">) {
return (
<Tag {...props} className="scroll-mt-24">
{children}
</Tag>
);
}
export const mdxComponents = {
h1: (props: ComponentPropsWithoutRef<"h1">) => (
<BlogHeading as="h1" {...props} />
),
h2: (props: ComponentPropsWithoutRef<"h2">) => (
<BlogHeading as="h2" {...props} />
),
h3: (props: ComponentPropsWithoutRef<"h3">) => (
<BlogHeading as="h3" {...props} />
),
h4: (props: ComponentPropsWithoutRef<"h4">) => (
<BlogHeading as="h4" {...props} />
),
a: ({ href, children, ...props }: ComponentPropsWithoutRef<"a">) => {
const isExternal = href?.startsWith("http");
if (isExternal) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-[#9564ff] hover:text-[#b08aff] underline underline-offset-2 transition-colors"
{...props}
>
{children}
</a>
);
}
return (
<Link
href={href || "#"}
className="text-[#9564ff] hover:text-[#b08aff] underline underline-offset-2 transition-colors"
{...props}
>
{children}
</Link>
);
},
pre: ({ children, ...props }: ComponentPropsWithoutRef<"pre">) => (
<pre
className="rounded-lg border border-[#292929] bg-[#141414] p-4 overflow-x-auto text-sm leading-relaxed"
{...props}
>
{children}
</pre>
),
code: ({ children, ...props }: ComponentPropsWithoutRef<"code">) => {
const isInline = typeof children === "string";
if (isInline && !props.className) {
return (
<code className="rounded bg-[#1c1c1c] border border-[#292929] px-1.5 py-0.5 text-[0.875em] text-[#e0e0e0] font-mono">
{children}
</code>
);
}
return <code {...props}>{children}</code>;
},
blockquote: ({
children,
...props
}: ComponentPropsWithoutRef<"blockquote">) => (
<blockquote
className="border-l-2 border-[#9564ff] pl-4 italic text-[#FFFFFF99]"
{...props}
>
{children}
</blockquote>
),
hr: (props: ComponentPropsWithoutRef<"hr">) => (
<hr className="border-[#292929] my-8" {...props} />
),
table: ({ children, ...props }: ComponentPropsWithoutRef<"table">) => (
<div className="overflow-x-auto my-6">
<table className="w-full border-collapse text-sm" {...props}>
{children}
</table>
</div>
),
th: ({ children, ...props }: ComponentPropsWithoutRef<"th">) => (
<th
className="border border-[#292929] bg-[#141414] px-4 py-2 text-left font-medium text-white"
{...props}
>
{children}
</th>
),
td: ({ children, ...props }: ComponentPropsWithoutRef<"td">) => (
<td className="border border-[#292929] px-4 py-2" {...props}>
{children}
</td>
),
};

View File

@@ -1,65 +0,0 @@
"use client";
import { forwardRef, useImperativeHandle, useRef, useEffect } from "react";
import gsap from "gsap";
export const DashboardIconPixel = forwardRef(function DashboardIconPixel(
{ Icon },
ref,
) {
const iconRef = useRef(null);
const tlRef = useRef(null);
useImperativeHandle(ref, () => ({
restart: () => tlRef.current?.play(),
reverse: () => tlRef.current?.reverse(),
}));
useEffect(() => {
const pixelEls = iconRef.current?.querySelectorAll(".icon-pixel-path");
if (!pixelEls?.length) return;
// Sort pixels by visual position: bottom-left → top-right diagonal
const pixels = Array.from(pixelEls).sort((a, b) => {
const aBox = a.getBBox();
const bBox = b.getBBox();
return (
aBox.x +
aBox.width / 2 -
(aBox.y + aBox.height / 2) -
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
);
});
gsap.set(pixels, {
opacity: 0.15,
scale: 0.8,
transformOrigin: "left bottom",
fill: "currentColor",
});
tlRef.current = gsap.timeline({ paused: true });
tlRef.current
.to(pixels, {
opacity: 1,
scale: 1.15,
fill: "#FFFFFF",
duration: 0.2,
stagger: 0.01,
ease: "power2.out",
})
.to(pixels, {
scale: 1,
duration: 0.01,
ease: "back.out(3)",
});
return () => tlRef.current?.kill();
}, []);
return (
<span className="relative inline-flex items-center justify-center w-[14px] h-[14px]">
<Icon ref={iconRef} className="w-full h-full text-white" />
</span>
);
});

View File

@@ -0,0 +1,70 @@
"use client";
import gsap from "gsap";
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
import type { PixelHoverHandle, PixelIconComponent } from "@/lib/types";
export const DashboardIconPixel = forwardRef<
PixelHoverHandle,
{ Icon: PixelIconComponent }
>(function DashboardIconPixel({ Icon }, ref) {
const iconRef = useRef<SVGSVGElement | null>(null);
const tlRef = useRef<gsap.core.Timeline | null>(null);
useImperativeHandle(ref, () => ({
restart: () => tlRef.current?.play(),
reverse: () => tlRef.current?.reverse(),
}));
useEffect(() => {
const pixelEls =
iconRef.current?.querySelectorAll<SVGGraphicsElement>(".icon-pixel-path");
if (!pixelEls?.length) return;
// Sort pixels by visual position: bottom-left → top-right diagonal
const pixels = Array.from(pixelEls).sort((a, b) => {
const aBox = a.getBBox();
const bBox = b.getBBox();
return (
aBox.x +
aBox.width / 2 -
(aBox.y + aBox.height / 2) -
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
);
});
gsap.set(pixels, {
opacity: 0.15,
scale: 0.8,
transformOrigin: "left bottom",
fill: "currentColor",
});
tlRef.current = gsap.timeline({ paused: true });
tlRef.current
.to(pixels, {
opacity: 1,
scale: 1.15,
fill: "#FFFFFF",
duration: 0.2,
stagger: 0.01,
ease: "power2.out",
})
.to(pixels, {
scale: 1,
duration: 0.01,
ease: "back.out(3)",
});
return () => {
tlRef.current?.kill();
};
}, []);
return (
<span className="relative inline-flex items-center justify-center w-[14px] h-[14px]">
<Icon ref={iconRef} className="w-full h-full text-white" />
</span>
);
});
DashboardIconPixel.displayName = "DashboardIconPixel";

View File

@@ -1,143 +0,0 @@
"use client";
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
import { useEffect, useState } from "react";
import AnimatedFooterImage from "./animated-footer-image";
export default function ElasticRecoil({ children }) {
const liftAmount = useMotionValue(0);
const [showFooter, setShowFooter] = useState(false);
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const check = () => setIsMobile(window.innerWidth < 768);
check();
window.addEventListener("resize", check);
return () => window.removeEventListener("resize", check);
}, []);
const desktopSpring = { stiffness: 200, damping: 15, mass: 0.5 };
const mobileSpring = { stiffness: 800, damping: 60, mass: 0.2 };
const animatedLift = useSpring(
liftAmount,
isMobile ? mobileSpring : desktopSpring,
);
const cappedMax = isMobile ? -420 : -580;
const y = useTransform(animatedLift, [0, 400], [0, cappedMax]);
useEffect(() => {
return animatedLift.on("change", (v) => {
setShowFooter(v > 1);
});
}, [animatedLift]);
useEffect(() => {
let timeout;
let recoilFired = false;
let isTouching = false; // Track if finger is on screen
const triggerRebound = () => {
liftAmount.set(0);
if (!recoilFired) {
recoilFired = true;
window.dispatchEvent(new CustomEvent("elastic-recoil"));
}
};
const handleWheel = (e) => {
const isAtBottom =
window.innerHeight + window.pageYOffset >=
document.documentElement.scrollHeight - 5;
if (isAtBottom && e.deltaY > 0) {
const normalizedDelta =
e.deltaMode === 1
? e.deltaY * 20
: e.deltaMode === 2
? e.deltaY * 300
: e.deltaY;
liftAmount.set(Math.min(liftAmount.get() + normalizedDelta * 0.5, 400));
recoilFired = false;
clearTimeout(timeout);
timeout = setTimeout(() => {
if (!isTouching) {
triggerRebound();
}
}, 1500);
} else if (e.deltaY < 0) {
if (!isTouching) {
liftAmount.set(0);
recoilFired = false;
}
}
};
let lastTouchY = 0;
let atBottomOnStart = false;
const handleTouchStart = (e) => {
isTouching = true;
lastTouchY = e.touches[0].clientY;
atBottomOnStart =
window.innerHeight + window.pageYOffset >=
document.documentElement.scrollHeight - 5;
recoilFired = false;
clearTimeout(timeout);
};
const handleTouchMove = (e) => {
const currentY = e.touches[0].clientY;
const delta = lastTouchY - currentY;
lastTouchY = currentY;
if (!atBottomOnStart) {
atBottomOnStart =
window.innerHeight + window.pageYOffset >=
document.documentElement.scrollHeight - 5;
return;
}
if (delta > 0) {
liftAmount.set(Math.min(liftAmount.get() + delta * 4, 400));
recoilFired = false;
} else if (liftAmount.get() > 0) {
liftAmount.set(Math.max(0, liftAmount.get() + delta * 4));
}
};
const handleTouchEnd = () => {
isTouching = false;
atBottomOnStart = false;
if (liftAmount.get() > 0) {
clearTimeout(timeout);
timeout = setTimeout(() => {
triggerRebound();
}, 1500);
}
};
window.addEventListener("wheel", handleWheel, { passive: true });
window.addEventListener("touchstart", handleTouchStart, { passive: true });
window.addEventListener("touchmove", handleTouchMove, { passive: true });
window.addEventListener("touchend", handleTouchEnd, { passive: true });
window.addEventListener("touchcancel", handleTouchEnd, { passive: true });
return () => {
clearTimeout(timeout);
window.removeEventListener("wheel", handleWheel);
window.removeEventListener("touchstart", handleTouchStart);
window.removeEventListener("touchmove", handleTouchMove);
window.removeEventListener("touchend", handleTouchEnd);
window.removeEventListener("touchcancel", handleTouchEnd);
};
}, [liftAmount]);
return (
<div className="relative w-full overflow-hidden">
{showFooter && <AnimatedFooterImage />}
<motion.div style={{ y }} className="relative z-10 bg-black">
{children}
</motion.div>
</div>
);
}

View File

@@ -0,0 +1,144 @@
"use client";
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
import { useEffect, useState } from "react";
import type { LayoutProps } from "@/lib/types";
import AnimatedFooterImage from "./animated-footer-image";
export default function ElasticRecoil({ children }: LayoutProps) {
const liftAmount = useMotionValue(0);
const [showFooter, setShowFooter] = useState(false);
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const check = () => setIsMobile(window.innerWidth < 768);
check();
window.addEventListener("resize", check);
return () => window.removeEventListener("resize", check);
}, []);
const desktopSpring = { stiffness: 200, damping: 15, mass: 0.5 };
const mobileSpring = { stiffness: 800, damping: 60, mass: 0.2 };
const animatedLift = useSpring(
liftAmount,
isMobile ? mobileSpring : desktopSpring,
);
const cappedMax = isMobile ? -420 : -580;
const y = useTransform(animatedLift, [0, 400], [0, cappedMax]);
useEffect(() => {
return animatedLift.on("change", (v) => {
setShowFooter(v > 1);
});
}, [animatedLift]);
useEffect(() => {
let timeout: ReturnType<typeof setTimeout> | null = null;
let recoilFired = false;
let isTouching = false;
const triggerRebound = () => {
liftAmount.set(0);
if (!recoilFired) {
recoilFired = true;
window.dispatchEvent(new CustomEvent("elastic-recoil"));
}
};
const handleWheel = (e: WheelEvent) => {
const isAtBottom =
window.innerHeight + window.pageYOffset >=
document.documentElement.scrollHeight - 5;
if (isAtBottom && e.deltaY > 0) {
const normalizedDelta =
e.deltaMode === 1
? e.deltaY * 20
: e.deltaMode === 2
? e.deltaY * 300
: e.deltaY;
liftAmount.set(Math.min(liftAmount.get() + normalizedDelta * 0.5, 400));
recoilFired = false;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
if (!isTouching) {
triggerRebound();
}
}, 1500);
} else if (e.deltaY < 0) {
if (!isTouching) {
liftAmount.set(0);
recoilFired = false;
}
}
};
let lastTouchY = 0;
let atBottomOnStart = false;
const handleTouchStart = (e: TouchEvent) => {
isTouching = true;
lastTouchY = e.touches[0].clientY;
atBottomOnStart =
window.innerHeight + window.pageYOffset >=
document.documentElement.scrollHeight - 5;
recoilFired = false;
if (timeout) clearTimeout(timeout);
};
const handleTouchMove = (e: TouchEvent) => {
const currentY = e.touches[0].clientY;
const delta = lastTouchY - currentY;
lastTouchY = currentY;
if (!atBottomOnStart) {
atBottomOnStart =
window.innerHeight + window.pageYOffset >=
document.documentElement.scrollHeight - 5;
return;
}
if (delta > 0) {
liftAmount.set(Math.min(liftAmount.get() + delta * 4, 400));
recoilFired = false;
} else if (liftAmount.get() > 0) {
liftAmount.set(Math.max(0, liftAmount.get() + delta * 4));
}
};
const handleTouchEnd = () => {
isTouching = false;
atBottomOnStart = false;
if (liftAmount.get() > 0) {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
triggerRebound();
}, 1500);
}
};
window.addEventListener("wheel", handleWheel, { passive: true });
window.addEventListener("touchstart", handleTouchStart, { passive: true });
window.addEventListener("touchmove", handleTouchMove, { passive: true });
window.addEventListener("touchend", handleTouchEnd, { passive: true });
window.addEventListener("touchcancel", handleTouchEnd, { passive: true });
return () => {
if (timeout) clearTimeout(timeout);
window.removeEventListener("wheel", handleWheel);
window.removeEventListener("touchstart", handleTouchStart);
window.removeEventListener("touchmove", handleTouchMove);
window.removeEventListener("touchend", handleTouchEnd);
window.removeEventListener("touchcancel", handleTouchEnd);
};
}, [liftAmount]);
return (
<div className="relative w-full overflow-hidden">
{showFooter && <AnimatedFooterImage />}
<motion.div style={{ y }} className="relative z-10 bg-black">
{children}
</motion.div>
</div>
);
}

View File

@@ -1,147 +0,0 @@
"use client";
import { faqData, AnimatedPlusMinus } from "@/app/constant";
import Image from "next/image";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
export default function FAQ() {
const [openId, setOpenId] = useState(3);
const springConfig = {
type: "spring",
stiffness: 280,
damping: 32,
mass: 1,
restDelta: 0.01,
};
const toggleAccordion = (id) => {
setOpenId(openId === id ? null : id);
};
return (
<section
className="bg-[#000000] text-white overflow-hidden relative border-b border-[#292929]"
style={{
width: "calc(100% + var(--page-pad) * 2)",
marginLeft: "calc(var(--page-pad) * -1)",
paddingLeft: "var(--page-pad)",
paddingRight: "var(--page-pad)",
}}
>
<div className="grid grid-cols-1 lg:grid-cols-2 w-full min-h-[500px]">
<div className="lg:border-r border-b border-[#292929] pl-4 md:pl-4 xl:pl-[90px] pr-6 lg:pr-12 py-12 md:py-[60px] flex flex-col justify-center">
<h2 className="text-[30px] md:text-[40px] font-normal tracking-[-2%] leading-[1.1]">
<span className="text-[#FFFFFF99] font-light">
Frequently Asked
</span>
<br />
<span className="text-white font-normal">Questions</span>
</h2>
</div>
<div className="hidden lg:block border-b border-[#292929] h-full w-full"></div>
<div className="hidden lg:block lg:border-r border-[#292929] h-full w-full relative z-0">
<div className="absolute bottom-0 w-full h-full pb-32"></div>
</div>
<div className="flex flex-col h-full w-full relative z-10 ">
{faqData.map((faq) => {
const isOpen = openId === faq.id;
return (
<div
key={faq.id}
onClick={() => toggleAccordion(faq.id)}
className={`group relative border-b last:border-b-0 border-[#292929] cursor-pointer w-full flex flex-col justify-center transition-colors duration-300 ${isOpen ? "" : "hover:bg-[#080808]"}`}
>
<div
className={`absolute inset-0 z-0 overflow-hidden pointer-events-none transition-opacity duration-500 ${
isOpen ? "opacity-100" : "opacity-0"
}`}
>
<div className="absolute inset-0 bg-white/[0.03] md:group-hover:block hidden" />
<img
src="/images/pricing/FAQ/faqbg.svg"
alt="faq background"
loading="lazy"
className="absolute right-0 top-0 w-full h-[400px] md:h-full object-contain object-top-right md:object-cover md:object-right border-none opacity-60"
/>
<div className="absolute inset-0 bg-linear-to-r from-[#351B6D]/90 via-[#351B6D]/80 to-black/75 mix-blend-normal" />
</div>
<div className="relative z-10 px-4.5 md:px-4 lg:px-[20px] py-[30px]">
<div className="flex items-center justify-between gap-4">
<h3
className={`text-base lg:text-[18px] tracking-[-2%] transition-all duration-400 ${
isOpen
? "text-white font-normal"
: "text-[#FFFFFF66] font-light md:group-hover:text-white"
}`}
>
{faq.question}
</h3>
<div className="shrink-0 overflow-hidden">
<AnimatedPlusMinus
isOpen={isOpen}
className={`w-5 h-5 transition-colors duration-400 ${
isOpen
? "text-white"
: "text-[#FFFFFF66] md:group-hover:text-white"
}`}
/>
</div>
</div>
<div
className={`grid transition-all duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] ${
isOpen
? "grid-rows-[1fr] opacity-100"
: "grid-rows-[0fr] opacity-0"
}`}
>
<div className="overflow-hidden">
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
initial={{ y: -15, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -10, opacity: 0 }}
transition={{
height: springConfig,
opacity: { duration: 0.25 },
y: springConfig,
}}
className="pt-5 text-[#ffffff] leading-[16px] md:leading-[20px] font-light text-[12px] md:text-[14px] max-w-[85%] flex flex-col gap-3.5 tracking-[-0.5%]"
>
{faq.answer.split("\n\n").map((paragraph, idx) => (
<p key={idx}>{paragraph}</p>
))}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
</div>
);
})}
<div
className="relative z-0 px-8 lg:px-[20px] py-[30px] pointer-events-none opacity-0 select-none"
aria-hidden="true"
>
<div className="flex items-center justify-between gap-4">
<h3 className="text-base lg:text-[18px] tracking-[-2%] text-transparent select-none">
&nbsp;
</h3>
<div className="shrink-0 w-5 h-5"></div>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,154 @@
"use client";
import { AnimatePresence, motion } from "framer-motion";
import { useState } from "react";
import { AnimatedPlusMinus, faqData } from "@/app/constant";
import { cn } from "@/lib/utils";
export default function FAQ() {
const [openId, setOpenId] = useState<number | null>(3);
const springConfig = {
type: "spring" as const,
stiffness: 280,
damping: 32,
mass: 1,
restDelta: 0.01,
};
const toggleAccordion = (id: number) => {
setOpenId(openId === id ? null : id);
};
return (
<section
className="bg-[#000000] text-white overflow-hidden relative border-b border-[#292929]"
style={{
width: "calc(100% + var(--page-pad) * 2)",
marginLeft: "calc(var(--page-pad) * -1)",
paddingLeft: "var(--page-pad)",
paddingRight: "var(--page-pad)",
}}
>
<div className="grid grid-cols-1 lg:grid-cols-2 w-full min-h-[500px]">
<div className="lg:border-r border-b border-[#292929] pl-4 md:pl-4 xl:pl-[90px] pr-6 lg:pr-12 py-12 md:py-[60px] flex flex-col justify-center">
<h2 className="text-[30px] md:text-[40px] font-normal tracking-[-2%] leading-[1.1]">
<span className="text-[#FFFFFF99] font-light">
Frequently Asked
</span>
<br />
<span className="text-white font-normal">Questions</span>
</h2>
</div>
<div className="hidden lg:block border-b border-[#292929] h-full w-full"></div>
<div className="hidden lg:block lg:border-r border-[#292929] h-full w-full relative z-0">
<div className="absolute bottom-0 w-full h-full pb-32"></div>
</div>
<div className="flex flex-col h-full w-full relative z-10 ">
{faqData.map((faq) => {
const isOpen = openId === faq.id;
return (
<div
key={faq.id}
onClick={() => toggleAccordion(faq.id)}
className={cn(
"group relative flex w-full cursor-pointer flex-col justify-center border-b border-[#292929] transition-colors duration-300 last:border-b-0",
!isOpen && "hover:bg-[#080808]",
)}
>
<div
className={cn(
"absolute inset-0 z-0 overflow-hidden pointer-events-none transition-opacity duration-500",
isOpen ? "opacity-100" : "opacity-0",
)}
>
<div className="absolute inset-0 bg-white/[0.03] md:group-hover:block hidden" />
<img
src="/images/pricing/FAQ/faqbg.svg"
alt="faq background"
loading="lazy"
className="absolute right-0 top-0 w-full h-[400px] md:h-full object-contain object-top-right md:object-cover md:object-right border-none opacity-60"
/>
<div className="absolute inset-0 bg-linear-to-r from-[#351B6D]/90 via-[#351B6D]/80 to-black/75 mix-blend-normal" />
</div>
<div className="relative z-10 px-4.5 md:px-4 lg:px-[20px] py-[30px]">
<div className="flex items-center justify-between gap-4">
<h3
className={cn(
"text-base tracking-[-2%] transition-all duration-400 lg:text-[18px]",
isOpen
? "font-normal text-white"
: "font-light text-[#FFFFFF66] md:group-hover:text-white",
)}
>
{faq.question}
</h3>
<div className="shrink-0 overflow-hidden">
<AnimatedPlusMinus
isOpen={isOpen}
className={cn(
"h-5 w-5 transition-colors duration-400",
isOpen
? "text-white"
: "text-[#FFFFFF66] md:group-hover:text-white",
)}
/>
</div>
</div>
<div
className={cn(
"grid transition-all duration-500 ease-[cubic-bezier(0.16,1,0.3,1)]",
isOpen
? "grid-rows-[1fr] opacity-100"
: "grid-rows-[0fr] opacity-0",
)}
>
<div className="overflow-hidden">
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
initial={{ y: -15, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -10, opacity: 0 }}
transition={{
height: springConfig,
opacity: { duration: 0.25 },
y: springConfig,
}}
className="pt-5 text-[#ffffff] leading-[16px] md:leading-[20px] font-light text-[12px] md:text-[14px] max-w-[85%] flex flex-col gap-3.5 tracking-[-0.5%]"
>
{faq.answer.split("\n\n").map((paragraph, idx) => (
<p key={idx}>{paragraph}</p>
))}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
</div>
);
})}
<div
className="relative z-0 px-8 lg:px-[20px] py-[30px] pointer-events-none opacity-0 select-none"
aria-hidden="true"
>
<div className="flex items-center justify-between gap-4">
<h3 className="text-base lg:text-[18px] tracking-[-2%] text-transparent select-none">
&nbsp;
</h3>
<div className="shrink-0 w-5 h-5"></div>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -1,68 +0,0 @@
"use client";
import { forwardRef, useImperativeHandle, useRef, useEffect } from "react";
import gsap from "gsap";
export const FeatureIconAnimation = forwardRef(({ Icon }, ref) => {
const iconRef = useRef(null);
const tlRef = useRef(null);
useImperativeHandle(ref, () => ({
play: () => tlRef.current?.play(),
reverse: () => tlRef.current?.reverse(),
}));
useEffect(() => {
const pixelEls = iconRef.current?.querySelectorAll("path");
if (!pixelEls?.length) return;
// Sort pixels by distance from center — center pixels animate first, outer ones last
const centers = Array.from(pixelEls).map((el) => {
const b = el.getBBox();
return { el, cx: b.x + b.width / 2, cy: b.y + b.height / 2 };
});
const midX = centers.reduce((s, c) => s + c.cx, 0) / centers.length;
const midY = centers.reduce((s, c) => s + c.cy, 0) / centers.length;
const pixels = centers
.sort((a, b) => {
const da = (a.cx - midX) ** 2 + (a.cy - midY) ** 2;
const db = (b.cx - midX) ** 2 + (b.cy - midY) ** 2;
return da - db;
})
.map((c) => c.el);
gsap.set(pixels, {
opacity: 0.15,
scale: 0.8,
transformOrigin: "center center",
fill: "currentColor",
});
tlRef.current = gsap.timeline({ paused: true });
tlRef.current
.to(pixels, {
opacity: 1,
scale: 1.15,
fill: "#9564ff",
duration: 0.2,
stagger: 0.04,
ease: "power2.out",
})
.to(pixels, {
scale: 1,
duration: 0.15,
ease: "back.out(3)",
});
return () => tlRef.current?.kill();
}, []);
return (
<div className="relative flex items-center justify-start w-12 h-12 group/icon">
<div className="relative w-[24px] h-[24px] flex items-center justify-center">
<Icon ref={iconRef} className="w-full h-full text-white" />
</div>
</div>
);
});
FeatureIconAnimation.displayName = "FeatureIconAnimation";

View File

@@ -0,0 +1,75 @@
"use client";
import gsap from "gsap";
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
import type { PixelAnimationHandle, PixelIconComponent } from "@/lib/types";
export const FeatureIconAnimation = forwardRef<
PixelAnimationHandle,
{ Icon: PixelIconComponent }
>(({ Icon }, ref) => {
const iconRef = useRef<SVGSVGElement | null>(null);
const tlRef = useRef<gsap.core.Timeline | null>(null);
useImperativeHandle(ref, () => ({
play: () => tlRef.current?.play(),
reverse: () => tlRef.current?.reverse(),
}));
useEffect(() => {
const pixelEls =
iconRef.current?.querySelectorAll<SVGGraphicsElement>("path");
if (!pixelEls?.length) return;
// Sort pixels by distance from center — center pixels animate first, outer ones last
const centers = Array.from(pixelEls).map((el) => {
const b = el.getBBox();
return { el, cx: b.x + b.width / 2, cy: b.y + b.height / 2 };
});
const midX = centers.reduce((s, c) => s + c.cx, 0) / centers.length;
const midY = centers.reduce((s, c) => s + c.cy, 0) / centers.length;
const pixels = centers
.sort((a, b) => {
const da = (a.cx - midX) ** 2 + (a.cy - midY) ** 2;
const db = (b.cx - midX) ** 2 + (b.cy - midY) ** 2;
return da - db;
})
.map((c) => c.el);
gsap.set(pixels, {
opacity: 0.15,
scale: 0.8,
transformOrigin: "center center",
fill: "currentColor",
});
tlRef.current = gsap.timeline({ paused: true });
tlRef.current
.to(pixels, {
opacity: 1,
scale: 1.15,
fill: "#9564ff",
duration: 0.2,
stagger: 0.04,
ease: "power2.out",
})
.to(pixels, {
scale: 1,
duration: 0.15,
ease: "back.out(3)",
});
return () => {
tlRef.current?.kill();
};
}, []);
return (
<div className="relative flex items-center justify-start w-12 h-12 group/icon">
<div className="relative w-[24px] h-[24px] flex items-center justify-center">
<Icon ref={iconRef} className="w-full h-full text-white" />
</div>
</div>
);
});
FeatureIconAnimation.displayName = "FeatureIconAnimation";

View File

@@ -1,73 +0,0 @@
"use client";
import { featuresData } from "@/app/constant";
import { useRef } from "react";
import { FeatureIconAnimation } from "./feature-icon-animation";
function FeatureCard({ feature }) {
const isDesktop =
typeof window !== "undefined" &&
window.matchMedia("(hover: hover)").matches;
const iconRef = useRef(null);
return (
<div
onMouseEnter={() => isDesktop && iconRef.current?.play()}
onMouseLeave={() => isDesktop && iconRef.current?.reverse()}
className="group relative flex px-4 md:px-4 flex-col justify-between p-6 bg-[#0F0F0F] min-h-[200px] md:min-h-[280px] border-r border-b border-[#292929] overflow-hidden cursor-pointer"
>
<div className="absolute inset-0 opacity-0 translate-y-6 md:group-hover:opacity-100 md:group-hover:translate-y-0 pointer-events-none z-0 hidden md:block">
<video
src="/images/features/pixel effect.webm"
autoPlay
loop
muted
playsInline
className="w-full h-full object-cover"
/>
</div>
{/* Hover Gradient Overlay */}
<div className="absolute inset-x-0 bottom-0 h-[70%] bg-[linear-gradient(to_bottom,rgba(10,10,10,0)_0%,rgba(135,82,250,0.15)_40%,rgba(135,82,250,0.45)_70%,rgba(135,82,250,0.85)_90%)] opacity-0 md:group-hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
<div className="relative z-10 flex flex-col h-full gap-[42px] md:gap-24.5">
<feature.Icon className="w-6 h-6 text-white md:hidden" />
<div className="hidden md:block">
<FeatureIconAnimation Icon={feature.Icon} ref={iconRef} />
</div>
<div className="flex flex-col gap-2 md:gap-4.5">
<h3 className="text-white font-normal tracking-[-5%] leading-6 text-[20px] md:text-[24px] font-sans">
{feature.title}
</h3>
<p className="text-[#FFFFFF99] w-full font-light tracking-[-2%] text-[14px] md:text-[16px] font-sans leading-[18px] md:leading-[20px] pr-2 md:pr-4 text-pretty">
{feature.description}
</p>
</div>
</div>
</div>
);
}
export default function Features() {
return (
<section className="bg-[#000000] w-full">
<div className="flex flex-col px-4 md:px-4 sm:px-8 py-12 md:py-16 xl:px-22.75 items-start">
<h2 className="text-[30px] md:text-[40px] leading-[32px] md:leading-[44px] font-sans tracking-[-4%]">
<div className="text-[#FFFFFF99]">Everything you need</div>
<div className="text-white">for AI and usage billing.</div>
</h2>
<div className="mt-4 text-[16px] tracking-[-2%] font-sans font-light text-[#FFFFFF99] leading-[20px] max-w-[420px]">
Your entire billing infrastructure, {" "}
<span className="text-white">
fully managed.
</span>
</div>
</div>
<div className="border-t border-[#292929] w-full" />
<div className="grid grid-cols-1 gap-[-3px] md:grid-cols-2 lg:grid-cols-3 xl:px-22.75 border-[#292929] [&>*:last-child]:border-b-0 [&>*:nth-last-child(2)]:border-b-0 md:[&>*:nth-last-child(-n+2)]:border-b-0 lg:[&>*:nth-last-child(-n+3)]:border-b-0 *:border-l md:[&>*:nth-child(2n)]:border-l-0 lg:[&>*:nth-child(3n+1)]:border-l lg:[&>*:nth-child(3n+2)]:border-l-0 lg:[&>*:nth-child(3n)]:border-l-0">
{featuresData.map((feature, i) => (
<FeatureCard key={i} feature={feature} />
))}
{/* <div className="bg-[#0f0f0f] w-full h-full min-h-[280px] hidden lg:block border-r border-b border-[#292929]" /> */}
</div>
</section>
);
}

View File

@@ -0,0 +1,72 @@
"use client";
import { useRef } from "react";
import { featuresData } from "@/app/constant";
import type { PixelAnimationHandle } from "@/lib/types";
import { FeatureIconAnimation } from "./feature-icon-animation";
function FeatureCard({ feature }: { feature: (typeof featuresData)[number] }) {
const isDesktop =
typeof window !== "undefined" &&
window.matchMedia("(hover: hover)").matches;
const iconRef = useRef<PixelAnimationHandle | null>(null);
return (
<div
onMouseEnter={() => isDesktop && iconRef.current?.play()}
onMouseLeave={() => isDesktop && iconRef.current?.reverse()}
className="group relative flex px-4 md:px-4 flex-col justify-between p-6 bg-[#0F0F0F] min-h-[200px] md:min-h-[280px] border-r border-b border-[#292929] overflow-hidden cursor-pointer"
>
<div className="absolute inset-0 opacity-0 translate-y-6 md:group-hover:opacity-100 md:group-hover:translate-y-0 pointer-events-none z-0 hidden md:block">
<video
src="/images/features/pixel effect.webm"
autoPlay
loop
muted
playsInline
className="w-full h-full object-cover"
/>
</div>
{/* Hover Gradient Overlay */}
<div className="absolute inset-x-0 bottom-0 h-[70%] bg-[linear-gradient(to_bottom,rgba(10,10,10,0)_0%,rgba(135,82,250,0.15)_40%,rgba(135,82,250,0.45)_70%,rgba(135,82,250,0.85)_90%)] opacity-0 md:group-hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
<div className="relative z-10 flex flex-col h-full gap-[42px] md:gap-24.5">
<feature.Icon className="w-6 h-6 text-white md:hidden" />
<div className="hidden md:block">
<FeatureIconAnimation Icon={feature.Icon} ref={iconRef} />
</div>
<div className="flex flex-col gap-2 md:gap-4.5">
<h3 className="text-white font-normal tracking-[-5%] leading-6 text-[20px] md:text-[24px] font-sans">
{feature.title}
</h3>
<p className="text-[#FFFFFF99] w-full font-light tracking-[-2%] text-[14px] md:text-[16px] font-sans leading-[18px] md:leading-[20px] pr-2 md:pr-4 text-pretty">
{feature.description}
</p>
</div>
</div>
</div>
);
}
export default function Features() {
return (
<section className="bg-[#000000] w-full">
<div className="flex flex-col px-4 md:px-4 sm:px-8 py-12 md:py-16 xl:px-22.75 items-start">
<h2 className="text-[30px] md:text-[40px] leading-[32px] md:leading-[44px] font-sans tracking-[-4%]">
<div className="text-[#FFFFFF99]">Everything you need</div>
<div className="text-white">for AI and usage billing.</div>
</h2>
<div className="mt-4 text-[16px] tracking-[-2%] font-sans font-light text-[#FFFFFF99] leading-[20px] max-w-[420px]">
Your entire billing infrastructure,{" "}
<span className="text-white">fully managed.</span>
</div>
</div>
<div className="border-t border-[#292929] w-full" />
<div className="grid grid-cols-1 gap-[-3px] md:grid-cols-2 lg:grid-cols-3 xl:px-22.75 border-[#292929] [&>*:last-child]:border-b-0 [&>*:nth-last-child(2)]:border-b-0 md:[&>*:nth-last-child(-n+2)]:border-b-0 lg:[&>*:nth-last-child(-n+3)]:border-b-0 *:border-l md:[&>*:nth-child(2n)]:border-l-0 lg:[&>*:nth-child(3n+1)]:border-l lg:[&>*:nth-child(3n+2)]:border-l-0 lg:[&>*:nth-child(3n)]:border-l-0">
{featuresData.map((feature, i) => (
<FeatureCard key={i} feature={feature} />
))}
{/* <div className="bg-[#0f0f0f] w-full h-full min-h-[280px] hidden lg:block border-r border-b border-[#292929]" /> */}
</div>
</section>
);
}

View File

@@ -1,181 +0,0 @@
import Image from "next/image";
import Link from "next/link";
const footerColumns = [
{
title: "PRODUCT",
links: [
{ label: "FEATURES", href: "#" },
{ label: "INTEGRATIONS", href: "#" },
{ label: "PRICING", href: "#" },
{ label: "CHANGELOG", href: "#" },
{ label: "ROADMAP", href: "#" },
],
},
{
title: "COMPANY",
links: [
{ label: "OUR TEAM", href: "#" },
{ label: "OUR VALUES", href: "/privacy" },
{ label: "BLOG", href: "/blog" },
],
},
{
title: "RESOURCES",
links: [
{ label: "DOWNLOADS", href: "https://useautumn.com/" },
{ label: "DOCUMENTATION", href: "https://docs.useautumn.com/welcome" },
{ label: "CONTACT", href: "https://cal.com/ayrod/a?user=ayrod" },
],
},
];
export default function Footer() {
return (
<>
<footer
style={{
width: "calc(100% + var(--page-pad) * 2)",
marginLeft: "calc(var(--page-pad) * -1)",
paddingLeft: "var(--page-pad)",
paddingRight: "var(--page-pad)",
}}
className="mt-[50px] relative bg-[#000000] border-t border-[#292929] overflow-hidden text-[#FFFFFF99] grid"
>
<div className="col-start-1 row-start-1 flex flex-col z-10 w-full relative">
<div className="relative w-full flex flex-col justify-between min-h-[400px]">
<div className="absolute bottom-0 left-0 right-0 h-[300px] sm:h-[550px] lg:h-[400px] pointer-events-none opacity-70 bg-[url('/images/footer/footerbg.svg')] bg-cover sm:bg-contain bg-bottom bg-no-repeat z-0" />
<div className="flex flex-col lg:flex-row justify-between pt-[32px] md:pt-24 pb-32 md:pb-16 pl-4.5 xl:pl-[90px] pr-4 sm:pr-8 lg:pr-12 gap-10 md:gap-16 lg:gap-8 relative z-10">
{/* Left Side: Logo and Description */}
<div className="flex flex-col max-w-sm">
<Link href="/" className="mb-3 md:mb-6 inline-block">
<Image
src="/images/navbar/autumnlogo.svg"
width={195}
height={48}
alt="Autumn"
className="brightness-0 invert w-[130px] h-[32px] md:w-[195px] md:h-[48px]"
/>
</Link>
<p className="text-[14px] md:text-[16px] leading-[18px] md:leading-[20px] tracking-[-2%] text-[#FFFFFF99] font-light">
Autumn is built on top of Stripe Billing (for now), so their
fees (0.7%, and 2.9% + 30c) still apply.
</p>
</div>
{/* Right Side: Columns */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-12 sm:gap-16 lg:gap-24">
{footerColumns.map((col, index) => (
<div key={index} className="flex flex-col">
<h3 className="flex items-center gap-2 text-white font-mono text-[14px] uppercase tracking-[-1%] mb-6">
<div className="w-[8px] h-[8px] bg-[#FFFFFF99]"></div>
{col.title}
</h3>
<ul className="flex flex-col gap-1">
{col.links.map((link, i) => (
<li key={i} className="group/strip">
<Link
href={link.href}
target="_blank"
className="group/strip flex items-center gap-3 text-[14px] font-mono uppercase tracking-[-1%] transition-colors duration-300"
>
<span className="w-1 h-1 bg-[#FFFFFF99] group-hover/strip:bg-white group-active/strip:bg-white transition-colors duration-300 shrink-0"></span>
<span className="relative">
<span className="text-[#FFFFFF99]">
{link.label}
</span>
<span
className="absolute inset-y-0 left-0 w-0 group-hover/strip:w-full group-active/strip:w-full overflow-hidden transition-all duration-300 ease-in-out bg-white text-black font-normal pointer-events-none flex items-center"
aria-hidden="true"
>
<span className="whitespace-nowrap">
{link.label}
</span>
</span>
</span>
</Link>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</div>
<div
className="relative block z-10 bg-black/40 backdrop-blur-md border-y border-[#292929]"
style={{
width: "calc(100% + var(--page-pad) * 2)",
marginLeft: "calc(var(--page-pad) * -1)",
paddingLeft: "var(--page-pad)",
paddingRight: "var(--page-pad)",
}}
>
<div className="max-w-[1400px] mx-auto flex flex-col md:flex-row justify-between items-stretch h-auto md:h-14 font-mono text-[10px] sm:text-[11px] tracking-[0.2em]">
{/* Social Left Box */}
<div className="flex flex-row md:flex-row border-b md:border-b-0 border-[#292929] w-full md:w-auto">
<div className="px-4 sm:px-8 py-4.5 md:py-0 border-r border-[#292929] tracking-[-2%] flex items-center justify-center md:justify-start text-[12px] md:text-[14px]">
SOCIAL
</div>
<div className="px-6 sm:px-8 py-4 md:py-0 flex items-center justify-center md:justify-start gap-3 sm:gap-4 md:border-r border-[#292929]">
<Link
href="https://www.linkedin.com/company/useautumn"
className="group/strip flex items-center text-[14px] tracking-[-2%] transition-colors duration-300"
>
<span className="relative">
<span className="text-[#FFFFFF99] group-hover/strip:text-[#ffffff] text-[12px] md:text-[14px] transition-colors duration-300">
LINKEDIN
</span>
<span
className="absolute inset-y-0 left-0 w-0 group-hover/strip:w-full group-active/strip:w-full overflow-hidden transition-all duration-300 ease-in-out bg-white text-black font-normal pointer-events-none flex items-center"
aria-hidden="true"
>
<span className="whitespace-nowrap">LINKEDIN</span>
</span>
</span>
</Link>
<div className="w-[5px] h-[5px] bg-[#FFFFFF]"></div>
<Link
href="https://x.com/autumnpricing"
className="group/strip flex items-center text-[14px] tracking-[-2%] transition-colors duration-300"
>
<span className="relative">
<span className="text-[#FFFFFF99] group-hover/strip:text-[#ffffff] transition-colors duration-300 text-[12px] md:text-[14px]">
TWITTER
</span>
<span
className="absolute inset-y-0 left-0 w-0 group-hover/strip:w-full group-active/strip:w-full overflow-hidden transition-all duration-300 ease-in-out bg-white text-black font-normal pointer-events-none flex items-center"
aria-hidden="true"
>
<span className="whitespace-nowrap">TWITTER</span>
</span>
</span>
</Link>
</div>
</div>
<div className="px-4.5 py-4 md:py-0 flex items-center justify-start md:justify-end text-center md:text-right text-[#FFFFFF99] text-[12px] md:text-[14px] tracking-[-2%] border-b md:border-b-0 border-[#292929] w-full md:w-auto">
Copyright © 2026 Autumn All rights reserved
<div className="hidden lg:flex gap-3 ml-[15px] h-[54px]">
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
</div>
</div>
</div>
</div>
</div>
</footer>
</>
);
}

View File

@@ -0,0 +1,180 @@
import Image from "next/image";
import Link from "next/link";
const footerColumns: any[] = [
// {
// title: "PRODUCT",
// links: [
// { label: "FEATURES", href: "#" },
// { label: "INTEGRATIONS", href: "#" },
// { label: "PRICING", href: "#" },
// { label: "CHANGELOG", href: "#" },
// { label: "ROADMAP", href: "#" },
// ],
// },
// {
// title: "COMPANY",
// links: [
// { label: "OUR TEAM", href: "#" },
// { label: "OUR VALUES", href: "/privacy" },
// { label: "BLOG", href: "/blog" },
// ],
// },
// {
// title: "RESOURCES",
// links: [
// { label: "DOWNLOADS", href: "https://useautumn.com/" },
// { label: "DOCUMENTATION", href: "https://docs.useautumn.com/welcome" },
// { label: "CONTACT", href: "https://cal.com/ayrod/a?user=ayrod" },
// ],
// },
];
export default function Footer() {
return (
<>
<footer
style={{
width: "calc(100% + var(--page-pad) * 2)",
marginLeft: "calc(var(--page-pad) * -1)",
paddingLeft: "var(--page-pad)",
paddingRight: "var(--page-pad)",
}}
className="mt-[50px] relative bg-[#000000] border-t border-[#292929] overflow-hidden text-[#FFFFFF99] grid"
>
<div className="col-start-1 row-start-1 flex flex-col z-10 w-full relative">
<div className="relative w-full flex flex-col justify-between min-h-[400px]">
<div className="absolute bottom-0 left-0 right-0 h-[300px] sm:h-[550px] lg:h-[400px] pointer-events-none opacity-70 bg-[url('/images/footer/footerbg.svg')] bg-cover sm:bg-contain bg-bottom bg-no-repeat z-0" />
<div className="flex flex-col lg:flex-row justify-between pt-[32px] md:pt-24 pb-32 md:pb-16 pl-4.5 xl:pl-[90px] pr-4 sm:pr-8 lg:pr-12 gap-10 md:gap-16 lg:gap-8 relative z-10">
{/* Left Side: Logo and Description */}
<div className="flex flex-col max-w-sm">
<Link href="/" className="mb-3 md:mb-6 inline-block">
<Image
src="/images/navbar/autumnlogo.svg"
width={195}
height={48}
alt="Autumn"
className="brightness-0 invert w-[130px] h-[32px] md:w-[195px] md:h-[48px]"
/>
</Link>
<p className="text-[14px] md:text-[16px] leading-[18px] md:leading-[20px] tracking-[-2%] text-[#FFFFFF99] font-light">
Autumn is drop-in billing for AI companies.
</p>
</div>
{/* Right Side: Columns */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-12 sm:gap-16 lg:gap-24">
{footerColumns.map((col, index) => (
<div key={index} className="flex flex-col">
<h3 className="flex items-center gap-2 text-white font-mono text-[14px] uppercase tracking-[-1%] mb-6">
<div className="w-[8px] h-[8px] bg-[#FFFFFF99]"></div>
{col.title}
</h3>
<ul className="flex flex-col gap-1">
{col.links.map((link, i) => (
<li key={i} className="group/strip">
<Link
href={link.href}
target="_blank"
className="group/strip flex items-center gap-3 text-[14px] font-mono uppercase tracking-[-1%] transition-colors duration-300"
>
<span className="w-1 h-1 bg-[#FFFFFF99] group-hover/strip:bg-white group-active/strip:bg-white transition-colors duration-300 shrink-0"></span>
<span className="relative">
<span className="text-[#FFFFFF99]">
{link.label}
</span>
<span
className="absolute inset-y-0 left-0 w-0 group-hover/strip:w-full group-active/strip:w-full overflow-hidden transition-all duration-300 ease-in-out bg-white text-black font-normal pointer-events-none flex items-center"
aria-hidden="true"
>
<span className="whitespace-nowrap">
{link.label}
</span>
</span>
</span>
</Link>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</div>
<div
className="relative block z-10 bg-black/40 backdrop-blur-md border-y border-[#292929]"
style={{
width: "calc(100% + var(--page-pad) * 2)",
marginLeft: "calc(var(--page-pad) * -1)",
paddingLeft: "var(--page-pad)",
paddingRight: "var(--page-pad)",
}}
>
<div className="max-w-[1400px] mx-auto flex flex-col md:flex-row justify-between items-stretch h-auto md:h-14 font-mono text-[10px] sm:text-[11px] tracking-[0.2em]">
{/* Social Left Box */}
<div className="flex flex-row md:flex-row border-b md:border-b-0 border-[#292929] w-full md:w-auto">
<div className="px-4 sm:px-8 py-4.5 md:py-0 border-r border-[#292929] tracking-[-2%] flex items-center justify-center md:justify-start text-[12px] md:text-[14px]">
SOCIAL
</div>
<div className="px-6 sm:px-8 py-4 md:py-0 flex items-center justify-center md:justify-start gap-3 sm:gap-4 md:border-r border-[#292929]">
<Link
href="https://www.linkedin.com/company/useautumn"
className="group/strip flex items-center text-[14px] tracking-[-2%] transition-colors duration-300"
>
<span className="relative">
<span className="text-[#FFFFFF99] group-hover/strip:text-[#ffffff] text-[12px] md:text-[14px] transition-colors duration-300">
LINKEDIN
</span>
<span
className="absolute inset-y-0 left-0 w-0 group-hover/strip:w-full group-active/strip:w-full overflow-hidden transition-all duration-300 ease-in-out bg-white text-black font-normal pointer-events-none flex items-center"
aria-hidden="true"
>
<span className="whitespace-nowrap">LINKEDIN</span>
</span>
</span>
</Link>
<div className="w-[5px] h-[5px] bg-[#FFFFFF]"></div>
<Link
href="https://x.com/autumnpricing"
className="group/strip flex items-center text-[14px] tracking-[-2%] transition-colors duration-300"
>
<span className="relative">
<span className="text-[#FFFFFF99] group-hover/strip:text-[#ffffff] transition-colors duration-300 text-[12px] md:text-[14px]">
TWITTER
</span>
<span
className="absolute inset-y-0 left-0 w-0 group-hover/strip:w-full group-active/strip:w-full overflow-hidden transition-all duration-300 ease-in-out bg-white text-black font-normal pointer-events-none flex items-center"
aria-hidden="true"
>
<span className="whitespace-nowrap">TWITTER</span>
</span>
</span>
</Link>
</div>
</div>
<div className="px-4.5 py-4 md:py-0 flex items-center justify-start md:justify-end text-center md:text-right text-[#FFFFFF99] text-[12px] md:text-[14px] tracking-[-2%] border-b md:border-b-0 border-[#292929] w-full md:w-auto">
Copyright © 2026 Autumn All rights reserved
<div className="hidden lg:flex gap-3 ml-[15px] h-[54px]">
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
<div className="border-r border-[#292929]" />
</div>
</div>
</div>
</div>
</div>
</footer>
</>
);
}

307
apps/website/components/hero.tsx Executable file
View File

@@ -0,0 +1,307 @@
"use client";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { CTALines, IconCTADocs, IconCTAStart } from "@/app/constant";
import AutumnConfig from "./autumn-config";
// import dynamic from "next/dynamic";
// const AutumnConfig = dynamic(() => import("./autumn-config"), { ssr: false });
const BADGE_TEXT = "// 100% open source";
const getLoggedInHintCookie = () => {
if (typeof window === "undefined") return null;
return (
document.cookie
.split("; ")
.find((row) => row.startsWith("logged_in_hint="))
?.split("=")[1] === "1"
);
};
export default function Hero() {
const containerRef = useRef<HTMLDivElement | null>(null);
const heroTlRef = useRef<gsap.core.Timeline | null>(null);
const [displayedText, setDisplayedText] = useState("");
const [isLoggedIn, setIsLoggedIn] = useState(false);
// Read the hint cookie after mount to avoid SSR/CSR hydration mismatch.
useEffect(() => {
setIsLoggedIn(getLoggedInHintCookie() === true);
}, []);
// Badge typewriter
useEffect(() => {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*0123456789";
let intervalId: ReturnType<typeof setInterval> | null = null;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const startTypewriter = () => {
timeoutId = setTimeout(() => {
let iteration = 0;
intervalId = setInterval(() => {
setDisplayedText(
BADGE_TEXT.split("")
.map((char, index) => {
if (char === " ") return " ";
if (index < Math.floor(iteration)) return char;
if (index === Math.floor(iteration))
return chars[Math.floor(Math.random() * chars.length)];
return "";
})
.join(""),
);
iteration += 0.4;
if (iteration >= BADGE_TEXT.length) {
if (intervalId) clearInterval(intervalId);
setDisplayedText(BADGE_TEXT);
}
}, 30);
}, 150);
};
window.addEventListener("preloader:complete", startTypewriter, {
once: true,
});
return () => {
window.removeEventListener("preloader:complete", startTypewriter);
if (timeoutId) clearTimeout(timeoutId);
if (intervalId) clearInterval(intervalId);
};
}, []);
useEffect(() => {
const handler = () => heroTlRef.current?.play();
window.addEventListener("preloader:complete", handler, { once: true });
return () => window.removeEventListener("preloader:complete", handler);
}, []);
useGSAP(
() => {
gsap.set(".hero-root", { opacity: 0 });
gsap.set(".hero-bg", {
opacity: 0,
filter: "blur(6px) brightness(1)",
scale: 0.97,
transformOrigin: "center top",
});
gsap.set(".hero-reveal", {
opacity: 0,
y: 25,
filter: "blur(12px)",
scale: 0.96,
transformOrigin: "center bottom",
});
gsap.set(".hero-cta", { opacity: 0, scale: 0.95 });
const tl = gsap.timeline({
paused: true,
defaults: { overwrite: "auto" },
});
heroTlRef.current = tl;
tl.to(".hero-root", { opacity: 1, duration: 0.3, ease: "none" })
.to(".hero-bg", {
opacity: 1,
filter: "blur(0px) brightness(1)",
scale: 1,
duration: 0.4,
ease: "power2.out",
})
.to(".hero-bg", {
filter: "blur(0px) brightness(1.6)",
duration: 0.125,
ease: "power2.in",
})
.to(".hero-bg", {
filter: "blur(0px) brightness(1)",
duration: 0.125,
ease: "power2.out",
})
.to(
".hero-reveal",
{
opacity: 1,
y: 0,
filter: "blur(0px)",
scale: 1,
duration: 1.1,
stagger: 0.1,
ease: "power3.out",
},
"-=0.2",
)
.to(
".hero-cta",
{
opacity: 1,
scale: 1,
duration: 0.3,
stagger: 0.06,
ease: "back.out(1.5)",
},
"-=0.1",
);
},
{ scope: containerRef },
);
return (
<div ref={containerRef}>
<div className="relative hero-root opacity-0 flex flex-col items-stretch pb-0 md:pb-12 mb-0 bg-[#0F0F0F]">
<div className="flex justify-between">
<div className="flex flex-col gap-6 px-4 xl:px-22.75 py-8 bg-[#0F0F0F] mt-26">
<h4 className="hero-reveal relative uppercase font-mono tracking-[-2%] text-[12px] md:text-sm leading-sm text-white md:text-[#FFFFFF99] bg-[#2c2c2d] w-fit p-2 min-h-[30px] md:min-h-[36px] flex items-center">
<span className="invisible select-none" aria-hidden="true">
{BADGE_TEXT}
</span>
<span className="absolute inset-0 flex items-center p-2">
{displayedText}
</span>
</h4>
<div className="flex flex-col gap-6 w-full px-0 lg:px-0">
<h1 className="hero-reveal text-[44px] md:text-[56px] w-full max-w-sm sm:max-w-[480px] md:max-w-xl leading-[44px] tracking-[-5%] md:leading-14 font-sans">
<span className="text-[#FFFFFF99] font-normal">
The drop-in billing layer for
</span>{" "}
<span className="text-white block md:inline">AI startups</span>
</h1>
<p className="hero-reveal tracking-[-2%] w-full max-w-xs sm:max-w-[480px] md:max-w-xl text-[#FFFFFF99] md:text-[16px] text-[14px] font-light leading-5 font-sans">
Stop rebuilding usage limits, credit ledgers and payment logic.{" "}
<span className="text-white font-light">
Autumn is your customer database
</span>{" "}
that scales from your first user to your largest contract.
</p>
</div>
</div>
<div className="hero-reveal relative w-[50vw] max-w-[720px] p-16 py-0 mx-auto hidden xl:block">
<div className="absolute inset-0 z-0 pointer-events-none">
<video
src="/images/pricing-models/pricingbg.webm"
autoPlay
loop
muted
playsInline
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100"
/>
</div>
<div className="relative z-10 translate-y-16 w-full flex justify-center">
<AutumnConfig
initialDelay={200}
awaitEvent="preloader:complete"
/>
</div>
</div>
</div>
<div className="border-t border-[#292929]" />
<div className="flex flex-nowrap items-center xl:px-22.75 px-4 bg-[#0F0F0F] w-full overflow-hidden">
{/* Primary CTA */}
<div className="hero-cta w-full md:w-fit md:flex-shrink-0">
<Link
href={
isLoggedIn
? "https://app.useautumn.com"
: "https://app.useautumn.com/sign-in"
}
>
<motion.div
initial="initial"
whileHover="hover"
whileTap="hover"
className="relative"
>
{/* Adjusted px-3 for mobile, md:px-4 for desktop */}
<div className="relative overflow-hidden flex items-center gap-1.5 md:gap-2.5 cursor-pointer justify-between py-2 px-3 md:px-4 md:py-3.5 md:w-50 font-sans bg-[#9564ff] hover:bg-[#7D46F4] transition-colors duration-300">
<CTALines />
<span className="relative z-10 tracking-[-2%] uppercase md:normal-case text-white font-medium text-[12px] md:text-base whitespace-nowrap">
{isLoggedIn ? "Dashboard" : "Start for free"}
</span>
<span className="relative z-10 scale-95 md:scale-100">
<IconCTAStart />
</span>
</div>
</motion.div>
</Link>
</div>
{/* Secondary CTA */}
<div className="hero-cta w-full md:w-fit md:flex-shrink-0">
<Link href={"https://cal.com/ayrod"}>
<motion.div
initial="initial"
whileHover="hover"
whileTap="hover"
className="relative"
>
<div className="relative overflow-hidden flex items-center gap-1.5 md:gap-2.5 border-r border-[#292929] text-white cursor-pointer justify-between py-2 px-3 md:px-4 md:py-3.5 md:w-50 font-sans bg-[#0F0F0F] hover:bg-[#FFFFFF1F] transition-colors duration-300">
<CTALines />
<span className="relative z-10 tracking-[-2%] text-[12px] uppercase md:normal-case md:text-[16px] whitespace-nowrap">
Book a call
</span>
<span className="relative z-10 scale-100">
<IconCTADocs />
</span>
</div>
</motion.div>
</Link>
</div>
<div className="hero-cta hidden md:flex flex-nowrap gap-2 md:gap-3 ml-2 md:ml-3 h-10.5 md:h-12.5 flex-1">
<div className="border-r border-[#292929] h-full hidden md:block" />
<div className="border-r border-[#292929] h-full hidden md:block" />
<div className="border-r border-[#292929] h-full hidden md:block" />
<div className="border-r border-[#292929] h-full hidden md:block" />
<div className="border-r border-[#292929] h-full hidden md:block" />
<div className="border-r border-[#292929] h-full hidden md:block" />
<div className="border-r border-[#292929] h-full hidden md:block" />
<div className="border-r border-[#292929] h-full hidden md:block" />
<div className="border-r border-[#292929] h-full hidden md:block" />
</div>
</div>
<div className="border-b border-[#292929]" />
{/* MOBILE VIEW*/}
<div className="relative block xl:hidden w-full overflow-hidden bg-[#0F0F0F] mt-12">
<div className="relative overflow-hidden w-full p-7 flex items-center justify-center">
<video
src="/images/pricing-models/pricingbg.webm"
autoPlay
loop
muted
playsInline
className="hero-bg absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100"
/>
<div className="hero-reveal relative z-10 w-[96%] sm:w-[90%] max-w-[520px] flex justify-center items-center">
{/* <AutumnConfig lines={16} initialDelay={1000} awaitEvent="preloader:complete" /> */}
<Image
src={"/images/hero/autumn_mobile.svg"}
width={1600}
height={1600}
alt="xyz"
priority
/>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,46 @@
"use client";
import dynamic from "next/dynamic";
import Hero from "./hero";
import SectionDivider from "./section-divider";
const ProductionScale = dynamic(() => import("@/components/production-scale"), {
ssr: false,
});
const Problem = dynamic(() => import("@/components/problem"), { ssr: false });
const Solution = dynamic(() => import("@/components/solution"), { ssr: false });
const Features = dynamic(() => import("@/components/features"), { ssr: false });
const PricingModels = dynamic(() => import("@/components/pricing-models"), {
ssr: false,
});
const Testimonials = dynamic(() => import("@/components/testimonials"), {
ssr: false,
});
const Pricing = dynamic(() => import("@/components/pricing"), { ssr: false });
const FAQ = dynamic(() => import("@/components/faq"), { ssr: false });
const Footer = dynamic(() => import("@/components/footer"), { ssr: false });
export default function HomeSections() {
return (
<>
<Hero />
<SectionDivider title="THE PROBLEM" />
<Problem />
<SectionDivider title="THE SOLUTION" />
<Solution />
<SectionDivider title="PRICING MODELS" />
<PricingModels />
<SectionDivider title="FEATURES" />
<Features />
<SectionDivider title="TESTIMONIALS" />
<Testimonials />
<SectionDivider title="PRODUCTION SCALE" />
<ProductionScale />
<SectionDivider title="PRICING" />
<Pricing />
<SectionDivider title="FAQ" />
<FAQ />
<Footer />
</>
);
}

View File

@@ -1,421 +0,0 @@
"use client";
import {
CTALines,
IconBlog,
IconCTAStart,
IconDashboard,
IconDiscord,
IconDocs,
IconPricing,
MenuGridIcon,
} from "@/app/constant";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
import { DashboardIconPixel } from "./dashboard-icon-pixel";
const NAV_LINKS = [
{ label: "Docs", href: "https://docs.useautumn.com/welcome", Icon: IconDocs },
{ label: "Blog", href: "/blog", Icon: IconBlog },
{ label: "Pricing", href: "#pricing", Icon: IconPricing },
{
label: "Discord",
href: "https://discord.com/invite/STqxY92zuS",
Icon: IconDiscord,
},
];
const NavIconPixel = forwardRef(function NavIconPixel({ Icon }, ref) {
const iconRef = useRef(null);
const tlRef = useRef(null);
useImperativeHandle(ref, () => ({
restart: () => tlRef.current?.play(),
reverse: () => tlRef.current?.reverse(),
}));
useEffect(() => {
const pixelEls = iconRef.current?.querySelectorAll(".icon-pixel-path");
if (!pixelEls?.length) return;
const pixels = Array.from(pixelEls).sort((a, b) => {
const aBox = a.getBBox();
const bBox = b.getBBox();
return (
aBox.x +
aBox.width / 2 -
(aBox.y + aBox.height / 2) -
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
);
});
gsap.set(pixels, {
opacity: 0.15,
scale: 0.8,
transformOrigin: "left bottom",
fill: "currentColor",
});
tlRef.current = gsap.timeline({ paused: true });
tlRef.current
.to(pixels, {
opacity: 1,
scale: 1.15,
fill: "#FFFFFF",
duration: 0.01,
stagger: 0.025,
ease: "power2.out",
})
.to(pixels, {
scale: 1,
duration: 0.01,
ease: "back.out(3)",
});
return () => tlRef.current?.kill();
}, []);
return (
<span className="relative inline-flex items-center justify-center w-8 h-8 group/icon">
<div className="relative w-[16px] h-[16px] flex items-center justify-center">
<Icon ref={iconRef} className="w-full h-full text-white" />
</div>
</span>
);
});
function NavLinkItem({ item }) {
const iconRef = useRef(null);
const isAnchor = item.href.startsWith("#");
const handleClick = (e) => {
if (!isAnchor) return;
e.preventDefault();
const target = document.querySelector(item.href);
if (target) target.scrollIntoView({ behavior: "smooth" });
};
return (
<div className="nav-link">
<Link
href={item.href}
onClick={handleClick}
className="group inline-flex items-center py-2 text-[#FFFFFF99] hover:text-white transition-colors"
onMouseEnter={() => iconRef.current?.restart()}
onMouseLeave={() => iconRef.current?.reverse()}
>
<NavIconPixel Icon={item.Icon} ref={iconRef} />
<span className="font-mono text-[14px] uppercase tracking-widest transition-colors group-hover:text-white">
{item.label}
</span>
</Link>
</div>
);
}
export default function Navbar() {
const containerRef = useRef(null);
const dashboardIconRef = useRef(null);
const RECOIL_DELAY = 1000;
const [recoilHidden, setRecoilHidden] = useState(false);
const recoilTimerRef = useRef(null);
const [menuOpen, setMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 10);
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
useEffect(() => {
const onRecoil = () => {
setRecoilHidden(true);
clearTimeout(recoilTimerRef.current);
recoilTimerRef.current = setTimeout(
() => setRecoilHidden(false),
RECOIL_DELAY,
);
};
window.addEventListener("elastic-recoil", onRecoil);
return () => {
window.removeEventListener("elastic-recoil", onRecoil);
clearTimeout(recoilTimerRef.current);
};
}, []);
// Lock body scroll while mobile menu is open
useEffect(() => {
document.documentElement.style.overflow = menuOpen ? "hidden" : "";
document.body.style.overflow = menuOpen ? "hidden" : "";
return () => {
document.documentElement.style.overflow = "";
document.body.style.overflow = "";
};
}, [menuOpen]);
useGSAP(
() => {
gsap.set(".nav-root", { opacity: 0 });
gsap.set(".nav-logo", {
opacity: 0,
filter: "blur(6px) brightness(1)",
scale: 0.92,
transformOrigin: "left center",
});
gsap.set(".nav-link", { opacity: 0, y: -8 });
gsap.set(".nav-dashboard", { opacity: 0, scale: 0.95 });
const tl = gsap.timeline({ defaults: { overwrite: "auto" } });
tl.to(".nav-root", { opacity: 1, duration: 0.3, ease: "none" })
.to(".nav-logo", {
opacity: 1,
filter: "blur(0px) brightness(1)",
scale: 1,
duration: 0.7,
ease: "power2.out",
})
.to(".nav-logo", {
filter: "blur(0px) brightness(1.6)",
duration: 0.225,
ease: "power2.in",
})
.to(".nav-logo", {
filter: "blur(0px) brightness(1)",
duration: 0.125,
ease: "power2.out",
})
.to(
".nav-link",
{
opacity: 1,
y: 0,
duration: 0.25,
stagger: 0.06,
ease: "power2.out",
},
"-=0.05",
)
.to(
".nav-dashboard",
{
opacity: 1,
scale: 1,
duration: 0.3,
ease: "back.out(1.5)",
},
"-=0.1",
);
},
{ scope: containerRef },
);
const mobileTl = useRef(null);
useGSAP(
() => {
gsap.set(".nav-mobile", {
opacity: 0,
clipPath: "inset(0% 0 100% 0)",
pointerEvents: "none",
});
gsap.set(
".nav-mobile a, .nav-mobile img, .nav-mobile button, .nav-mobile .border-b",
{
opacity: 0,
filter: "blur(8px)",
scale: 0.95,
},
);
mobileTl.current = gsap.timeline({
paused: true,
defaults: { overwrite: "auto" },
});
mobileTl.current
.to(".nav-mobile", {
opacity: 1,
y: 0,
pointerEvents: "auto",
clipPath: "inset(0% 0 0% 0)",
duration: 0.65,
ease: "power3.inOut",
})
.to(
".nav-mobile a, .nav-mobile img, .nav-mobile button, .nav-mobile .border-b",
{
opacity: 1,
filter: "blur(0px)",
scale: 1,
duration: 0.4,
stagger: 0.02,
ease: "power2.out",
},
"-=0.35",
);
},
{ scope: containerRef },
);
useGSAP(
() => {
if (mobileTl.current) {
if (menuOpen) {
mobileTl.current.timeScale(1).play();
} else {
mobileTl.current.timeScale(1.8).reverse();
}
}
},
{ scope: containerRef, dependencies: [menuOpen] },
);
return (
<div
style={{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" }}
ref={containerRef}
>
<div className="absolute pointer-events-none left-0 border-t border-[#292929] w-screen z-50" />
{scrolled && !recoilHidden && (
<div className="h-[56px] md:h-[44px] w-fit" />
)}
<div
className={`${scrolled && !recoilHidden
? "fixed top-0 left-0 z-80 w-full px-4 md:px-(--page-pad) bg-[#0F0F0F] backdrop-blur-md border-b border-t pt-4 border-[#292929]"
: "relative"
}`}
>
{scrolled && !recoilHidden && (
<>
<div className="absolute pointer-events-none top-4 left-0 w-full border-t border-[#292929]" />
<div className="absolute pointer-events-none left-4 md:left-(--page-pad) top-0 bottom-0 border-l border-[#292929]" />
<div className="absolute pointer-events-none right-4 md:right-(--page-pad) top-0 bottom-0 border-r border-[#292929]" />
</>
)}
<nav className="nav-root bg-[#0F0F0F] flex items-center justify-between font-mono uppercase text-xs pl-2 md:pl-0 h-[44px] md:h-[44px] lg:pb-0 xl:pb-0">
<Link href={"/"}>
<Image
src="/images/navbar/autumnlogo.svg"
width={114}
height={28}
alt="Autumn"
loading="lazy"
className="nav-logo ml-2 block w-[90px] sm:w-[110px] lg:w-[114px] h-auto"
/>
</Link>
<div className="hidden lg:flex items-center gap-6">
{NAV_LINKS.map((item) => (
<NavLinkItem key={item.label} item={item} />
))}
</div>
<div className="nav-dashboard hidden lg:block">
<motion.div
initial="initial"
whileHover="hover"
whileTap={{ scale: 0.97 }}
className="relative"
>
<Link
href="https://useautumn.com"
target="_blank"
className="relative overflow-hidden inline-flex items-center gap-2 bg-[#9564ff] hover:bg-[#7D46F4] active:bg-[#7D46F4] transition-colors duration-300 px-4 py-3.5 text-white cursor-pointer whitespace-nowrap"
onMouseEnter={() => dashboardIconRef.current?.restart()}
onMouseLeave={() => dashboardIconRef.current?.reverse()}
>
<CTALines />
<div className="relative z-10 flex items-center gap-2">
<DashboardIconPixel
Icon={IconDashboard}
ref={dashboardIconRef}
/>
<span className="font-sans font-medium tracking-tight">
Dashboard
</span>
</div>
</Link>
</motion.div>
</div>
<motion.button
className="lg:hidden mr-2 p-1.5 text-[#FFFFFF99] cursor-pointer"
onClick={() => setMenuOpen(!menuOpen)}
whileTap={{ scale: 0.9 }}
aria-label="Toggle menu"
>
<MenuGridIcon isOpen={menuOpen} />
</motion.button>
</nav>
</div>
<div
className={`fixed nav-mobile inset-x-0 overflow-y-auto overflow-x-hidden ${scrolled && !recoilHidden ? "top-[58px] sm:top-[60px]" : "top-[66px] sm:top-[62px]"} bg-[#000000] flex flex-col font-mono uppercase lg:top-5 h-[calc(100dvh-58px)] z-40 px-4 md:px-(--page-pad) pb-8 transition-all duration-300`}
style={{
opacity: 0,
pointerEvents: "none",
clipPath: "inset(0% 0 100% 0)",
}}
>
{/* Nav items */}
<div className="flex flex-col">
{NAV_LINKS.map((item) => {
const isAnchor = item.href.startsWith("#");
const isExternal = item.href.startsWith("http");
return (
<Link
key={item.label}
href={item.href}
target={isExternal ? "_blank" : undefined}
onClick={isAnchor ? (e) => {
e.preventDefault();
setMenuOpen(false);
const target = document.querySelector(item.href);
if (target) target.scrollIntoView({ behavior: "smooth" });
} : undefined}
className="flex items-center gap-4 px-4 py-3.5 border-b border-[#292929] active:bg-[#141414ea] text-[#ffffff99] hover:text-white active:text-white transition-colors text-sm tracking-[-1%]"
>
<item.Icon className="h-3.5 w-3.5 shrink-0" />
<span>{item.label}</span>
</Link>
);
})}
</div>
<div className="flex flex-col mt-auto -mb-2.5">
<div className="border-t border-[#292929] py-1.5" />
<div className="px-4">
<Link
href="https://useautumn.com"
target="_blank"
onClick={() => setMenuOpen(false)}
className="flex items-center justify-between gap-4 px-2 py-2.5 bg-[#9564ff] active:bg-[#7D46F4] transition-colors duration-300 text-white text-sm tracking-widest"
>
<span className="tracking-[-2%] text-sm">Start for free</span>
<IconCTAStart className="h-3.5 w-3.5" />
</Link>
</div>
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="border-b border-[#292929] py-1.5" />
))}
</div>
</div>
{!scrolled && (
<div className="absolute pointer-events-none left-0 border-b border-[#292929] w-full z-50" />
)}
</div>
);
}

View File

@@ -0,0 +1,453 @@
"use client";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import {
forwardRef,
type MouseEvent,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
import {
CTALines,
IconBlog,
IconCTAStart,
IconDashboard,
IconDiscord,
IconDocs,
IconPricing,
MenuGridIcon,
} from "@/app/constant";
import type {
PageStyle,
PixelHoverHandle,
PixelIconComponent,
} from "@/lib/types";
import { cn } from "@/lib/utils";
import { DashboardIconPixel } from "./dashboard-icon-pixel";
const NAV_LINKS = [
{ label: "Docs", href: "https://docs.useautumn.com/welcome", Icon: IconDocs },
{ label: "Blog", href: "/blog", Icon: IconBlog },
{ label: "Pricing", href: "#pricing", Icon: IconPricing },
{
label: "Discord",
href: "https://discord.com/invite/STqxY92zuS",
Icon: IconDiscord,
},
];
const NavIconPixel = forwardRef<PixelHoverHandle, { Icon: PixelIconComponent }>(
function NavIconPixel({ Icon }, ref) {
const iconRef = useRef<SVGSVGElement | null>(null);
const tlRef = useRef<gsap.core.Timeline | null>(null);
useImperativeHandle(ref, () => ({
restart: () => tlRef.current?.play(),
reverse: () => tlRef.current?.reverse(),
}));
useEffect(() => {
const pixelEls =
iconRef.current?.querySelectorAll<SVGPathElement>(".icon-pixel-path");
if (!pixelEls?.length) return;
const pixels = Array.from(pixelEls).sort((a, b) => {
const aBox = a.getBBox();
const bBox = b.getBBox();
return (
aBox.x +
aBox.width / 2 -
(aBox.y + aBox.height / 2) -
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
);
});
gsap.set(pixels, {
opacity: 0.15,
scale: 0.8,
transformOrigin: "left bottom",
fill: "currentColor",
});
tlRef.current = gsap.timeline({ paused: true });
tlRef.current
.to(pixels, {
opacity: 1,
scale: 1.15,
fill: "#FFFFFF",
duration: 0.01,
stagger: 0.025,
ease: "power2.out",
})
.to(pixels, {
scale: 1,
duration: 0.01,
ease: "back.out(3)",
});
return () => {
tlRef.current?.kill();
};
}, []);
return (
<span className="group/icon relative inline-flex h-8 w-8 items-center justify-center">
<div className="relative flex h-[16px] w-[16px] items-center justify-center">
<Icon ref={iconRef} className="h-full w-full text-white" />
</div>
</span>
);
},
);
NavIconPixel.displayName = "NavIconPixel";
function NavLinkItem({ item }: { item: (typeof NAV_LINKS)[number] }) {
const iconRef = useRef<PixelHoverHandle | null>(null);
const isAnchor = item.href.startsWith("#");
const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {
if (!isAnchor) return;
e.preventDefault();
const target = document.querySelector(item.href);
if (target) target.scrollIntoView({ behavior: "smooth" });
};
return (
<div className="nav-link">
<Link
href={item.href}
onClick={handleClick}
className="group inline-flex items-center py-2 text-[#FFFFFF99] hover:text-white transition-colors"
onMouseEnter={() => iconRef.current?.restart()}
onMouseLeave={() => iconRef.current?.reverse()}
>
<NavIconPixel Icon={item.Icon} ref={iconRef} />
<span className="font-mono text-[14px] uppercase tracking-widest transition-colors group-hover:text-white">
{item.label}
</span>
</Link>
</div>
);
}
export default function Navbar({
animateIntro = true,
}: {
animateIntro?: boolean;
}) {
const containerRef = useRef<HTMLDivElement | null>(null);
const dashboardIconRef = useRef<PixelHoverHandle | null>(null);
const RECOIL_DELAY = 1000;
const [recoilHidden, setRecoilHidden] = useState(false);
const recoilTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [menuOpen, setMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 10);
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
useEffect(() => {
const onRecoil = () => {
setRecoilHidden(true);
if (recoilTimerRef.current) clearTimeout(recoilTimerRef.current);
recoilTimerRef.current = setTimeout(
() => setRecoilHidden(false),
RECOIL_DELAY,
);
};
window.addEventListener("elastic-recoil", onRecoil);
return () => {
window.removeEventListener("elastic-recoil", onRecoil);
if (recoilTimerRef.current) clearTimeout(recoilTimerRef.current);
};
}, []);
// Lock body scroll while mobile menu is open
useEffect(() => {
document.documentElement.style.overflow = menuOpen ? "hidden" : "";
document.body.style.overflow = menuOpen ? "hidden" : "";
return () => {
document.documentElement.style.overflow = "";
document.body.style.overflow = "";
};
}, [menuOpen]);
useGSAP(
() => {
if (!animateIntro) return;
gsap.set(".nav-root", { opacity: 0 });
gsap.set(".nav-logo", {
opacity: 0,
filter: "blur(6px) brightness(1)",
scale: 0.92,
transformOrigin: "left center",
});
gsap.set(".nav-link", { opacity: 0, y: -8 });
gsap.set(".nav-dashboard", { opacity: 0, scale: 0.95 });
const tl = gsap.timeline({ defaults: { overwrite: "auto" } });
tl.to(".nav-root", { opacity: 1, duration: 0.3, ease: "none" })
.to(".nav-logo", {
opacity: 1,
filter: "blur(0px) brightness(1)",
scale: 1,
duration: 0.7,
ease: "power2.out",
})
.to(".nav-logo", {
filter: "blur(0px) brightness(1.6)",
duration: 0.225,
ease: "power2.in",
})
.to(".nav-logo", {
filter: "blur(0px) brightness(1)",
duration: 0.125,
ease: "power2.out",
})
.to(
".nav-link",
{
opacity: 1,
y: 0,
duration: 0.25,
stagger: 0.06,
ease: "power2.out",
},
"-=0.05",
)
.to(
".nav-dashboard",
{
opacity: 1,
scale: 1,
duration: 0.3,
ease: "back.out(1.5)",
},
"-=0.1",
);
},
{ scope: containerRef, dependencies: [animateIntro] },
);
const mobileTl = useRef<gsap.core.Timeline | null>(null);
useGSAP(
() => {
gsap.set(".nav-mobile", {
opacity: 0,
clipPath: "inset(0% 0 100% 0)",
pointerEvents: "none",
});
gsap.set(
".nav-mobile a, .nav-mobile img, .nav-mobile button, .nav-mobile .border-b",
{
opacity: 0,
filter: "blur(8px)",
scale: 0.95,
},
);
mobileTl.current = gsap.timeline({
paused: true,
defaults: { overwrite: "auto" },
});
mobileTl.current
.to(".nav-mobile", {
opacity: 1,
y: 0,
pointerEvents: "auto",
clipPath: "inset(0% 0 0% 0)",
duration: 0.65,
ease: "power3.inOut",
})
.to(
".nav-mobile a, .nav-mobile img, .nav-mobile button, .nav-mobile .border-b",
{
opacity: 1,
filter: "blur(0px)",
scale: 1,
duration: 0.4,
stagger: 0.02,
ease: "power2.out",
},
"-=0.35",
);
},
{ scope: containerRef },
);
useGSAP(
() => {
if (mobileTl.current) {
if (menuOpen) {
mobileTl.current.timeScale(1).play();
} else {
mobileTl.current.timeScale(1.8).reverse();
}
}
},
{ scope: containerRef, dependencies: [menuOpen] },
);
return (
<div
style={
{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" } as PageStyle
}
ref={containerRef}
>
<div className="absolute pointer-events-none left-0 border-t border-[#292929] w-screen z-50" />
{scrolled && !recoilHidden && (
<div className="h-[56px] md:h-[44px] w-fit" />
)}
<div
className={cn(
scrolled && !recoilHidden
? "fixed top-0 left-0 z-80 w-full border-t border-b border-[#292929] bg-[#0F0F0F] px-4 pt-4 backdrop-blur-md md:px-(--page-pad)"
: "relative",
)}
>
{scrolled && !recoilHidden && (
<>
<div className="absolute pointer-events-none top-4 left-0 w-full border-t border-[#292929]" />
<div className="absolute pointer-events-none left-4 md:left-(--page-pad) top-0 bottom-0 border-l border-[#292929]" />
<div className="absolute pointer-events-none right-4 md:right-(--page-pad) top-0 bottom-0 border-r border-[#292929]" />
</>
)}
<nav className="nav-root bg-[#0F0F0F] flex items-center justify-between font-mono uppercase text-xs pl-2 md:pl-0 h-[44px] md:h-[44px] lg:pb-0 xl:pb-0">
<Link href={"/"}>
<Image
src="/images/navbar/autumnlogo.svg"
width={114}
height={28}
alt="Autumn"
loading="lazy"
className="nav-logo ml-2 block w-[90px] sm:w-[110px] lg:w-[114px] h-auto"
/>
</Link>
<div className="hidden lg:flex items-center gap-6">
{NAV_LINKS.map((item) => (
<NavLinkItem key={item.label} item={item} />
))}
</div>
<div className="nav-dashboard hidden lg:block">
<motion.div
initial="initial"
whileHover="hover"
whileTap={{ scale: 0.97 }}
className="relative"
>
<Link
href="https://app.useautumn.com"
target="_blank"
className="relative overflow-hidden inline-flex items-center gap-2 bg-[#9564ff] hover:bg-[#7D46F4] active:bg-[#7D46F4] transition-colors duration-300 px-4 py-3.5 text-white cursor-pointer whitespace-nowrap"
onMouseEnter={() => dashboardIconRef.current?.restart()}
onMouseLeave={() => dashboardIconRef.current?.reverse()}
>
<CTALines />
<div className="relative z-10 flex items-center gap-2">
<DashboardIconPixel
Icon={IconDashboard}
ref={dashboardIconRef}
/>
<span className="font-sans font-medium tracking-tight">
Dashboard
</span>
</div>
</Link>
</motion.div>
</div>
<motion.button
className="lg:hidden mr-2 p-1.5 text-[#FFFFFF99] cursor-pointer"
onClick={() => setMenuOpen(!menuOpen)}
whileTap={{ scale: 0.9 }}
aria-label="Toggle menu"
>
<MenuGridIcon isOpen={menuOpen} />
</motion.button>
</nav>
</div>
<div
className={cn(
"fixed nav-mobile inset-x-0 z-40 flex h-[calc(100dvh-58px)] flex-col overflow-x-hidden overflow-y-auto bg-[#000000] px-4 pb-8 font-mono uppercase transition-all duration-300 md:px-(--page-pad) lg:top-5",
scrolled && !recoilHidden
? "top-[58px] sm:top-[60px]"
: "top-[66px] sm:top-[62px]",
)}
style={{
opacity: 0,
pointerEvents: "none",
clipPath: "inset(0% 0 100% 0)",
}}
>
{/* Nav items */}
<div className="flex flex-col">
{NAV_LINKS.map((item) => {
const isAnchor = item.href.startsWith("#");
const isExternal = item.href.startsWith("http");
return (
<Link
key={item.label}
href={item.href}
target={isExternal ? "_blank" : undefined}
onClick={
isAnchor
? (e) => {
e.preventDefault();
setMenuOpen(false);
const target = document.querySelector(item.href);
if (target)
target.scrollIntoView({ behavior: "smooth" });
}
: undefined
}
className="flex items-center gap-4 px-4 py-3.5 border-b border-[#292929] active:bg-[#141414ea] text-[#ffffff99] hover:text-white active:text-white transition-colors text-sm tracking-[-1%]"
>
<item.Icon className="h-3.5 w-3.5 shrink-0" />
<span>{item.label}</span>
</Link>
);
})}
</div>
<div className="flex flex-col mt-auto -mb-2.5">
<div className="border-t border-[#292929] py-1.5" />
<div className="px-4">
<Link
href="https://useautumn.com"
target="_blank"
onClick={() => setMenuOpen(false)}
className="flex items-center justify-between gap-4 px-2 py-2.5 bg-[#9564ff] active:bg-[#7D46F4] transition-colors duration-300 text-white text-sm tracking-widest"
>
<span className="tracking-[-2%] text-sm">Start for free</span>
<IconCTAStart className="h-3.5 w-3.5" />
</Link>
</div>
<div className="border-b border-[#292929] py-1.5" />
<div className="border-b border-[#292929] py-1.5" />
<div className="border-b border-[#292929] py-1.5" />
</div>
</div>
{!scrolled && (
<div className="absolute pointer-events-none left-0 border-b border-[#292929] w-full z-50" />
)}
</div>
);
}

View File

@@ -1,238 +0,0 @@
"use client";
import { useEffect, useRef, useState } from "react";
import gsap from "gsap";
import {
PRELOADER_LOGO_PATH,
PRELOADER_LOGO_VIEWBOX,
PreloaderLogo,
} from "@/app/constant";
export default function Preloader() {
const wrapperRef = useRef(null);
const logoWrapRef = useRef(null);
const gridWrapRef = useRef(null);
const blackCoverRef = useRef(null);
const fallbackRef = useRef(null);
const [done, setDone] = useState(false);
if (typeof window !== "undefined") {
window.history.scrollRestoration = "manual";
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
}
useEffect(() => {
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
if (fallbackRef.current) {
fallbackRef.current.style.opacity = "0";
}
const preventScroll = (e) => {
e.preventDefault();
e.stopPropagation();
};
window.addEventListener("wheel", preventScroll, {
passive: false,
capture: true,
});
window.addEventListener("touchmove", preventScroll, {
passive: false,
capture: true,
});
const wrapper = wrapperRef.current;
const logoWrap = logoWrapRef.current;
const gridWrap = gridWrapRef.current;
const blackCover = blackCoverRef.current;
gsap.set(gridWrap, {
clipPath: "inset(0 100% 0 0)",
willChange: "clip-path",
force3D: true,
});
gsap.set(blackCover, { opacity: 0, willChange: "opacity" });
const logoSvg = logoWrap.querySelector("svg");
gsap.set(logoSvg, { opacity: 0 });
const canvas = document.createElement("canvas");
Object.assign(canvas.style, {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
pointerEvents: "none",
});
wrapper.appendChild(canvas);
const cleanup = () => {
document.documentElement.style.overflow = "";
document.body.style.overflow = "";
document.body.style.paddingRight = "";
window.removeEventListener("wheel", preventScroll, { capture: true });
window.removeEventListener("touchmove", preventScroll, {
capture: true,
});
if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
};
const runExitSequence = () => {
const tl = gsap.timeline({ defaults: { overwrite: "auto" } });
tl.to(canvas, { opacity: 0, duration: 0.35, ease: "power2.out" })
.to(
gridWrap,
{
clipPath: "inset(0 0% 0 0)",
duration: 0.35,
ease: "power2.out",
force3D: true,
},
"<",
)
.to(blackCover, {
opacity: 1,
duration: 0.45,
ease: "power2.inOut",
})
.to(wrapper, {
opacity: 0,
duration: 0.45,
ease: "power2.inOut",
onStart: () => {
window.dispatchEvent(new CustomEvent("preloader:complete"));
},
onComplete: () => {
cleanup();
setDone(true);
},
});
};
gsap.to(gridWrap, {
clipPath: "inset(0 12% 0 0)",
duration: 0.8,
ease: "power2.in",
force3D: true,
});
setTimeout(() => {
const W = PRELOADER_LOGO_VIEWBOX.width;
const H = PRELOADER_LOGO_VIEWBOX.height;
const offscreen = document.createElement("canvas");
offscreen.width = W;
offscreen.height = H;
const octx = offscreen.getContext("2d");
const path2d = new Path2D(PRELOADER_LOGO_PATH);
octx.fillStyle = "white";
octx.fill(path2d);
const { data } = octx.getImageData(0, 0, W, H);
const dpr = window.devicePixelRatio || 1;
const displayW =
logoSvg.offsetWidth || logoSvg.getBoundingClientRect().width || 54;
const displayH = Math.round((displayW / W) * H);
const isMobile = displayW < 70;
const STEP = isMobile ? 6 : 4;
const DOT_R = isMobile
? Math.max(0.6, (displayW / W) * STEP * 0.3)
: Math.max(1, (displayW / W) * STEP * 0.45);
const dots = [];
for (let y = 0; y < H; y += STEP) {
for (let x = 0; x < W; x += STEP) {
const i = (y * W + x) * 4;
if (data[i + 3] > 40) {
dots.push({ nx: x / W, ny: y / H });
}
}
}
for (let i = dots.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[dots[i], dots[j]] = [dots[j], dots[i]];
}
canvas.width = displayW * dpr;
canvas.height = displayH * dpr;
canvas.style.width = `${displayW}px`;
canvas.style.height = `${displayH}px`;
const ctx = canvas.getContext("2d");
ctx.scale(dpr, dpr);
let painted = 0;
const total = dots.length;
const progress = { value: 0 };
gsap.to(progress, {
value: 1,
duration: 1.05,
ease: "power1.inOut",
onUpdate() {
const target = Math.floor(progress.value * total);
while (painted < target) {
const d = dots[painted];
ctx.beginPath();
ctx.arc(d.nx * displayW, d.ny * displayH, DOT_R, 0, Math.PI * 2);
ctx.fillStyle = "#9564FF";
ctx.fill();
painted++;
}
},
onComplete() {
gsap.delayedCall(0.2, runExitSequence);
},
});
}, 120);
return () => {
cleanup();
};
}, []);
if (done) return null;
return (
<div
ref={wrapperRef}
className="fixed inset-0 z-[9999] bg-black"
aria-hidden="true"
>
<div
ref={fallbackRef}
className="absolute inset-0 flex items-center justify-center z-10 pointer-events-none transition-opacity duration-300"
>
<PreloaderLogo className="w-[53.57px] md:w-[97.94px] h-auto opacity-20" />
</div>
<div className="absolute inset-0 flex items-center justify-center">
<div ref={logoWrapRef}>
<PreloaderLogo className="w-[53.57px] md:w-[97.94px] h-auto" />
</div>
</div>
<div ref={gridWrapRef} className="absolute bottom-0 left-0 w-full">
<img
src="/images/preloader/grid.png"
width={1440}
height={217}
alt=""
className="w-full h-auto"
/>
</div>
<div
ref={blackCoverRef}
className="absolute inset-0 bg-black pointer-events-none"
/>
</div>
);
}

View File

@@ -0,0 +1,244 @@
"use client";
import gsap from "gsap";
import { useEffect, useRef, useState } from "react";
import {
PRELOADER_LOGO_PATH,
PRELOADER_LOGO_VIEWBOX,
PreloaderLogo,
} from "@/app/constant";
export default function Preloader() {
const wrapperRef = useRef<HTMLDivElement | null>(null);
const logoWrapRef = useRef<HTMLDivElement | null>(null);
const gridWrapRef = useRef<HTMLDivElement | null>(null);
const blackCoverRef = useRef<HTMLDivElement | null>(null);
const fallbackRef = useRef<HTMLDivElement | null>(null);
const [done, setDone] = useState(false);
if (typeof window !== "undefined") {
window.history.scrollRestoration = "manual";
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
}
useEffect(() => {
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
if (fallbackRef.current) {
fallbackRef.current.style.opacity = "0";
}
const preventScroll = (e: Event) => {
e.preventDefault();
e.stopPropagation();
};
window.addEventListener("wheel", preventScroll, {
passive: false,
capture: true,
});
window.addEventListener("touchmove", preventScroll, {
passive: false,
capture: true,
});
const wrapper = wrapperRef.current;
const logoWrap = logoWrapRef.current;
const gridWrap = gridWrapRef.current;
const blackCover = blackCoverRef.current;
if (!wrapper || !logoWrap || !gridWrap || !blackCover) return;
gsap.set(gridWrap, {
clipPath: "inset(0 100% 0 0)",
willChange: "clip-path",
force3D: true,
});
gsap.set(blackCover, { opacity: 0, willChange: "opacity" });
const logoSvg = logoWrap.querySelector<SVGSVGElement>("svg");
if (!logoSvg) return;
gsap.set(logoSvg, { opacity: 0 });
const canvas = document.createElement("canvas");
Object.assign(canvas.style, {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
pointerEvents: "none",
});
wrapper.appendChild(canvas);
const cleanup = () => {
document.documentElement.style.overflow = "";
document.body.style.overflow = "";
document.body.style.paddingRight = "";
window.removeEventListener("wheel", preventScroll, { capture: true });
window.removeEventListener("touchmove", preventScroll, {
capture: true,
});
if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
};
const runExitSequence = () => {
const tl = gsap.timeline({ defaults: { overwrite: "auto" } });
tl.to(canvas, { opacity: 0, duration: 0.35, ease: "power2.out" })
.to(
gridWrap,
{
clipPath: "inset(0 0% 0 0)",
duration: 0.35,
ease: "power2.out",
force3D: true,
},
"<",
)
.to(blackCover, {
opacity: 1,
duration: 0.45,
ease: "power2.inOut",
})
.to(wrapper, {
opacity: 0,
duration: 0.45,
ease: "power2.inOut",
onStart: () => {
window.dispatchEvent(new CustomEvent("preloader:complete"));
},
onComplete: () => {
cleanup();
setDone(true);
},
});
};
gsap.to(gridWrap, {
clipPath: "inset(0 12% 0 0)",
duration: 0.8,
ease: "power2.in",
force3D: true,
});
setTimeout(() => {
const W = PRELOADER_LOGO_VIEWBOX.width;
const H = PRELOADER_LOGO_VIEWBOX.height;
const offscreen = document.createElement("canvas");
offscreen.width = W;
offscreen.height = H;
const octx = offscreen.getContext("2d");
if (!octx) return;
const path2d = new Path2D(PRELOADER_LOGO_PATH);
octx.fillStyle = "white";
octx.fill(path2d);
const { data } = octx.getImageData(0, 0, W, H);
const dpr = window.devicePixelRatio || 1;
const displayW =
logoWrap.getBoundingClientRect().width ||
logoSvg.getBoundingClientRect().width ||
54;
const displayH = Math.round((displayW / W) * H);
const isMobile = displayW < 70;
const STEP = isMobile ? 6 : 4;
const DOT_R = isMobile
? Math.max(0.6, (displayW / W) * STEP * 0.3)
: Math.max(1, (displayW / W) * STEP * 0.45);
const dots: Array<{ nx: number; ny: number }> = [];
for (let y = 0; y < H; y += STEP) {
for (let x = 0; x < W; x += STEP) {
const i = (y * W + x) * 4;
if (data[i + 3] > 40) {
dots.push({ nx: x / W, ny: y / H });
}
}
}
for (let i = dots.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[dots[i], dots[j]] = [dots[j], dots[i]];
}
canvas.width = displayW * dpr;
canvas.height = displayH * dpr;
canvas.style.width = `${displayW}px`;
canvas.style.height = `${displayH}px`;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.scale(dpr, dpr);
let painted = 0;
const total = dots.length;
const progress = { value: 0 };
gsap.to(progress, {
value: 1,
duration: 1.05,
ease: "power1.inOut",
onUpdate() {
const target = Math.floor(progress.value * total);
while (painted < target) {
const d = dots[painted];
ctx.beginPath();
ctx.arc(d.nx * displayW, d.ny * displayH, DOT_R, 0, Math.PI * 2);
ctx.fillStyle = "#9564FF";
ctx.fill();
painted++;
}
},
onComplete() {
gsap.delayedCall(0.2, runExitSequence);
},
});
}, 120);
return () => {
cleanup();
};
}, []);
if (done) return null;
return (
<div
ref={wrapperRef}
className="fixed inset-0 z-[9999] bg-black"
aria-hidden="true"
>
<div
ref={fallbackRef}
className="absolute inset-0 flex items-center justify-center z-10 pointer-events-none transition-opacity duration-300"
>
<PreloaderLogo className="w-[53.57px] md:w-[97.94px] h-auto opacity-20" />
</div>
<div className="absolute inset-0 flex items-center justify-center">
<div ref={logoWrapRef}>
<PreloaderLogo className="w-[53.57px] md:w-[97.94px] h-auto" />
</div>
</div>
<div ref={gridWrapRef} className="absolute bottom-0 left-0 w-full">
<img
src="/images/preloader/grid.png"
width={1440}
height={217}
alt=""
className="w-full h-auto"
/>
</div>
<div
ref={blackCoverRef}
className="absolute inset-0 bg-black pointer-events-none"
/>
</div>
);
}

View File

@@ -1,252 +0,0 @@
"use client";
import { CTALines, IconCTAStart } from "@/app/constant";
import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
const sidebarItems = [
{
id: "subscriptions",
label: "Subscriptions",
model: 0,
desc: "Monthly or yearly plans with feature gating. Upgrades and downgrades handled automatically. Proration included.",
},
{
id: "free-trials",
label: "Free Trials",
model: 2,
desc: "Card-required or card-optional trials. Auto-convert to paid. Configurable trial lengths per plan.",
},
{
id: "credits",
label: "Credits & Top-ups",
model: 1,
desc: "Prepaid credits that features draw from. One-time purchases or auto-refill. Set minimum thresholds.",
},
{
id: "usage",
label: "Usage-Based",
model: 4,
desc: "Pay for what you use. Real-time metering, overage handling, usage resets. Combine with base\u00a0subscriptions.",
},
{
id: "seat",
label: "Seat-Based",
model: 6,
desc: "Per-user pricing with seat limits. Add or remove seats dynamically. Automatic proration.",
},
{
id: "hybrid",
label: "Hybrid Models",
model: 5,
desc: "Mix subscriptions, usage, credits, and seats in one plan. Example: $50/month + $0.02/token + 5 seats included.",
},
{
id: "rollovers",
label: "Rollovers & Expirations",
model: 7,
desc: "Roll credits to next period or expire after X days. Configure per plan or per feature.",
},
{
id: "enterprise",
label: "Custom Enterprise",
model: 3,
desc: "One-off pricing for large customers. Unique limits, custom billing cycles, manual overrides—all in the dashboard.",
},
];
export default function PricingModels() {
const [activeTab, setActiveTab] = useState(sidebarItems[0]);
const images = {
0: "/images/pricing-models/Subscriptions.webp",
1: "/images/pricing-models/Subscriptions (1).webp",
2: "/images/pricing-models/Trial Configuration.webp",
3: "/images/pricing-models/Hybrid Plan Builder.webp",
4: "/images/pricing-models/Usage Metering.webp",
5: "/images/pricing-models/Hybrid Plan Builder (1).webp",
6: "/images/pricing-models/Subscriptions (2).webp",
7: "/images/pricing-models/Subscriptions (3).webp",
};
return (
<section className="bg-[#000000] w-full overflow-hidden flex flex-col">
<div className="hidden lg:flex w-full items-center justify-between py-12 xl:py-24 relative z-10 px-4 xl:px-22.75">
<div className="flex w-full">
<div className="">
<h2 className="text-[40px] leading-[1.1] font-sans tracking-[-2%] font-normal">
<span className="text-[#FFFFFF99]">Any pricing model.</span>{" "}
<span className="text-white">Seriously.</span>
</h2>
<div className="mt-4 text-[16px] font-sans leading-relaxed tracking-[-1%] font-light">
<span className="text-[#FFFFFF99]">
Configure in the dashboard or CLI.
</span>{" "}
<span className="text-[#FFFFFF99]">Rollout to all customers, or create custom plans for your largest customers.</span>
</div>
</div>
</div>
<div className="hero-cta">
<Link href={"https://docs.useautumn.com/examples/monetary-credits"} target="_blank">
<motion.div
initial="initial"
whileHover="hover"
whileTap="tap"
className="relative"
>
<div className="relative overflow-hidden flex items-center cursor-pointer justify-between px-4 py-3.5 md:w-50 font-sans bg-[#9564ff] hover:bg-[#7D46F4] active:bg-[#7D46F4] transition-colors duration-300 whitespace-nowrap">
<CTALines />
<span className="relative z-10 tracking-tight text-white font-medium">
View templates
</span>
<span className="relative z-10">
<IconCTAStart />
</span>
</div>
</motion.div>
</Link>
</div>
</div>
<div className="flex lg:hidden flex-col px-4 py-12 relative z-10">
<h2 className="text-[28px] sm:text-[32px] leading-[1.1] font-sans tracking-[-2%] font-normal">
<span className="text-[#FFFFFF99]">Any pricing model.</span>{" "}
<span className="text-white">Seriously.</span>
</h2>
<div className="mt-4 text-[15px] font-sans leading-relaxed tracking-[-1%] font-light">
<span className="text-[#FFFFFF99]">
Configure in the dashboard or CLI.
</span>{" "}
<span className="text-[#FFFFFF99]">Rollout to all customers, or create custom plans for your largest customers.</span>
</div>
</div>
<div className="border-t-0 lg:border-t border-[#292929] w-full relative grid grid-cols-1 lg:grid-cols-[60px_220px_220px_1fr] xl:grid-cols-[90px_291px_300px_1fr] auto-rows-auto lg:grid-rows-[200px_260px] xl:grid-rows-[260px_340px]">
<div className="hidden lg:block border-l border-r border-b border-[#292929] min-h-[120px] lg:min-h-[260px]"></div>
<div className="hidden lg:block border-r border-b border-[#292929]"></div>
<div className="hidden z-20 lg:block border-r border-b bg-[#0F0F0F] border-[#292929]"></div>
<div className="hidden lg:flex lg:row-span-2 items-end justify-center lg:pl-4 lg:pr-4 relative z-10 w-full lg:h-full order-first lg:order-0 mt-0 xl:mt-10.5">
<div className="relative z-10 w-full lg:max-w-120 xl:max-w-150 h-auto overflow-hidden">
<div className="relative w-full aspect-square">
{Object.entries(images).map(([key, src]) => {
const isActive = activeTab.model === Number(key);
return (
<motion.div
key={key}
initial={false}
animate={{
opacity: isActive ? 1 : 0,
scale: isActive ? 1 : 0.98,
}}
transition={{ duration: 0.25 }}
className="absolute inset-0"
>
<Image
src={src}
alt="Pricing Model"
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-contain"
priority={key === "0"}
/>
</motion.div>
);
})}
</div>
</div>
</div>
<div className="hidden lg:block border-l border-r border-[#292929]"></div>
<div
className={`border-r border-[#292929] flex flex-col py-0 lg:py-[32px] z-10 relative bg-[#000000]`}
>
<ul className="flex flex-col">
{sidebarItems.map((item) => {
const isActive = activeTab.id === item.id;
return (
<li
key={item.id}
onClick={() => setActiveTab(item)}
className={`flex flex-col cursor-pointer transition-colors border-b last:border-b-0 lg:border-none border-[#292929] ${
isActive ? "lg:bg-transparent bg-[#0f0f0f]" : ""
}`}
>
{/* Mobile Accordion Image */}
<AnimatePresence>
{isActive && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="block lg:hidden w-full relative overflow-hidden"
>
<div className="relative w-full aspect-3/2.5 sm:aspect-square">
<div className="absolute bottom-0 left-0 right-0 z-10 flex items-center justify-center px-3 pt-2">
<img
src={images[item.model]}
alt="Pricing Model"
className="w-full h-auto max-h-full object-contain object-bottom"
/>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div
className={`text-[20px] lg:text-[20px] tracking-[-5%] leading-none lg:leading-[20px] flex items-center gap-2 font-sans px-4 lg:px-[14px] py-5 lg:py-1.5 ${
isActive
? "text-white lg:text-[#FFFFFF99]"
: "text-[#FFFFFF99] lg:text-[#FFFFFF99] lg:opacity-50"
}`}
>
<div className="w-[3px] h-[24px] hidden lg:block">
{isActive && (
<motion.div
layoutId="activeTabIndicator"
className="w-[3px] h-[24px] bg-[#9564FF]"
/>
)}
</div>
{item.label}
</div>
{/* Mobile Accordion Description */}
<AnimatePresence>
{isActive && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="lg:hidden px-4 border-b border-[#8752FA] overflow-hidden text-[#FFFFFF99] text-[14px] md:text-[16px] lg:text-[14px] leading-[1.4] tracking-[-2%] font-light text-pretty"
>
<div className="pb-6">{item.desc}</div>
</motion.div>
)}
</AnimatePresence>
</li>
);
})}
</ul>
</div>
<div className="hidden lg:flex border-b border-r lg:border-b-0 bg-[#0F0F0F] border-[#292929] flex-col justify-end p-6 z-10 relative">
<div className="text-[#FFFFFF99] text-[16px] font-sans leading-relaxed tracking-[-2%] font-light text-pretty">
<AnimatePresence mode="wait">
<motion.div
key={activeTab.id}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.2 }}
>
{activeTab.desc}
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,263 @@
"use client";
import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { CTALines, IconCTAStart } from "@/app/constant";
import { cn } from "@/lib/utils";
const sidebarItems = [
{
id: "subscriptions",
label: "Subscriptions",
model: 0,
desc: "Monthly or yearly plans with feature gating. Upgrades and downgrades handled automatically. Proration included.",
},
{
id: "free-trials",
label: "Free Trials",
model: 2,
desc: "Card-required or card-optional trials. Auto-convert to paid. Configurable trial lengths per plan.",
},
{
id: "credits",
label: "Credits & Top-ups",
model: 1,
desc: "Prepaid credits that features draw from. One-time purchases or auto-refill. Set minimum thresholds.",
},
{
id: "usage",
label: "Usage-Based",
model: 4,
desc: "Pay for what you use. Real-time metering, overage handling, usage resets. Combine with base\u00a0subscriptions.",
},
{
id: "seat",
label: "Seat-Based",
model: 6,
desc: "Per-user pricing with seat limits. Add or remove seats dynamically. Automatic proration.",
},
{
id: "hybrid",
label: "Hybrid Models",
model: 5,
desc: "Mix subscriptions, usage, credits, and seats in one plan. Example: $50/month + $0.02/token + 5 seats included.",
},
{
id: "rollovers",
label: "Rollovers & Expirations",
model: 7,
desc: "Roll credits to next period or expire after X days. Configure per plan or per feature.",
},
{
id: "enterprise",
label: "Custom Enterprise",
model: 3,
desc: "One-off pricing for large customers. Unique limits, custom billing cycles, manual overrides—all in the dashboard.",
},
] as const;
const images: Record<(typeof sidebarItems)[number]["model"], string> = {
0: "/images/pricing-models/Subscriptions.webp",
1: "/images/pricing-models/Subscriptions (1).webp",
2: "/images/pricing-models/Trial Configuration.webp",
3: "/images/pricing-models/Hybrid Plan Builder.webp",
4: "/images/pricing-models/Usage Metering.webp",
5: "/images/pricing-models/Hybrid Plan Builder (1).webp",
6: "/images/pricing-models/Subscriptions (2).webp",
7: "/images/pricing-models/Subscriptions (3).webp",
};
export default function PricingModels() {
const [activeTab, setActiveTab] = useState<(typeof sidebarItems)[number]>(
sidebarItems[0],
);
return (
<section className="bg-[#000000] w-full overflow-hidden flex flex-col">
<div className="hidden lg:flex w-full items-center justify-between py-12 xl:py-24 relative z-10 px-4 xl:px-22.75">
<div className="flex w-full">
<div className="">
<h2 className="text-[40px] leading-[1.1] font-sans tracking-[-2%] font-normal">
<span className="text-[#FFFFFF99]">Any pricing model.</span>{" "}
<span className="text-white">Seriously.</span>
</h2>
<div className="mt-4 text-[16px] font-sans leading-relaxed tracking-[-1%] font-light">
<span className="text-[#FFFFFF99]">
Configure in the dashboard or CLI.
</span>{" "}
<span className="text-[#FFFFFF99]">
Rollout to all customers, or create custom plans for your
largest customers.
</span>
</div>
</div>
</div>
<div className="hero-cta">
<Link
href={"https://docs.useautumn.com/examples/monetary-credits"}
target="_blank"
>
<motion.div
initial="initial"
whileHover="hover"
whileTap="tap"
className="relative"
>
<div className="relative overflow-hidden flex items-center cursor-pointer justify-between px-4 py-3.5 md:w-50 font-sans bg-[#9564ff] hover:bg-[#7D46F4] active:bg-[#7D46F4] transition-colors duration-300 whitespace-nowrap">
<CTALines />
<span className="relative z-10 tracking-tight text-white font-medium">
View templates
</span>
<span className="relative z-10">
<IconCTAStart />
</span>
</div>
</motion.div>
</Link>
</div>
</div>
<div className="flex lg:hidden flex-col px-4 py-12 relative z-10">
<h2 className="text-[28px] sm:text-[32px] leading-[1.1] font-sans tracking-[-2%] font-normal">
<span className="text-[#FFFFFF99]">Any pricing model.</span>{" "}
<span className="text-white">Seriously.</span>
</h2>
<div className="mt-4 text-[15px] font-sans leading-relaxed tracking-[-1%] font-light">
<span className="text-[#FFFFFF99]">
Configure in the dashboard or CLI.
</span>{" "}
<span className="text-[#FFFFFF99]">
Rollout to all customers, or create custom plans for your largest
customers.
</span>
</div>
</div>
<div className="border-t-0 lg:border-t border-[#292929] w-full relative grid grid-cols-1 lg:grid-cols-[60px_220px_220px_1fr] xl:grid-cols-[90px_291px_300px_1fr] auto-rows-auto lg:grid-rows-[200px_260px] xl:grid-rows-[260px_340px]">
<div className="hidden lg:block border-l border-r border-b border-[#292929] min-h-[120px] lg:min-h-[260px]"></div>
<div className="hidden lg:block border-r border-b border-[#292929]"></div>
<div className="hidden z-20 lg:block border-r border-b bg-[#0F0F0F] border-[#292929]"></div>
<div className="hidden lg:flex lg:row-span-2 items-end justify-center lg:pl-4 lg:pr-4 relative z-10 w-full lg:h-full order-first lg:order-0 mt-0 xl:mt-10.5">
<div className="relative z-10 w-full lg:max-w-120 xl:max-w-150 h-auto overflow-hidden">
<div className="relative w-full aspect-square">
{Object.entries(images).map(([key, src]) => {
const isActive = activeTab.model === Number(key);
return (
<motion.div
key={key}
initial={false}
animate={{
opacity: isActive ? 1 : 0,
scale: isActive ? 1 : 0.98,
}}
transition={{ duration: 0.25 }}
className="absolute inset-0"
>
<Image
src={src}
alt="Pricing Model"
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-contain"
priority={key === "0"}
/>
</motion.div>
);
})}
</div>
</div>
</div>
<div className="hidden lg:block border-l border-r border-[#292929]"></div>
<div className="relative z-10 flex flex-col border-r border-[#292929] bg-[#000000] py-0 lg:py-[32px]">
<ul className="flex flex-col">
{sidebarItems.map((item) => {
const isActive = activeTab.id === item.id;
return (
<li
key={item.id}
onClick={() => setActiveTab(item)}
className={cn(
"flex cursor-pointer flex-col border-b border-[#292929] transition-colors last:border-b-0 lg:border-none",
isActive && "bg-[#0f0f0f] lg:bg-transparent",
)}
>
{/* Mobile Accordion Image */}
<AnimatePresence>
{isActive && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="block lg:hidden w-full relative overflow-hidden"
>
<div className="relative w-full aspect-3/2.5 sm:aspect-square">
<div className="absolute bottom-0 left-0 right-0 z-10 flex items-center justify-center px-3 pt-2">
<img
src={images[item.model]}
alt="Pricing Model"
className="w-full h-auto max-h-full object-contain object-bottom"
/>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div
className={cn(
"flex items-center gap-2 px-4 py-5 font-sans text-[20px] leading-none tracking-[-5%] lg:px-[14px] lg:py-1.5 lg:text-[20px] lg:leading-[20px]",
isActive
? "text-white lg:text-[#FFFFFF99]"
: "text-[#FFFFFF99] lg:text-[#FFFFFF99] lg:opacity-50",
)}
>
<div className="w-[3px] h-[24px] hidden lg:block">
{isActive && (
<motion.div
layoutId="activeTabIndicator"
className="w-[3px] h-[24px] bg-[#9564FF]"
/>
)}
</div>
{item.label}
</div>
{/* Mobile Accordion Description */}
<AnimatePresence>
{isActive && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="lg:hidden px-4 border-b border-[#8752FA] overflow-hidden text-[#FFFFFF99] text-[14px] md:text-[16px] lg:text-[14px] leading-[1.4] tracking-[-2%] font-light text-pretty"
>
<div className="pb-6">{item.desc}</div>
</motion.div>
)}
</AnimatePresence>
</li>
);
})}
</ul>
</div>
<div className="hidden lg:flex border-b border-r lg:border-b-0 bg-[#0F0F0F] border-[#292929] flex-col justify-end p-6 z-10 relative">
<div className="text-[#FFFFFF99] text-[16px] font-sans leading-relaxed tracking-[-2%] font-light text-pretty">
<AnimatePresence mode="wait">
<motion.div
key={activeTab.id}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.2 }}
>
{activeTab.desc}
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
</section>
);
}

View File

@@ -1,177 +0,0 @@
"use client";
import { IconArrowRightSmall, IconTick } from "@/app/constant";
import { motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
const plans = [
{
name: "FREE",
price: "0",
description: "Perfect while finding PMF. Everything you need to start.",
features: [
"Up to 8K monthly revenue",
"All core features",
"Community support",
],
buttonText: "Get started",
href: "https://app.useautumn.com/sign-in",
isPro: false,
},
{
name: "PRO",
price: "375",
description: "For teams scaling with real usage-based pricing.",
features: [
"Up to 50K monthly revenue",
"Priority support",
"Custom plans",
"Usage analytics",
],
buttonText: "Start with Pro",
href: "https://app.useautumn.com/sign-in",
isPro: true,
},
{
name: "ENTERPRISE",
price: "Custom",
description: "For compliance, scale, or custom requirements.",
features: [
"Dedicated support",
"Multi-region",
"Compliance assistance",
],
buttonText: "Book a call",
href: "https://cal.com/ayrod/a?user=ayrod",
isPro: false,
},
];
export default function Pricing() {
return (
<>
<div id="pricing" className="min-h-screen relative flex w-full lg:w-[calc(100%+calc(var(--page-pad)*2))] lg:-ml-(--page-pad) items-center justify-center lg:py-24 pb-3">
{/* Desktop Background */}
<Image
src="/images/pricing/pricing.webp"
alt="pricing background desktop"
fill
className="object-cover absolute z-10 lg:z-50 hidden md:block"
loading="lazy"
/>
{/* Mobile Background */}
<Image
src="/images/pricing/pricing-mob.webp"
alt="pricing background mobile"
fill
className="object-cover absolute z-10 lg:z-50 block md:hidden"
loading="lazy"
/>
<div className="relative z-20 lg:z-60 w-full pt-0 lg:pt-8 pl-4 lg:pl-[calc(var(--page-pad)+22px)] xl:pl-[calc(var(--page-pad)+90px)] pr-4 lg:pr-[calc(var(--page-pad)+22px)] xl:pr-[calc(var(--page-pad)+90px)]">
<div className="lg:bg-black text-white lg:border lg:border-[#292929] flex flex-col gap-6 lg:gap-0 border-none">
{/* Header */}
<div className="bg-black lg:bg-transparent -mx-4 px-4 lg:mx-0 lg:px-8 py-10 lg:py-8 border-b-0 lg:border-b border-[#292929]">
<h1 className="text-[30px] leading-[32px] md:leading-[40px] md:text-3xl lg:text-[40px] tracking-[-4%] text-white font-normal w-[85%] md:w-full font-sans">
Start free. Scale with confidence.
</h1>
</div>
{/* Pricing Columns */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 relative z-60 gap-8 lg:gap-0">
{plans.map((plan, index) => (
<div
key={plan.name}
className={`relative flex flex-col bg-black lg:bg-transparent border lg:border-0 border-[#292929] ${index === 0 ? "lg:border-r" : ""
} ${index === 1 ? "lg:border-r" : ""
} ${index === 2 ? "md:col-span-2 lg:col-span-1 md:w-[calc(50%-16px)] md:justify-self-center lg:w-full lg:justify-self-auto" : ""}`}
>
{plan.isPro && (
<div className="hidden lg:block absolute -inset-px z-20 pointer-events-none border border-transparent [border-image:linear-gradient(to_bottom,#A175FF,#000000)_1]"></div>
)}
{plan.isPro && (
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 z-30 bg-black text-[14px] font-mono tracking-[-1%] text-white px-3 py-1.5 md:p-2.5 border border-[#A175FF]">
RECOMMENDED
</div>
)}
<div
className={
"flex flex-col h-full px-4 md:px-5 lg:px-8 pt-10 md:pt-10 lg:pt-15 pb-6 md:pb-0 relative z-10 w-full overflow-hidden"
}
>
<div className="mb-6">
<span className="inline-block px-3 py-1 bg-[#8752FA]/20 text-[#9564FF] text-[12px] md:text-[13px] lg:text-[16px] font-mono tracking-[-2%] uppercase mb-3 md:mb-2">
{plan.name}
</span>
<div className="flex items-start gap-1 mb-2">
{plan.price !== "Custom" && (
<span className="text-sm md:text-base lg:text-xl text-white mt-1.5 md:mt-0">
$
</span>
)}
<span className="text-[30px] md:text-[40px] leading-[44px] lg:text-5xl text-white font-normal tracking-[-2%] font-sans">
{plan.price}
</span>
{plan.price !== "Custom" && (
<span className="text-[#FFFFFF99] font-light self-end text-[13px] md:text-sm lg:text-base tracking-[-2%] mb-1.5 md:mb-0">
/month
</span>
)}
</div>
<p className="md:text-white font-light md:font-extralight text-[13px] md:text-[16px] tracking-[-2%] leading-[18px] md:leading-5 w-full md:w-[95%] text-pretty">
{plan.description}
</p>
</div>
<div className="border-t border-[#27272A] w-full mb-6"></div>
<ul className="flex flex-col gap-3 md:gap-4 grow mb-10 md:mb-16.5">
{plan.features.map((feature, i) => (
<li key={i} className="flex items-start gap-3">
<IconTick className="w-4 h-4 mt-[3px] shrink-0" />
<span className="md:text-white font-light text-[14px] md:text-[16px]">
{feature}
</span>
</li>
))}
</ul>
<div className="flex w-full mb-0 md:mb-8 mt-auto mx-auto pt-4 md:pt-0">
<motion.div
initial="initial"
whileHover="hover"
whileTap="hover"
className="w-full"
>
<Link
href={plan.href || "#"}
target="_blank"
className="group cursor-pointer w-full flex items-center md:items-stretch justify-between transition-colors duration-300 border bg-transparent py-1 md:py-0 hover:bg-[#7641E8] active:bg-[#7641E8] border-[#292929]"
>
<span className="text-white text-[16px] md:text-[18px] pl-4 flex items-center tracking-[-1%] font-sans">
{plan.buttonText}
</span>
<div className="flex items-center justify-center w-8 h-8 md:h-auto md:w-[26px] aspect-square transition-colors duration-300 m-1.5 md:m-2.5 bg-[#514D5A] text-white group-hover:bg-white group-hover:text-[#8752FA] group-active:bg-white group-active:text-[#8752FA]">
<IconArrowRightSmall className="w-4 h-4" />
</div>
</Link>
</motion.div>
</div>
</div>
</div>
))}
</div>
<div className="px-6 md:px-8 py-5 md:py-6 lg:mt-[32px] border border-[#292929] lg:border-x-0 lg:border-b-0 text-left md:text-center bg-black lg:bg-transparent">
<p className="text-white text-pretty font-light leading-[18px] tracking-[-2%] md:font-extralight text-[16px] md:leading-[1.6] text-wrap-balance">
Autumn is built on top of Stripe billing, so Stripe fees (0.7%
and 2.9% + 30¢) still apply.
</p>
</div>
</div>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,178 @@
"use client";
import { motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import { IconArrowRightSmall, IconTick } from "@/app/constant";
const plans = [
{
name: "FREE",
price: "0",
description: "Perfect while finding PMF. Everything you need to start.",
features: [
"Up to 8K monthly revenue",
"All core features",
"Community support",
],
buttonText: "Get started",
href: "https://app.useautumn.com/sign-in",
isPro: false,
},
{
name: "PRO",
price: "375",
description: "For teams scaling with real usage-based pricing.",
features: [
"Up to 50K monthly revenue",
"Priority support",
"Custom plans",
"Usage analytics",
],
buttonText: "Start with Pro",
href: "https://app.useautumn.com/sign-in",
isPro: true,
},
{
name: "ENTERPRISE",
price: "Custom",
description: "For compliance, scale, or custom requirements.",
features: ["Dedicated support", "Multi-region", "Compliance assistance"],
buttonText: "Book a call",
href: "https://cal.com/ayrod/a?user=ayrod",
isPro: false,
},
];
export default function Pricing() {
return (
<>
<div
id="pricing"
className="min-h-screen relative flex w-full lg:w-[calc(100%+calc(var(--page-pad)*2))] lg:-ml-(--page-pad) items-center justify-center lg:py-24 pb-3"
>
{/* Desktop Background */}
<Image
src="/images/pricing/pricing.webp"
alt="pricing background desktop"
fill
className="object-cover absolute z-10 lg:z-50 hidden md:block"
loading="lazy"
/>
{/* Mobile Background */}
<Image
src="/images/pricing/pricing-mob.webp"
alt="pricing background mobile"
fill
className="object-cover absolute z-10 lg:z-50 block md:hidden"
loading="lazy"
/>
<div className="relative z-20 lg:z-60 w-full pt-0 lg:pt-8 pl-4 lg:pl-[calc(var(--page-pad)+22px)] xl:pl-[calc(var(--page-pad)+90px)] pr-4 lg:pr-[calc(var(--page-pad)+22px)] xl:pr-[calc(var(--page-pad)+90px)]">
<div className="lg:bg-black text-white lg:border lg:border-[#292929] flex flex-col gap-6 lg:gap-0 border-none">
{/* Header */}
<div className="bg-black lg:bg-transparent -mx-4 px-4 lg:mx-0 lg:px-8 py-10 lg:py-8 border-b-0 lg:border-b border-[#292929]">
<h1 className="text-[30px] leading-[32px] md:leading-[40px] md:text-3xl lg:text-[40px] tracking-[-4%] text-white font-normal w-[85%] md:w-full font-sans">
Start free. Scale with confidence.
</h1>
</div>
{/* Pricing Columns */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 relative z-60 gap-8 lg:gap-0">
{plans.map((plan, index) => (
<div
key={plan.name}
className={`relative flex flex-col bg-black lg:bg-transparent border lg:border-0 border-[#292929] ${
index === 0 ? "lg:border-r" : ""
} ${
index === 1 ? "lg:border-r" : ""
} ${index === 2 ? "md:col-span-2 lg:col-span-1 md:w-[calc(50%-16px)] md:justify-self-center lg:w-full lg:justify-self-auto" : ""}`}
>
{plan.isPro && (
<div className="hidden lg:block absolute -inset-px z-20 pointer-events-none border border-transparent [border-image:linear-gradient(to_bottom,#A175FF,#000000)_1]"></div>
)}
{plan.isPro && (
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 z-30 bg-black text-[14px] font-mono tracking-[-1%] text-white px-3 py-1.5 md:p-2.5 border border-[#A175FF]">
RECOMMENDED
</div>
)}
<div
className={
"flex flex-col h-full px-4 md:px-5 lg:px-8 pt-10 md:pt-10 lg:pt-15 pb-6 md:pb-0 relative z-10 w-full overflow-hidden"
}
>
<div className="mb-6">
<span className="inline-block px-3 py-1 bg-[#8752FA]/20 text-[#9564FF] text-[12px] md:text-[13px] lg:text-[16px] font-mono tracking-[-2%] uppercase mb-3 md:mb-2">
{plan.name}
</span>
<div className="flex items-start gap-1 mb-2">
{plan.price !== "Custom" && (
<span className="text-sm md:text-base lg:text-xl text-white mt-1.5 md:mt-0">
$
</span>
)}
<span className="text-[30px] md:text-[40px] leading-[44px] lg:text-5xl text-white font-normal tracking-[-2%] font-sans">
{plan.price}
</span>
{plan.price !== "Custom" && (
<span className="text-[#FFFFFF99] font-light self-end text-[13px] md:text-sm lg:text-base tracking-[-2%] mb-1.5 md:mb-0">
/month
</span>
)}
</div>
<p className="md:text-white font-light md:font-extralight text-[13px] md:text-[16px] tracking-[-2%] leading-[18px] md:leading-5 w-full md:w-[95%] text-pretty">
{plan.description}
</p>
</div>
<div className="border-t border-[#27272A] w-full mb-6"></div>
<ul className="flex flex-col gap-3 md:gap-4 grow mb-10 md:mb-16.5">
{plan.features.map((feature, i) => (
<li key={i} className="flex items-start gap-3">
<IconTick className="w-4 h-4 mt-[3px] shrink-0" />
<span className="md:text-white font-light text-[14px] md:text-[16px]">
{feature}
</span>
</li>
))}
</ul>
<div className="flex w-full mb-0 md:mb-8 mt-auto mx-auto pt-4 md:pt-0">
<motion.div
initial="initial"
whileHover="hover"
whileTap="hover"
className="w-full"
>
<Link
href={plan.href || "#"}
target="_blank"
className="group cursor-pointer w-full flex items-center md:items-stretch justify-between transition-colors duration-300 border bg-transparent py-1 md:py-0 hover:bg-[#7641E8] active:bg-[#7641E8] border-[#292929]"
>
<span className="text-white text-[16px] md:text-[18px] pl-4 flex items-center tracking-[-1%] font-sans">
{plan.buttonText}
</span>
<div className="flex items-center justify-center w-8 h-8 md:h-auto md:w-[26px] aspect-square transition-colors duration-300 m-1.5 md:m-2.5 bg-[#514D5A] text-white group-hover:bg-white group-hover:text-[#8752FA] group-active:bg-white group-active:text-[#8752FA]">
<IconArrowRightSmall className="w-4 h-4" />
</div>
</Link>
</motion.div>
</div>
</div>
</div>
))}
</div>
<div className="px-6 md:px-8 py-5 md:py-6 lg:mt-[32px] border border-[#292929] lg:border-x-0 lg:border-b-0 text-left md:text-center bg-black lg:bg-transparent">
<p className="text-white text-pretty font-light leading-[18px] tracking-[-2%] md:font-extralight text-[16px] md:leading-[1.6] text-wrap-balance">
Autumn is built on top of Stripe billing, so Stripe fees (0.7%
and 2.9% + 30¢) still apply.
</p>
</div>
</div>
</div>
</div>
</>
);
}

View File

@@ -1,80 +0,0 @@
"use client";
const IMAGE_SOURCES = [
"/images/issues/Slack.svg",
"/images/issues/Frame%202147243001.svg",
"/images/issues/Frame%202147242729.svg",
"/images/issues/Slack-1.svg",
"/images/issues/Frame%202147243134.svg",
"/images/issues/Slack-2.svg",
"/images/issues/Frame%202147243146.svg",
"/images/issues/Stripe.svg",
];
const ITEM_COUNT = IMAGE_SOURCES.length;
const PAUSE_SECONDS = 2;
const SCROLL_SECONDS = 0.5;
const TOTAL_SECONDS = ITEM_COUNT * (PAUSE_SECONDS + SCROLL_SECONDS);
function buildKeyframes() {
const stepPct = 100 / ITEM_COUNT;
const pauseRatio = PAUSE_SECONDS / (PAUSE_SECONDS + SCROLL_SECONDS);
const perItemTranslate = 50 / ITEM_COUNT;
const frames = ["0% { transform: translateY(0); }"];
for (let i = 0; i < ITEM_COUNT; i++) {
const holdY = i * perItemTranslate;
const nextY = (i + 1) * perItemTranslate;
const pauseEndPct = (i * stepPct + stepPct * pauseRatio).toFixed(4);
const stepEndPct = ((i + 1) * stepPct).toFixed(4);
const holdTransform =
holdY === 0 ? "translateY(0)" : `translateY(-${holdY.toFixed(4)}%)`;
frames.push(`${pauseEndPct}% { transform: ${holdTransform}; }`);
frames.push(
`${stepEndPct}% { transform: translateY(-${nextY.toFixed(4)}%); }`,
);
}
return frames.join("\n ");
}
const keyframesCSS = buildKeyframes();
export default function ProblemAnimation() {
return (
<div className="relative w-full h-140 md:h-175 lg:h-175 xl:h-full bg-[#080808] overflow-hidden">
<style>{`
@keyframes carouselUp {
${keyframesCSS}
}
`}</style>
<div className="pointer-events-none absolute inset-x-0 top-0 h-20 z-10 bg-gradient-to-b from-[#080808] to-transparent" />
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-20 z-10 bg-gradient-to-t from-[#080808] to-transparent" />
<div
className="flex flex-col items-center will-change-transform"
style={{
animation: `carouselUp ${TOTAL_SECONDS}s ease-in-out infinite`,
}}
>
{[...IMAGE_SOURCES, ...IMAGE_SOURCES].map((src, i) => (
<div
key={i}
className="flex items-center justify-center shrink-0 w-full h-[80px] md:h-[140px]"
>
<img
src={src}
alt=""
className="block max-h-full max-w-[90%] object-contain"
draggable={false}
/>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,80 @@
"use client";
const IMAGE_SOURCES = [
"/images/issues/Slack.svg",
"/images/issues/Frame%202147243001.svg",
"/images/issues/Frame%202147242729.svg",
"/images/issues/Slack-1.svg",
"/images/issues/Frame%202147243134.svg",
"/images/issues/Slack-2.svg",
"/images/issues/Frame%202147243146.svg",
"/images/issues/Stripe.svg",
];
const ITEM_COUNT = IMAGE_SOURCES.length;
const PAUSE_SECONDS = 2;
const SCROLL_SECONDS = 0.5;
const TOTAL_SECONDS = ITEM_COUNT * (PAUSE_SECONDS + SCROLL_SECONDS);
function buildKeyframes() {
const stepPct = 100 / ITEM_COUNT;
const pauseRatio = PAUSE_SECONDS / (PAUSE_SECONDS + SCROLL_SECONDS);
const perItemTranslate = 50 / ITEM_COUNT;
const frames = ["0% { transform: translateY(0); }"];
for (let i = 0; i < ITEM_COUNT; i++) {
const holdY = i * perItemTranslate;
const nextY = (i + 1) * perItemTranslate;
const pauseEndPct = (i * stepPct + stepPct * pauseRatio).toFixed(4);
const stepEndPct = ((i + 1) * stepPct).toFixed(4);
const holdTransform =
holdY === 0 ? "translateY(0)" : `translateY(-${holdY.toFixed(4)}%)`;
frames.push(`${pauseEndPct}% { transform: ${holdTransform}; }`);
frames.push(
`${stepEndPct}% { transform: translateY(-${nextY.toFixed(4)}%); }`,
);
}
return frames.join("\n ");
}
const keyframesCSS = buildKeyframes();
export default function ProblemAnimation() {
return (
<div className="relative w-full h-140 md:h-175 lg:h-175 xl:h-full bg-[#080808] overflow-hidden">
<style>{`
@keyframes carouselUp {
${keyframesCSS}
}
`}</style>
<div className="pointer-events-none absolute inset-x-0 top-0 h-20 z-10 bg-gradient-to-b from-[#080808] to-transparent" />
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-20 z-10 bg-gradient-to-t from-[#080808] to-transparent" />
<div
className="flex flex-col items-center will-change-transform"
style={{
animation: `carouselUp ${TOTAL_SECONDS}s ease-in-out infinite`,
}}
>
{[...IMAGE_SOURCES, ...IMAGE_SOURCES].map((src, i) => (
<div
key={i}
className="flex items-center justify-center shrink-0 w-full h-[80px] md:h-[140px]"
>
<img
src={src}
alt=""
className="block max-h-full max-w-[90%] object-contain"
draggable={false}
/>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,100 @@
"use client";
import dynamic from "next/dynamic";
import { ProblemBgSvg } from "../app/constant";
const ProblemAnimation = dynamic(() => import("./problem-animation"), {
ssr: false,
});
export default function Problem() {
return (
<section className="bg-[#000000]">
{/* <div
className="w-full py-5 border-l border-r border-[#292929]"
style={{ backgroundColor: "rgba(229, 46, 185, 0.12)" }}
>
<div className="flex items-start md:items-center gap-2 pl-4 xl:pl-[90px]">
<Image
src="/images/problems/warning.svg"
width={18}
height={16}
alt="warning"
className="shrink-0 mt-1 md:mt-0"
style={{ width: "auto", height: "auto" }}
/>
<p className="text-[#D942B5] font-light tracking-[-2%] text-[14px] md:text-[16px] xl:text-[16px] leading-[18px] md:leading-[20px]">
<span className="font-light mr-1">The hidden complexity:</span>
Payment processors move money. They don&apos;t handle the state
management that happens between payments.
</p>
</div>
</div> */}
<div className="grid xl:grid-cols-2 gap-0 xl:h-[800px]">
{/* LEFT COLUMN */}
<div className="bg-[#0D0D0D] flex flex-col border-r border-[#292929] border-l border-l-[#292929] pb-[20px] md:pb-0">
<div className="flex flex-col mb-2 gap-3 pl-[28px] xl:pl-[90px] pr-6 xl:pr-8 pt-14 sm:pt-16 xl:pt-16 items-center xl:items-start">
<h2 className="font-normal tracking-[-4%] leading-[32px] xl:leading-[40px] mb-2 xl:mb-4 text-center xl:text-left">
<span className="block text-[#686868] text-[30px] md:text-[36px] xl:text-[40px]">
Hard to ship,
</span>
<span className="block text-white text-[30px] md:text-[36px] xl:text-[40px]">
harder to scale.
</span>
</h2>
<p className="text-[#888888] font-light text-[16px] md:text-[18px] xl:text-[16px] tracking-[-2%] leading-[20px] mb-8 xl:mb-10 max-w-sm md:max-w-lg xl:max-w-sm text-center xl:text-left">
Maintaining payment logic, customer balances and feature access
across pricing and product changes is months of work and
unreliable.
<span className="text-white">
{" "}
Autumn replaces all the billing code you're building yourself.
</span>
</p>
</div>
<div className="mt-auto">
<div className="grid grid-cols-2 xl:grid-cols-[max-content_max-content_max-content_auto] border-t border-b border-[#1E1E1E]">
{[
{ value: "36 months", label: "work per year" },
{ value: "7+ webhook", label: "events to handle" },
{ value: "Edge cases", label: "100s to debug" },
].map((stat, i) => (
<div
key={i}
className={`py-6 pr-9 border-[#292929] border-r
${i === 2 ? "col-span-2 xl:col-span-1 border-t xl:border-t-0 border-r-0 xl:border-r" : ""}
${i === 1 ? "border-r-0 xl:border-r" : ""}
${i === 0 || i === 2 ? "pl-4" : "pl-4"}
xl:pl-6 ${i === 0 ? "xl:pl-[90px]" : ""}`}
>
<p className="text-white tracking-[-5%] font-normal text-[18px] xl:text-[24px] leading-none">
{stat.value}
</p>
<p className="text-[#767676] font-light tracking-[-5%] text-[16px] xl:text-[18px] mt-1.5 leading-[20px] whitespace-nowrap">
{stat.label}
</p>
</div>
))}
<div className="hidden xl:flex flex-1 items-stretch">
{[...Array(4)].map((_, i) => (
<div
key={i}
className={`flex-1 ${i < 3 ? "border-r border-[#292929]" : ""}`}
/>
))}
</div>
</div>
<ProblemBgSvg />
</div>
</div>
{/* RIGHT COLUMN — problem animation */}
<div className="relative overflow-hidden">
<ProblemAnimation />
</div>
</div>
</section>
);
}

View File

@@ -1,229 +0,0 @@
"use client";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import Image from "next/image";
import { useEffect, useRef } from "react";
gsap.registerPlugin(ScrollTrigger);
const SCRAMBLE_CHARS =
"!@#$%^&*()_+-=[]{}|;:,.<>?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
function scrambleText(
el,
finalText,
{ charDuration = 60, cycleSpeed = 30 } = {},
) {
const len = finalText.length;
let settled = 0;
const timeouts = [];
const cycleInterval = setInterval(() => {
const display = finalText
.split("")
.map((ch, i) =>
i < settled
? ch
: SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)],
)
.join("");
el.textContent = display;
}, cycleSpeed);
for (let i = 0; i < len; i++) {
const t = setTimeout(
() => {
settled = i + 1;
if (settled === len) {
clearInterval(cycleInterval);
el.textContent = finalText;
}
},
(i + 1) * charDuration,
);
timeouts.push(t);
}
return function cleanup() {
clearInterval(cycleInterval);
timeouts.forEach(clearTimeout);
el.textContent = finalText;
};
}
const cards = [
{
bg: "#A175FF",
icon: "/images/production/uptime2.svg",
metric: "1 Billion +",
label: "Monthly events",
description:
"Autumn handles billions of billing events monthly for some of your favorite apps.",
clipart: true,
},
{
bg: "#FFE8FA",
icon: "/images/production/latency.svg",
metric: "<50ms",
label: "US latency",
description:
"Every billing check resolves in under 50ms. Your users never wait for a gate.",
clipart: true,
},
{
bg: "#D698FF",
icon: "/images/production/uptiime.svg",
metric: "10 minutes",
label: "Support SLA",
description:
"Billing is critical. We're loved for our rapid response times.",
clipart: true,
},
{
bg: "#F55DD0",
icon: "/images/production/churn.svg",
metric: "Zero",
label: "Churn rate",
description: "Unless their company shuts down, our customers stay with us.",
clipart: true,
},
];
export default function ProductionScale() {
const containerRef = useRef(null);
const scrambleCleanups = useRef([]);
useEffect(() => {
const cleanups = scrambleCleanups.current;
return () => {
cleanups.forEach((fn) => fn());
};
}, []);
useGSAP(
() => {
const isMobile = window.innerWidth < 768;
const cardY = isMobile ? 16 : 30;
gsap.set(".ps-card", { opacity: 0, y: cardY, scale: 0.97 });
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".ps-section",
start: "top 75%",
},
defaults: { overwrite: "auto" },
});
const cardEls = gsap.utils.toArray(".ps-card");
cardEls.forEach((card, i) => {
tl.to(
card,
{
opacity: 1,
y: 0,
scale: 1,
duration: 0.9,
ease: "power3.out",
onComplete() {
const metricEl = card.querySelector(".ps-metric");
if (!metricEl) return;
const finalText = metricEl.dataset.final;
const delayTimer = setTimeout(() => {
const cleanup = scrambleText(metricEl, finalText);
scrambleCleanups.current.push(cleanup);
}, 200);
scrambleCleanups.current.push(() => clearTimeout(delayTimer));
},
},
0.3 + i * 0.15,
);
});
},
{ scope: containerRef },
);
return (
<div ref={containerRef} className="overflow-hidden">
<section className="ps-section flex flex-col lg:flex-row items-start justify-between py-12 lg:py-16 gap-12 lg:gap-0 bg-[#0F0F0F]">
<div className="flex px-4 xl:pl-22.5 lg:pr-0 flex-col my-auto gap-4 lg:gap-6 pt-2 w-full lg:w-auto">
<div className="leading-none lg:leading-10">
<p className="text-[#FFFFFF99] tracking-[-4%] text-[30px] lg:text-[40px] font-normal">
You're in
</p>
<h2 className="text-white tracking-[-4%] text-[30px] lg:text-[40px] font-normal mt-1 lg:mt-0">
good hands
</h2>
</div>
<p className="text-[#FFFFFF99] font-light text-[16px] lg:text-sm lg:w-sm leading-[20px] lg:leading-5">
Autumn is trusted by some of fastest-growing teams. Open source core,
self-host ready.{" "}
<span className="text-white">
We'll help you go live quickly
<br className="hidden lg:block" />{" "}
and get back to what's important.
</span>
</p>
</div>
<div className="flex flex-col items-end gap-3 lg:gap-4 w-full pl-6 lg:pl-0 lg:w-[50%] [--card-step:24px] lg:[--card-step:52px]">
{cards.map((card, i) => (
<div
key={card.metric}
className="ps-card relative flex items-center justify-between pl-4 pr-3 py-4 lg:pl-6 lg:pr-10 lg:py-3.5 gap-2 lg:gap-6"
style={{
width: `calc(100% - (var(--card-step) * ${i}))`,
backgroundColor: card.bg,
}}
>
<div className="flex flex-col gap-1 min-w-[95px] lg:min-w-auto shrink-0">
<div className="flex items-center gap-1.5 lg:gap-2">
<Image
src={card.icon}
width={18}
height={18}
alt={card.label}
className="w-[14px] h-[14px] lg:w-[18px] lg:h-[18px]"
/>
<span
className="ps-metric text-xl lg:text-2xl font-medium tracking-[-5%] text-[#1A0A2E]"
data-final={card.metric}
>
{card.metric}
</span>
</div>
<span className="text-[11px] lg:text-[14px] leading-[1] lg:leading-4.5 tracking-[-2%] font-normal text-[#1A0A2E]/60">
{card.label}
</span>
</div>
<p className="text-[10.5px] lg:text-[14px] text-[#1A0A2E]/80 font-normal leading-[1.3] lg:leading-4.5 tracking-[0] lg:tracking-[-2%] flex-1 lg:flex-none lg:w-66 lg:shrink-0 text-left">
{card.description}
</p>
{card.badge && (
<span className="absolute top-0 right-0 bg-[#1A0A2E] text-white font-mono text-xs px-2.5 py-1">
{card.badge}
</span>
)}
{card.clipart && (
<Image
src="/images/production/clipart.svg"
width={12}
height={12}
alt="clipart"
className="absolute bottom-0 right-0 max-lg:w-[8px] max-lg:h-[8px]"
/>
)}
</div>
))}
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,223 @@
"use client";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import Image from "next/image";
import { useEffect, useRef } from "react";
gsap.registerPlugin(ScrollTrigger);
const SCRAMBLE_CHARS =
"!@#$%^&*()_+-=[]{}|;:,.<>?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
function scrambleText(
el: HTMLElement,
finalText: string,
{ charDuration = 60, cycleSpeed = 30 } = {},
) {
const len = finalText.length;
let settled = 0;
const timeouts: Array<ReturnType<typeof setTimeout>> = [];
const cycleInterval = setInterval(() => {
const display = finalText
.split("")
.map((ch, i) =>
i < settled
? ch
: SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)],
)
.join("");
el.textContent = display;
}, cycleSpeed);
for (let i = 0; i < len; i++) {
const t = setTimeout(
() => {
settled = i + 1;
if (settled === len) {
clearInterval(cycleInterval);
el.textContent = finalText;
}
},
(i + 1) * charDuration,
);
timeouts.push(t);
}
return function cleanup() {
clearInterval(cycleInterval);
timeouts.forEach(clearTimeout);
el.textContent = finalText;
};
}
const cards = [
{
bg: "#A175FF",
icon: "/images/production/uptime2.svg",
metric: "1 Billion +",
label: "Monthly events",
description:
"Autumn handles billions of billing events monthly for some of your favorite apps.",
clipart: true,
},
{
bg: "#FFE8FA",
icon: "/images/production/latency.svg",
metric: "<50ms",
label: "US latency",
description:
"Every billing check resolves in under 50ms. Your users never wait for a gate.",
clipart: true,
},
{
bg: "#D698FF",
icon: "/images/production/uptiime.svg",
metric: "10 minutes",
label: "Support SLA",
description:
"Billing is critical. We're loved for our rapid response times.",
clipart: true,
},
{
bg: "#F55DD0",
icon: "/images/production/churn.svg",
metric: "Zero",
label: "Churn rate",
description: "Unless their company shuts down, our customers stay with us.",
clipart: true,
},
];
export default function ProductionScale() {
const containerRef = useRef<HTMLDivElement | null>(null);
const scrambleCleanups = useRef<Array<() => void>>([]);
useEffect(() => {
const cleanups = scrambleCleanups.current;
return () => {
cleanups.forEach((fn) => fn());
};
}, []);
useGSAP(
() => {
const isMobile = window.innerWidth < 768;
const cardY = isMobile ? 16 : 30;
gsap.set(".ps-card", { opacity: 0, y: cardY, scale: 0.97 });
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".ps-section",
start: "top 75%",
},
defaults: { overwrite: "auto" },
});
const cardEls = gsap.utils.toArray<HTMLElement>(".ps-card");
cardEls.forEach((card, i) => {
tl.to(
card,
{
opacity: 1,
y: 0,
scale: 1,
duration: 0.9,
ease: "power3.out",
onComplete() {
const metricEl = card.querySelector<HTMLElement>(".ps-metric");
if (!metricEl) return;
const finalText = metricEl.dataset.final;
if (!finalText) return;
const delayTimer = setTimeout(() => {
const cleanup = scrambleText(metricEl, finalText);
scrambleCleanups.current.push(cleanup);
}, 200);
scrambleCleanups.current.push(() => clearTimeout(delayTimer));
},
},
0.3 + i * 0.15,
);
});
},
{ scope: containerRef },
);
return (
<div ref={containerRef} className="overflow-hidden">
<section className="ps-section flex flex-col lg:flex-row items-start justify-between py-12 lg:py-16 gap-12 lg:gap-0 bg-[#0F0F0F]">
<div className="flex px-4 xl:pl-22.5 lg:pr-0 flex-col my-auto gap-4 lg:gap-6 pt-2 w-full lg:w-auto">
<div className="leading-none lg:leading-10">
<p className="text-[#FFFFFF99] tracking-[-4%] text-[30px] lg:text-[40px] font-normal">
You're in
</p>
<h2 className="text-white tracking-[-4%] text-[30px] lg:text-[40px] font-normal mt-1 lg:mt-0">
good hands
</h2>
</div>
<p className="text-[#FFFFFF99] font-light text-[16px] lg:text-sm lg:w-sm leading-[20px] lg:leading-5">
Autumn is trusted by some of fastest-growing teams. Open source
core, self-host ready.{" "}
<span className="text-white">
We'll help you go live quickly
<br className="hidden lg:block" /> and get back to what's
important.
</span>
</p>
</div>
<div className="flex flex-col items-end gap-3 lg:gap-4 w-full pl-6 lg:pl-0 lg:w-[50%] [--card-step:24px] lg:[--card-step:52px]">
{cards.map((card, i) => (
<div
key={card.metric}
className="ps-card relative flex items-center justify-between pl-4 pr-3 py-4 lg:pl-6 lg:pr-10 lg:py-3.5 gap-2 lg:gap-6"
style={{
width: `calc(100% - (var(--card-step) * ${i}))`,
backgroundColor: card.bg,
}}
>
<div className="flex flex-col gap-1 min-w-[95px] lg:min-w-auto shrink-0">
<div className="flex items-center gap-1.5 lg:gap-2">
<Image
src={card.icon}
width={18}
height={18}
alt={card.label}
className="w-[14px] h-[14px] lg:w-[18px] lg:h-[18px]"
/>
<span
className="ps-metric text-xl lg:text-2xl font-medium tracking-[-5%] text-[#1A0A2E]"
data-final={card.metric}
>
{card.metric}
</span>
</div>
<span className="text-[11px] lg:text-[14px] leading-[1] lg:leading-4.5 tracking-[-2%] font-normal text-[#1A0A2E]/60">
{card.label}
</span>
</div>
<p className="text-[10.5px] lg:text-[14px] text-[#1A0A2E]/80 font-normal leading-[1.3] lg:leading-4.5 tracking-[0] lg:tracking-[-2%] flex-1 lg:flex-none lg:w-66 lg:shrink-0 text-left">
{card.description}
</p>
{card.clipart && (
<Image
src="/images/production/clipart.svg"
width={12}
height={12}
alt="clipart"
className="absolute bottom-0 right-0 max-lg:w-[8px] max-lg:h-[8px]"
/>
)}
</div>
))}
</div>
</section>
</div>
);
}

View File

@@ -1,19 +0,0 @@
export default function SectionDivider({ title }) {
return (
<>
<div className="flex flex-col gap-2.5">
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
</div>
<div className="w-[calc(100%+calc(var(--page-pad)*2))] -ml-(--page-pad) flex border-y border-[#292929] bg-[#000000] mt-2.5">
<div className="flex-1 py-6.5 pl-[calc(var(--page-pad)+16px)] xl:pl-[calc(var(--page-pad)+90px)] flex items-center">
<span className="font-mono text-[#FFFFFF99] text-[14px] tracking-[-2%] leading-[14px] uppercase">
// {title}
</span>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,19 @@
export default function SectionDivider({ title }: { title: string }) {
return (
<>
<div className="flex flex-col gap-2.5">
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full" />
</div>
<div className="w-[calc(100%+calc(var(--page-pad)*2))] -ml-(--page-pad) flex border-y border-[#292929] bg-[#000000] mt-2.5">
<div className="flex-1 py-6.5 pl-[calc(var(--page-pad)+16px)] xl:pl-[calc(var(--page-pad)+90px)] flex items-center">
<span className="font-mono text-[#FFFFFF99] text-[14px] tracking-[-2%] leading-[14px] uppercase">
// {title}
</span>
</div>
</div>
</>
);
}

View File

@@ -1,54 +0,0 @@
"use client";
import { useEffect, useRef } from "react";
import lottie, { AnimationItem } from "lottie-web";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import desktopAnimationData from "@/public/animation/solution-desktop.json";
import mobileAnimationData from "@/public/animation/solution-mobile.json";
gsap.registerPlugin(ScrollTrigger);
export default function SolutionAnimation() {
const containerRef = useRef(null);
const animRef = useRef(null);
useEffect(() => {
if (!containerRef.current) return;
const isMobile = window.innerWidth < 768;
const animationData = isMobile ? mobileAnimationData : desktopAnimationData;
const anim = lottie.loadAnimation({
container: containerRef.current,
renderer: "svg",
loop: false,
autoplay: false,
animationData,
});
animRef.current = anim;
const totalFrames = anim.totalFrames;
const loopStart = Math.floor(totalFrames * 0.2);
anim.addEventListener("complete", () => {
anim.loop = true;
anim.playSegments([loopStart, totalFrames], true);
});
const st = ScrollTrigger.create({
trigger: containerRef.current,
start: "top 75%", // Plays when the top of the animation reaches 75% viewport height
onEnter: () => anim.play(),
});
return () => {
st.kill();
anim.destroy();
};
}, []);
return <div ref={containerRef} style={{ width: "100%" }} />;
}

View File

@@ -0,0 +1,54 @@
"use client";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import lottie, { type AnimationItem } from "lottie-web";
import { useEffect, useRef } from "react";
import desktopAnimationData from "@/public/animation/solution-desktop.json";
import mobileAnimationData from "@/public/animation/solution-mobile.json";
gsap.registerPlugin(ScrollTrigger);
export default function SolutionAnimation() {
const containerRef = useRef<HTMLDivElement | null>(null);
const animRef = useRef<AnimationItem | null>(null);
useEffect(() => {
if (!containerRef.current) return;
const isMobile = window.innerWidth < 768;
const animationData = isMobile ? mobileAnimationData : desktopAnimationData;
const anim = lottie.loadAnimation({
container: containerRef.current,
renderer: "svg",
loop: false,
autoplay: false,
animationData,
});
animRef.current = anim;
const totalFrames = anim.totalFrames;
const loopStart = Math.floor(totalFrames * 0.2);
anim.addEventListener("complete", () => {
anim.loop = true;
anim.playSegments([loopStart, totalFrames], true);
});
const st = ScrollTrigger.create({
trigger: containerRef.current,
start: "top 75%", // Plays when the top of the animation reaches 75% viewport height
onEnter: () => anim.play(),
});
return () => {
st.kill();
anim.destroy();
};
}, []);
return <div ref={containerRef} style={{ width: "100%" }} />;
}

View File

@@ -1,68 +0,0 @@
import Image from "next/image";
import React from "react";
import SolutionAnimation from "./solution-animation";
const dbItems = [
{ text: "SUBSCRIPTION STATE", active: true },
{ text: "CREDIT BALANCES & ROLLOVERS", active: false },
{ text: "FEATURE ENTITLEMENTS", active: false },
{ text: "WEBHOOK HANDLING", active: false },
{ text: "USAGE RESETS & PRORATION", active: false },
];
const appItems = [
{ text: "PRODUCT FEATURES", icon: "grid" },
{ text: "USER INTERFACE", icon: "crossfade" },
{ text: "BUSINESS LOGIC", icon: "box-grid" },
];
const stripeItems = [
{ text: "MOVES MONEY", icon: "grid" },
{ text: "INVOICING", icon: "crossfade" },
{ text: "CARD PROCESSING", icon: "box-grid" },
];
const iconSrc = {
grid: "/images/solutions/grid.svg",
crossfade: "/images/solutions/crossfade.svg",
"box-grid": "/images/solutions/box-grid.svg",
};
export default function Solution() {
return (
<section className="relative w-full bg-[#000000] border-t border-[#292929] overflow-hidden pt-24">
<div
className="absolute inset-0 z-0 pointer-events-none"
style={{
backgroundImage:
"linear-gradient(to right, rgba(128,128,128,0.07) 1px, transparent 1px), linear-gradient(to bottom, rgba(128,128,128,0.07) 1px, transparent 1px)",
backgroundSize: "40px 40px",
maskImage:
"linear-gradient(to bottom, transparent 0%, black 18%, black 78%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to bottom, transparent 0%, black 18%, black 78%, transparent 100%)",
}}
/>
<div className="relative z-10 max-w-[1400px] mx-auto px-4 flex flex-col items-center">
{/* Heading */}
<div className="text-center mb-16 lg:mb-4 flex flex-col items-center">
<h2 className="text-[30px] leading-[30px] md:text-[40px] md:leading-[40px] font-normal tracking-tight mb-6">
<span className="text-[#A3A3A3]">Replace it all with </span>
<span className="text-white">Autumn</span>
</h2>
<p className="text-[#A3A3A3] text-[14px] md:text-[16px] sm:text-base max-w-2xl mx-auto font-light leading-[20px] tracking-[-2%]">
Autumn is a database purpose-built for billing state. Configure your
pricing
<br className="hidden sm:block" />
in the dashboard.{" "}
<span className="text-white">
Three API calls handle everything else.
</span>
</p>
</div>
<SolutionAnimation />
</div>
</section>
);
}

View File

@@ -0,0 +1,68 @@
import Image from "next/image";
import React from "react";
import SolutionAnimation from "./solution-animation";
const dbItems = [
{ text: "SUBSCRIPTION STATE", active: true },
{ text: "CREDIT BALANCES & ROLLOVERS", active: false },
{ text: "FEATURE ENTITLEMENTS", active: false },
{ text: "WEBHOOK HANDLING", active: false },
{ text: "USAGE RESETS & PRORATION", active: false },
];
const appItems = [
{ text: "PRODUCT FEATURES", icon: "grid" },
{ text: "USER INTERFACE", icon: "crossfade" },
{ text: "BUSINESS LOGIC", icon: "box-grid" },
];
const stripeItems = [
{ text: "MOVES MONEY", icon: "grid" },
{ text: "INVOICING", icon: "crossfade" },
{ text: "CARD PROCESSING", icon: "box-grid" },
];
const iconSrc = {
grid: "/images/solutions/grid.svg",
crossfade: "/images/solutions/crossfade.svg",
"box-grid": "/images/solutions/box-grid.svg",
};
export default function Solution() {
return (
<section className="relative w-full bg-[#000000] border-t border-[#292929] overflow-hidden pt-24">
<div
className="absolute inset-0 z-0 pointer-events-none"
style={{
backgroundImage:
"linear-gradient(to right, rgba(128,128,128,0.07) 1px, transparent 1px), linear-gradient(to bottom, rgba(128,128,128,0.07) 1px, transparent 1px)",
backgroundSize: "40px 40px",
maskImage:
"linear-gradient(to bottom, transparent 0%, black 18%, black 78%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to bottom, transparent 0%, black 18%, black 78%, transparent 100%)",
}}
/>
<div className="relative z-10 max-w-[1400px] mx-auto px-4 flex flex-col items-center">
{/* Heading */}
<div className="text-center mb-16 lg:mb-4 flex flex-col items-center">
<h2 className="text-[30px] leading-[30px] md:text-[40px] md:leading-[40px] font-normal tracking-tight mb-6">
<span className="text-[#A3A3A3]">Replace it all with </span>
<span className="text-white">Autumn</span>
</h2>
<p className="text-[#A3A3A3] text-[14px] md:text-[16px] sm:text-base max-w-2xl mx-auto font-light leading-[20px] tracking-[-2%]">
Autumn is a database purpose-built for billing state. Configure your
pricing
<br className="hidden sm:block" />
in the dashboard.{" "}
<span className="text-white">
Three API calls handle everything else.
</span>
</p>
</div>
<SolutionAnimation />
</div>
</section>
);
}

View File

@@ -1,222 +0,0 @@
"use client";
import {
IconArrowLeft,
IconArrowRight,
IconQuotes,
PixelatedPattern,
} from "@/app/constant";
import { useEffect, useRef, useState } from "react";
const testimonialsData = [
{
id: 1,
quote:
"Literally cannot imagine going without it. Thank you. We had some pretty crazy usage-based limitations for different features, as well as a free trial.",
author: "DANIEL EDRISIAR",
},
{
id: 2,
quote: "Amazing product. Amazing founders.",
author: "NIZZY",
},
{
id: 3,
quote:
"Autumn is awesome. We've been happy customers since the very beginning - it was a no-brainer to be honest. The founders are in true founder mode.",
author: "MAX PRILUTSKIY",
},
{
id: 4,
quote: "What migrating to Autumn does (scroll!)",
author: "Ben Y",
},
{
id: 5,
quote:
"Autumn fixed stripe. Trust me. Save you at least a week and potentially months of Stripe integration time. I wish we could have discovered Autumn earlier.",
author: "Benny Kok",
},
{
id: 6,
quote:
"@autumnpricing is so good it ruined every other tool for me. nothing else even feels right anymore",
author: "Can Vardar",
},
];
const Testimonials = () => {
const videoRef = useRef(null);
const scrollRef = useRef(null);
const progressRef = useRef(null);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(true);
const handleScroll = () => {
if (scrollRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current;
setCanScrollLeft(scrollLeft > 0);
setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 1);
if (progressRef.current) {
const maxScroll = scrollWidth - clientWidth;
const progress = maxScroll > 0 ? (scrollLeft / maxScroll) * 100 : 0;
progressRef.current.style.transform = `translateX(${progress}%)`;
}
}
};
useEffect(() => {
if (videoRef.current) {
videoRef.current.playbackRate = 0.4;
}
}, []);
useEffect(() => {
handleScroll();
window.addEventListener("resize", handleScroll);
return () => window.removeEventListener("resize", handleScroll);
}, []);
const scrollByAmount = (amount) => {
if (scrollRef.current) {
scrollRef.current.scrollBy({ left: amount, behavior: "smooth" });
}
};
return (
<section className="w-full bg-[#000000] text-white overflow-hidden">
<div className=" mx-auto">
<div className="px-4 sm:px-6 md:px-4 lg:px-4 xl:px-22.75 pt-[48px] xl:pt-32 pb-[48px] flex flex-row items-start justify-between">
<h2 className="w-full text-center md:w-auto md:text-left text-[30px] leading-[32px] sm:text-5xl md:text-[40px] font-normal tracking-[-5%]">
<span className="text-[#FFFFFF99]">Built for </span>
<span className="text-white">teams</span>
<br className="sm:hidden" />
<span className="text-white"> that move fast</span>
</h2>
<div className="hidden md:flex items-center space-x-4">
<button
onClick={() => scrollByAmount(-400)}
disabled={!canScrollLeft}
className="group p-1.5 bg-transparent cursor-pointer flex items-center justify-center transition-all duration-300 border border-[#292929]"
aria-label="Previous testimonials"
>
<IconArrowLeft
disabled={!canScrollLeft}
className="w-6 h-6 text-gray-400 hover:text-white"
/>
</button>
<button
onClick={() => scrollByAmount(400)}
disabled={!canScrollRight}
className="group p-1.5 bg-transparent cursor-pointer flex items-center justify-center transition-all duration-300 border border-[#292929]"
aria-label="Next testimonials"
>
<IconArrowRight
disabled={!canScrollRight}
className="w-6 h-6 text-gray-400 hover:text-white"
/>
</button>
</div>
</div>
<div className="border-t border-[#1A1A1A] w-full"></div>
<div className="px-0 xl:pl-22.75">
<div
ref={scrollRef}
onScroll={handleScroll}
className="flex overflow-x-auto snap-x snap-mandatory hide-scrollbar group/track"
style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
>
{testimonialsData.map((testimonial) => (
<div
key={testimonial.id}
className="group cursor-pointer shrink-0 w-[300px] sm:w-[300px] md:w-[360px] snap-start min-h-[360px] flex flex-col justify-between p-4 sm:p-10 border-l border-r border-b border-[#1A1A1A] transition-all duration-300 relative overflow-hidden"
>
{/* Hover Pixelated Pattern (Masked) */}
<div className="absolute inset-x-0 bottom-0 h-[120%] pointer-events-none z-0 overflow-hidden">
<div className="absolute inset-0 opacity-0 translate-y-6 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-500 ease-out pointer-events-none z-0 hidden md:block">
<video
ref={videoRef}
src="/images/testimonials/testimonial section.webm"
autoPlay
loop
muted
playsInline
className="w-full h-full object-cover"
/>
</div>
</div>
{/* Purple Glow Gradient Overlay */}
<div className="absolute inset-x-0 bottom-0 h-[70%] bg-[linear-gradient(to_bottom,rgba(10,10,10,0)_0%,rgba(135,82,250,0.15)_40%,rgba(135,82,250,0.45)_70%,rgba(135,82,250,0.85)_90%)] opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none z-0" />
<div className="relative z-10 flex flex-col h-full justify-start">
<IconQuotes className="w-8 h-8 text-[#4C4C4C] opacity-60 group-hover:text-[#9564FF] group-hover:opacity-100 transition-colors duration-500 mb-6" />
<p className="text-white text-[14px] leading-[18px] sm:text-xl sm:leading-6 font-extralight tracking-[-2%] transition-colors duration-500">
{testimonial.quote}
</p>
</div>
<div className="relative z-10 font-mono text-sm tracking-[-2%] uppercase text-white opacity-60 group-hover:opacity-100 transition-colors duration-500 mt-33.5">
{testimonial.author}
</div>
</div>
))}
</div>
</div>
<div className="border-t border-[#1A1A1A] w-full"></div>
<div className="flex md:hidden justify-end px-4 mt-6">
<div className="flex items-center gap-4">
<button
onClick={() => scrollByAmount(-400)}
disabled={!canScrollLeft}
className="p-1.5 bg-transparent flex items-center justify-center border border-[#292929]"
aria-label="Previous testimonials"
>
<IconArrowLeft
disabled={!canScrollLeft}
className="w-6 h-6 text-white"
/>
</button>
<button
onClick={() => scrollByAmount(400)}
disabled={!canScrollRight}
className="p-1.5 bg-transparent flex items-center justify-center border border-[#292929]"
aria-label="Next testimonials"
>
<IconArrowRight
disabled={!canScrollRight}
className="w-6 h-6 text-white"
/>
</button>
</div>
</div>
<div className="flex justify-center mt-5 md:mt-10 pb-0 md:pb-10">
<div className="w-[168px] hidden md:block h-1 bg-[#1A1A1A] rounded-full overflow-hidden relative">
<div
ref={progressRef}
className="absolute left-0 top-0 h-full bg-[#8752FA] w-[84px] rounded-full"
style={{ transform: `translateX(0%)` }}
></div>
</div>
</div>
</div>
<style jsx global>{`
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.arrow-corners {
transform-box: fill-box;
transform-origin: center;
transition: transform 0.25s ease;
}
.group:hover .arrow-corners {
transform: scale(1.4);
}
`}</style>
</section>
);
};
export default Testimonials;

View File

@@ -0,0 +1,222 @@
"use client";
import { useEffect, useRef, useState } from "react";
import {
IconArrowLeft,
IconArrowRight,
IconQuotes,
PixelatedPattern,
} from "@/app/constant";
const testimonialsData = [
{
id: 1,
quote:
"Literally cannot imagine going without it. Thank you. We had some pretty crazy usage-based limitations for different features, as well as a free trial.",
author: "DANIEL EDRISIAR",
},
{
id: 2,
quote: "Amazing product. Amazing founders.",
author: "NIZZY",
},
{
id: 3,
quote:
"Autumn is awesome. We've been happy customers since the very beginning - it was a no-brainer to be honest. The founders are in true founder mode.",
author: "MAX PRILUTSKIY",
},
{
id: 4,
quote: "What migrating to Autumn does (scroll!)",
author: "Ben Y",
},
{
id: 5,
quote:
"Autumn fixed stripe. Trust me. Save you at least a week and potentially months of Stripe integration time. I wish we could have discovered Autumn earlier.",
author: "Benny Kok",
},
{
id: 6,
quote:
"@autumnpricing is so good it ruined every other tool for me. nothing else even feels right anymore",
author: "Can Vardar",
},
];
const Testimonials = () => {
const videoRef = useRef<HTMLVideoElement | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null);
const progressRef = useRef<HTMLDivElement | null>(null);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(true);
const handleScroll = () => {
if (scrollRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current;
setCanScrollLeft(scrollLeft > 0);
setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 1);
if (progressRef.current) {
const maxScroll = scrollWidth - clientWidth;
const progress = maxScroll > 0 ? (scrollLeft / maxScroll) * 100 : 0;
progressRef.current.style.transform = `translateX(${progress}%)`;
}
}
};
useEffect(() => {
if (videoRef.current) {
videoRef.current.playbackRate = 0.4;
}
}, []);
useEffect(() => {
handleScroll();
window.addEventListener("resize", handleScroll);
return () => window.removeEventListener("resize", handleScroll);
}, []);
const scrollByAmount = (amount: number) => {
if (scrollRef.current) {
scrollRef.current.scrollBy({ left: amount, behavior: "smooth" });
}
};
return (
<section className="w-full bg-[#000000] text-white overflow-hidden">
<div className=" mx-auto">
<div className="px-4 sm:px-6 md:px-4 lg:px-4 xl:px-22.75 pt-[48px] xl:pt-32 pb-[48px] flex flex-row items-start justify-between">
<h2 className="w-full text-center md:w-auto md:text-left text-[30px] leading-[32px] sm:text-5xl md:text-[40px] font-normal tracking-[-5%]">
<span className="text-[#FFFFFF99]">Built for </span>
<span className="text-white">teams</span>
<br className="sm:hidden" />
<span className="text-white"> that move fast</span>
</h2>
<div className="hidden md:flex items-center space-x-4">
<button
onClick={() => scrollByAmount(-400)}
disabled={!canScrollLeft}
className="group p-1.5 bg-transparent cursor-pointer flex items-center justify-center transition-all duration-300 border border-[#292929]"
aria-label="Previous testimonials"
>
<IconArrowLeft
disabled={!canScrollLeft}
className="w-6 h-6 text-gray-400 hover:text-white"
/>
</button>
<button
onClick={() => scrollByAmount(400)}
disabled={!canScrollRight}
className="group p-1.5 bg-transparent cursor-pointer flex items-center justify-center transition-all duration-300 border border-[#292929]"
aria-label="Next testimonials"
>
<IconArrowRight
disabled={!canScrollRight}
className="w-6 h-6 text-gray-400 hover:text-white"
/>
</button>
</div>
</div>
<div className="border-t border-[#1A1A1A] w-full"></div>
<div className="px-0 xl:pl-22.75">
<div
ref={scrollRef}
onScroll={handleScroll}
className="flex overflow-x-auto snap-x snap-mandatory hide-scrollbar group/track"
style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
>
{testimonialsData.map((testimonial) => (
<div
key={testimonial.id}
className="group cursor-pointer shrink-0 w-[300px] sm:w-[300px] md:w-[360px] snap-start min-h-[360px] flex flex-col justify-between p-4 sm:p-10 border-l border-r border-b border-[#1A1A1A] transition-all duration-300 relative overflow-hidden"
>
{/* Hover Pixelated Pattern (Masked) */}
<div className="absolute inset-x-0 bottom-0 h-[120%] pointer-events-none z-0 overflow-hidden">
<div className="absolute inset-0 opacity-0 translate-y-6 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-500 ease-out pointer-events-none z-0 hidden md:block">
<video
ref={videoRef}
src="/images/testimonials/testimonial section.webm"
autoPlay
loop
muted
playsInline
className="w-full h-full object-cover"
/>
</div>
</div>
{/* Purple Glow Gradient Overlay */}
<div className="absolute inset-x-0 bottom-0 h-[70%] bg-[linear-gradient(to_bottom,rgba(10,10,10,0)_0%,rgba(135,82,250,0.15)_40%,rgba(135,82,250,0.45)_70%,rgba(135,82,250,0.85)_90%)] opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none z-0" />
<div className="relative z-10 flex flex-col h-full justify-start">
<IconQuotes className="w-8 h-8 text-[#4C4C4C] opacity-60 group-hover:text-[#9564FF] group-hover:opacity-100 transition-colors duration-500 mb-6" />
<p className="text-white text-[14px] leading-[18px] sm:text-xl sm:leading-6 font-extralight tracking-[-2%] transition-colors duration-500">
{testimonial.quote}
</p>
</div>
<div className="relative z-10 font-mono text-sm tracking-[-2%] uppercase text-white opacity-60 group-hover:opacity-100 transition-colors duration-500 mt-33.5">
{testimonial.author}
</div>
</div>
))}
</div>
</div>
<div className="border-t border-[#1A1A1A] w-full"></div>
<div className="flex md:hidden justify-end px-4 mt-6">
<div className="flex items-center gap-4">
<button
onClick={() => scrollByAmount(-400)}
disabled={!canScrollLeft}
className="p-1.5 bg-transparent flex items-center justify-center border border-[#292929]"
aria-label="Previous testimonials"
>
<IconArrowLeft
disabled={!canScrollLeft}
className="w-6 h-6 text-white"
/>
</button>
<button
onClick={() => scrollByAmount(400)}
disabled={!canScrollRight}
className="p-1.5 bg-transparent flex items-center justify-center border border-[#292929]"
aria-label="Next testimonials"
>
<IconArrowRight
disabled={!canScrollRight}
className="w-6 h-6 text-white"
/>
</button>
</div>
</div>
<div className="flex justify-center mt-5 md:mt-10 pb-0 md:pb-10">
<div className="w-[168px] hidden md:block h-1 bg-[#1A1A1A] rounded-full overflow-hidden relative">
<div
ref={progressRef}
className="absolute left-0 top-0 h-full bg-[#8752FA] w-[84px] rounded-full"
style={{ transform: `translateX(0%)` }}
></div>
</div>
</div>
</div>
<style jsx global>{`
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.arrow-corners {
transform-box: fill-box;
transform-origin: center;
transition: transform 0.25s ease;
}
.group:hover .arrow-corners {
transform: scale(1.4);
}
`}</style>
</section>
);
};
export default Testimonials;

View File

@@ -1,62 +0,0 @@
import fs from "fs";
import path from "path";
import matter from "gray-matter";
const CONTENT_DIR = path.join(process.cwd(), "content", "blog");
export function getAllPosts() {
if (!fs.existsSync(CONTENT_DIR)) return [];
const files = fs
.readdirSync(CONTENT_DIR)
.filter((file) => file.endsWith(".mdx"));
const posts = files.map((filename) => {
const filePath = path.join(CONTENT_DIR, filename);
const raw = fs.readFileSync(filePath, "utf-8");
const { data } = matter(raw);
return {
slug: data.slug || filename.replace(/\.mdx$/, ""),
title: data.title || "Untitled",
description: data.description || "",
date: data.date || null,
author: data.author || "Autumn Team",
image: data.image || null,
};
});
return posts.sort((a, b) => {
if (!a.date || !b.date) return 0;
return new Date(b.date) - new Date(a.date);
});
}
export function getPostBySlug({ slug }) {
if (!fs.existsSync(CONTENT_DIR)) return null;
const files = fs
.readdirSync(CONTENT_DIR)
.filter((file) => file.endsWith(".mdx"));
for (const filename of files) {
const filePath = path.join(CONTENT_DIR, filename);
const raw = fs.readFileSync(filePath, "utf-8");
const { data, content } = matter(raw);
const fileSlug = data.slug || filename.replace(/\.mdx$/, "");
if (fileSlug === slug) {
return {
slug: fileSlug,
title: data.title || "Untitled",
description: data.description || "",
date: data.date || null,
author: data.author || "Autumn Team",
image: data.image || null,
source: content,
};
}
}
return null;
}

View File

@@ -0,0 +1,75 @@
import fs from "fs";
import matter from "gray-matter";
import path from "path";
const CONTENT_DIR = path.join(process.cwd(), "content", "blog");
export type BlogPostSummary = {
slug: string;
title: string;
description: string;
date: string | null;
author: string;
image: string | null;
};
export type BlogPost = BlogPostSummary & {
source: string;
};
export function getAllPosts(): BlogPostSummary[] {
if (!fs.existsSync(CONTENT_DIR)) return [];
const files = fs
.readdirSync(CONTENT_DIR)
.filter((file) => file.endsWith(".mdx"));
const posts = files.map((filename) => {
const filePath = path.join(CONTENT_DIR, filename);
const raw = fs.readFileSync(filePath, "utf-8");
const { data } = matter(raw);
return {
slug: data.slug || filename.replace(/\.mdx$/, ""),
title: data.title || "Untitled",
description: data.description || "",
date: data.date || null,
author: data.author || "Autumn Team",
image: data.image || null,
};
});
return posts.sort((a, b) => {
if (!a.date || !b.date) return 0;
return new Date(b.date).getTime() - new Date(a.date).getTime();
});
}
export function getPostBySlug({ slug }: { slug: string }): BlogPost | null {
if (!fs.existsSync(CONTENT_DIR)) return null;
const files = fs
.readdirSync(CONTENT_DIR)
.filter((file) => file.endsWith(".mdx"));
for (const filename of files) {
const filePath = path.join(CONTENT_DIR, filename);
const raw = fs.readFileSync(filePath, "utf-8");
const { data, content } = matter(raw);
const fileSlug = data.slug || filename.replace(/\.mdx$/, "");
if (fileSlug === slug) {
return {
slug: fileSlug,
title: data.title || "Untitled",
description: data.description || "",
date: data.date || null,
author: data.author || "Autumn Team",
image: data.image || null,
source: content,
};
}
}
return null;
}

34
apps/website/lib/types.ts Normal file
View File

@@ -0,0 +1,34 @@
import type {
ComponentPropsWithoutRef,
ComponentType,
CSSProperties,
PropsWithChildren,
RefAttributes,
} from "react";
export type PageStyle = CSSProperties & {
"--page-pad"?: string;
};
export type SvgIconProps = ComponentPropsWithoutRef<"svg">;
export type ImgProps = ComponentPropsWithoutRef<"img">;
export type PixelIconComponent = ComponentType<
SvgIconProps & Partial<RefAttributes<SVGSVGElement>>
>;
export type PixelAnimationHandle = {
play: () => void;
reverse: () => void;
};
export type PixelHoverHandle = {
restart: () => void;
reverse: () => void;
};
export type LayoutProps = PropsWithChildren;
export type BlogParams = Promise<{
slug: string;
}>;

View File

@@ -0,0 +1,2 @@
export const cn = (...inputs: Array<string | false | null | undefined>) =>
inputs.filter(Boolean).join(" ");

View File

@@ -0,0 +1,7 @@
import typography from "@tailwindcss/typography";
const config = {
plugins: [typography],
};
export default config;

View File

@@ -0,0 +1,50 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"module": "esnext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"typeRoots": [
"../../node_modules/@types"
],
"types": [
"node",
"react",
"react-dom"
],
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
},
"allowJs": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": [
"node_modules"
]
}

View File

@@ -50,14 +50,10 @@ export const auth = betterAuth({
session: {
create: {
before: beforeSessionCreated,
after: (session, context) => {
return afterSessionCreated(session, context);
},
after: afterSessionCreated,
},
delete: {
after: (session, context) => {
return afterSessionDeleted(session, context);
},
after: afterSessionDeleted,
},
},
},

View File

@@ -5,6 +5,7 @@ export const afterSessionCreated = async (
session: Session,
context: GenericEndpointContext<BetterAuthOptions> | null,
) => {
console.log("Running afterSessionCreated for user ", session.userId);
try {
if (!context) return;
@@ -19,7 +20,7 @@ export const afterSessionCreated = async (
path: "/",
maxAge: 60 * 60 * 24 * 7, // 7 days
secure: true,
sameSite: "none",
sameSite: "lax",
httpOnly: false,
});
} catch (_) {

View File

@@ -20,7 +20,7 @@ export const afterSessionDeleted = async (
path: "/",
maxAge: 0,
secure: true,
sameSite: "none",
sameSite: "lax",
httpOnly: false,
});
} catch (_) {

View File

@@ -52,15 +52,12 @@ export default function App() {
org_id: data.session.activeOrganizationId ?? "unknown_org",
});
// Set a non-httpOnly hint cookie so the landing page can detect login state.
// In production this is handled server-side on .useautumn.com domain.
if (window.location.hostname === "localhost") {
if (data?.user) {
document.cookie =
"logged_in_hint=1; path=/; max-age=604800; SameSite=Lax";
} else {
document.cookie = "logged_in_hint=; path=/; max-age=0; SameSite=Lax";
}
const isLocal = window.location.hostname === "localhost";
const extras = isLocal ? "" : "; domain=.useautumn.com; Secure";
if (data?.user) {
document.cookie = `logged_in_hint=1; path=/; max-age=604800; SameSite=Lax${extras}`;
} else {
document.cookie = `logged_in_hint=; path=/; max-age=0; SameSite=Lax${extras}`;
}
}
}, [data]);