chore: merge remote-tracking branch 'origin/main' into dev
82
apps/website/.gitignore
vendored
Executable file → Normal file
@@ -1,41 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
12
apps/website/README.md
Executable file → Normal file
@@ -1,4 +1,4 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with `[create-next-app](https://github.com/vercel/next.js/tree/canary/packages/create-next-app)`.
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -18,7 +18,7 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
|
||||
|
||||
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses `[next/font](https://nextjs.org/docs/app/building-your-application/optimizing/fonts)` to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
@@ -34,11 +34,3 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
|
||||
Designed by
|
||||
|
||||
██████ ██ ██ ██ ███████ ██ ██ ██ ██████ ██ █████ ██████ ███████
|
||||
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||
██████ ██ ███ █████ ██ ██ ██ ██████ ██ ███████ ██████ ███████
|
||||
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||
██ ██ ██ ██ ███████ ███████ ██████ ██ ███████ ██ ██ ██████ ███████
|
||||
108
apps/website/app/blog/[slug]/page.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import { getAllPosts, getPostBySlug } from "@/lib/blogUtils";
|
||||
import { mdxComponents } from "@/components/blogComponents";
|
||||
import { MDXRemote } from "next-mdx-remote/rsc";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export function generateStaticParams() {
|
||||
return getAllPosts().map((post) => ({ slug: post.slug }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
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",
|
||||
publishedTime: post.date,
|
||||
authors: [post.author],
|
||||
...(post.image && {
|
||||
images: [{ url: post.image }],
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return "";
|
||||
return new Date(dateString).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({ params }) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
30
apps/website/app/blog/layout.js
Normal file
@@ -0,0 +1,30 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
79
apps/website/app/blog/page.js
Normal file
@@ -0,0 +1,79 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -459,9 +459,9 @@ export const IconAnalytics = (props) => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const IconTeam = forwardRef((props, ref) => (
|
||||
// Team Billing: billing.svg
|
||||
export const IconTeam = (props) => (
|
||||
<svg
|
||||
ref={ref}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -469,43 +469,17 @@ export const IconTeam = forwardRef((props, ref) => (
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
{/* Top Pixel */}
|
||||
<path
|
||||
className="icon-pixel-path"
|
||||
d="M9.6 0H14.4V4.8H9.6V0Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
{/* Right Pixel */}
|
||||
<path
|
||||
className="icon-pixel-path"
|
||||
d="M19.2 9.6H24V14.4H19.2V9.6Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
{/* Bottom Pixel */}
|
||||
<path
|
||||
className="icon-pixel-path"
|
||||
d="M9.6 19.2H14.4V24H9.6V19.2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
{/* Left Pixel */}
|
||||
<path
|
||||
className="icon-pixel-path"
|
||||
d="M0 9.6H4.8V14.4H0V9.6Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
|
||||
{/* Center Block with REAL HOLES (The Magic Fix) */}
|
||||
<path
|
||||
className="icon-pixel-path"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.8 4.8H19.2V19.2H4.8V4.8ZM9.6 4.8H14.4V9.6H9.6V4.8ZM9.6 14.4H14.4V19.2H9.6V14.4ZM4.8 9.6H9.6V14.4H4.8V9.6ZM14.4 9.6H19.2V14.4H14.4V9.6Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M9.6 0H14.4V4.8H9.6V0Z" fill="currentColor" />
|
||||
<path d="M24 9.6V14.4H19.2V9.6H24Z" fill="currentColor" />
|
||||
<path d="M9.6 19.2H14.4V24H9.6V19.2Z" fill="currentColor" />
|
||||
<path d="M0 9.6H4.8V14.4H0V9.6Z" fill="currentColor" />
|
||||
<path d="M4.8 4.8H9.6V9.6H4.8V4.8Z" fill="currentColor" />
|
||||
<path d="M14.4 4.8H19.2V9.6H14.4V4.8Z" fill="currentColor" />
|
||||
<path d="M9.6 9.6H14.4V14.4H9.6V9.6Z" fill="currentColor" />
|
||||
<path d="M4.8 14.4H9.6V19.2H4.8V14.4Z" fill="currentColor" />
|
||||
<path d="M14.4 14.4H19.2V19.2H14.4V14.4Z" fill="currentColor" />
|
||||
</svg>
|
||||
));
|
||||
|
||||
IconTeam.displayName = "IconTeam";
|
||||
);
|
||||
|
||||
// Auto Top-ups: top-ups.svg
|
||||
export const IconTopUp = (props) => (
|
||||
@@ -1190,53 +1164,71 @@ export const faqData = [
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
question: "What if Autumn goes down?",
|
||||
question: "What if Autumn goes down? Will my app go down?",
|
||||
answer:
|
||||
"We run on redundant infrastructure with 99.99% uptime. If Autumn doesn't return a response, your app should default to allowing usage. This means Autumn will never take your app down—some users may temporarily get extra usage.\n\nWe'll work with you to reconcile usage tracking and balances afterward if needed.",
|
||||
"We run on redundant infrastructure and high availability is our priority. However, not being able to reach Autumn does not mean that your app will go down. Our SDKs default to fail open and fail fast, meaning that in a worst case scenario, some users may get temporary additional access.\n\nWe can work with you to reconcile usage tracking and balances afterward if needed.",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
question: "How is Autumn different from Orb or Metronome?",
|
||||
answer:
|
||||
"Orb and Metronome focus on usage metering—tracking how much customers consume. You still have to build entitlements, access control, and state management separately.\n\nAutumn is a complete system of record. We handle usage metering + entitlements + feature gating + billing state in one API. `check()` tells you if a user can access a feature in <50ms. You don't build that logic.",
|
||||
"Orb and Metronome focus on usage metering—tracking how much customers consume, suitable for end of month invoicing. You still have to build access control and state management separately, meaning you'll wire together your own logic, Stripe billing and a metering provider.\n\nAutumn is a complete system of record. We handle usage metering + entitlements + feature gating + billing state in one API. `check()` tells you if a user can access a feature in <50ms.",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
question: "What if I need to move off Autumn?",
|
||||
question: "What if I need to move off Autumn? Am I locked in?",
|
||||
answer:
|
||||
"Autumn is open source. You can self-host anytime, or export all your data. Your Stripe subscriptions remain yours—we never lock you in. Most customers who self-host do so for compliance reasons, not because they're leaving.",
|
||||
"Autumn is open source. You can self-host anytime, or export all your data. Your Stripe subscriptions, customers and payment details remain yours. Moving off Autumn is simply a case of building what you would have built in-house without Autumn (but this has never happened, touch wood!). ",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
question: "How long does integration take?",
|
||||
question: "How long would it take to go live?",
|
||||
answer:
|
||||
"Most teams go live in under an hour. Migrating from an existing billing system typically takes 1–2 weeks, depending on complexity. We provide migration guides and work directly with your team.",
|
||||
"If you're setting up payments for the first time, most teams go live in under an hour. Migrating from an existing billing system typically takes 1–2 weeks, depending on complexity.\n\n For Series A+ companies, we provide a forward deployed service to work with your team, dual-write to your internal system and Autumn, then smoothly migrate over. Minimal work needed on your part.",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
question: "Can you handle our event volume?",
|
||||
answer:
|
||||
"Yes. Autumn supports 10,000+ events per second per customer. We've processed millions of billing events daily for AI companies at scale. If you have specific requirements, reach out—we'll walk through your architecture.",
|
||||
"Yes. Autumn supports 10,000+ events per second per end customer. We've processed millions of billing events daily for AI companies at scale. If you have specific requirements, reach out—we'll walk through your architecture.",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
question: "What if I can't use `check()`?",
|
||||
answer:
|
||||
"Latency-sensitive customers may not be able to use `check()` in real-time. In these cases, you can cache the Autumn customer data on your end, or use our single `customer.products.updated` webhook to replicate the Autumn state into your own system.",
|
||||
},
|
||||
];
|
||||
|
||||
export const featuresData = [
|
||||
{
|
||||
title: "Webhooks Handled",
|
||||
title: "Usage Ledgers",
|
||||
description:
|
||||
"Gate features with a single API call. check() resolves what each customer can access in under 50ms no database queries, no hardcoded logic.",
|
||||
"Recurring, one-time and rollover credit balances. Stack balances across plans and topups. Deduct from soonest expiry first.",
|
||||
Icon: IconWebhooks,
|
||||
},
|
||||
{
|
||||
title: "Payment Logic",
|
||||
description:
|
||||
"Checkouts, upgrades, downgrades, add-ons, proration, 3DS, edge cases, webhooks: all handled in a single API call.",
|
||||
Icon: IconWebhooks,
|
||||
},
|
||||
{
|
||||
title: "Custom Plans",
|
||||
description:
|
||||
"Create one-off deals for enterprise customers. Unique pricing, features, and limits without touching code.",
|
||||
Icon: IconPlans,
|
||||
},
|
||||
{
|
||||
title: "Usage Analytics",
|
||||
description:
|
||||
"Timeseries charts and event logs out of the box. See usage trends, credit consumption, and feature adoption per customer.",
|
||||
"Fast timeseries charts and event logs out of the box. Powered by ClickHouse.",
|
||||
Icon: IconAnalytics,
|
||||
},
|
||||
{
|
||||
title: "Team Billing",
|
||||
description:
|
||||
"Bill organizations, track usage per seat. Add/remove team members. Prorate seat charges automatically.",
|
||||
"Grant plans and features to entities under an organization. Create pools of credits, or assign to users directly.",
|
||||
Icon: IconTeam,
|
||||
},
|
||||
{
|
||||
@@ -1245,26 +1237,20 @@ export const featuresData = [
|
||||
"Let users refill credits when balance runs low. Configure thresholds and amounts. Fully automated.",
|
||||
Icon: IconTopUp,
|
||||
},
|
||||
{
|
||||
title: "Custom Plans",
|
||||
description:
|
||||
"Create one-off deals for enterprise customers. Unique pricing, credits, and limits without touching code.",
|
||||
Icon: IconPlans,
|
||||
},
|
||||
{
|
||||
title: "Pricing Versioning",
|
||||
description:
|
||||
"Change your pricing model without breaking existing customers. Grandfather old plans or migrate users gradually. No database migrations.",
|
||||
"Change your pricing model without breaking existing customers. Grandfather old plans or migrate users gradually. No database or Stripe migrations.",
|
||||
Icon: IconVersioning,
|
||||
},
|
||||
{
|
||||
title: "React Components",
|
||||
title: "Alerts and Spend Limits",
|
||||
description:
|
||||
"Drop-in components for pricing tables, usage displays, and upgrade flows. Fully styled, production-ready.",
|
||||
"Give customers governance over their usage. Configure alerts, limits and overage per customer.",
|
||||
Icon: IconReact,
|
||||
},
|
||||
{
|
||||
title: "Referral Programs",
|
||||
title: "Coupons and Referrals",
|
||||
description:
|
||||
"Built-in referral system with rewards, tracking, and attribution. Launch referral programs in minutes.",
|
||||
Icon: IconReferral,
|
||||
|
||||
0
apps/website/app/favicon.ico
Executable file → Normal file
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
571
apps/website/app/globals.css
Executable file → Normal file
@@ -1,237 +1,334 @@
|
||||
/* @import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
} */
|
||||
|
||||
@import "tailwindcss";
|
||||
|
||||
/* ==========================================================================
|
||||
RAW TOKENS — single source of truth
|
||||
⚠️ All hex values marked "verify" are best-guesses from the screenshot.
|
||||
Confirm against Figma before shipping.
|
||||
========================================================================== */
|
||||
|
||||
:root {
|
||||
/* --- Fonts ---------------------------------------------------------------- */
|
||||
--font-geist-sans: "Geist", sans-serif; /* injected by next/font or CDN */
|
||||
--font-geist-mono: "Geist Mono", monospace;
|
||||
|
||||
/* --- Font weights --------------------------------------------------------- */
|
||||
--font-weight-light: 300;
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
/* --- Type scale: Display (desktop) --------------------------------------- */
|
||||
--text-display-1: 4.5rem; /* verify */
|
||||
--leading-display-1: 1.1;
|
||||
--text-display-2: 3.5rem; /* verify */
|
||||
--leading-display-2: 1.1;
|
||||
--text-display-3: 2.5rem; /* verify */
|
||||
--leading-display-3: 1.15;
|
||||
--text-display-4: 2rem; /* verify */
|
||||
--leading-display-4: 1.2;
|
||||
--text-display-5: 1.5rem; /* verify */
|
||||
--leading-display-5: 1.25;
|
||||
|
||||
/* --- Type scale: Body ----------------------------------------------------- */
|
||||
--text-body-18: 1.125rem;
|
||||
--leading-body-18: 1.6;
|
||||
--text-body-16: 1rem;
|
||||
--leading-body-16: 1.6;
|
||||
--text-body-14: 0.875rem;
|
||||
--leading-body-14: 1.5;
|
||||
|
||||
/* --- Type scale: Label (Geist Mono) --------------------------------------- */
|
||||
--text-label-32: 2rem;
|
||||
--text-label-14: 0.875rem;
|
||||
--leading-label: 1.4;
|
||||
|
||||
/* --- Surface / Neutral ---------------------------------------------------- */
|
||||
--color-surface-primary: #7c3aed; /* brand purple — verify */
|
||||
--color-surface-950: #0f0f0f;
|
||||
--color-surface-900: #1c1c1c; /* verify */
|
||||
--color-surface-800: #2c2c2c; /* verify */
|
||||
--color-surface-600: #3f3f3f;
|
||||
--color-surface-200: #d1d1d1; /* verify */
|
||||
--color-surface-100: #ececec; /* verify */
|
||||
|
||||
/* --- Brand: Pink ---------------------------------------------------------- */
|
||||
--color-pink-dark: #ec0267; /* verify */
|
||||
--color-pink-base: #f60060; /* verify */
|
||||
--color-pink-light: #f860c4; /* verify */
|
||||
--color-pink-light-1: #fbc8e0; /* verify */
|
||||
|
||||
/* --- Brand: Blue ---------------------------------------------------------- */
|
||||
--color-blue-dark: #3f0cf3; /* verify */
|
||||
--color-blue-base: #4040f0; /* verify */
|
||||
--color-blue-light: #8080f5; /* verify */
|
||||
--color-blue-light-1: #b8b8ff; /* verify */
|
||||
|
||||
/* --- Brand: Purple -------------------------------------------------------- */
|
||||
--color-purple-dark: #6f0cf8; /* verify */
|
||||
--color-purple-base: #8f3cf8; /* verify */
|
||||
--color-purple-light: #b040f8; /* verify */
|
||||
--color-purple-light-1: #d4b0ff; /* verify */
|
||||
|
||||
/* --- Legacy Next.js scaffold (kept for compatibility) --------------------- */
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--text-display-1: 3.5rem; /* verify */
|
||||
--text-display-2: 2.5rem; /* verify */
|
||||
--text-display-3: 2rem; /* verify */
|
||||
--text-display-4: 1.5rem; /* verify */
|
||||
--text-display-5: 1.25rem; /* verify */
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
TAILWIND v4 THEME — maps raw tokens → utility classes
|
||||
Everything in @theme becomes a Tailwind utility automatically:
|
||||
--color-* → bg-*, text-*, border-*, ring-*, fill-*, stroke-*
|
||||
--font-* → font-*
|
||||
--text-* → text-* (when paired with a size value)
|
||||
--leading-* → leading-*
|
||||
========================================================================== */
|
||||
|
||||
@theme inline {
|
||||
/* Background & foreground (Next.js scaffold) */
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
|
||||
/* Fonts */
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
|
||||
/* Surface / Neutral */
|
||||
--color-surface-primary: var(--color-surface-primary);
|
||||
--color-surface-950: var(--color-surface-950);
|
||||
--color-surface-900: var(--color-surface-900);
|
||||
--color-surface-800: var(--color-surface-800);
|
||||
--color-surface-600: var(--color-surface-600);
|
||||
--color-surface-200: var(--color-surface-200);
|
||||
--color-surface-100: var(--color-surface-100);
|
||||
|
||||
/* Brand: Pink */
|
||||
--color-pink-dark: var(--color-pink-dark);
|
||||
--color-pink-base: var(--color-pink-base);
|
||||
--color-pink-light: var(--color-pink-light);
|
||||
--color-pink-light-1: var(--color-pink-light-1);
|
||||
|
||||
/* Brand: Blue */
|
||||
--color-blue-dark: var(--color-blue-dark);
|
||||
--color-blue-base: var(--color-blue-base);
|
||||
--color-blue-light: var(--color-blue-light);
|
||||
--color-blue-light-1: var(--color-blue-light-1);
|
||||
|
||||
/* Brand: Purple */
|
||||
--color-purple-dark: var(--color-purple-dark);
|
||||
--color-purple-base: var(--color-purple-base);
|
||||
--color-purple-light: var(--color-purple-light);
|
||||
--color-purple-light-1: var(--color-purple-light-1);
|
||||
|
||||
/* Type scale — Display */
|
||||
--text-display-1: var(--text-display-1);
|
||||
--text-display-2: var(--text-display-2);
|
||||
--text-display-3: var(--text-display-3);
|
||||
--text-display-4: var(--text-display-4);
|
||||
--text-display-5: var(--text-display-5);
|
||||
|
||||
/* Type scale — Body */
|
||||
--text-body-18: var(--text-body-18);
|
||||
--text-body-16: var(--text-body-16);
|
||||
--text-body-14: var(--text-body-14);
|
||||
|
||||
/* Type scale — Label */
|
||||
--text-label-32: var(--text-label-32);
|
||||
--text-label-14: var(--text-label-14);
|
||||
|
||||
/* Leading */
|
||||
--leading-display-1: var(--leading-display-1);
|
||||
--leading-display-2: var(--leading-display-2);
|
||||
--leading-display-3: var(--leading-display-3);
|
||||
--leading-display-4: var(--leading-display-4);
|
||||
--leading-display-5: var(--leading-display-5);
|
||||
--leading-body-18: var(--leading-body-18);
|
||||
--leading-body-16: var(--leading-body-16);
|
||||
--leading-body-14: var(--leading-body-14);
|
||||
--leading-label: var(--leading-label);
|
||||
|
||||
/* Font weights */
|
||||
--font-weight-light: var(--font-weight-light);
|
||||
--font-weight-regular: var(--font-weight-regular);
|
||||
--font-weight-medium: var(--font-weight-medium);
|
||||
--font-weight-semibold: var(--font-weight-semibold);
|
||||
--font-weight-bold: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
BASE STYLES
|
||||
========================================================================== */
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-geist-sans);
|
||||
}
|
||||
|
||||
|
||||
.grid-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
/* pointer-events: none; */
|
||||
}
|
||||
|
||||
.grid-overlay::before,
|
||||
.grid-overlay::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
/* left floating line */
|
||||
.grid-overlay::before {
|
||||
left: 3%;
|
||||
}
|
||||
|
||||
/* right floating line */
|
||||
.grid-overlay::after {
|
||||
right: 20%;
|
||||
}
|
||||
/* @import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
} */
|
||||
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
/* ==========================================================================
|
||||
RAW TOKENS — single source of truth
|
||||
⚠️ All hex values marked "verify" are best-guesses from the screenshot.
|
||||
Confirm against Figma before shipping.
|
||||
========================================================================== */
|
||||
|
||||
:root {
|
||||
/* --- Fonts ---------------------------------------------------------------- */
|
||||
--font-geist-sans: "Geist", sans-serif; /* injected by next/font or CDN */
|
||||
--font-geist-mono: "Geist Mono", monospace;
|
||||
|
||||
/* --- Font weights --------------------------------------------------------- */
|
||||
--font-weight-light: 300;
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
/* --- Type scale: Display (desktop) --------------------------------------- */
|
||||
--text-display-1: 4.5rem; /* verify */
|
||||
--leading-display-1: 1.1;
|
||||
--text-display-2: 3.5rem; /* verify */
|
||||
--leading-display-2: 1.1;
|
||||
--text-display-3: 2.5rem; /* verify */
|
||||
--leading-display-3: 1.15;
|
||||
--text-display-4: 2rem; /* verify */
|
||||
--leading-display-4: 1.2;
|
||||
--text-display-5: 1.5rem; /* verify */
|
||||
--leading-display-5: 1.25;
|
||||
|
||||
/* --- Type scale: Body ----------------------------------------------------- */
|
||||
--text-body-18: 1.125rem;
|
||||
--leading-body-18: 1.6;
|
||||
--text-body-16: 1rem;
|
||||
--leading-body-16: 1.6;
|
||||
--text-body-14: 0.875rem;
|
||||
--leading-body-14: 1.5;
|
||||
|
||||
/* --- Type scale: Label (Geist Mono) --------------------------------------- */
|
||||
--text-label-32: 2rem;
|
||||
--text-label-14: 0.875rem;
|
||||
--leading-label: 1.4;
|
||||
|
||||
/* --- Surface / Neutral ---------------------------------------------------- */
|
||||
--color-surface-primary: #7c3aed; /* brand purple — verify */
|
||||
--color-surface-950: #0f0f0f;
|
||||
--color-surface-900: #1c1c1c; /* verify */
|
||||
--color-surface-800: #2c2c2c; /* verify */
|
||||
--color-surface-600: #3f3f3f;
|
||||
--color-surface-200: #d1d1d1; /* verify */
|
||||
--color-surface-100: #ececec; /* verify */
|
||||
|
||||
/* --- Brand: Pink ---------------------------------------------------------- */
|
||||
--color-pink-dark: #ec0267; /* verify */
|
||||
--color-pink-base: #f60060; /* verify */
|
||||
--color-pink-light: #f860c4; /* verify */
|
||||
--color-pink-light-1: #fbc8e0; /* verify */
|
||||
|
||||
/* --- Brand: Blue ---------------------------------------------------------- */
|
||||
--color-blue-dark: #3f0cf3; /* verify */
|
||||
--color-blue-base: #4040f0; /* verify */
|
||||
--color-blue-light: #8080f5; /* verify */
|
||||
--color-blue-light-1: #b8b8ff; /* verify */
|
||||
|
||||
/* --- Brand: Purple -------------------------------------------------------- */
|
||||
--color-purple-dark: #6f0cf8; /* verify */
|
||||
--color-purple-base: #8f3cf8; /* verify */
|
||||
--color-purple-light: #b040f8; /* verify */
|
||||
--color-purple-light-1: #d4b0ff; /* verify */
|
||||
|
||||
/* --- Legacy Next.js scaffold (kept for compatibility) --------------------- */
|
||||
--background: #000000;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #000000;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--text-display-1: 3.5rem; /* verify */
|
||||
--text-display-2: 2.5rem; /* verify */
|
||||
--text-display-3: 2rem; /* verify */
|
||||
--text-display-4: 1.5rem; /* verify */
|
||||
--text-display-5: 1.25rem; /* verify */
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
TAILWIND v4 THEME — maps raw tokens → utility classes
|
||||
Everything in @theme becomes a Tailwind utility automatically:
|
||||
--color-* → bg-*, text-*, border-*, ring-*, fill-*, stroke-*
|
||||
--font-* → font-*
|
||||
--text-* → text-* (when paired with a size value)
|
||||
--leading-* → leading-*
|
||||
========================================================================== */
|
||||
|
||||
@theme inline {
|
||||
/* Background & foreground (Next.js scaffold) */
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
|
||||
/* Fonts */
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
|
||||
/* Surface / Neutral */
|
||||
--color-surface-primary: var(--color-surface-primary);
|
||||
--color-surface-950: var(--color-surface-950);
|
||||
--color-surface-900: var(--color-surface-900);
|
||||
--color-surface-800: var(--color-surface-800);
|
||||
--color-surface-600: var(--color-surface-600);
|
||||
--color-surface-200: var(--color-surface-200);
|
||||
--color-surface-100: var(--color-surface-100);
|
||||
|
||||
/* Brand: Pink */
|
||||
--color-pink-dark: var(--color-pink-dark);
|
||||
--color-pink-base: var(--color-pink-base);
|
||||
--color-pink-light: var(--color-pink-light);
|
||||
--color-pink-light-1: var(--color-pink-light-1);
|
||||
|
||||
/* Brand: Blue */
|
||||
--color-blue-dark: var(--color-blue-dark);
|
||||
--color-blue-base: var(--color-blue-base);
|
||||
--color-blue-light: var(--color-blue-light);
|
||||
--color-blue-light-1: var(--color-blue-light-1);
|
||||
|
||||
/* Brand: Purple */
|
||||
--color-purple-dark: var(--color-purple-dark);
|
||||
--color-purple-base: var(--color-purple-base);
|
||||
--color-purple-light: var(--color-purple-light);
|
||||
--color-purple-light-1: var(--color-purple-light-1);
|
||||
|
||||
/* Type scale — Display */
|
||||
--text-display-1: var(--text-display-1);
|
||||
--text-display-2: var(--text-display-2);
|
||||
--text-display-3: var(--text-display-3);
|
||||
--text-display-4: var(--text-display-4);
|
||||
--text-display-5: var(--text-display-5);
|
||||
|
||||
/* Type scale — Body */
|
||||
--text-body-18: var(--text-body-18);
|
||||
--text-body-16: var(--text-body-16);
|
||||
--text-body-14: var(--text-body-14);
|
||||
|
||||
/* Type scale — Label */
|
||||
--text-label-32: var(--text-label-32);
|
||||
--text-label-14: var(--text-label-14);
|
||||
|
||||
/* Leading */
|
||||
--leading-display-1: var(--leading-display-1);
|
||||
--leading-display-2: var(--leading-display-2);
|
||||
--leading-display-3: var(--leading-display-3);
|
||||
--leading-display-4: var(--leading-display-4);
|
||||
--leading-display-5: var(--leading-display-5);
|
||||
--leading-body-18: var(--leading-body-18);
|
||||
--leading-body-16: var(--leading-body-16);
|
||||
--leading-body-14: var(--leading-body-14);
|
||||
--leading-label: var(--leading-label);
|
||||
|
||||
/* Font weights */
|
||||
--font-weight-light: var(--font-weight-light);
|
||||
--font-weight-regular: var(--font-weight-regular);
|
||||
--font-weight-medium: var(--font-weight-medium);
|
||||
--font-weight-semibold: var(--font-weight-semibold);
|
||||
--font-weight-bold: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
BASE STYLES
|
||||
========================================================================== */
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-geist-sans);
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
|
||||
.grid-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
/* pointer-events: none; */
|
||||
}
|
||||
|
||||
.grid-overlay::before,
|
||||
.grid-overlay::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
/* left floating line */
|
||||
.grid-overlay::before {
|
||||
left: 3%;
|
||||
}
|
||||
|
||||
/* right floating line */
|
||||
.grid-overlay::after {
|
||||
right: 20%;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
BLOG PROSE OVERRIDES — dark theme, matches landing-page tokens
|
||||
========================================================================== */
|
||||
|
||||
.prose.prose-invert {
|
||||
--tw-prose-body: rgba(255, 255, 255, 0.6);
|
||||
--tw-prose-headings: #ffffff;
|
||||
--tw-prose-lead: rgba(255, 255, 255, 0.7);
|
||||
--tw-prose-links: #9564ff;
|
||||
--tw-prose-bold: #ffffff;
|
||||
--tw-prose-counters: rgba(255, 255, 255, 0.6);
|
||||
--tw-prose-bullets: rgba(255, 255, 255, 0.4);
|
||||
--tw-prose-hr: #292929;
|
||||
--tw-prose-quotes: rgba(255, 255, 255, 0.7);
|
||||
--tw-prose-quote-borders: #9564ff;
|
||||
--tw-prose-code: #e0e0e0;
|
||||
--tw-prose-pre-code: rgba(255, 255, 255, 0.6);
|
||||
--tw-prose-pre-bg: #141414;
|
||||
--tw-prose-th-borders: #292929;
|
||||
--tw-prose-td-borders: #292929;
|
||||
font-family: var(--font-geist-sans);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.prose.prose-invert p {
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.prose.prose-invert p {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.prose.prose-invert h2 {
|
||||
font-weight: 400;
|
||||
font-size: 22px;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.2;
|
||||
margin-top: 2.5em;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.prose.prose-invert h2 {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.prose.prose-invert h3 {
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.25;
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.prose.prose-invert h3 {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.prose.prose-invert strong {
|
||||
color: #ffffff;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.prose.prose-invert li {
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.prose.prose-invert li {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.prose.prose-invert li::marker {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.prose.prose-invert a {
|
||||
text-decoration-color: rgba(149, 100, 255, 0.4);
|
||||
transition: color 0.3s, text-decoration-color 0.3s;
|
||||
}
|
||||
|
||||
.prose.prose-invert a:hover {
|
||||
color: #b08aff;
|
||||
text-decoration-color: #b08aff;
|
||||
}
|
||||
|
||||
6
apps/website/app/layout.js
Executable file → Normal file
@@ -30,11 +30,11 @@ export const metadata = {
|
||||
],
|
||||
authors: [{ name: "Autumn" }],
|
||||
creator: "Autumn",
|
||||
metadataBase: new URL("https://useautumn.com/"),
|
||||
metadataBase: new URL("https://autumndev.vercel.app"),
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
url: "https://useautumn.com/",
|
||||
url: "https://autumndev.vercel.app",
|
||||
siteName: "Autumn",
|
||||
title: "Autumn — Billing Infrastructure for AI Startups",
|
||||
description:
|
||||
@@ -73,7 +73,7 @@ export default function RootLayout({ children }) {
|
||||
<html
|
||||
lang="en"
|
||||
suppressHydrationWarning
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased bg-black`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
</html>
|
||||
|
||||
0
apps/website/app/not-found.js
Executable file → Normal file
3
apps/website/app/page.js
Executable file → Normal file
@@ -26,8 +26,7 @@ export default function Home() {
|
||||
</div>
|
||||
<HomeSections />
|
||||
|
||||
<div
|
||||
className="w-full flex-col gap-2.5 mt-10.5 hidden md:flex">
|
||||
<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" />
|
||||
|
||||
0
apps/website/app/privacy/page.js
Executable file → Normal file
1252
apps/website/bun.lock
Normal file
56
apps/website/components/animated-footer-image.jsx
Executable file → Normal file
@@ -1,31 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
|
||||
export default function AnimatedFooterImage() {
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 w-full z-0 pointer-events-none">
|
||||
{/* Mobile */}
|
||||
<Image
|
||||
src="/images/footer/maskedmobile.webp"
|
||||
alt="footer background"
|
||||
loading="lazy"
|
||||
width={0}
|
||||
height={0}
|
||||
sizes="100vw"
|
||||
className="block sm:hidden w-full h-auto"
|
||||
/>
|
||||
|
||||
{/* Desktop */}
|
||||
<Image
|
||||
src="/images/footer/maskedimage.webp"
|
||||
alt="footer background"
|
||||
width={0}
|
||||
height={0}
|
||||
sizes="100vw"
|
||||
loading="lazy"
|
||||
className="hidden sm:block w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
"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;
|
||||
|
||||
494
apps/website/components/autumn-config.jsx
Executable file → Normal file
@@ -1,247 +1,247 @@
|
||||
"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-1">
|
||||
autumn.config.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>
|
||||
);
|
||||
}
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
89
apps/website/components/blogComponents.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
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>
|
||||
),
|
||||
};
|
||||
136
apps/website/components/dashboard-icon-pixel.jsx
Executable file → Normal file
@@ -1,71 +1,65 @@
|
||||
"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 pulseRef = useRef(null);
|
||||
const tlRef = useRef(null);
|
||||
const frames = [
|
||||
"\u2800\u2836\u2800",
|
||||
"\u2830\u28FF\u2806",
|
||||
"\u28BE\u28C9\u2877",
|
||||
"\u28CF\u2800\u28F9",
|
||||
"\u2841\u2800\u2888",
|
||||
];
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
restart: () => tlRef.current?.play(),
|
||||
reverse: () => tlRef.current?.reverse(),
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
const pixels = iconRef.current?.querySelectorAll(".icon-pixel-path");
|
||||
const pulseEl = pulseRef.current;
|
||||
if (!pixels || !pulseEl) return;
|
||||
|
||||
gsap.set(pixels, {
|
||||
opacity: 0.25,
|
||||
scale: 0.8,
|
||||
transformOrigin: "left bottom",
|
||||
});
|
||||
gsap.set(pulseEl, { opacity: 0 });
|
||||
|
||||
tlRef.current = gsap
|
||||
.timeline({ paused: true })
|
||||
.to(pixels, { opacity: 0, duration: 0.05 })
|
||||
.to(pulseEl, { opacity: 1, duration: 0.05 }, "<")
|
||||
.to(pulseEl, {
|
||||
duration: 0.2,
|
||||
onUpdate: function () {
|
||||
pulseEl.innerText =
|
||||
frames[Math.floor(this.progress() * (frames.length - 1))];
|
||||
},
|
||||
})
|
||||
.to(pulseEl, { opacity: 0, duration: 0.1 })
|
||||
.to(
|
||||
pixels,
|
||||
{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: 0.2,
|
||||
stagger: { grid: [5, 5], from: [0, 4], amount: 0.2 },
|
||||
},
|
||||
"-=0.1",
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
// Height and Width match your original icon (14px)
|
||||
<span className="relative inline-flex items-center justify-center w-[14px] h-[14px]">
|
||||
<div
|
||||
ref={pulseRef}
|
||||
className="absolute z-20 font-mono text-[14px] text-white opacity-0"
|
||||
/>
|
||||
<Icon ref={iconRef} className="w-full h-full text-white" />
|
||||
</span>
|
||||
);
|
||||
});
|
||||
"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>
|
||||
);
|
||||
});
|
||||
|
||||
250
apps/website/components/elastic-footer.jsx
Executable file → Normal file
@@ -1,107 +1,143 @@
|
||||
"use client";
|
||||
import { motion, useSpring, useTransform, useMotionValue } from "framer-motion";
|
||||
import { useEffect } from "react";
|
||||
import AnimatedFooterImage from "./animated-footer-image";
|
||||
|
||||
export default function ElasticRecoil({ children }) {
|
||||
const liftAmount = useMotionValue(0);
|
||||
|
||||
const springConfig = { stiffness: 600, damping: 35, mass: 1 };
|
||||
const animatedLift = useSpring(liftAmount, springConfig);
|
||||
const y = useTransform(animatedLift, [0, 400], [0, -280]);
|
||||
|
||||
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) {
|
||||
liftAmount.set(liftAmount.get() + e.deltaY * 0.5);
|
||||
recoilFired = false;
|
||||
|
||||
// Reset timeout on every wheel event
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
if (!isTouching) {
|
||||
triggerRebound();
|
||||
}
|
||||
}, 1500); // Rebounds 1.5 seconds after wheel stops
|
||||
} else if (e.deltaY < 0) {
|
||||
if (!isTouching) {
|
||||
liftAmount.set(0);
|
||||
recoilFired = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let touchStart = 0;
|
||||
const handleTouchStart = (e) => {
|
||||
isTouching = true;
|
||||
touchStart = e.touches[0].clientY;
|
||||
recoilFired = false;
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
|
||||
const handleTouchMove = (e) => {
|
||||
const isAtBottom =
|
||||
window.innerHeight + window.pageYOffset >=
|
||||
document.documentElement.scrollHeight - 5;
|
||||
|
||||
if (isAtBottom) {
|
||||
const touchDelta = touchStart - e.touches[0].clientY;
|
||||
if (touchDelta > 0) {
|
||||
liftAmount.set(touchDelta * 1.5);
|
||||
recoilFired = false;
|
||||
// Notice: We don't set a timeout here, so it never snaps back while touching
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
isTouching = false;
|
||||
if (liftAmount.get() > 0) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
triggerRebound();
|
||||
}, 1500); // Rebounds 1.5 seconds after lifting finger
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
<AnimatedFooterImage />
|
||||
<motion.div style={{ y }} className="relative z-10 bg-black">
|
||||
{children}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
294
apps/website/components/faq.jsx
Executable file → Normal file
@@ -1,147 +1,147 @@
|
||||
"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 md:grid-cols-2 w-full min-h-[500px]">
|
||||
<div className="md: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 md:block border-b border-[#292929] h-full w-full"></div>
|
||||
|
||||
<div className="hidden md:block md: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-8 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">
|
||||
|
||||
</h3>
|
||||
<div className="shrink-0 w-5 h-5"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
"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">
|
||||
|
||||
</h3>
|
||||
<div className="shrink-0 w-5 h-5"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
174
apps/website/components/feature-icon-animation.jsx
Executable file → Normal file
@@ -1,106 +1,68 @@
|
||||
"use client";
|
||||
import { forwardRef, useImperativeHandle, useRef, useEffect } from "react";
|
||||
import gsap from "gsap";
|
||||
|
||||
export const FeatureIconAnimation = forwardRef(({ Icon }, ref) => {
|
||||
const iconRef = useRef(null);
|
||||
const pulseRef = useRef(null);
|
||||
const tlRef = useRef(null);
|
||||
|
||||
const frames = [
|
||||
"\u2800\u2836\u2800",
|
||||
"\u2830\u28FF\u2806",
|
||||
"\u28BE\u28C9\u2877",
|
||||
"\u28CF\u2800\u28F9",
|
||||
"\u2841\u2800\u2888",
|
||||
];
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
play: () => tlRef.current?.play(),
|
||||
reverse: () => tlRef.current?.reverse(),
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
const pixels = iconRef.current?.querySelectorAll("path");
|
||||
const pulseEl = pulseRef.current;
|
||||
if (!pixels || !pulseEl) return;
|
||||
|
||||
// IDLE: Subjugated/Ghost state
|
||||
gsap.set(pixels, {
|
||||
opacity: 0.15,
|
||||
scale: 0.8,
|
||||
transformOrigin: "left bottom",
|
||||
});
|
||||
gsap.set(pulseEl, { opacity: 0 });
|
||||
|
||||
tlRef.current = gsap.timeline({ paused: true });
|
||||
|
||||
tlRef.current
|
||||
// SCRAMBLE
|
||||
.to(pixels, { opacity: 0, duration: 0.05 })
|
||||
.to(pulseEl, { opacity: 1, duration: 0.05 }, "<")
|
||||
.to(pulseEl, {
|
||||
duration: 0.3,
|
||||
onUpdate: function () {
|
||||
const frameIndex = Math.floor(this.progress() * (frames.length - 1));
|
||||
pulseEl.innerText = frames[frameIndex];
|
||||
},
|
||||
ease: "none",
|
||||
})
|
||||
// REVEAL
|
||||
.to(pulseEl, { opacity: 0, duration: 0.1, scale: 1.2 })
|
||||
.to(
|
||||
pixels,
|
||||
{
|
||||
opacity: 1,
|
||||
scale: 1.15,
|
||||
fill: "#9564ff", // Autumn Purple highlight for features
|
||||
duration: 0.2,
|
||||
stagger: {
|
||||
grid: [5, 5],
|
||||
from: [0, 4], // Bottom-Left Sweep
|
||||
amount: 0.25,
|
||||
},
|
||||
ease: "power2.out",
|
||||
},
|
||||
"-=0.1",
|
||||
)
|
||||
.to(pixels, {
|
||||
scale: 1,
|
||||
duration: 0.15,
|
||||
ease: "back.out(3)",
|
||||
});
|
||||
|
||||
return () => tlRef.current?.kill();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center justify-center w-12 h-12 group/icon">
|
||||
{/* 5x5 Grid Mask */}
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div className="grid grid-cols-5 grid-rows-5 gap-[3px]">
|
||||
{Array.from({ length: 25 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-white/[0.08] w-[2.5px] h-[2.5px] rounded-[0.5px]"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pulse Shuffle Layer */}
|
||||
<div
|
||||
ref={pulseRef}
|
||||
className="absolute z-20 font-mono text-[20px] text-[#9564ff] pointer-events-none select-none"
|
||||
>
|
||||
{"\u2800\u2836\u2800"}
|
||||
</div>
|
||||
|
||||
{/* Feature Icon Layer */}
|
||||
<div className="relative z-10 w-[24px] h-[24px] flex items-center justify-center">
|
||||
<Icon ref={iconRef} className="w-full h-full text-white" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
FeatureIconAnimation.displayName = "FeatureIconAnimation";
|
||||
"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";
|
||||
|
||||
@@ -4,14 +4,17 @@ 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={() => iconRef.current?.play()}
|
||||
onMouseLeave={() => iconRef.current?.reverse()}
|
||||
className="group relative flex px-4 md:px-6 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"
|
||||
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 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-500 ease-out pointer-events-none z-0 hidden md:block">
|
||||
<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
|
||||
@@ -22,7 +25,7 @@ function FeatureCard({ feature }) {
|
||||
/>
|
||||
</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 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
|
||||
<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" />
|
||||
@@ -47,13 +50,13 @@ export default function Features() {
|
||||
<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">Nothing you have to build.</div>
|
||||
<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]">
|
||||
Eight features that eliminate your entire{" "}
|
||||
Your entire billing infrastructure, {" "}
|
||||
<span className="text-white">
|
||||
billing infrastructure, fully managed.
|
||||
fully managed.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,7 +66,7 @@ export default function Features() {
|
||||
{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 className="bg-[#0f0f0f] w-full h-full min-h-[280px] hidden lg:block border-r border-b border-[#292929]" /> */}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
362
apps/website/components/footer.jsx
Executable file → Normal file
@@ -1,181 +1,181 @@
|
||||
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: "https://useautumn.com/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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import AutumnConfig from "./autumn-config";
|
||||
|
||||
// const AutumnConfig = dynamic(() => import("./autumn-config"), { ssr: false });
|
||||
|
||||
const BADGE_TEXT = "// billing infrastructure for ai";
|
||||
const BADGE_TEXT = "// 100% open source";
|
||||
|
||||
export default function Hero() {
|
||||
const containerRef = useRef(null);
|
||||
@@ -147,24 +147,9 @@ export default function Hero() {
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<div className="relative hero-root opacity-0 flex flex-col items-stretch pb-0 xl:pb-2.5 mb-0 lg:mb-[33px] bg-[#0F0F0F]">
|
||||
<div className="relative hidden xl:block">
|
||||
<Image
|
||||
className="hero-bg w-full hidden md:block"
|
||||
src={"/images/hero/hero_img.webp"}
|
||||
width={1359}
|
||||
height={343}
|
||||
sizes="100vw"
|
||||
style={{ width: "100%", height: "auto" }}
|
||||
alt="hero-bg"
|
||||
priority
|
||||
/>
|
||||
<div className="hero-reveal absolute right-4 xl:right-8.5 top-38 w-[36vw] max-w-[520px]">
|
||||
<AutumnConfig initialDelay={200} awaitEvent="preloader:complete" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6 px-4 xl:px-22.75 py-8 bg-[#0F0F0F]">
|
||||
<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}
|
||||
@@ -181,16 +166,32 @@ export default function Hero() {
|
||||
<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 systems, and subscription
|
||||
Stop rebuilding usage limits, credit ledgers and payment
|
||||
logic.{" "}
|
||||
<span className="text-white font-light">
|
||||
Autumn is the source of truth
|
||||
Autumn is your customer database
|
||||
</span>{" "}
|
||||
that keeps webhooks, payments and usage perfectly in-sync.
|
||||
that scales from your first user to your largest contract.
|
||||
</p>
|
||||
</div>
|
||||
</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 */}
|
||||
@@ -205,10 +206,10 @@ export default function Hero() {
|
||||
{/* 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-tight text-white font-medium text-[13px] md:text-base whitespace-nowrap">
|
||||
<span className="relative z-10 tracking-[-2%] uppercase md:normal-case text-white font-medium text-[12px] md:text-base whitespace-nowrap">
|
||||
Start for free
|
||||
</span>
|
||||
<span className="relative z-10 scale-75 md:scale-100">
|
||||
<span className="relative z-10 scale-95 md:scale-100">
|
||||
<IconCTAStart />
|
||||
</span>
|
||||
</div>
|
||||
@@ -218,7 +219,7 @@ export default function Hero() {
|
||||
|
||||
{/* Secondary CTA */}
|
||||
<div className="hero-cta w-full md:w-fit md:flex-shrink-0">
|
||||
<Link href={"https://docs.useautumn.com/welcome"}>
|
||||
<Link href={"https://cal.com/ayrod"}>
|
||||
<motion.div
|
||||
initial="initial"
|
||||
whileHover="hover"
|
||||
@@ -227,23 +228,24 @@ export default function Hero() {
|
||||
>
|
||||
<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-tight text-[12px] md:text-[16px] whitespace-nowrap">
|
||||
Read docs
|
||||
<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-75 md:scale-100">
|
||||
<span className="relative z-10 scale-100">
|
||||
<IconCTADocs />
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="hero-cta flex flex-nowrap gap-2 md:gap-3 ml-2 md:ml-3 h-10.5 md:h-12.5 flex-1">
|
||||
<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 min-[400px]: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" />
|
||||
@@ -255,13 +257,13 @@ export default function Hero() {
|
||||
{/* 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">
|
||||
<Image
|
||||
className="hero-bg absolute inset-0 w-full h-full object-cover"
|
||||
src="/images/hero/hero_mobile.webp"
|
||||
width={900}
|
||||
height={800}
|
||||
alt="hero-bg-mobile"
|
||||
priority
|
||||
<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">
|
||||
|
||||
@@ -24,18 +24,18 @@ export default function HomeSections() {
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
<SectionDivider title="PRODUCTION SCALE" />
|
||||
<ProductionScale />
|
||||
<SectionDivider title="THE PROBLEM" />
|
||||
<Problem />
|
||||
<SectionDivider title="THE SOLUTION" />
|
||||
<Solution />
|
||||
<SectionDivider title="PRICING MODELS" />
|
||||
<PricingModels />
|
||||
<SectionDivider title="FEATURES" />
|
||||
<Features />
|
||||
<SectionDivider title="PRICING MODELS" />
|
||||
<PricingModels />
|
||||
<SectionDivider title="TESTIMONIALS" />
|
||||
<Testimonials />
|
||||
<SectionDivider title="PRODUCTION SCALE" />
|
||||
<ProductionScale />
|
||||
<SectionDivider title="PRICING" />
|
||||
<Pricing />
|
||||
<SectionDivider title="FAQ" />
|
||||
|
||||
863
apps/website/components/navbar.jsx
Executable file → Normal file
@@ -1,442 +1,421 @@
|
||||
"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: "Discord",
|
||||
href: "https://discord.com/invite/STqxY92zuS",
|
||||
Icon: IconDiscord,
|
||||
},
|
||||
{ label: "Blog", href: "https://useautumn.com/blog", Icon: IconBlog },
|
||||
{ label: "Docs", href: "https://docs.useautumn.com/welcome", Icon: IconDocs },
|
||||
{ label: "Pricing", href: "#", Icon: IconPricing },
|
||||
];
|
||||
|
||||
const NavIconPixel = forwardRef(function NavIconPixel({ Icon }, ref) {
|
||||
const iconRef = useRef(null);
|
||||
const pulseRef = useRef(null);
|
||||
const tlRef = useRef(null);
|
||||
|
||||
const frames = [
|
||||
"\u2800\u2836\u2800",
|
||||
"\u2830\u28FF\u2806",
|
||||
"\u28BE\u28C9\u2877",
|
||||
"\u28CF\u2800\u28F9",
|
||||
"\u2841\u2800\u2888",
|
||||
];
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
restart: () => {
|
||||
tlRef.current?.play();
|
||||
},
|
||||
reverse: () => {
|
||||
tlRef.current?.reverse();
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
const pixels = iconRef.current?.querySelectorAll(".icon-pixel-path");
|
||||
const pulseEl = pulseRef.current;
|
||||
if (!pixels || !pulseEl) return;
|
||||
|
||||
gsap.set(pixels, {
|
||||
opacity: 0.15,
|
||||
scale: 0.8,
|
||||
transformOrigin: "left bottom",
|
||||
fill: "currentColor",
|
||||
});
|
||||
gsap.set(pulseEl, { opacity: 0 });
|
||||
|
||||
tlRef.current = gsap.timeline({ paused: true });
|
||||
|
||||
tlRef.current
|
||||
.to(pixels, { opacity: 0, duration: 0.05 })
|
||||
.to(pulseEl, { opacity: 1, duration: 0.05 }, "<")
|
||||
.to(pulseEl, {
|
||||
duration: 0.3,
|
||||
onUpdate: function () {
|
||||
const frameIndex = Math.floor(this.progress() * (frames.length - 1));
|
||||
pulseEl.innerText = frames[frameIndex];
|
||||
},
|
||||
ease: "none",
|
||||
})
|
||||
// 3. REVEAL: Pulse fades, SVG Icon sweeps in from bottom-left
|
||||
.to(pulseEl, { opacity: 0, duration: 0.1, scale: 1.2 })
|
||||
.to(
|
||||
pixels,
|
||||
{
|
||||
opacity: 1,
|
||||
scale: 1.15,
|
||||
fill: "#FFFFFF",
|
||||
duration: 0.2,
|
||||
stagger: {
|
||||
grid: [5, 5],
|
||||
from: [0, 4], // Bottom-Left scan
|
||||
amount: 0.25,
|
||||
},
|
||||
ease: "power2.out",
|
||||
},
|
||||
"-=0.1",
|
||||
)
|
||||
.to(pixels, {
|
||||
scale: 1,
|
||||
duration: 0.15,
|
||||
ease: "back.out(3)",
|
||||
});
|
||||
|
||||
return () => tlRef.current?.kill();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<span className="relative inline-flex items-center justify-center w-8 h-8 group/icon">
|
||||
{/* BACKGROUND MASK: Static field dots */}
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div className="grid grid-cols-5 grid-rows-5 gap-[3px]">
|
||||
{Array.from({ length: 25 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-white/[0.08] w-[1px] h-[1px] rounded-[0.5px]"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BRAILLE PULSE LAYER */}
|
||||
<div
|
||||
ref={pulseRef}
|
||||
className="absolute z-20 font-mono text-[16px] text-white pointer-events-none select-none"
|
||||
>
|
||||
{"\u2800\u2836\u2800"}
|
||||
</div>
|
||||
|
||||
{/* SVG ICON LAYER (Idle: Faded / Hover: Solid) */}
|
||||
<div className="relative z-10 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);
|
||||
return (
|
||||
<div className="nav-link">
|
||||
<Link
|
||||
href={item.href}
|
||||
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 pt-2.5 lg:py-0 xl:py-0 pb-2.5 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 md:flex items-center gap-6">
|
||||
{NAV_LINKS.map((item) => (
|
||||
<NavLinkItem key={item.label} item={item} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="nav-dashboard hidden md: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="md: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 w-full nav-mobile left-0 overflow-y-auto bg-[#000000] flex flex-col font-mono uppercase ${scrolled && !recoilHidden ? "top-[62px] sm:top-[60px]" : "top-[66px] sm:top-[62px]"} md:top-5 h-[calc(100dvh-58px)] z-40 px-4 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) => (
|
||||
<Link
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
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>
|
||||
);
|
||||
}
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
476
apps/website/components/preloader.jsx
Executable file → Normal file
@@ -1,238 +1,238 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,258 +1,252 @@
|
||||
"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-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.
|
||||
</span>{" "}
|
||||
<span className="text-white">Change without code deploys.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hero-cta">
|
||||
<Link href={"https://useautumn.com/"} 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="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-[260px_340px]">
|
||||
<div className="absolute top-0 bottom-0 right-0 lg:w-[calc(100%-500px)] xl:w-[calc(100%-680px)] z-0 overflow-hidden hidden lg:block pointer-events-none">
|
||||
<video
|
||||
src="/images/pricing-models/pricingbg.webm"
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className="absolute inset-0 w-full h-full object-cover object-left 2xl:object-center mix-blend-screen opacity-100"
|
||||
/>
|
||||
</div>
|
||||
<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 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 object-left mix-blend-screen opacity-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 z-10 flex items-center justify-center px-2.5 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-6 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-6 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>
|
||||
);
|
||||
}
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,183 +1,177 @@
|
||||
"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 100 active customers",
|
||||
"All core features",
|
||||
"Community support",
|
||||
"Unlimited API calls",
|
||||
],
|
||||
buttonText: "Get started",
|
||||
href: "https://app.useautumn.com/sign-in",
|
||||
isPro: false,
|
||||
},
|
||||
{
|
||||
name: "PRO",
|
||||
price: "299",
|
||||
description: "For teams scaling with real usage-based pricing.",
|
||||
features: [
|
||||
"Unlimited customers",
|
||||
"Priority support",
|
||||
"Custom plans",
|
||||
"Usage analytics",
|
||||
"SLA guarantees",
|
||||
],
|
||||
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: [
|
||||
"Self-hosted option",
|
||||
"Dedicated support",
|
||||
"Custom SLAs",
|
||||
"Multi-region",
|
||||
"Compliance assistance",
|
||||
],
|
||||
buttonText: "Book a call",
|
||||
href: "https://cal.com/ayrod/a?user=ayrod",
|
||||
isPro: false,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Pricing() {
|
||||
return (
|
||||
<>
|
||||
<div 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/pricingbg.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_mobile.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">
|
||||
All plans include unlimited API calls · Pricing based on active
|
||||
customers, not events
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
"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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,211 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
import Matter from "matter-js";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
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 CATEGORY_DEFAULT = 0x0001;
|
||||
const CATEGORY_CEILING = 0x0002;
|
||||
|
||||
export default function ProblemAnimation() {
|
||||
const containerRef = useRef(null);
|
||||
const engineRef = useRef(Matter.Engine.create());
|
||||
const runnerRef = useRef(null);
|
||||
|
||||
const itemsMap = useRef(new Map());
|
||||
const grabbedBodies = useRef(new Set());
|
||||
const [loadedImages, setLoadedImages] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const { width, height } = container.getBoundingClientRect();
|
||||
const engine = engineRef.current;
|
||||
engine.gravity.y = 1.2;
|
||||
|
||||
const wallThickness = 100;
|
||||
|
||||
const ground = Matter.Bodies.rectangle(
|
||||
width / 2,
|
||||
height + wallThickness / 2,
|
||||
width,
|
||||
wallThickness,
|
||||
{ isStatic: true },
|
||||
);
|
||||
|
||||
const leftWall = Matter.Bodies.rectangle(
|
||||
-wallThickness / 2,
|
||||
height / 2,
|
||||
wallThickness,
|
||||
height * 2,
|
||||
{ isStatic: true },
|
||||
);
|
||||
|
||||
const rightWall = Matter.Bodies.rectangle(
|
||||
width + wallThickness / 2,
|
||||
height / 2,
|
||||
wallThickness,
|
||||
height * 2,
|
||||
{ isStatic: true },
|
||||
);
|
||||
|
||||
// Ceiling — only collides with bodies that have been grabbed
|
||||
const ceiling = Matter.Bodies.rectangle(
|
||||
width / 2,
|
||||
-wallThickness / 2,
|
||||
width,
|
||||
wallThickness,
|
||||
{
|
||||
isStatic: true,
|
||||
collisionFilter: {
|
||||
category: CATEGORY_CEILING,
|
||||
mask: CATEGORY_CEILING,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
Matter.World.add(engine.world, [ground, leftWall, rightWall, ceiling]);
|
||||
|
||||
const isMobile = window.innerWidth < 768;
|
||||
|
||||
// Mouse Interaction (Desktop Only)
|
||||
if (!isMobile) {
|
||||
const mouse = Matter.Mouse.create(container);
|
||||
const mouseConstraint = Matter.MouseConstraint.create(engine, {
|
||||
mouse: mouse,
|
||||
constraint: { stiffness: 0.15, render: { visible: false } },
|
||||
});
|
||||
Matter.World.add(engine.world, mouseConstraint);
|
||||
mouse.element.removeEventListener("mousewheel", mouse.mousewheel);
|
||||
|
||||
// On grab: mark body as grabbed + enable ceiling collision
|
||||
Matter.Events.on(mouseConstraint, "startdrag", (event) => {
|
||||
grabbedBodies.current.add(event.body.id);
|
||||
|
||||
Matter.Body.set(event.body, {
|
||||
collisionFilter: {
|
||||
category: CATEGORY_DEFAULT,
|
||||
mask: CATEGORY_DEFAULT | CATEGORY_CEILING,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Sync Loop
|
||||
const update = () => {
|
||||
itemsMap.current.forEach(({ body, element, w, h }) => {
|
||||
// Freeze rotation until the user grabs the body
|
||||
if (!grabbedBodies.current.has(body.id)) {
|
||||
Matter.Body.setAngle(body, 0);
|
||||
Matter.Body.setAngularVelocity(body, 0);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
gsap.set(element, {
|
||||
x: body.position.x - w / 2,
|
||||
y: body.position.y - h / 2,
|
||||
rotation: body.angle * (180 / Math.PI),
|
||||
opacity: 1,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Trigger Engine on Scroll
|
||||
const st = ScrollTrigger.create({
|
||||
trigger: container,
|
||||
start: "top 85%",
|
||||
onEnter: () => {
|
||||
runnerRef.current = Matter.Runner.create();
|
||||
Matter.Runner.run(runnerRef.current, engine);
|
||||
gsap.ticker.add(update);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
st.kill();
|
||||
gsap.ticker.remove(update);
|
||||
if (runnerRef.current) Matter.Runner.stop(runnerRef.current);
|
||||
Matter.World.clear(engine.world);
|
||||
Matter.Engine.clear(engine);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleImageLoad = (index, event) => {
|
||||
const img = event.target;
|
||||
const isMobile = window.innerWidth < 768;
|
||||
const scale = isMobile ? 0.6 : 1;
|
||||
|
||||
const w = img.naturalWidth * scale;
|
||||
const h = img.naturalHeight * scale;
|
||||
|
||||
const container = containerRef.current;
|
||||
const spawnX =
|
||||
container.offsetWidth / 2 +
|
||||
(Math.random() - 0.5) * (container.offsetWidth * 0.4);
|
||||
const spawnY = -100 - index * 50;
|
||||
|
||||
const body = Matter.Bodies.rectangle(spawnX, spawnY, w, h, {
|
||||
chamfer: { radius: h / 4 },
|
||||
restitution: 0.4,
|
||||
friction: 0.1,
|
||||
angle: 0,
|
||||
// Spawns ignoring the ceiling — falls in freely from above
|
||||
collisionFilter: {
|
||||
category: CATEGORY_DEFAULT,
|
||||
mask: CATEGORY_DEFAULT,
|
||||
},
|
||||
});
|
||||
|
||||
Matter.World.add(engineRef.current.world, body);
|
||||
|
||||
itemsMap.current.set(index, {
|
||||
body,
|
||||
element: img.parentElement,
|
||||
w,
|
||||
h,
|
||||
});
|
||||
|
||||
setLoadedImages((prev) => ({ ...prev, [index]: true }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative w-full h-[450px] lg:h-full bg-[#080808] overflow-hidden touch-none"
|
||||
>
|
||||
{IMAGE_SOURCES.map((src, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute top-0 left-0 opacity-0 will-change-transform"
|
||||
style={{ visibility: loadedImages[i] ? "visible" : "hidden" }}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
onLoad={(e) => handleImageLoad(i, e)}
|
||||
className="block cursor-grab active:cursor-grabbing"
|
||||
style={{ width: "auto", height: "auto" }}
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,98 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import Image from "next/image";
|
||||
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] lg:text-[16px] leading-[18px] md:leading-[20px]">
|
||||
<span className="font-light mr-1">The hidden complexity:</span>
|
||||
Payment processors move money. They don't handle the state
|
||||
management that happens between payments.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid lg:grid-cols-2 gap-0 lg: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 lg:pr-8 pt-14 sm:pt-16 lg:pt-16 items-center lg:items-start">
|
||||
<h2 className="font-normal tracking-[-4%] leading-[32px] lg:leading-[40px] mb-2 lg:mb-4 text-center lg:text-left">
|
||||
<span className="block text-white text-[30px] md:text-[36px] lg:text-[40px]">
|
||||
AI made billing
|
||||
</span>
|
||||
<span className="block text-[#686868] text-[30px] md:text-[36px] lg:text-[40px]">
|
||||
way harder
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<p className="text-[#888888] font-light text-[16px] md:text-[18px] lg:text-[16px] tracking-[-2%] leading-[20px] mb-8 lg:mb-10 max-w-sm md:max-w-lg lg:max-w-sm text-center lg:text-left">
|
||||
You're not charging for access anymore. You're metering
|
||||
tokens, tracking credits, resetting quotas —{" "}
|
||||
<span className="text-white">
|
||||
all before you process a single payment.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto">
|
||||
<div className="grid grid-cols-2 md:grid-cols-[max-content_max-content_max-content_auto] border-t border-b border-[#1E1E1E]">
|
||||
{[
|
||||
{ value: "3–6", label: "Uptime SLA target" },
|
||||
{ value: "40+ lines", label: "to gate one feature" },
|
||||
{ value: "Infinite cases", label: "To handle billing" },
|
||||
].map((stat, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`py-6 pr-9 border-[#292929] border-r
|
||||
${i === 2 ? "col-span-2 md:col-span-1 border-t md:border-t-0 border-r-0 md:border-r" : ""}
|
||||
${/* Mobile: Infinite cases (i=2) now matches the 28px padding of the first item (i=0) */ ""}
|
||||
${i === 0 || i === 2 ? "pl-[28px]" : "pl-4"}
|
||||
md:pl-6 ${i === 0 ? "xl:pl-[90px]" : ""}`}
|
||||
>
|
||||
<p className="text-white tracking-[-5%] font-normal text-[18px] lg:text-[24px] leading-none">
|
||||
{stat.value}
|
||||
</p>
|
||||
{/* whitespace-nowrap ensures it stays on one line regardless of container width */}
|
||||
<p className="text-[#767676] font-light tracking-[-5%] text-[16px] lg:text-[18px] mt-1.5 leading-[20px] whitespace-nowrap">
|
||||
{stat.label}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
<div className="hidden md:flex flex-1 items-stretch">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<div key={i} className="flex-1 border-r border-[#292929]" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProblemBgSvg />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT COLUMN — problem animation */}
|
||||
<div className="relative overflow-hidden ">
|
||||
<ProblemAnimation />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
"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'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: "3–6 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,227 +1,229 @@
|
||||
"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: "#FFE8FA",
|
||||
icon: "/images/production/latency.svg",
|
||||
metric: "<100ms",
|
||||
label: "US latency",
|
||||
description:
|
||||
"Every billing check resolves in under 100ms. Your users never wait for a gate.",
|
||||
clipart: true,
|
||||
},
|
||||
{
|
||||
bg: "#D698FF",
|
||||
icon: "/images/production/uptiime.svg",
|
||||
metric: "99.99%",
|
||||
label: "Uptime SLA",
|
||||
description:
|
||||
"Designed for teams that can't afford billing downtime. Four nines, guaranteed.",
|
||||
clipart: true,
|
||||
},
|
||||
{
|
||||
bg: "#A175FF",
|
||||
icon: "/images/production/uptime2.svg",
|
||||
metric: "10,000+",
|
||||
label: "Uptime SLA",
|
||||
description:
|
||||
"Autumn handles billions of billing events monthly. Open source core, self-host ready.",
|
||||
clipart: true,
|
||||
},
|
||||
{
|
||||
bg: "#F55DD0",
|
||||
icon: "/images/production/churn.svg",
|
||||
metric: "Zero",
|
||||
label: "Churn rate",
|
||||
description: "Per customer. When billing works, customers stay.",
|
||||
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-6 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">
|
||||
Built for
|
||||
</p>
|
||||
<h2 className="text-white tracking-[-4%] text-[30px] lg:text-[40px] font-normal mt-1 lg:mt-0">
|
||||
production scale
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-[#FFFFFF99] font-light text-[16px] lg:text-sm lg:w-sm leading-[20px] lg:leading-5">
|
||||
Autumn handles billions of billing events monthly. Open source core,
|
||||
self-host ready,{" "}
|
||||
<span className="text-white">
|
||||
designed for teams that <br className="hidden lg:block" /> can't afford billing downtime.
|
||||
</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>
|
||||
);
|
||||
}
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
38
apps/website/components/section-divider.jsx
Executable file → Normal file
@@ -1,19 +1,19 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
108
apps/website/components/solution-animation.jsx
Executable file → Normal file
@@ -1,54 +1,54 @@
|
||||
"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%" }} />;
|
||||
}
|
||||
"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%" }} />;
|
||||
}
|
||||
|
||||
136
apps/website/components/solution.jsx
Executable file → Normal file
@@ -1,68 +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>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,206 +1,222 @@
|
||||
"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 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(() => {
|
||||
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 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 by </span>
|
||||
<span className="text-white">teams</span>
|
||||
<br className="sm:hidden" />
|
||||
<span className="text-white"> who ship</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 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">
|
||||
<PixelatedPattern className="w-full h-full object-cover object-bottom opacity-0 translate-y-6 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-500 ease-out" />
|
||||
</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;
|
||||
"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;
|
||||
|
||||
95
apps/website/content/blog/attach.mdx
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: "Architecture to run 100 stripe cases in 1 endpoint"
|
||||
description: "Handling stripes upgrades, downgrades, schedules, one off and subscriptions all in 1 endpoint."
|
||||
date: "2025-06-11"
|
||||
author: "Ayush, Autumn Co-Founder"
|
||||
slug: "attach"
|
||||
image: "/images/blog/attach.png"
|
||||
featured: true
|
||||
---
|
||||
|
||||
We’re building an engine to run software pricing models. Something we still struggle to conceptualize is the number of cases that need to be handled. Take some common examples and how you’d do it in Stripe:
|
||||
|
||||
**Scenario**
|
||||
|
||||
**Endpoint**
|
||||
|
||||
Upgrading a subscription
|
||||
|
||||
`POST /subscriptions/:subscription_id`
|
||||
|
||||
Scheduling a downgrade
|
||||
|
||||
`POST /subscription_schedules`
|
||||
|
||||
Creating a checkout session
|
||||
|
||||
`POST /checkout/sessions`
|
||||
|
||||
Creating a new subscription (if card is on file)
|
||||
|
||||
`POST /subscriptions`
|
||||
|
||||
Creating a one off payment
|
||||
|
||||
`POST /invoices/:invoice_id/pay`
|
||||
|
||||
There are more complex scenarios that require updating individual items within a subscription (eg. decrease the price of a metered feature when upgrading).
|
||||
|
||||
Stripe’s low-level design maps each action to a different function. When designing Autumn, we were pretty strong in our belief that all these cases should just be 1 endpoint: `POST /attach`
|
||||
|
||||
Initially we just handled basic cases, so a set of if else statements was enough. As we’ve started handling more cases, the if else spaghetti was becoming a nightmare of bugs. We spent last week rewriting the architecture into 5 steps so we can handle it more logically.
|
||||
|
||||
### Step 1: Input validation and parsing
|
||||
|
||||
This is the body that the `/attach` request takes in, and handles all request related errors. We use Zod to parse the overall schema, then use it’s `refine` method for more granular error throwing (eg if conflicting fields are passed in).
|
||||
|
||||
### Step 2: Building the AttachContext
|
||||
|
||||
Using the inputs, we then make all the DB queries and calculations we need to gather the necessary data. These include:
|
||||
|
||||
1. The product data → it’s prices and features it gives access to
|
||||
|
||||
2. The customer data → Their current product, existing configuration, payment method details
|
||||
|
||||
3. Data from the request body → eg. checkout session params
|
||||
|
||||
|
||||
### Step 3: AttachBranch
|
||||
|
||||
This step is a first order categorisation of the `/attach` function based on the request body and context.
|
||||
|
||||
In our previous architecture, interpretability was a mess. We had our branching logic in multiple files and no concrete control / understanding of which path attach runs given the inputs.
|
||||
|
||||
We realised that different branches could be run through the same function. For instance, add ons and new subscription products branches, while separate scenarios, can now both be routed to the same `addProduct` function (see step 5). However they can also be routed to `createCheckout` depending on the config params in the next step.
|
||||
|
||||

|
||||
|
||||
### Step 4: AttachConfig
|
||||
|
||||
These are a set of parameters that control the specific behavior of the product enablement. They have default values determined by the previous stages of the pipeline.
|
||||
|
||||
For instance, we can control:
|
||||
|
||||
- whether an upgrade is prorated, or charged in full
|
||||
|
||||
- whether a new product should create a checkout session, charge a default payment method or just generate an invoice
|
||||
|
||||
- whether any existing meter usage should carry over to the new product, or be reset
|
||||
|
||||
|
||||

|
||||
|
||||
### Step 5: AttachFunction
|
||||
|
||||
The last step of the attach call is to determine which function to run, based on all of the prior categorisations. We referred to this in the AttachBranch step.
|
||||
|
||||
For instance, updating a custom product and upgrading to a new product technically can go through the same function, just with different configs (eg. proration behavior), and therefore can be routed to the same function — “UpdateProduct”
|
||||
|
||||

|
||||
|
||||
Then within each attach function, we make all the relevant calls to Stripe to ensure the scenario occurs successfully.
|
||||
|
||||
And then of course for each of these cases, you need to handle the downstream logic of actually giving the customer what they’ve paid for, which usually involves listening to webhooks, updating some permissions and maybe reseting usage limits.
|
||||
|
||||
We made that into another endpoint (`check`)… but that’s a story for another day.
|
||||
76
apps/website/content/blog/backendless.mdx
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: "We tried to make billing backendless"
|
||||
description: "We wanted to make payments, billing, usage limits and tracking as easy as possible. We explored a billing integration that required no backend setup, but ultimately gave up."
|
||||
date: "2025-05-13"
|
||||
author: "Ayush, Autumn Co-Founder"
|
||||
slug: "backendless"
|
||||
image: "/images/blog/backendless.jpeg"
|
||||
featured: true
|
||||
---
|
||||
|
||||
[Read the updated version of this article](/blog/thanks-hn)
|
||||
|
||||
We think handling payments on the frontend is a better developer experience.
|
||||
|
||||
Typically, billing is a backend job and requires webhooks, state syncing, then passing the data to the frontend. We wanted to offer a more "out-of-the-box" experience when handling things like payment links, paywalls and up/downgrade flows, and spent a bunch of time thinking about how we can perform sensitive operations without needing to perform the "round trip" to the backend.
|
||||
|
||||
This is a short write up of our exploration around the problem and why we ultimately are giving up.
|
||||
|
||||
**Part 1: The Publishable Key**
|
||||
|
||||
When we launched, we had a secret key that could be used securely from the backend just as [Stripe](/blog/what-if-stripe-builds-this) does. Many of our first users had actually never set up Stripe before, and immediately told us they wish they could just do it from the frontend.
|
||||
|
||||
Our first solution was to create a "publishable key" which would let developers get payment links and check feature access (eg, does my user have any remaining credits) directly from the frontend, in an unprotected way. These functions alone can't really be abused.
|
||||
|
||||
The initial response was good and people were happy to use it with their initial set up. But we quickly ran into a couple problems:
|
||||
|
||||
1. It only worked with some endpoints (eg, tracking billable usage events had to be done via the secret key) and ended up confusing devs around which endpoints could be used with which keys.
|
||||
|
||||
2. Most software billing flows allow you to automatically purchase something if you've made a purchase before. This automatic purchasing (eg for upgrades) definitely couldn't be done with a public key.
|
||||
|
||||
3. Although it helped people spin up a sample integration fast, it quickly had to be ripped out anyway, so ended up being pretty pointless.
|
||||
|
||||
|
||||
This still exists in our docs today and constantly trips people up. Can't wait to get rid of it.
|
||||
|
||||
**Part 2: Server Actions**
|
||||
|
||||
When we launched our Next.js library, we were excited to use server actions. The DX felt magical because users could:
|
||||
|
||||
1. Call them from the frontend like any normal function
|
||||
|
||||
2. The functions run on the server and can access our secret key stored as an ENV variable
|
||||
|
||||
3. No route set up needed, and the request is secure — nice!
|
||||
|
||||
|
||||
Unfortunately we soon discovered our approach was flawed. Server actions are public, unauthenticated routes, and our API calls updates resources based on a `customer_id` field (eg. upgrade / downgrade requests, tracking usage for a feature, etc).
|
||||
|
||||
So if you got a hold of someone else’s customer ID, you could make requests to the public server actions as if you were that customer—making this method insecure.
|
||||
|
||||
**Part 3: Server actions + encryption**
|
||||
|
||||
We really really liked the DX of server actions though, and so we had to brainstorm a way to overcome the customer ID being expoed in server action routes.
|
||||
|
||||
A few options came to mind, like using a middleware, or registering an authentication function, but the cleanest and simplest method we thought of was simply encrypting the customer ID:
|
||||
|
||||
Here’s how it worked:
|
||||
|
||||
1. Our Provider was a server component, and so it’d take in a customer ID/user ID (server side), encrypt it using their API key, and pass it to context on the client side (see image below)
|
||||
|
||||
2. We wrap each server action with a client side function which grabs the `encryptedCustomerId` from context and passes it to the server action. These are all exported through a hook — `useAutumn`
|
||||
|
||||
3. Each server action first decrypts the customer ID then calls the Autumn API
|
||||
|
||||
|
||||

|
||||
|
||||
Essentially, we baked our own layer of auth into the server actions, and this is how our Next.js library works today. I haven't seen this approach by anyone else but essentially lets us fully handle secure payments, upgrades, downgrades, usage-based billing events — all from the frontend.
|
||||
|
||||
We’re still not fully satisfied since this only works with frameworks that support server actions and SPA / vite is kinda making a comeback. It also makes the implementation different across frameworks.
|
||||
|
||||
**The future**
|
||||
|
||||
Ultimately I think we'll reach a point where we give up on this approach, and move towards a more framework agnostic approach. Rather than trying to abandon the backend route setup, we'll just make it easy to do. Take better-auth and how they generate their backend routes in just a couple lines of code — they’ve standardised the backend and frontend installation, and is pretty hard to get wrong.
|
||||
|
||||
Other companies like Polar have a similar approach and are heavily praised for it. We probably don't need to reinvent the wheel (as fun as it is).
|
||||
151
apps/website/content/blog/component-fail.mdx
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: "React vs shadcn, and trying to be too clever"
|
||||
description: "Learnings about building a component library for a billing platform for AI companies"
|
||||
date: "2025-06-30"
|
||||
author: "Ayush, Autumn Co-Founder"
|
||||
slug: "component-fail"
|
||||
image: "/images/blog/component-fail.png"
|
||||
featured: true
|
||||
---
|
||||
|
||||
You know those moments where you have an idea so brilliant you feel like Steve Jobs? Launching our component library felt like that. Novel, high risk, and a radically different approach to what was on the market. If it worked out, it'd be incredible.
|
||||
|
||||
Unfortunately, it didn't. We ended up with a weird mess and had to spend the last week rewriting everything.
|
||||
|
||||
_For context: we're building Autumn, pricing and billing software. Part of our offering is frontend components for things like a pricing table, paywalls etc. You can read more about them_ [_here_](https://docs.useautumn.com/quickstart/shadcn)_._
|
||||
|
||||
### **V1 as a React library**
|
||||
|
||||
We first attempted this as a standard npm React library:
|
||||
|
||||
```
|
||||
import PricingPage from "autumn-js"
|
||||
```
|
||||
|
||||
It's biggest issue was customisability**.** There are two ways to make npm components customisable: through props, or a no-code interface in our app.
|
||||
|
||||
1. With props there were too many class variables. For example with the pricing page, each pricing card is composed of many headers, descriptions, buttons and more. And each card itself can have variants (recommended, discounts, annual etc).
|
||||
|
||||
2. A low code interface is a bad experience for devs, who want full customisability and have 10x experience designing with tailwind/css. Even more so with the era of cursor. This is subjective but I’ve never had a good experience with one.
|
||||
|
||||
|
||||
_Sidenote:_ If you ever try to make your npm component customisable via tailwind, all the best. That was one of the most frustrating days of our lives.
|
||||
|
||||
Also, If you ever forgot what jsx styles look like, here’s a snippet of what we had to write…
|
||||
|
||||

|
||||
|
||||
We made one measly post on linkedin about it, decided it was unusable and shelved it.
|
||||
|
||||
### **V2 as shadcn/ui components**
|
||||
|
||||
A few weeks later we started looking into it again with shadcn.
|
||||
|
||||
```
|
||||
npx shadcn@latest add https://ui.useautumn.com/classic/pricing-table.json
|
||||
```
|
||||
|
||||
This immediately felt like a more customizable and fluid DX, but had its own challenges:
|
||||
|
||||
1. Because shadcn components install as a user file, we cannot fully control it via our SDK. For example, if we wanted to trigger a modal/popup to confirm a plan upgrade, the user needs to explicitly pass it into a function. This does take away slightly from that “magical DX”.
|
||||
|
||||
```jsx
|
||||
//upgrading to Pro tier
|
||||
<Button
|
||||
onClick={async () =>
|
||||
await attach({
|
||||
productId: "pro",
|
||||
dialog: ProductChangeDialog,
|
||||
})
|
||||
}
|
||||
/>;
|
||||
```
|
||||
|
||||
2. Since users own and control the component files, deciding what data abstraction level to return to the frontend is hard. For example, our upgrade dialog shows different text and styling depending on the scenarios:
|
||||
|
||||
\- One time purchase vs subscription
|
||||
\- Upgrades vs downgrades vs cancellations vs renewals
|
||||
\- Does the upgrade require an input from users (eg, quantity of credits to purchase)
|
||||
|
||||
|
||||

|
||||
|
||||
With a React library, everything would be "completely processed", with no customizability. Initially this is the same approach we took with the shadcn registry, but users quickly told us they wanted to customize the text too. We switched to the "in-between" approach.
|
||||
|
||||
Our API will return a scenario (eg, upgrade, downgrade, add-on, free-trial etc), and our shadcn components install with a library of cases that users can edit to control the messaging.
|
||||
|
||||
```jsx
|
||||
switch (scenario) {
|
||||
case "scheduled":
|
||||
return {
|
||||
title: <p>Scheduled product already exists</p>,
|
||||
message: <p>You will downgrade on {scheduled_date}</p>,
|
||||
};
|
||||
|
||||
case "active":
|
||||
return {
|
||||
title: <p>Product already active</p>,
|
||||
message: <p>You are already subscribed to this product.</p>,
|
||||
};
|
||||
|
||||
case "new":
|
||||
if (recurring) {
|
||||
return {
|
||||
title: <p>Subscribe to {product_name}</p>,
|
||||
message: (
|
||||
<p>
|
||||
By clicking confirm, you will be subscribed to {product_name} and
|
||||
your card will be charged immediately.
|
||||
</p>
|
||||
),
|
||||
};
|
||||
}
|
||||
//..... etc
|
||||
```
|
||||
|
||||

|
||||
|
||||
However, the shadcn library turned out as a huge mess for several reasons:
|
||||
|
||||
**Mistake 1**
|
||||
|
||||
We decided to launch our components as a shadcn package instead of plain React. Our components approach was inspired by Clerk, but I always thought they looked out of place and wanted users to own them. Shadcn had launched registry functionality so people could share and download components, and [Supabase](https://supabase.com/ui) launched one too, so it seemed promising.
|
||||
|
||||
Unfortunately only half our users use shadcn, but the others wanted components and couldn't use them. More importantly, we were iterating quickly on the underlying API that controls the component content. Since the components weren't synced to our SDK, every update we made broke integrations. We should have kept it simple with React to start, then expanded to shadcn once stable.
|
||||
|
||||

|
||||
|
||||
**Mistake 2**
|
||||
|
||||
We launched a separate open source pricing component library called [pricecn](pricecn.com). We thought it would go viral like [React Email](https://react.email/) by Resend. We designed it so pricecn users could easily migrate to using Autumn components later later, and so made the Autumn component library have the pricecn library as a dependency.
|
||||
|
||||
Pricecn didn't take off. People liked generating pricing cards from JSON, but it's hard enough to promote one project, let alone two. Having Autumn components depend on pricecn ones was just a confusing experience for users, as each component came with several files in different folders.
|
||||
|
||||

|
||||
|
||||
**Mistake 3**
|
||||
|
||||
We launched with 3 different styles: classic, clean and dev. I thought this would grow adoption since people could pick what suits them.
|
||||
|
||||
Again, people liked the concept but it made maintenance hell. Every issue meant jumping into 3 component files, so we kept putting off fixes until it became unusable.
|
||||
|
||||

|
||||
|
||||
**What we have now**
|
||||
|
||||
1. We launched a React library as the core experience, but allowing users to download the same files as shadcn components to customize it
|
||||
|
||||
2. We only kept the "classic" style and simplified it.
|
||||
|
||||
3. We cut the dependency on pricecn.
|
||||
|
||||
|
||||
We kept the shadcn components since people liked owning them when they worked, and now our API is a little more stable, it's working better.
|
||||
|
||||
Maybe we're still trying too hard to be clever but here's the cool part: instead of maintaining two sets, our shadcn library automatically replicates from our React ones so they always stay synced. Here's the script that syncs them whenever we `npm run dev`:
|
||||
|
||||
[https://github.com/useautumn/autumn-js/blob/main/package/scripts/sync-registry.ts](https://github.com/useautumn/autumn-js/blob/main/package/scripts/sync-registry.ts)
|
||||
|
||||

|
||||
|
||||
To be honest we have no idea how that script works but it does. Thanks Claude Code!
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: "How we made our first sale as a startup"
|
||||
description: "How we generated our first revenue as a startup, and got into Y Combinator."
|
||||
date: "2025-01-23"
|
||||
author: "Ayush, Autumn Co-Founder"
|
||||
slug: "how-we-made-our-first-sale-as-a-startup"
|
||||
image: "/images/blog/how-we-made-our-first-sale-as-a-startup.jpeg"
|
||||
featured: true
|
||||
---
|
||||
|
||||
We worked on onboarding automation for fintechs throughout the YC F24 batch and made reasonable progress. We made our first sale the week before we flew to SF.
|
||||
|
||||
It was pretty simple. We saw there were some old companies in the space building the rule engines that fintechs use to determine whether to approve or deny a sign-up (for fraud/compliance reasons). We thought we could do this better with AI (though not clear how) and that it’d be faster to try sell this to early-stage companies.
|
||||
|
||||
We went on LinkedIn and reached out to about 100 operations leaders at early-stage fintechs (15-50 headcount). We got a bite from a small payment provider based in the UK. Here’s the message we used.
|
||||
|
||||

|
||||
|
||||
**First call**
|
||||
|
||||
We hopped on a call with them that week. They wanted to launch self-serve onboarding and had a super manual process of onboarding customers over email. They said they wanted to get this sorted that quarter. The timing was perfect.
|
||||
|
||||
**Second call**
|
||||
|
||||
After a bit of follow-up, they looped in the CEO, who asked us a few more questions about our background. We demoed a front-end version of our drag-and-drop signup form builder + rule engine, said it worked, and that we knew what we were doing.
|
||||
|
||||
_**“Let’s do it”**_
|
||||
|
||||
I couldn’t believe that was enough to land a 2000 USD / month contract within a week. We agreed to help them analyze their onboarding data, come up with a good sign-up flow, decide on approve/deny rules to implement, and build everything.
|
||||
|
||||
They wanted a consultancy/dev shop—it was definitely more of a services contract. We were content either way and thought we were “off to the races”. 100 outbound messages = a 2k contract? This was going to be easy…
|
||||
|
||||
We sent them a Stripe invoice link that day and the invoice was paid the following day. I remember it feeling almost underwhelming at the time. I didn’t feel as ‘happy’ as I thought I would as my goalpost immediately shifted to closing the next sale.
|
||||
|
||||
But after weeks of that same process, days of long and mind-numbing outreach, we couldn’t replicate it. We managed to sign some contracts towards the end of the batch (although I’m pretty convinced they did it as a favor to us with demo day coming up), lost conviction, and decided to pivot. That’s a story for another time with a bunch of learnings.
|
||||
|
||||
### First sale with Autumn
|
||||
|
||||
With Autumn our approach was less sales-heavy from the start. We just wanted to talk to founders and see what their problems were. This time we had the YC community at our disposal, so emailed about 150 founders of product-led companies to chat about the tools they used. We knew we wanted to try building in that space, optimizing for a company we’d enjoy building as this is what we struggled with when building Recase.
|
||||
|
||||

|
||||
|
||||
One fairly high-profile company responded and mentioned that billing was a nightmare for them. Their users paid for credits, and they wanted a system that could outsource all the credit balance and payment logic.
|
||||
|
||||
After speaking to a few more founders we decided to mockup a prototype in a Figma alternative (Subframe), went back to this same founder, and presented it. He was super chilled and agreed to pay us 400 USD a month for it.
|
||||
|
||||
If I had to pay forward the learnings from my very nascent sales career it would be:
|
||||
|
||||
- You probably don’t need to write any code to sell something. But you almost definitely need to show something that demonstrates you can solve the problem. That can be a mockup, but think about who you’re demoing to. Are they a founder who is okay with seeing something early? Are they a compliance person who need to get the impression of a fully formed product to take a chance?
|
||||
|
||||
- Getting the first sale can be pretty easy if you talk to people and stumble on an acute problem someone has at the right time.
|
||||
|
||||
- The question is whether multiple people fit that same persona, have the same problem, and want the same thing. It’s tempting to get that sale and run 100% towards it which is the approach we took, but a ‘safer’ one is probably to make multiple sales before building. People who tried to build in this space and failed advised us to do exactly this. If you can do this you’ve likely uncovered the makings of a massive company. The trade-off is you’ll likely spend a lot longer in the idea maze (aka pivot hell), which can be shit.
|
||||
|
||||
|
||||
It’s been 8 months for us and we’re still figuring it out. You decide what works best for the company you want to build.
|
||||
100
apps/website/content/blog/marketing.mdx
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: "Marketing is hard"
|
||||
description: "A little write up on our thoughts and exploration around how to market our developer tool for billing and payments."
|
||||
date: "2025-06-17"
|
||||
author: "Ayush, Autumn Co-Founder"
|
||||
slug: "marketing"
|
||||
image: "/images/blog/marketing.png"
|
||||
featured: true
|
||||
---
|
||||
|
||||
Are founders ever happy with their product? There are **so many** different bugs, ideas, features and requests that I would love nothing more than to spend all my hours on. Designing product is my happiest state.
|
||||
|
||||
Unfortunately, while I like to believe I have good taste, the product is the easy part. Getting people aware of what we're doing and convincing them its worth trying is what matters in today's world.
|
||||
|
||||
90% of my life goes into thinking about GTM. This is a write up of what we're exploring and some useful resources.
|
||||
|
||||
### Who are we targeting?
|
||||
|
||||
When we initially launched Autumn, we were targeting seed and series A VC backed startups. Our GTM was cold outbound, main KPI was revenue. It was actually growing pretty well. But we realized 2 things:
|
||||
|
||||
1. Getting people to integrate was heavily timing dependant
|
||||
|
||||
2. There was a small segment of inbound users using it for side projects
|
||||
|
||||
|
||||
We pivoted to make a bigger bet on a longer term strategy of becoming the next go-to in payments. Since April, our strategy has been to target day 0 builders and founders.
|
||||
|
||||
### How are we doing it?
|
||||
|
||||
Our retained users are growing ~11% week on week. It's a broad, bottoms-up GTM.
|
||||
|
||||

|
||||
|
||||
The main channels we spend time on are:
|
||||
|
||||
**X/Twitter**
|
||||
|
||||
This is where most people find us. We've had some success posting demos and launches on @johnyeo\_'s account (2K followers) and have a smallish following (600 followers) on @autumnpricing.
|
||||
|
||||
Our accounts are active (2-5 posts per day) and have reasonable engagement. 3 months ago, we had ~200 followers in total. If we were starting again we would:
|
||||
|
||||
- Follow relevant accounts in our ICP, and **other small active accounts trying to grow**. YC is an advantage here as we already have a network of people like this.
|
||||
|
||||
- Engage with these accounts a lot: 100+ replies per day, and follow even more accounts.
|
||||
|
||||
- People will follow you back and engage with you too, which is good for the algo. I personally follow back anyone who (1) follows me and (2) engages with me.
|
||||
|
||||
- Possibly even start a group chat to help keep accountable and comment, repost and bookmark each other consistently (if you're doing this, we'll join!)
|
||||
|
||||
|
||||
_Note: all of this only really works because there are people that like our product and are interested in what we're building. If you don't have a likeable product you should fix that first before shouting about it_
|
||||
|
||||
Our most successful post was honestly total nonsense clickbait: a 1 minute demo where we built "cursor for stripe". When you spend enough time on twitter, you start to get a sense for what a good tweet looks like and what the "current thing" is. AI video content doing something novel works the best, with a snappy tweet that jumps straight into it.
|
||||
|
||||
Going viral is great. When it works, it works well, but is super variable. Chasing virality over and over again is a repeatable GTM for consumer apps, but harder in the tech twitter sphere. A lot of the time it feels like shouting into the void.
|
||||
|
||||
[Resend's heartbeat framework](https://resend.com/handbook/marketing/how-we-keep-momentum) resonates a lot with us. We have to constantly be building, writing about it, and shipping.
|
||||
|
||||
**Hacker news**
|
||||
|
||||
We had 1 blog post of ours that hit front page of HN for about 3 hours, and it drove more traffic to our site than we'd seen in the whole month. Even though most of it was negative (people found the title to be misleading), it drove a lot of sign ups.
|
||||
|
||||
**Blogging (like this)**
|
||||
|
||||
Our blogs probably get about 300-400 reads per week, which we're pretty happy with as we've just started. We're trying to lean more into creating content that we'd find interesting, hoping that other founders and devs also like it. When we write, we share it on twitter, our email newsletter, and a few relevant subreddits.
|
||||
|
||||
**Word of mouth**
|
||||
|
||||
We're fortunate enough that some people have really experienced the pain we solve and like what we're doing. Because of that, we often find ourselves being recommended in twitter threads, and people saying friends said good things about them. This is one that feels more out of our hands.
|
||||
|
||||
Other than a few upcoming platform launches (Launch HN, Product Hunt), we want to try a few more channels properly like LinkedIn and Reddit. The advice we've heard is that you never really know what's going to work for your startup, so try a lot of things.
|
||||
|
||||
We are also thinking about how we can bake marketing into our product a bit more (eg Autumn branded receipts on purchase).
|
||||
|
||||
### What is our message?
|
||||
|
||||
I used to think this wasn't important, then I heard Ant (Supabase founder) talk on the scaling devtools podcast about how they went from 8 hosted DBs to 800 over a weekend, just by changing their messaging from
|
||||
|
||||
Real-time postgres DB -> Open source firebase alternative
|
||||
|
||||
And launching again on HN.
|
||||
|
||||
<iframe width="100%" height="400" src="https://www.youtube.com/embed/ptC_FTZ1hc0" frameBorder="0" allowFullScreen></iframe>
|
||||
|
||||
This is one we're working on. Some angles we've gone for are:
|
||||
|
||||
- Any pricing model in 6 lines of code
|
||||
|
||||
- Never deal with billing again
|
||||
|
||||
- Open source infrastructure for pricing and billing
|
||||
|
||||
- What supabase did for Postgres, we're doing to Stripe (?)
|
||||
|
||||
|
||||
A lot of devtools took off by iterating on the product over time, and really crystallizing who they're targeting. Shoutout to Clerk who grinded for 3 years, then found success by going all-in on frontend, Next.js devs. Polar.sh (another payments company) had a lot of success by pairing their setup with better-auth.
|
||||
|
||||
### Anyway
|
||||
|
||||
Marketing is a slog. It's trial and error, but with a long and hard to measure feedback loop. I wish we could be one of those teams that just builds it and they come. But until we hit PMF we'll just keep shitposting 🫡
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: "Post-mortem: Database outage caused by collation migration locking conflict"
|
||||
description: "Post-mortem write up of an outage that affected a number of requests between 15:00 and 16:00 UTC, 15th March 2026."
|
||||
date: "2026-03-15"
|
||||
author: "Ayush, Autumn Co-Founder"
|
||||
slug: "post-mortem-database-outage-caused-by-collation-migration-locking-conflict"
|
||||
image: "/images/blog/post-mortem-database-outage-caused-by-collation-migration-locking-conflict.png"
|
||||
---
|
||||
|
||||
On Sunday March 15, 2026 from 15:00 to 16:15 GMT, approximately 10% of API requests to Autumn failed due to a database outage caused by a collation migration on our production database.
|
||||
|
||||
## Timeline
|
||||
|
||||
- **~15:00** — Collation changes applied to `customer_entitlements` and `customer_products`. DB CPU spikes, queries begin failing.
|
||||
|
||||
- **15:05** — Issue identified. Attempted rollback via `ALTER TABLE` but blocked by active queries holding locks, causing repeated deadlocks.
|
||||
|
||||
- **15:20** — Attempted code-level fix: updated the main customer query to explicitly cast collation, hoping to force index usage without a schema rollback.
|
||||
|
||||
- **15:48** — With help from PlanetScale support, shut down all application connections, reverted collation changes, and ran `ANALYZE` to rebuild planner statistics.
|
||||
|
||||
- **16:15** — Full recovery confirmed across both regions. All APIs and dashboard operational.
|
||||
|
||||
|
||||
## What happened
|
||||
|
||||
We were optimizing our most frequently run queries and discovered that several indexes weren't being used due to a collation mismatch between parent and child tables (customers.internal\_id uses COLLATE "C" for KSUID ordering, but FK columns like customer\_entitlements.internal\_customer\_id used the default collation). Postgres can't use indexes across this mismatch, so our most frequent query was taking ~150ms when it should have been <10ms.
|
||||
|
||||
After changing the collation on customer\_entitlements, we saw CPU drop by 30% and much faster query times. To prevent this issue in the future, we decided to align collations across all related tables. There were 4 tables to change, in this order:
|
||||
|
||||
1. customer\_entitlements
|
||||
|
||||
2. customer\_products
|
||||
|
||||
3. entities
|
||||
|
||||
4. customers
|
||||
|
||||
|
||||
After changing collation on the first two tables, our main query's performance dropped dramatically due to missing indexes, and this made our DB CPU spike and queries fail. We tried to reverse the changes, but the ALTER TABLE needs an exclusive lock on the table, and there were always active queries holding locks — causing repeated deadlocks.
|
||||
|
||||
## Resolution
|
||||
|
||||
We first tried to push a code-level fix (aligning collations in the query SQL itself), but this didn't take effect fast enough with the DB under heavy load. Ultimately, we shut down all application connections, reversed the collation changes, ran ANALYZE to rebuild planner statistics, and restarted. Queries returned to normal immediately and CPU leveled off.
|
||||
|
||||
## Prevention & Remediation
|
||||
|
||||
All future column/schema migrations will be done using the duplicate-dual-write-cutover pattern. Instead of altering a column in place (which requires an exclusive lock and a full table rewrite), we create a new column with the desired type, dual-write to both columns, backfill existing rows in small batches, create indexes concurrently, then cut over.
|
||||
|
||||
More generally, we will be adding a status page to improve transparency when we have issues. We are also considering making a change to our `check` and `track` SDKs that would enable them to fail-open by default, so that in an outage there is no disruption to paying customers.
|
||||
111
apps/website/content/blog/shadcn.mdx
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: "Embedded UI components are easier with shadcn"
|
||||
description: "We decided to use a shadcn/ui registry to set up our pricing components, instead of a Reactjs library. We used this approach for pricing pages, upgrade and downgrade flows and paywalls."
|
||||
date: "2025-05-28"
|
||||
author: "John, Autumn Co-Founder"
|
||||
slug: "shadcn"
|
||||
image: "/images/blog/shadcn.png"
|
||||
featured: true
|
||||
---
|
||||
|
||||
We're building Autumn, which is a billing platform that helps devs integrate [stripe](/blog/what-if-stripe-builds-this) and manage their pricing model. One feature of this is a UI library to drop-in things like a pricing table, upgrade/downgrade flows, paywalls--similar to Clerk with auth.
|
||||
|
||||
We are heavy users of shadcn/ui and love having full design customisability. When Supabase launched it’s UI library through shadcn, we wanted to do the same.
|
||||
|
||||
Here's our exploration into it and some of the challenges we faced.
|
||||
|
||||
### **V1 as a React library**
|
||||
|
||||
We first attempted this as a standard npm React library:
|
||||
|
||||
```
|
||||
import PricingPage from "autumn-js"
|
||||
```
|
||||
|
||||
It's biggest issue was customisability**.** There are two ways to make npm components customisable: through props, or a no-code interface in our app.
|
||||
|
||||
1. With props there were too many class variables. For example with the pricing page, each pricing card is composed of many headers, descriptions, buttons and more. And each card itself can have variants (recommended, discounts, annual etc).
|
||||
|
||||
2. A low code interface is a bad experience for devs, who want full customisability and have 10x experience designing with tailwind/css. Even more so with the era of cursor. This is subjective but I’ve never had a good experience with one.
|
||||
|
||||
|
||||
_Sidenote:_ If you ever try to make your npm component customisable via tailwind, all the best. That was one of the most frustrating days of our lives.
|
||||
|
||||
Also, If you ever forgot what jsx styles look like, here’s a snippet of what we had to write…
|
||||
|
||||

|
||||
|
||||
We made one measly post on linkedin about it, decided it was unusable and shelved it.
|
||||
|
||||
### **V2 as shadcn/ui components**
|
||||
|
||||
A few weeks later we started looking into it again with shadcn.
|
||||
|
||||
```
|
||||
npx shadcn@latest add https://ui.useautumn.com/classic/pricing-table.json
|
||||
```
|
||||
|
||||
This immediately felt like a more customizable and fluid DX, but had its own challenges:
|
||||
|
||||
1. Because shadcn components install as a user file, we cannot fully control it via our SDK. For example, if we wanted to trigger a modal/popup to confirm a plan upgrade, the user needs to explicitly pass it into a function. This does take away slightly from that “magical DX”.
|
||||
|
||||
|
||||
```jsx
|
||||
//upgrading to Pro tier
|
||||
<Button
|
||||
onClick={async () =>
|
||||
await attach({
|
||||
productId: "pro",
|
||||
dialog: ProductChangeDialog,
|
||||
})
|
||||
}
|
||||
/>;
|
||||
```
|
||||
|
||||
2. Since users own and control the component files, deciding what data abstraction level to return to the frontend is hard. For example, our upgrade dialog shows different text and styling depending on the scenarios:
|
||||
|
||||
\- One time purchase vs subscription
|
||||
\- Upgrades vs downgrades vs cancellations vs renewals
|
||||
\- Does the upgrade require an input from users (eg, quantity of credits to purchase)
|
||||
|
||||
|
||||

|
||||
|
||||
With a React library, everything would be "completely processed", with no customizability. Initially this is the same approach we took with the shadcn registry, but users quickly told us they wanted to customize the text too. We switched to the "in-between" approach.
|
||||
|
||||
Our API will return a scenario (eg, upgrade, downgrade, add-on, free-trial etc), and our shadcn components install with a library of cases that users can edit to control the messaging.
|
||||
|
||||
```jsx
|
||||
switch (scenario) {
|
||||
case "scheduled":
|
||||
return {
|
||||
title: <p>Scheduled product already exists</p>,
|
||||
message: <p>You will downgrade on {scheduled_date}</p>,
|
||||
};
|
||||
|
||||
case "active":
|
||||
return {
|
||||
title: <p>Product already active</p>,
|
||||
message: <p>You are already subscribed to this product.</p>,
|
||||
};
|
||||
|
||||
case "new":
|
||||
if (recurring) {
|
||||
return {
|
||||
title: <p>Subscribe to {product_name}</p>,
|
||||
message: (
|
||||
<p>
|
||||
By clicking confirm, you will be subscribed to {product_name} and
|
||||
your card will be charged immediately.
|
||||
</p>
|
||||
),
|
||||
};
|
||||
}
|
||||
//..... etc
|
||||
```
|
||||
|
||||

|
||||
|
||||
Ultimately, after having used it ourselves and watching our users, we’re pretty happy with this approach. Shadcn is as popular as it is for a reason, and we believe in the paradigm of owning your own components and everything inside them.
|
||||
|
||||
We're maintaining these components for free separate to our main product here: [https://pricecn.com](https://pricecn.com/)
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
title: "Talking about billing with 30 YC founders"
|
||||
date: "2025-01-22"
|
||||
author: "Ayush, Autumn Co-Founder"
|
||||
slug: "talking-about-billing-with-30-yc-founders"
|
||||
image: "/images/blog/talking-about-billing-with-30-yc-founders.jpeg"
|
||||
featured: true
|
||||
---
|
||||
|
||||
We were in YC’s F24 batch and pivoted the day before demo day (story coming soon). Since then we’ve been in the idea maze and spoke to 30 very kind YC (and some non YC) founders/leaders about their billing systems.
|
||||
|
||||
Why billing? This space is big and crowded for sure. But I used to work for a payments company and I find this stuff to be super interesting.
|
||||
|
||||
We’re writing our learnings from this as an exercise both to start drawing interest in what we’re doing but also as a source of truth as we continue learning.
|
||||
|
||||
##### How do people manage billing?
|
||||
|
||||
As you can expect this depends on the stage of company and their billing model. Will distill it to these
|
||||
|
||||
**Seed Stage Companies**
|
||||
|
||||
_Sales-led:_
|
||||
|
||||
- Use Stripe for invoicing with manual setup
|
||||
|
||||
- Sometimes use simple, cheap tools for quick proposal generation in high-volume scenarios
|
||||
|
||||
|
||||
_Product-led:_
|
||||
|
||||
- Rely on Stripe billing and payment links
|
||||
|
||||
- Implement feature flags and entitlements directly in code
|
||||
|
||||
|
||||
**Series A Companies**
|
||||
|
||||
_Sales-led:_
|
||||
|
||||
- Have automated invoicing flows, integrated with CRM
|
||||
|
||||
- Use data tools for revenue data organization
|
||||
|
||||
|
||||
_Product-led:_
|
||||
|
||||
- Some adopt billing tools like Orb or Metronome. Although usually noting that they used it because Stripes offering was bad _at the time_
|
||||
|
||||
- Maintain simple, code-based entitlement systems
|
||||
|
||||
|
||||
**Series B-C Companies**
|
||||
|
||||
_Sales-led:_
|
||||
|
||||
- Finance teams take ownership of contract and pricing setup
|
||||
|
||||
|
||||
_Product-led:_
|
||||
|
||||
- Maintain custom admin panels for feature management in bespoke contracts
|
||||
|
||||
- Update pricing once or twice yearly to experiment with optimization
|
||||
|
||||
- Focus on metrics such as paywall conversion rates
|
||||
|
||||
|
||||
**Key learnings:**
|
||||
|
||||
- Stripe is everywhere (as expected) and getting better. It wasn’t loved but it was fine.
|
||||
|
||||
- Entitlement management is useful for later stage sales led companies, but also for earlier-stage companies managing both product-led + sales led models.
|
||||
|
||||
- When a company was using a vendor, the push came either from finance wanting better data for revenue reporting, or CTOs wanting to save dev time on billing infra. No one (apart from one ex-intercom founder) really cared about experimenting with pricing as a lever to drive more revenue. Standard practice was just looking at competition and charging a market rate.
|
||||
|
||||
|
||||
##### So what were the pain points?
|
||||
|
||||
As good as Stripe is, there were pain points that did come up repeatedly:
|
||||
|
||||
- Managing credit-based billing systems. There is no good tool out there for this today.
|
||||
|
||||
- Chasing up invoices that haven’t been paid. AI seems like a no-brainer here, but definitely a bunch of solutions already.
|
||||
|
||||
- Revenue recognition from Stripe. Usually around Series A there needs to be a big clean up for investors. People don’t enter the right contract terms in Stripe when they’re first starting out which leads to messy data.
|
||||
|
||||
- Maintaining a billing system and making pricing changes. Especially if supporting multiple versions (grandfathering) and moving people between plans.
|
||||
|
||||
- Dealing with custom contracts for sales-led and product-led teams. Stripe doesn’t seem to have great tooling for this, creating free trials etc.
|
||||
|
||||
- People had tried third-party billing software but dropped them when they couldn’t support a specific nuance of their pricing model (eg dealing with upgrade / downgrade pro rata meters differently)
|
||||
|
||||
|
||||
We didn’t get the sense that many people would switch to a whole new billing system to solve any one of these problems. Something that is plugged into their existing system may work better?
|
||||
|
||||
##### **Lots of people have tried this**
|
||||
|
||||
As soon as we started looking into this space it was immediately clear that there were a lot of people in this space—and a lot of people who had tried and failed.
|
||||
|
||||
Honestly it doesn’t faze us much anymore. This seems to be the state of SaaS markets in general and is the sign of a big market. From the people we spoke to who failed:
|
||||
|
||||
- Stripe billing has got pretty good recently and means it was hard to find customers who were interested in switching. Finding a customer is also super timing dependant and so will rely on a strong brand and inbound motion.
|
||||
|
||||
- Easier to get a few small customers who are starting out but moving up-market is hard, especially for a seed-stage company.
|
||||
|
||||
|
||||
A random learning is that people seem to be pretty salty about their old ideas that have failed. Didn’t seem very self-reflective on what they could have done better or differently. Just said the idea was stupid.
|
||||
|
||||
##### **What do we think?**
|
||||
|
||||
We’re still forming our opinion on the space so this is a braindump of our current thoughts on the space. As much as we hope, it’s unlikely this’ll be a rocketship from day 1.
|
||||
|
||||
But we are seeing more companies opt for off-the-shelf software like Clerk and Supabase. Greater preference for staying lean and moving faster.
|
||||
|
||||
Most companies are not going to switch from Stripe or another billing provider for this. These software are very engrained into the codebase and are generally ‘good enough’. And so while usage based pricing is increasing it’s still Stripe capturing the market share. We really need to think about our GTM. Who it’s for and how we get in:
|
||||
|
||||
- Having a modular suite of interconnected sales <> billing <> finance tools could be the way to wedge-in and expand within a larger company. Entitlement management seems like a great part of the stack to control.
|
||||
|
||||
- Or we could grow a brand for startups and make it super easy to implement billing.
|
||||
|
||||
- Or we could be the best ever billing provider for some type of company (eg credit companies)
|
||||
|
||||
|
||||
There is also some growing dissent with Stripe as they get bigger and bigger. Implementation effort is pretty tough and summarised well by [t3.gg](http://t3.gg/) here. In our view our approach makes this way easier.
|
||||
|
||||
##### **More generally**
|
||||
|
||||
Different people working on the same idea with different initial conditions can have massively different results. Eg Pylon were the 13th team to have the same insight they did, but they’re killing it. I think there’s some alpha in making a product people really love with a great brand around it.
|
||||
|
||||
We’re excited to have signed a fast growing, high profile AI startup already and will try to ride their growth wave with them. Maybe that’s the winning strategy over trying to time larger companies into switching billing systems and the one we’re leaning towards. But maybe we’ll kick ourselves down the line for not focusing up-market. What we build will likely look different (more horizontal earlier, more vertical for up-market).
|
||||
|
||||
We were previously working on fintech onboarding software selling to compliance teams. Contract sizes were bigger there but also was super slow. For whatever reason right now we’re having a lot more fun exploring this and talking to people about it. It feels like a problem we can become obsessed with, do our best work, and execute well.
|
||||
|
||||
TLDR: we're going to keep going and are excited.
|
||||
|
||||
Shoutout to Granola for making it a lot easier to keep track of our notes and learnings from customer discovery
|
||||
|
||||
Will check in next month to see what’s changed about what we think!
|
||||
64
apps/website/content/blog/thanks-hn.mdx
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: "A roasting from HN makes better devtools"
|
||||
date: "2025-05-20"
|
||||
author: "Ayush, Co-Founder at Autumn"
|
||||
slug: "thanks-hn"
|
||||
image: "/images/blog/thanks-hn.png"
|
||||
featured: true
|
||||
---
|
||||
|
||||
At Autumn, we're trying to build an integrated billing platform that can run any software pricing model.
|
||||
|
||||
A lot of the integration lift is on the frontend, since we handle user payment flows (payment links, upgrades/downgrades, paywalls etc). Making the developer experience simple for new devs, while keeping product flexible has been our main focus over the past few months.
|
||||
|
||||
Now Stripe is typically a backend-heavy integration, with data being passed to the frontend. One of the first pieces of feedback we got from users was:
|
||||
|
||||
> I wish I could direct a customer to a payment URL without needing to generate it on the backend, and pass it to the frontend
|
||||
|
||||
We became fixated with trying to let users handle payments with as little backend involvement as possible. Here's what we tried and how a roasting from HN helped us arrived in a better place.
|
||||
|
||||
### Part 1: Frontend-only integration with a public key
|
||||
|
||||
Our first solution was to create a "publishable key" that could be used straight from the frontend, to get payment URLs and check feature permissions (eg user\_123 has 10 credits).
|
||||
|
||||
People were initially satisfied, but there was no way to safely handle things like downgrades and cancellations. So even though setup was faster, it had to be ripped out quickly.
|
||||
|
||||
### Part 2: Nextjs server actions
|
||||
|
||||
Many of our users apps were built with Next.js, which has a feature called "server-actions". We were excited as it enabled our users to make calls on the frontend, and have them run from the backend. We thought this would solve our problem.
|
||||
|
||||
We didn't know much about how Next.js worked, and quickly found out that similar to our public key, server actions are just public endpoints to their own backend server. Since all our requests are based on a `user_id`, it meant that anyone with access to it would be able to perform billing actions.
|
||||
|
||||
### Part 3: Reinventing JWTs (badly)
|
||||
|
||||
In a moment of genius, we realized we could just encrypt the `user_id`. Users would pass it to us via a Next.js server-side component, we'd encrypt it with their API key, and use it from the client side to authenticate requests.
|
||||
|
||||
We posted this approach on Hacker News last week, and got promptly roasted for (amongst other things), simply reinventing JWTs, without token refresh and with more steps.
|
||||
|
||||
> Isn't it enough that this startup seems destined to failure? No need to beat a dead horse
|
||||
|
||||
Ouch. Thankfully the best part about a startup is you get to learn quickly.
|
||||
|
||||
### Part 4: Throwing it all out
|
||||
|
||||
The incentive for this exploration had been to spare the user from setting up backend routes. But we'd ended up with:
|
||||
|
||||
- A pretty poor developer experience, especially outside of Next.js
|
||||
|
||||
- An insecure integration, as if you knew the encrypted customer id, you'd again have full billing permissions
|
||||
|
||||
|
||||
Over the last few days we rewrote the whole library. Instead of reinventing the wheel, we made it easy to set up Autumn cleanly and safely:
|
||||
|
||||
- All functions are called from the backend.
|
||||
|
||||
- We wrote middleware for each framework to spin up these routes (to make a purchase, check feature permissions, open the billing portal, track billing usage etc).
|
||||
|
||||
- We hook into the existing auth system through this middleware, by providing an identify function where the user can decrypt their JWTs into the user\_id
|
||||
|
||||
|
||||

|
||||
|
||||
We're happier with this for now, although set up is slightly longer and possibly less "magical". It's a lot clearer what happens on the client, what happens on the server, and where to use each package.
|
||||
|
||||
Thanks HN for the tough love.
|
||||
84
apps/website/content/blog/usage-billing-bad.mdx
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: "People really don't like metered billing"
|
||||
description: "Replit’s 2026 pricing shift highlights three trends in AI-first products: usage-based billing creates anxiety, seats don’t map to AI value, and credits/effort-based pricing reduce transparency as agents run longer."
|
||||
date: "2026-01-26"
|
||||
author: "Ayush, Autumn Co-Founder"
|
||||
slug: "usage-billing-bad"
|
||||
image: "/images/blog/usage-billing-bad.png"
|
||||
featured: true
|
||||
---
|
||||
|
||||
Replit announced a big pricing change recently, and it put words to a few patterns I’ve been noticing across AI-first products. TL;DR:
|
||||
|
||||
- As agents run longer and more autonomously, pricing gets harder.
|
||||
|
||||
- People don’t like usage-based billing because they can’t predict it.
|
||||
|
||||
- Per-seat pricing doesn’t map cleanly to AI value.
|
||||
|
||||
|
||||
From this mess, the pricing model that has prevailed is the "credit": a pre-purchased token for "work".
|
||||
|
||||
## Users hate usage-based billing
|
||||
|
||||
Usage-based billing is “fair” in theory. You pay for what you use. In practice, especially for UI-first products, it creates background anxiety.
|
||||
|
||||
Every click feels like it might cost money, so you explore less, and subconsciously pause before taking each action. You feel out of control of your end of month bill.
|
||||
|
||||
This is why you keep seeing products move away from metered billing and more towards **prepaid credits** (a monthly bucket, and usually one-off packs on top). It doesn’t necessarily change the underlying economics, but it changes the product experience: controlled spend with a hard cut-off.
|
||||
|
||||
Even in APIs / infra, where usage-based has been the standard, we're seeing auto-topups replacing pure usage-based (OpenAI being a good example).
|
||||
|
||||
The direction of travel is the same: when AI costs are high and variable, predictability and hard-limits are necessary.
|
||||
|
||||
## Per-seat pricing is basically dead for AI-first products
|
||||
|
||||
Replit’s new Pro plan reflects this shift. Instead of $40/seat/mo, they've introduced a team-wide pool of credits, with a seat/collaborator cap (up to 15 builders).
|
||||
|
||||
Even for something like Cursor, where the price on their site is displayed "per-seat": what you're _actually_ paying for is the $40 USD of AI usage per-month that come with the seat -- not the seat itself.
|
||||
|
||||
In traditional SaaS, seats are a decent proxy for value. More people using the tool generally means more value. In AI-first products, the marginal value often isn’t “another seat.” It’s:
|
||||
|
||||
"how much work can the model/agent do for us?"
|
||||
|
||||
Replit has also brought “real collaboration” into Core (up to 5 people), and they’re sunsetting the old Teams plan. Importantly, this it’s a product strategy move:
|
||||
|
||||
- Bake collaboration into the default experience.
|
||||
|
||||
- Make “usage” the thing you sell, rather than individual power users.
|
||||
|
||||
- Put a cap on seats, but make the variable—and the real value—how much work gets done.
|
||||
|
||||
|
||||
## Pricing is getting less transparent as agents run longer
|
||||
|
||||
The more autonomous agents become, the harder it is to keep pricing legible. Replit is a clean example.
|
||||
|
||||
> Simple tasks may cost less than $0.25, more complex tasks may cost more than $0.25
|
||||
|
||||
They used to charge a fixed $0.25 per checkpoint. Simple mental model that you could predict it. Now it’s "effort-based": you get charged _some random amount_ based on time + compute for the request.
|
||||
|
||||
Mechanically, this makes sense. Agent requests vary wildly: a tiny change versus a long-running debugging session are not the same unit of work. But UX-wise, it shifts the feeling.
|
||||
|
||||
And credits add a second layer of opacity. You see a credit balance, but the mapping is fuzzy. How do model tokens map to Replit credits? What’s the range for a “normal” request? What's replit's markup on the credits?
|
||||
|
||||
When your costs are unpredictable you need some way of passing it on to your customers. Credits are great because they can effectively grant users the same number of credits, or even double them -- but silently increase their margins however they want.
|
||||
|
||||
## What's next?
|
||||
|
||||
Credits will probably stick around for a while because they’re the best abstraction we have right now. But they also feel like a patch over the harder question:
|
||||
|
||||
How do you make AI spend feel predictable when agents can do wildly unpredictable amounts of work?
|
||||
|
||||
I would personally love to see more AI products prioritize **billing observability**:
|
||||
|
||||
- flat costs for simple tasks
|
||||
|
||||
- estimates _before_ you run expensive tasks (“this will be ~X credits based on history”)
|
||||
|
||||
- a receipt _after_ you run (“this cost X because **\_”)**
|
||||
|
||||
- ranges + forecasting (“at this pace you’ll run out on **\_”)**
|
||||
|
||||
|
||||
That would make AI feel more like software and less like a casino. But my guess is that uncertainty will just become part of the new software contract.
|
||||
40
apps/website/content/blog/we-re-making-some-api-changes.mdx
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: "We're making some API changes"
|
||||
description: "A bit of context about what will be changing in the new version of the Autumn API and SDK."
|
||||
date: "2026-02-18"
|
||||
author: "Ayush, Autumn Co-Founder"
|
||||
slug: "we-re-making-some-api-changes"
|
||||
image: "/images/blog/we-re-making-some-api-changes.png"
|
||||
featured: true
|
||||
---
|
||||
|
||||
Over the last year we learned a lot about how you use Autumn. We have noticed which pain points come up repeatedly and where users face limitations. Our v2 SDK will soon be released in beta, which we think offers a more intuitive and flexible experience as you build on Autumn.
|
||||
|
||||
**There will be no breaking API changes:** everything is versioned on our end and your current version will continue to be supported.
|
||||
|
||||
### What’s changing?
|
||||
|
||||
**New objects / types.**
|
||||
|
||||
Our customer and plan objects are changing to better suit our abstraction:
|
||||
|
||||
- Any reference to `product` is now `plan` to better reflect our abstraction
|
||||
|
||||
- Customer products are now split into `subscriptions` and `purchases` for easier access
|
||||
|
||||
- Features that a customer has access to will now be under a `balances` object
|
||||
|
||||
- Subscription status can now only be one of `active`, `scheduled` or `expired` - with additional params to determine trialing, canceled and past due states
|
||||
|
||||
- Types are more consistent (eg, consolidation of `one_off` and `lifetime` intervals)
|
||||
|
||||
|
||||
**Attach is being split**
|
||||
|
||||
Today, the `attach` function is used for any operation that changes subscription state — checkouts, upgrades, downgrades, quantity updates, trial updates etc. This has become unpredictable to work with for you guys, and almost impossible for us to maintain. Adding functionality in one flow almost always breaks another.
|
||||
|
||||
We’re splitting this into 2 functions: `attach` will be used for operations that result in a plan change (checkout, upgrades etc), and `update` will be used for operations that update an existing customer subscription (cancelations, quantity adjustments, trial adjustments).
|
||||
|
||||
We know API changes are annoying, but we're biting the bullet now to set ourselves up for better stability in the long run. Again, your existing integrations will not break and we will continue to support the current version. For those of you that want to upgrade, we’ll be providing a short prompt/skill for Claude that can migrate your code for you.
|
||||
|
||||
As part of this, we’ve refactored a lot of code, which has allowed us to release a lot of new functionality and configurabilty. More details on that soon. Thank you all for helping make Autumn better ❤️🔥
|
||||
65
apps/website/content/blog/what-if-stripe-builds-this.mdx
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: "What if Stripe builds this"
|
||||
description: "Stripe has been the go-to for billing, subscriptions and managing payment logic for 16 years. We think that'll change soon."
|
||||
date: "2025-06-02"
|
||||
author: "Ayush, Autumn Co-Founder"
|
||||
slug: "what-if-stripe-builds-this"
|
||||
image: "/images/blog/what-if-stripe-builds-this.png"
|
||||
featured: true
|
||||
---
|
||||
|
||||
The "what if `${incumbent}` builds this" question is usually asked as a meme. Really the only time it comes up is when founders make fun of crappy VCs. For some reason though, we hear this a lot. I think it's because:
|
||||
|
||||
1. People don't quite understand what we do. Which is fair enough. It's quite new and we're still not great at explaining it.
|
||||
|
||||
2. Stripe is still totally revered in the Startup ecosystem.
|
||||
|
||||
|
||||
They've been around since 2009 and have had a complete monopoly over the early stage software market for over 16 years. There is no comparable company that can say the same:
|
||||
|
||||
- AWS (2006) -> Supabase
|
||||
|
||||
- Heroku (2007) -> Vercel or Render
|
||||
|
||||
- Twilio (2008) -> Resend
|
||||
|
||||
- Okta (2009) -> Clerk
|
||||
|
||||
- DataDog (2010) -> Sentry
|
||||
|
||||
|
||||
The tech stack has tended to higher levels of abstraction over time. And while there has been competition targeting later-stage companies (eg for usage-billing, CPQ solutions), the early stage software market is still dominated by Stripe.
|
||||
|
||||
However, they've grown to a size where cracks are starting to show. They're less focused on the early stage segment (take a look at r/stripe if you're not convinced). And new devs struggle to set it up.
|
||||
|
||||
Because Autumn abstracts away all that complexity, people are starting to like what we do. If it continues to work, I'm sure Stripe will make a play here. However I still think we have a pretty good shot.
|
||||
|
||||
|
||||
### 1\. We'll always have a better product
|
||||
|
||||
From a high level it seems like we're a couple features on top of Stripe, and I will concede a fair amount of our current value is represented by that. But truly solving this problem is complex and will take years to get right.
|
||||
|
||||
Creating a platform that is simple to start with, but is flexible enough to run any pricing model is an infinite matrix of logic, scenarios, payment states, edge cases etc. This only gets harder and harder as we move upmarket.
|
||||
|
||||
Technically speaking it's a totally different challenge too: at scale we look more like a database (eg Supabase/Convex), or feature flagging system like LaunchDarkly. This is pretty far away from what Stripe is good at.
|
||||
|
||||
That's not to say that the best product always wins. But in devtools it certainly matters more than in other product categories.
|
||||
|
||||
### 2\. Developers (and companies) like not being locked in
|
||||
|
||||
And there's 2 ways we play into that:
|
||||
|
||||
1. We're open source
|
||||
|
||||
2. We're not tied to Stripe. We can build our own system, and build integrations with Adyen, Chargebee, Checkout.com etc. This gives enterprises bargaining power with their payment providers.
|
||||
|
||||
|
||||
So even if they decide to throw out Stripe and rebuild exactly what we're doing, we still have a pretty strong place in the market.
|
||||
|
||||
### 3\. The market is very big
|
||||
|
||||
The market size for billing tools is around $10 billion and growing every year. Selling into a growing market is great because there are always new and interesting problems to focus on.
|
||||
|
||||
We're making a particular bet on early stage product-led companies, but every single company we've spoken to had their own unique problems. Whether in this space, earlier stage in the funnel during the sales process (CPQ), or later stage in the funnel (finance operations).
|
||||
|
||||
The timing feels right and I think what we're building is a good bet. That said Stripe if you wanna acquire us we'll still hear your offer 😙
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: "Consumer psychology is important in AI pricing (feat. T3 chat)"
|
||||
description: "We worked with Theo and Mark from T3 Chat to build a cool new way of pricing. We learned about the psychology behind how AI products are consumed, and what that means for how we should monetize them."
|
||||
date: "2026-02-20"
|
||||
author: "Ayush, Autumn Co-Founder"
|
||||
slug: "working-with-t3-chat-on-a-new-way-of-pricing"
|
||||
image: "/images/blog/working-with-t3-chat-on-a-new-way-of-pricing.png"
|
||||
featured: true
|
||||
---
|
||||
|
||||
[T3 Chat](https://t3.chat/) is the best alternative to AI apps like ChatGPT and Claude, created by [YouTuber Theo](https://www.youtube.com/@t3dotgg) and co-founder Mark. It gives you access to all the latest AI models in one place, is a beautifully designed product and has become my daily-driver for AI. It was no surprise to me that they have several 10s of thousands of subscribers.
|
||||
|
||||
Initially, the subscription gave access to 1500 "standard" messages, and 100 "premium" messages per month, where premium messages were reserved for more expensive models. We helped them move to a multi-tier, waterfall rate limits model.
|
||||
|
||||
It was a pretty interesting insight into the psychology behind how AI products are consumed and what that means for monetization.
|
||||
|
||||
### FORO
|
||||
|
||||
The first reason behind the change is to address what Theo calls "fear of running out". Whenever users hit enter to send a message to T3, there's a latent anxiety that comes with watching your usage counter slowly tick up.
|
||||
|
||||
Almost no users ever hit their limit of 1500 messages, but that didn't really matter -- they were paranoid that they would.
|
||||
|
||||
Similar to a video game where you hoard your items til the final boss, users would subconsciously attempt to ration their usage for the month, in case they needed it. The feeling of being limited was often cited as a reason that a user didn't convert.
|
||||
|
||||
With the new pricing model, users now have a bucket of usage credits that replenishes every 4 hours. You either use it or lose it, making you feel less guilty about consuming it. If your quota runs out, you simply wait a few hours before coming back.
|
||||
|
||||

|
||||
|
||||
### Prepaid credits > metered pricing
|
||||
|
||||
Economically, prepaid credits and metered billing can be identical. A user might consume $6 worth of AI in a month either way. But the experience of spending that $6 is completely different.
|
||||
|
||||
With metered pricing, every message feels like a purchase decision. Users are constantly running a background cost-benefit analysis: "is this prompt worth it?"
|
||||
|
||||
Prepaid credits flip this. Once the money is spent, using the product stops feeling like spending and starts feeling like redeeming. It becomes "I should use what I've already paid for."
|
||||
|
||||
Replit is a great example of a company that just made this change, introducing prepaid credit tiers in their latest pricing change.
|
||||
|
||||
Metered is still great for predictable, infrastructure-level usage (cloud compute, API calls at scale) where buyers are sophisticated and want to pay for exactly what they use. But for products where you want users to engage freely and habitually, prepaid credits seem to have become the preferred billing method.
|
||||
|
||||
### Variable usage patterns
|
||||
|
||||
Users will often send messages in short, frequent sessions. In these cases, each session only uses a small number of tokens.
|
||||
|
||||
Other times, usage can be more "bursty", when working on a bigger task. If the 4 hour quota was the only available option, this type of usage would be often interrupted and users would need to wait.
|
||||
|
||||
To get around this, some of the monthly quota is assigned to a monthly overage bucket. If a certain session uses more than the 4 hour limit, it will start drawing from this bucket instead.
|
||||
|
||||
The challenge with moving to this model was previously, users were able to by lifetime top-up messages, and we needed a way to still honor this purchase. As part of the migration, we converted these prepaid top-up messages into a standalone credit balance that lasts forever. This bucket of messages will be drawn from last in the waterfall.
|
||||
|
||||

|
||||
|
||||
[OpenAI's billing system](https://openai.com/index/beyond-rate-limits/) for Codex and Sora works in a very similar way. The team also added a "premier" tier for $50, with 10x more usage, better suited for power users that were previously purchasing multiple top ups.
|
||||
|
||||
### Unpredictable costs
|
||||
|
||||
Messages are not created equally. Each would consume a fixed quota, but the underlying costs would vary dramatically depending on the input context. Some user behaviors were painfully expensive:
|
||||
|
||||
- Dumping many files from their codebase into the chat
|
||||
|
||||
- Using expensive models to analyze PDFs and images
|
||||
|
||||
- Continuing long threads with expensive models, instead of starting new ones
|
||||
|
||||
|
||||
Any of these actions could cost several dollars each and a small (but regular) proportion of the user base ended up being seriously unprofitable.
|
||||
|
||||
The team switched to a credits system. Every model and action (eg web search) has an underlying credit cost associated with it.
|
||||
|
||||
When a message is requested, an estimated number of credits for that message is prematurely deducted from the user's balance (ie, reserving balance). After the message is completed, an adjustment event is sent to either refund (if the estimate was higher) or capture additional credits (if the estimate lower). Reserving credits before the message is sent protects against overconsumption.
|
||||
|
||||
A secondary benefit of this system is that users can now use premium and basic models as they wish interchangeably.
|
||||
|
||||

|
||||
|
||||
### How Autumn helped
|
||||
|
||||
Before using Autumn, the team had followed [Theo's viral guide](https://github.com/t3dotgg/stripe-recommendations) on how to manage Stripe. Subscription statuses and balances of messages were stored in a Redis instance that ran on upstash. Even though it's the easiest setup to manage, they were still dealing with race conditions around failed payments and race conditions.
|
||||
|
||||
With the increase in complexity of the new pricing model, they decided that they didn't want to be slowed down by billing anymore. Using Autumn meant they didn't have to deal with stripe code, webhooks, waterfall credit deduction, cron jobs, cache layers, failed payments / 3DS, etc. All while having full flexibility to change their rate limits at any point, without code changes or migrations.
|
||||
|
||||
> Stripe is a hassle. It's an amazing platform with a lot of capability, but with that capability comes a ton of little details that you have to manage.
|
||||
>
|
||||
> Autumn ticked all of our boxes and allowed us to consolidate a bunch of services into one place, and with a team we trust to know the intracacies and do it right.
|
||||
>
|
||||
> We were spending nearly as much on the infrastructure to handle this ourselves (less well) than the cost with autumn. It makes the switch even more of a no brainer.
|
||||
|
||||
\-Mark (T3 Chat Co-Founder)
|
||||
|
||||
### Thinking about doing something similar?
|
||||
|
||||
The specific limits and buckets will still be refined, but having used it myself over a couple weeks, it really does feel great to use and is more profitable for the company. To build a similar model:
|
||||
|
||||
1. Define the average margin per user you're looking to make, and how many credits a user can use each month to hit that. Eg, to make $2 on a $8 sub, you have $6 of credits an average user can use.
|
||||
|
||||
2. Allocate ~30-50% of those credits to the monthly overage bucket
|
||||
|
||||
3. With the remaining, divide them across the average number of sessions a user has in a month
|
||||
|
||||
|
||||
This will give you a good place to start. Your users will very quickly tell you how they feel about it, and you can refine it from there.
|
||||
32
apps/website/eslint.config.mjs
Executable file → Normal file
@@ -1,16 +1,16 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
|
||||
0
apps/website/jsconfig.json
Executable file → Normal file
62
apps/website/lib/blogUtils.js
Normal file
@@ -0,0 +1,62 @@
|
||||
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;
|
||||
}
|
||||
13
apps/website/next.config.mjs
Executable file → Normal file
@@ -1,6 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
/* config options here */
|
||||
allowedDevOrigins: ['*.ngrok-free.dev']
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
445
apps/website/package-lock.json
generated
Executable file → Normal file
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "pixelup-dev",
|
||||
"name": "autumndev",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pixelup-dev",
|
||||
"name": "autumndev",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@gsap/react": "^2.1.2",
|
||||
@@ -14,6 +14,7 @@
|
||||
"matter-js": "^0.20.0",
|
||||
"motion": "^12.38.0",
|
||||
"next": "16.2.1",
|
||||
"ngrok": "^5.0.0-beta.2",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-syntax-highlighter": "^16.1.1"
|
||||
@@ -1254,6 +1255,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
@@ -1263,6 +1276,18 @@
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@szmarczak/http-timer": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"defer-to-connect": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
|
||||
@@ -1545,6 +1570,18 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cacheable-request": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-cache-semantics": "*",
|
||||
"@types/keyv": "^3.1.4",
|
||||
"@types/node": "*",
|
||||
"@types/responselike": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -1561,6 +1598,12 @@
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/http-cache-semantics": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@@ -1575,18 +1618,55 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/keyv": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
|
||||
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
|
||||
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prismjs": {
|
||||
"version": "1.26.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz",
|
||||
"integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/responselike": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
|
||||
"integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.57.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz",
|
||||
@@ -2514,6 +2594,42 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-lookup": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-request": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
|
||||
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clone-response": "^1.0.2",
|
||||
"get-stream": "^5.1.0",
|
||||
"http-cache-semantics": "^4.0.0",
|
||||
"keyv": "^4.0.0",
|
||||
"lowercase-keys": "^2.0.0",
|
||||
"normalize-url": "^6.0.1",
|
||||
"responselike": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||
@@ -2647,6 +2763,18 @@
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clone-response": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
|
||||
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -2771,7 +2899,6 @@
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -2798,6 +2925,33 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response/node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
@@ -2805,6 +2959,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/defer-to-connect": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
|
||||
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
@@ -2893,6 +3056,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.20.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
|
||||
@@ -3538,6 +3710,26 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-zip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"get-stream": "^5.1.0",
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"bin": {
|
||||
"extract-zip": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.17.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@types/yauzl": "^2.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -3612,6 +3804,15 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -3827,6 +4028,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pump": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-symbol-description": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
|
||||
@@ -3914,6 +4130,31 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/got": {
|
||||
"version": "11.8.6",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
|
||||
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^4.0.0",
|
||||
"@szmarczak/http-timer": "^4.0.5",
|
||||
"@types/cacheable-request": "^6.0.1",
|
||||
"@types/responselike": "^1.0.0",
|
||||
"cacheable-lookup": "^5.0.3",
|
||||
"cacheable-request": "^7.0.2",
|
||||
"decompress-response": "^6.0.0",
|
||||
"http2-wrapper": "^1.0.0-beta.5.2",
|
||||
"lowercase-keys": "^2.0.0",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"responselike": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -4083,6 +4324,32 @@
|
||||
"integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/hpagent": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/hpagent/-/hpagent-0.1.2.tgz",
|
||||
"integrity": "sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/http2-wrapper": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve-alpn": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -4673,7 +4940,6 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
@@ -4723,7 +4989,6 @@
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
@@ -5040,6 +5305,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@@ -5079,6 +5350,15 @@
|
||||
"integrity": "sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lowlight": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
|
||||
@@ -5153,6 +5433,15 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
@@ -5221,7 +5510,6 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
@@ -5346,6 +5634,29 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/ngrok": {
|
||||
"version": "5.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/ngrok/-/ngrok-5.0.0-beta.2.tgz",
|
||||
"integrity": "sha512-UzsyGiJ4yTTQLCQD11k1DQaMwq2/SsztBg2b34zAqcyjS25qjDpogMKPaCKHwe/APRTHeel3iDXcVctk5CNaCQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"extract-zip": "^2.0.1",
|
||||
"got": "^11.8.5",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"uuid": "^7.0.0 || ^8.0.0",
|
||||
"yaml": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"ngrok": "bin/ngrok"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"hpagent": "^0.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/node-exports-info": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
|
||||
@@ -5372,6 +5683,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-url": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
|
||||
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -5495,6 +5818,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@@ -5531,6 +5863,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/p-cancelable": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
|
||||
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
@@ -5628,6 +5969,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -5727,6 +6074,16 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -5758,6 +6115,18 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quick-lru": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
@@ -5887,6 +6256,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-alpn": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
|
||||
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
@@ -5907,6 +6282,18 @@
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/responselike": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
|
||||
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lowercase-keys": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
@@ -6720,6 +7107,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.19.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
||||
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unrs-resolver": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
|
||||
@@ -6796,6 +7189,15 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -6911,6 +7313,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
@@ -6918,6 +7326,31 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
|
||||
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
8
apps/website/package.json
Executable file → Normal file
@@ -1,26 +1,30 @@
|
||||
{
|
||||
"name": "pixelup-dev",
|
||||
"name": "autumndev",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npx next dev -p 3005",
|
||||
"dev": "next dev -p 3004",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gsap/react": "^2.1.2",
|
||||
"gray-matter": "^4.0.3",
|
||||
"gsap": "^3.14.2",
|
||||
"lottie-react": "^2.4.1",
|
||||
"matter-js": "^0.20.0",
|
||||
"motion": "^12.38.0",
|
||||
"next": "16.2.1",
|
||||
"next-mdx-remote": "^6.0.0",
|
||||
"ngrok": "^5.0.0-beta.2",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-syntax-highlighter": "^16.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.1",
|
||||
"tailwindcss": "^4.2.2"
|
||||
|
||||
14
apps/website/postcss.config.mjs
Executable file → Normal file
@@ -1,7 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
0
apps/website/public/animation/problem_desktop.json
Executable file → Normal file
0
apps/website/public/animation/problem_mobile.json
Executable file → Normal file
0
apps/website/public/animation/solution-desktop.json
Executable file → Normal file
0
apps/website/public/animation/solution-mobile.json
Executable file → Normal file
30
apps/website/public/images/404.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 4.4 MiB After Width: | Height: | Size: 4.4 MiB |
6
apps/website/public/images/autumn-notfound.svg
Executable file → Normal file
@@ -1,3 +1,3 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M48 48H0V0H48V48ZM18.3667 15.5466C16.761 19.2189 15.1552 22.8912 13.5495 26.5634C14.8079 28.1941 16.0664 29.8258 17.3248 31.4564C21.056 26.6349 24.7883 21.8133 28.5206 16.9917L15.8928 39.3452C22.142 35.5746 28.3912 31.8049 34.6404 28.0343V8.31487L18.3667 15.5466Z" fill="white"/>
|
||||
</svg>
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M48 48H0V0H48V48ZM18.3667 15.5466C16.761 19.2189 15.1552 22.8912 13.5495 26.5634C14.8079 28.1941 16.0664 29.8258 17.3248 31.4564C21.056 26.6349 24.7883 21.8133 28.5206 16.9917L15.8928 39.3452C22.142 35.5746 28.3912 31.8049 34.6404 28.0343V8.31487L18.3667 15.5466Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 435 B After Width: | Height: | Size: 432 B |
BIN
apps/website/public/images/blog/attach.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
apps/website/public/images/blog/backendless.jpeg
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
apps/website/public/images/blog/component-fail.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 63 KiB |
BIN
apps/website/public/images/blog/marketing.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/website/public/images/blog/shadcn.png
Normal file
|
After Width: | Height: | Size: 336 KiB |
|
After Width: | Height: | Size: 858 KiB |
BIN
apps/website/public/images/blog/thanks-hn.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/website/public/images/blog/usage-billing-bad.png
Normal file
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 33 KiB |
BIN
apps/website/public/images/blog/what-if-stripe-builds-this.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
0
apps/website/public/images/docs.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
0
apps/website/public/images/features/analytics.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 552 B After Width: | Height: | Size: 552 B |
0
apps/website/public/images/features/billing.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 678 B After Width: | Height: | Size: 678 B |
0
apps/website/public/images/features/custom-plans.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 550 B After Width: | Height: | Size: 550 B |
BIN
apps/website/public/images/features/pixel effect.webm
Executable file → Normal file
0
apps/website/public/images/features/pricing.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
0
apps/website/public/images/features/react-components.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 745 B After Width: | Height: | Size: 745 B |
0
apps/website/public/images/features/referral.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 873 B After Width: | Height: | Size: 873 B |
0
apps/website/public/images/features/top-ups.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 756 B After Width: | Height: | Size: 756 B |
0
apps/website/public/images/features/webhooks.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 676 B After Width: | Height: | Size: 676 B |
BIN
apps/website/public/images/footer/footer-mob.webp
Normal file
|
After Width: | Height: | Size: 594 KiB |
BIN
apps/website/public/images/footer/footer.webp
Normal file
|
After Width: | Height: | Size: 698 KiB |
38
apps/website/public/images/footer/footerbg.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 416 KiB After Width: | Height: | Size: 416 KiB |
BIN
apps/website/public/images/footer/masked-mobile.webp
Normal file
|
After Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 641 KiB |
BIN
apps/website/public/images/footer/maskedimaged.webp
Normal file
|
After Width: | Height: | Size: 791 KiB |
|
Before Width: | Height: | Size: 540 KiB |
0
apps/website/public/images/hero-bg.png
Executable file → Normal file
|
Before Width: | Height: | Size: 884 KiB After Width: | Height: | Size: 884 KiB |
18
apps/website/public/images/hero-bg.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 22 MiB After Width: | Height: | Size: 22 MiB |
114
apps/website/public/images/hero/autumn_mobile.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 226 KiB |
6
apps/website/public/images/hero/box.svg
Executable file → Normal file
@@ -1,3 +1,3 @@
|
||||
<svg width="74" height="14" viewBox="0 0 74 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.324219 4.45108H72.7829M0.324219 8.57648H72.7829M0.324219 0.325684C0.324219 0.325684 44.486 0.325684 72.7829 0.325684V12.7019H0.324219V0.325684Z" stroke="#333333" stroke-width="0.651379"/>
|
||||
</svg>
|
||||
<svg width="74" height="14" viewBox="0 0 74 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.324219 4.45108H72.7829M0.324219 8.57648H72.7829M0.324219 0.325684C0.324219 0.325684 44.486 0.325684 72.7829 0.325684V12.7019H0.324219V0.325684Z" stroke="#333333" stroke-width="0.651379"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 306 B After Width: | Height: | Size: 303 B |
6
apps/website/public/images/hero/cross.svg
Executable file → Normal file
@@ -1,3 +1,3 @@
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.359375 0.359375L10.5228 10.5228M10.5228 0.359375L0.359375 10.5228" stroke="#292929" stroke-width="1.01634"/>
|
||||
</svg>
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.359375 0.359375L10.5228 10.5228M10.5228 0.359375L0.359375 10.5228" stroke="#292929" stroke-width="1.01634"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 227 B After Width: | Height: | Size: 224 B |
18
apps/website/public/images/hero/cta_bg.svg
Executable file → Normal file
@@ -1,9 +1,9 @@
|
||||
<svg width="200" height="18" viewBox="0 0 200 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 17.5H200M0 14.6667H200M0 11.8333H200M0 9H200M0 6.16667H200M0 3.33333H200M0 0.5H200" stroke="url(#paint0_linear_4873_221)" stroke-opacity="0.12"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4873_221" x1="100" y1="17.5" x2="100" y2="0.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg width="200" height="18" viewBox="0 0 200 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 17.5H200M0 14.6667H200M0 11.8333H200M0 9H200M0 6.16667H200M0 3.33333H200M0 0.5H200" stroke="url(#paint0_linear_4873_221)" stroke-opacity="0.12"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4873_221" x1="100" y1="17.5" x2="100" y2="0.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 500 B After Width: | Height: | Size: 491 B |
0
apps/website/public/images/hero/hero_img.webp
Executable file → Normal file
|
Before Width: | Height: | Size: 587 KiB After Width: | Height: | Size: 587 KiB |
0
apps/website/public/images/hero/hero_mobile.webp
Executable file → Normal file
|
Before Width: | Height: | Size: 307 KiB After Width: | Height: | Size: 307 KiB |
0
apps/website/public/images/issues/Frame 2147242729.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |