feat: migrate landing page to TypeScript and fix auth cookies
- Migrate all apps/website components from JS/JSX to TypeScript/TSX - Add blog pages (layout, listing, slug) in TypeScript - Add tsconfig.json and tailwind.config.mjs to website - Fix cookie domain handling in vite/App.tsx (set .useautumn.com in prod) - Update auth.ts, afterSessionCreated, afterSessionDeleted Made-with: Cursor
This commit is contained in:
114
apps/website/app/blog/[slug]/page.tsx
Normal file
114
apps/website/app/blog/[slug]/page.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { MDXRemote } from "next-mdx-remote/rsc";
|
||||||
|
import { mdxComponents } from "@/components/blogComponents";
|
||||||
|
import { getAllPosts, getPostBySlug } from "@/lib/blogUtils";
|
||||||
|
import type { BlogParams } from "@/lib/types";
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return getAllPosts().map((post) => ({ slug: post.slug }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: BlogParams;
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
const { slug } = await params;
|
||||||
|
const post = getPostBySlug({ slug });
|
||||||
|
if (!post) return { title: "Post Not Found" };
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: post.title,
|
||||||
|
description: post.description,
|
||||||
|
openGraph: {
|
||||||
|
title: post.title,
|
||||||
|
description: post.description,
|
||||||
|
type: "article",
|
||||||
|
...(post.date ? { publishedTime: post.date } : {}),
|
||||||
|
authors: [post.author],
|
||||||
|
...(post.image && {
|
||||||
|
images: [{ url: post.image }],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateString: string | null) {
|
||||||
|
if (!dateString) return "";
|
||||||
|
return new Date(dateString).toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function BlogPostPage({ params }: { params: BlogParams }) {
|
||||||
|
const { slug } = await params;
|
||||||
|
const post = getPostBySlug({ slug });
|
||||||
|
if (!post) notFound();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-16 md:py-24 bg-[#0F0F0F]">
|
||||||
|
<div className="max-w-[720px] mx-auto px-4 xl:px-0">
|
||||||
|
<Link
|
||||||
|
href="/blog"
|
||||||
|
className="inline-flex items-center gap-2 font-mono text-[12px] md:text-[14px] uppercase tracking-[-2%] text-[#FFFFFF66] hover:text-white transition-colors duration-300 mb-10"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
className="rotate-180"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6 3L11 8L6 13"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Back to blog
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<header className="mb-12">
|
||||||
|
<div className="flex items-center gap-3 font-mono text-[12px] md:text-[14px] uppercase tracking-[-2%] text-[#FFFFFF66] mb-4">
|
||||||
|
<span>{formatDate(post.date)}</span>
|
||||||
|
<span className="w-1 h-1 bg-[#FFFFFF44] rounded-full" />
|
||||||
|
<span>{post.author}</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-[30px] md:text-[40px] font-normal tracking-[-2%] leading-[1.1] font-sans text-white mb-4">
|
||||||
|
{post.title}
|
||||||
|
</h1>
|
||||||
|
{post.description && (
|
||||||
|
<p className="text-[14px] md:text-[16px] leading-5 text-[#FFFFFF99] font-light font-sans">
|
||||||
|
{post.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{post.image && (
|
||||||
|
<div className="relative w-full aspect-[2/1] overflow-hidden border border-[#292929] mb-12">
|
||||||
|
<Image
|
||||||
|
src={post.image}
|
||||||
|
alt={post.title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<hr className="border-[#292929] mb-12" />
|
||||||
|
|
||||||
|
<article className="prose prose-invert prose-lg max-w-none">
|
||||||
|
<MDXRemote source={post.source} components={mdxComponents} />
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import Navbar from "@/components/navbar";
|
|
||||||
import Footer from "@/components/footer";
|
|
||||||
|
|
||||||
export default function BlogLayout({ children }) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="w-full overflow-x-clip"
|
|
||||||
style={{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" }}
|
|
||||||
>
|
|
||||||
<div className="relative z-10 bg-[#0F0F0F] min-h-screen">
|
|
||||||
<div className="relative w-full px-4 md:px-(--page-pad) pt-5">
|
|
||||||
<div className="absolute pointer-events-none top-0 bottom-0 left-4 md:left-(--page-pad) border-l border-[#292929] z-50" />
|
|
||||||
<Navbar />
|
|
||||||
<div className="absolute pointer-events-none top-0 bottom-0 right-4 md:right-(--page-pad) border-r border-[#292929] z-50" />
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2.5 mt-2.5">
|
|
||||||
<div className="border-t border-[#292929] w-full" />
|
|
||||||
<div className="border-t border-[#292929] w-full hidden md:block" />
|
|
||||||
<div className="border-t border-[#292929] w-full hidden md:block" />
|
|
||||||
<div className="border-t border-[#292929] w-full hidden md:block" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{children}
|
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
33
apps/website/app/blog/layout.tsx
Normal file
33
apps/website/app/blog/layout.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import Footer from "@/components/footer";
|
||||||
|
import Navbar from "@/components/navbar";
|
||||||
|
import type { LayoutProps, PageStyle } from "@/lib/types";
|
||||||
|
|
||||||
|
export default function BlogLayout({ children }: LayoutProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full overflow-x-clip"
|
||||||
|
style={
|
||||||
|
{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" } as PageStyle
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="relative z-10 bg-[#0F0F0F] min-h-screen">
|
||||||
|
<div className="relative w-full px-4 md:px-(--page-pad) pt-5">
|
||||||
|
<div className="absolute pointer-events-none top-0 bottom-0 left-4 md:left-(--page-pad) border-l border-[#292929] z-50" />
|
||||||
|
<Navbar />
|
||||||
|
<div className="absolute pointer-events-none top-0 bottom-0 right-4 md:right-(--page-pad) border-r border-[#292929] z-50" />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2.5 mt-2.5">
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
<div className="border-t border-[#292929] w-full hidden md:block" />
|
||||||
|
<div className="border-t border-[#292929] w-full hidden md:block" />
|
||||||
|
<div className="border-t border-[#292929] w-full hidden md:block" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import { getAllPosts } from "@/lib/blogUtils";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: "Blog",
|
|
||||||
description:
|
|
||||||
"Thoughts on billing infrastructure, usage-based pricing, and building for AI startups.",
|
|
||||||
};
|
|
||||||
|
|
||||||
function formatDate(dateString) {
|
|
||||||
if (!dateString) return "";
|
|
||||||
return new Date(dateString).toLocaleDateString("en-US", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BlogListingPage() {
|
|
||||||
const posts = getAllPosts();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="py-16 md:py-24 bg-[#0F0F0F]">
|
|
||||||
<div className="max-w-[800px] mx-auto px-4 xl:px-0">
|
|
||||||
<h1 className="text-[30px] md:text-[40px] font-normal tracking-[-2%] leading-[1.1] font-sans mb-4">
|
|
||||||
<span className="text-[#FFFFFF99] font-light">From the </span>
|
|
||||||
<span className="text-white">Blog</span>
|
|
||||||
</h1>
|
|
||||||
<p className="text-[14px] md:text-[16px] leading-5 text-[#FFFFFF99] font-light font-sans mb-16">
|
|
||||||
Thoughts on billing infrastructure, usage-based pricing, and building
|
|
||||||
for AI startups.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{posts.length === 0 && (
|
|
||||||
<p className="text-[#FFFFFF66] text-center py-16 font-light">
|
|
||||||
No posts yet. Check back soon.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
{posts.map((post) => (
|
|
||||||
<Link
|
|
||||||
key={post.slug}
|
|
||||||
href={`/blog/${post.slug}`}
|
|
||||||
className="group flex items-center gap-6 border border-[#292929] hover:border-[#3f3f3f] hover:bg-[#080808] transition-colors duration-300 p-6 md:p-8"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-3 flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-3 font-mono text-[12px] md:text-[14px] uppercase tracking-[-2%] text-[#FFFFFF66]">
|
|
||||||
<span>{formatDate(post.date)}</span>
|
|
||||||
<span className="w-1 h-1 bg-[#FFFFFF44] rounded-full" />
|
|
||||||
<span>{post.author}</span>
|
|
||||||
</div>
|
|
||||||
<h2 className="font-sans text-[18px] md:text-[22px] tracking-[-2%] leading-[1.25] font-normal text-white group-hover:text-[#9564ff] transition-colors duration-300">
|
|
||||||
{post.title}
|
|
||||||
</h2>
|
|
||||||
{post.description && (
|
|
||||||
<p className="text-[14px] md:text-[16px] leading-5 text-[#FFFFFF99] font-light font-sans">
|
|
||||||
{post.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{post.image && (
|
|
||||||
<div className="relative hidden sm:block w-[140px] md:w-[180px] aspect-[3/2] overflow-hidden shrink-0">
|
|
||||||
<Image
|
|
||||||
src={post.image}
|
|
||||||
alt={post.title}
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
80
apps/website/app/blog/page.tsx
Normal file
80
apps/website/app/blog/page.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { getAllPosts } from "@/lib/blogUtils";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Blog",
|
||||||
|
description:
|
||||||
|
"Thoughts on billing infrastructure, usage-based pricing, and building for AI startups.",
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatDate(dateString: string | null) {
|
||||||
|
if (!dateString) return "";
|
||||||
|
return new Date(dateString).toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlogListingPage() {
|
||||||
|
const posts = getAllPosts();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-16 md:py-24 bg-[#0F0F0F]">
|
||||||
|
<div className="max-w-[800px] mx-auto px-4 xl:px-0">
|
||||||
|
<h1 className="text-[30px] md:text-[40px] font-normal tracking-[-2%] leading-[1.1] font-sans mb-4">
|
||||||
|
<span className="text-[#FFFFFF99] font-light">From the </span>
|
||||||
|
<span className="text-white">Blog</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-[14px] md:text-[16px] leading-5 text-[#FFFFFF99] font-light font-sans mb-16">
|
||||||
|
Thoughts on billing infrastructure, usage-based pricing, and building
|
||||||
|
for AI startups.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{posts.length === 0 && (
|
||||||
|
<p className="text-[#FFFFFF66] text-center py-16 font-light">
|
||||||
|
No posts yet. Check back soon.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{posts.map((post) => (
|
||||||
|
<Link
|
||||||
|
key={post.slug}
|
||||||
|
href={`/blog/${post.slug}`}
|
||||||
|
className="group flex items-center gap-6 border border-[#292929] hover:border-[#3f3f3f] hover:bg-[#080808] transition-colors duration-300 p-6 md:p-8"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-3 flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-3 font-mono text-[12px] md:text-[14px] uppercase tracking-[-2%] text-[#FFFFFF66]">
|
||||||
|
<span>{formatDate(post.date)}</span>
|
||||||
|
<span className="w-1 h-1 bg-[#FFFFFF44] rounded-full" />
|
||||||
|
<span>{post.author}</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="font-sans text-[18px] md:text-[22px] tracking-[-2%] leading-[1.25] font-normal text-white group-hover:text-[#9564ff] transition-colors duration-300">
|
||||||
|
{post.title}
|
||||||
|
</h2>
|
||||||
|
{post.description && (
|
||||||
|
<p className="text-[14px] md:text-[16px] leading-5 text-[#FFFFFF99] font-light font-sans">
|
||||||
|
{post.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{post.image && (
|
||||||
|
<div className="relative hidden sm:block w-[140px] md:w-[180px] aspect-[3/2] overflow-hidden shrink-0">
|
||||||
|
<Image
|
||||||
|
src={post.image}
|
||||||
|
alt={post.title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
1340
apps/website/app/constant.tsx
Executable file
1340
apps/website/app/constant.tsx
Executable file
File diff suppressed because it is too large
Load Diff
@@ -1,81 +0,0 @@
|
|||||||
import { Geist, Geist_Mono } from "next/font/google";
|
|
||||||
import "./globals.css";
|
|
||||||
|
|
||||||
const geistSans = Geist({
|
|
||||||
variable: "--font-geist-sans",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
|
||||||
variable: "--font-geist-mono",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: {
|
|
||||||
default: "Autumn — Billing Infrastructure for AI Startups",
|
|
||||||
template: "%s | Autumn",
|
|
||||||
},
|
|
||||||
description:
|
|
||||||
"The drop-in billing layer for AI startups. Stop rebuilding usage limits, credit systems, and subscription logic. Autumn keeps webhooks, payments, and usage perfectly in-sync.",
|
|
||||||
keywords: [
|
|
||||||
"AI billing",
|
|
||||||
"usage-based billing",
|
|
||||||
"subscription management",
|
|
||||||
"AI startups",
|
|
||||||
"billing infrastructure",
|
|
||||||
"payment integration",
|
|
||||||
"usage limits",
|
|
||||||
"credit system",
|
|
||||||
],
|
|
||||||
authors: [{ name: "Autumn" }],
|
|
||||||
creator: "Autumn",
|
|
||||||
metadataBase: new URL("https://autumndev.vercel.app"),
|
|
||||||
openGraph: {
|
|
||||||
type: "website",
|
|
||||||
locale: "en_US",
|
|
||||||
url: "https://autumndev.vercel.app",
|
|
||||||
siteName: "Autumn",
|
|
||||||
title: "Autumn — Billing Infrastructure for AI Startups",
|
|
||||||
description:
|
|
||||||
"The drop-in billing layer for AI startups. Stop rebuilding usage limits, credit systems, and subscription logic.",
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: "/images/og-image.png",
|
|
||||||
width: 1200,
|
|
||||||
height: 630,
|
|
||||||
alt: "Autumn — Billing Infrastructure for AI Startups",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
card: "summary_large_image",
|
|
||||||
title: "Autumn — Billing Infrastructure for AI Startups",
|
|
||||||
description:
|
|
||||||
"The drop-in billing layer for AI startups. Stop rebuilding usage limits, credit systems, and subscription logic.",
|
|
||||||
images: ["/images/og-image.png"],
|
|
||||||
},
|
|
||||||
robots: {
|
|
||||||
index: true,
|
|
||||||
follow: true,
|
|
||||||
googleBot: {
|
|
||||||
index: true,
|
|
||||||
follow: true,
|
|
||||||
"max-video-preview": -1,
|
|
||||||
"max-image-preview": "large",
|
|
||||||
"max-snippet": -1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({ children }) {
|
|
||||||
return (
|
|
||||||
<html
|
|
||||||
lang="en"
|
|
||||||
suppressHydrationWarning
|
|
||||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased bg-black`}
|
|
||||||
>
|
|
||||||
<body className="min-h-full flex flex-col">{children}</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
90
apps/website/app/layout.tsx
Normal file
90
apps/website/app/layout.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import type { LayoutProps } from "@/lib/types";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = "https://useautumn.com";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: {
|
||||||
|
default: "Autumn — Billing Infrastructure for AI Startups",
|
||||||
|
template: "%s | Autumn",
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
"The drop-in billing layer for AI startups. Stop rebuilding usage limits, credit systems, and subscription logic. Autumn keeps webhooks, payments, and usage perfectly in-sync.",
|
||||||
|
keywords: [
|
||||||
|
"AI billing",
|
||||||
|
"usage-based billing",
|
||||||
|
"subscription management",
|
||||||
|
"AI startups",
|
||||||
|
"billing infrastructure",
|
||||||
|
"payment integration",
|
||||||
|
"usage limits",
|
||||||
|
"credit system",
|
||||||
|
],
|
||||||
|
authors: [{ name: "Autumn" }],
|
||||||
|
creator: "Autumn",
|
||||||
|
metadataBase: new URL(url),
|
||||||
|
openGraph: {
|
||||||
|
type: "website",
|
||||||
|
locale: "en_US",
|
||||||
|
url,
|
||||||
|
siteName: "Autumn",
|
||||||
|
title: "Autumn — Billing Infrastructure for AI Startups",
|
||||||
|
description:
|
||||||
|
"The drop-in billing layer for AI startups. Stop rebuilding usage limits, credit systems, and subscription logic.",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: "/images/og-image.png",
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: "Autumn — Billing Infrastructure for AI Startups",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: "Autumn — Billing Infrastructure for AI Startups",
|
||||||
|
description:
|
||||||
|
"The drop-in billing layer for AI startups. Stop rebuilding usage limits, credit systems, and subscription logic.",
|
||||||
|
images: ["/images/og-image.png"],
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
"max-video-preview": -1,
|
||||||
|
"max-image-preview": "large",
|
||||||
|
"max-snippet": -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: LayoutProps) {
|
||||||
|
return (
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
suppressHydrationWarning
|
||||||
|
className={cn(
|
||||||
|
geistSans.variable,
|
||||||
|
geistMono.variable,
|
||||||
|
"h-full bg-black antialiased",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<body className="min-h-full flex flex-col">{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import Navbar from "@/components/navbar";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { IconArrowRightSmall } from "@/app/constant";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
|
|
||||||
export default function NotFound() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="w-full min-h-screen overflow-x-hidden overflow-y-auto bg-[#0f0f0f] flex flex-col"
|
|
||||||
style={{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" }}
|
|
||||||
>
|
|
||||||
<div className="relative w-full px-4 md:px-(--page-pad) pt-5 flex-1 block">
|
|
||||||
<div className="absolute pointer-events-none top-0 bottom-0 left-4 md:left-(--page-pad) border-l border-[#292929] z-50" />
|
|
||||||
<div className="absolute pointer-events-none top-0 bottom-0 right-4 md:right-(--page-pad) border-r border-[#292929] z-50" />
|
|
||||||
|
|
||||||
<Navbar animateIntro={false} />
|
|
||||||
|
|
||||||
<div className="relative flex-1 w-full min-h-[calc(100vh-100px)] flex flex-col items-center justify-center -mt-[1px]">
|
|
||||||
<div className="absolute inset-0 w-full h-full z-0 overflow-hidden">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
transition={{ duration: 1, ease: "easeOut" }}
|
|
||||||
className="absolute inset-0 w-full h-full"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src="/images/404.svg"
|
|
||||||
alt="404 Background City Base"
|
|
||||||
fill
|
|
||||||
className="object-cover object-center"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, display: "none" }}
|
|
||||||
animate={{
|
|
||||||
opacity: [0, 1, 0.8, 1, 0.6, 0],
|
|
||||||
display: ["block", "block", "block", "block", "block", "none"],
|
|
||||||
x: [0, -15, 20, -10, 15, 0],
|
|
||||||
y: [0, 8, -15, 5, -5, 0],
|
|
||||||
filter: [
|
|
||||||
"brightness(3) contrast(200%) hue-rotate(90deg)",
|
|
||||||
"brightness(2) contrast(300%) invert(30%)",
|
|
||||||
"brightness(1.5) contrast(150%) hue-rotate(-45deg)",
|
|
||||||
"brightness(3) contrast(200%) hue-rotate(180deg)",
|
|
||||||
"brightness(2) contrast(150%)",
|
|
||||||
"brightness(1)",
|
|
||||||
],
|
|
||||||
clipPath: [
|
|
||||||
"inset(10% 0% 60% 0%)",
|
|
||||||
"inset(20% 0% 30% 0%)",
|
|
||||||
"inset(80% 0% 5% 0%)",
|
|
||||||
"inset(40% 0% 40% 0%)",
|
|
||||||
"inset(5% 0% 80% 0%)",
|
|
||||||
"inset(0% 0% 0% 0%)",
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 0.45,
|
|
||||||
ease: "easeInOut",
|
|
||||||
delay: 0.1,
|
|
||||||
times: [0, 0.2, 0.6, 0.75, 0.9, 1],
|
|
||||||
}}
|
|
||||||
className="absolute inset-0 w-full h-full mix-blend-screen pointer-events-none z-10"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src="/images/404.svg"
|
|
||||||
alt="404 Background City Glitch"
|
|
||||||
fill
|
|
||||||
className="object-cover object-center scale-105"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative z-10 flex flex-col items-center justify-center w-full px-4 text-center mt-[-40px]">
|
|
||||||
<Image
|
|
||||||
src="/images/autumn-notfound.svg"
|
|
||||||
alt="Autumn Logo"
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
className="w-[56px] h-[56px] md:w-[64px] md:h-[64px] mb-6"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h1 className="text-white text-[48px] md:text-[64px] font-normal tracking-[-3%] mb-3 font-sans leading-[1.1]">
|
|
||||||
Page not found
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className="text-[#FFFFFF99] font-light text-[15px] md:text-[18px] mb-8 tracking-[-1%] text-center">
|
|
||||||
The page you are looking for doesn't exist or has been moved.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Link href="/">
|
|
||||||
<button className="group relative flex items-stretch justify-between transition-colors duration-300 bg-[#8752FA] hover:bg-[#7641E8] w-[200px] md:w-[210px] h-[48px] md:h-[54px] border border-[#8752FA]">
|
|
||||||
<span className="text-white text-[14px] md:text-[15px] pl-6 flex items-center font-sans tracking-[-1%]">
|
|
||||||
Back to home
|
|
||||||
</span>
|
|
||||||
<div className="flex items-center justify-center w-[36px] md:w-[40px] transition-colors duration-300 bg-white text-[#8752FA] m-1 md:m-1.5 shrink-0">
|
|
||||||
<IconArrowRightSmall className="w-4 h-4" />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
114
apps/website/app/not-found.tsx
Normal file
114
apps/website/app/not-found.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
"use client";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { IconArrowRightSmall } from "@/app/constant";
|
||||||
|
import Navbar from "@/components/navbar";
|
||||||
|
import type { PageStyle } from "@/lib/types";
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full min-h-screen overflow-x-hidden overflow-y-auto bg-[#0f0f0f] flex flex-col"
|
||||||
|
style={
|
||||||
|
{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" } as PageStyle
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="relative w-full px-4 md:px-(--page-pad) pt-5 flex-1 block">
|
||||||
|
<div className="absolute pointer-events-none top-0 bottom-0 left-4 md:left-(--page-pad) border-l border-[#292929] z-50" />
|
||||||
|
<div className="absolute pointer-events-none top-0 bottom-0 right-4 md:right-(--page-pad) border-r border-[#292929] z-50" />
|
||||||
|
|
||||||
|
<Navbar animateIntro={false} />
|
||||||
|
|
||||||
|
<div className="relative flex-1 w-full min-h-[calc(100vh-100px)] flex flex-col items-center justify-center -mt-[1px]">
|
||||||
|
<div className="absolute inset-0 w-full h-full z-0 overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 1, ease: "easeOut" }}
|
||||||
|
className="absolute inset-0 w-full h-full"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/images/404.svg"
|
||||||
|
alt="404 Background City Base"
|
||||||
|
fill
|
||||||
|
className="object-cover object-center"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, display: "none" }}
|
||||||
|
animate={{
|
||||||
|
opacity: [0, 1, 0.8, 1, 0.6, 0],
|
||||||
|
display: ["block", "block", "block", "block", "block", "none"],
|
||||||
|
x: [0, -15, 20, -10, 15, 0],
|
||||||
|
y: [0, 8, -15, 5, -5, 0],
|
||||||
|
filter: [
|
||||||
|
"brightness(3) contrast(200%) hue-rotate(90deg)",
|
||||||
|
"brightness(2) contrast(300%) invert(30%)",
|
||||||
|
"brightness(1.5) contrast(150%) hue-rotate(-45deg)",
|
||||||
|
"brightness(3) contrast(200%) hue-rotate(180deg)",
|
||||||
|
"brightness(2) contrast(150%)",
|
||||||
|
"brightness(1)",
|
||||||
|
],
|
||||||
|
clipPath: [
|
||||||
|
"inset(10% 0% 60% 0%)",
|
||||||
|
"inset(20% 0% 30% 0%)",
|
||||||
|
"inset(80% 0% 5% 0%)",
|
||||||
|
"inset(40% 0% 40% 0%)",
|
||||||
|
"inset(5% 0% 80% 0%)",
|
||||||
|
"inset(0% 0% 0% 0%)",
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 0.45,
|
||||||
|
ease: "easeInOut",
|
||||||
|
delay: 0.1,
|
||||||
|
times: [0, 0.2, 0.6, 0.75, 0.9, 1],
|
||||||
|
}}
|
||||||
|
className="absolute inset-0 w-full h-full mix-blend-screen pointer-events-none z-10"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/images/404.svg"
|
||||||
|
alt="404 Background City Glitch"
|
||||||
|
fill
|
||||||
|
className="object-cover object-center scale-105"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10 flex flex-col items-center justify-center w-full px-4 text-center mt-[-40px]">
|
||||||
|
<Image
|
||||||
|
src="/images/autumn-notfound.svg"
|
||||||
|
alt="Autumn Logo"
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
|
className="w-[56px] h-[56px] md:w-[64px] md:h-[64px] mb-6"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h1 className="text-white text-[48px] md:text-[64px] font-normal tracking-[-3%] mb-3 font-sans leading-[1.1]">
|
||||||
|
Page not found
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="text-[#FFFFFF99] font-light text-[15px] md:text-[18px] mb-8 tracking-[-1%] text-center">
|
||||||
|
The page you are looking for doesn't exist or has been moved.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Link href="/">
|
||||||
|
<button className="group relative flex items-stretch justify-between transition-colors duration-300 bg-[#8752FA] hover:bg-[#7641E8] w-[200px] md:w-[210px] h-[48px] md:h-[54px] border border-[#8752FA]">
|
||||||
|
<span className="text-white text-[14px] md:text-[15px] pl-6 flex items-center font-sans tracking-[-1%]">
|
||||||
|
Back to home
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center justify-center w-[36px] md:w-[40px] transition-colors duration-300 bg-white text-[#8752FA] m-1 md:m-1.5 shrink-0">
|
||||||
|
<IconArrowRightSmall className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
apps/website/app/page.tsx
Normal file
44
apps/website/app/page.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import ElasticRecoil from "@/components/elastic-footer";
|
||||||
|
import HomeSections from "@/components/home-sections";
|
||||||
|
import Navbar from "@/components/navbar";
|
||||||
|
import Preloader from "@/components/preloader";
|
||||||
|
import type { PageStyle } from "@/lib/types";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full overflow-x-clip"
|
||||||
|
style={
|
||||||
|
{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" } as PageStyle
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Preloader />
|
||||||
|
<ElasticRecoil>
|
||||||
|
<div className="relative z-10 bg-[#000000] min-h-screen">
|
||||||
|
<div className="relative w-full px-4 md:px-(--page-pad) pt-5">
|
||||||
|
<div className="absolute pointer-events-none top-0 bottom-0 left-4 md:left-(--page-pad) border-l border-[#292929] z-50" />
|
||||||
|
<Navbar />
|
||||||
|
<div className="absolute pointer-events-none top-0 bottom-0 right-4 md:right-(--page-pad) border-r border-[#292929] z-50" />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2.5">
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
<div className="border-t border-[#292929] w-full hidden md:block" />
|
||||||
|
<div className="border-t border-[#292929] w-full hidden md:block" />
|
||||||
|
<div className="border-t border-[#292929] w-full hidden md:block" />
|
||||||
|
<div className="border-t border-[#292929] w-full hidden md:block" />
|
||||||
|
</div>
|
||||||
|
<HomeSections />
|
||||||
|
|
||||||
|
<div className="w-full flex-col gap-2.5 mt-10.5 hidden md:flex">
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElasticRecoil>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
import AnimatedFooterImage from "@/components/animated-footer-image";
|
|
||||||
import Footer from "@/components/footer";
|
|
||||||
import Navbar from "@/components/navbar";
|
|
||||||
|
|
||||||
const privacyTerms = [
|
|
||||||
{
|
|
||||||
title: "1. Acceptance of Terms",
|
|
||||||
content:
|
|
||||||
'By accessing or using the Autumn platform, APIs, SDKs, or any related services (collectively, the "Services") provided by Rebase, Inc., a Delaware corporation ("Autumn," "we," "us," or "our"), you agree to be bound by these Terms of Service ("Terms"). If you do not agree to these Terms, do not use our Services.',
|
|
||||||
isUppercase: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "2. Description of Services",
|
|
||||||
content:
|
|
||||||
"Autumn provides billing infrastructure, including usage-based billing, credits management, pricing configuration, and entitlements management for software applications. Our Services are designed for developers and businesses building products that require flexible billing and pricing capabilities.",
|
|
||||||
isUppercase: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "3. Account Registration",
|
|
||||||
content:
|
|
||||||
"To use certain features of our Services, you must create an account. You agree to provide accurate, current, and complete information during registration and to update such information as necessary. You are responsible for maintaining the confidentiality of your account credentials and for all activities that occur under your account.",
|
|
||||||
isUppercase: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "4. Acceptable Use",
|
|
||||||
content:
|
|
||||||
"You agree not to use the Services for any unlawful purpose or in violation of any applicable laws; (b) interfere with or disrupt the integrity or performance of the Services; (c) attempt to gain unauthorized access to the Services or related systems; (d) use the Services to transmit malicious code or harmful content; or (e) resell or redistribute the Services without our prior written consent.",
|
|
||||||
isUppercase: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "5. Payment Terms",
|
|
||||||
content:
|
|
||||||
"Fees for the Services are set forth on our pricing page or in a separate agreement. You agree to pay all applicable fees in accordance with the payment terms. All fees are non-refundable except as expressly stated otherwise. We reserve the right to modify our pricing with 30 days' notice.",
|
|
||||||
isUppercase: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "6. Data and Privacy",
|
|
||||||
content:
|
|
||||||
'Your use of the Services is subject to our Privacy Policy. You retain ownership of any data you submit to the Services ("Customer Data"). You grant us a limited license to process Customer Data solely to provide the Services. We implement reasonable security measures to protect Customer Data, but you acknowledge that no system is completely secure.',
|
|
||||||
isUppercase: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "7. Intellectual Property",
|
|
||||||
content:
|
|
||||||
"The Services, including all software, APIs, documentation, and related materials, are owned by Autumn and protected by intellectual property laws. We grant you a limited, non-exclusive, non-transferable license to use the Services in accordance with these Terms. You may not copy, modify, or create derivative works of the Services except as expressly permitted.",
|
|
||||||
isUppercase: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "8. Confidentiality",
|
|
||||||
content:
|
|
||||||
"Each party agrees to maintain the confidentiality of any non-public information disclosed by the other party and to use such information only for the purposes of these Terms. This obligation does not apply to information that is publicly available, independently developed, or rightfully received from a third party.",
|
|
||||||
isUppercase: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "9. Warranties and Disclaimers",
|
|
||||||
content:
|
|
||||||
'THE SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. WE DO NOT WARRANT THAT THE SERVICES WILL BE UNINTERRUPTED OR ERROR-FREE.',
|
|
||||||
isUppercase: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "10. Limitation of Liability",
|
|
||||||
content:
|
|
||||||
"TO THE MAXIMUM EXTENT PERMITTED BY LAW, AUTUMN SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS OR REVENUES. OUR TOTAL LIABILITY FOR ANY CLAIMS ARISING FROM THESE TERMS SHALL NOT EXCEED THE AMOUNTS PAID BY YOU TO AUTUMN IN THE TWELVE MONTHS PRECEDING THE CLAIM.",
|
|
||||||
isUppercase: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "11. Indemnification",
|
|
||||||
content:
|
|
||||||
"You agree to indemnify, defend, and hold harmless Autumn and its officers, directors, employees, and agents from any claims, liabilities, damages, losses, or expenses arising from your use of the Services, your violation of these Terms, or your infringement of any third-party rights.",
|
|
||||||
isUppercase: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "12. Term and Termination",
|
|
||||||
content:
|
|
||||||
"These Terms remain in effect until terminated. Either party may terminate for convenience with 30 days' written notice. We may suspend or terminate your access immediately if you breach these Terms. Upon termination, your right to use the Services ceases, and you must pay any outstanding fees.",
|
|
||||||
isUppercase: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "13. Modifications",
|
|
||||||
content:
|
|
||||||
"We may modify these Terms at any time by posting the revised Terms on our website. Material changes will be communicated with at least 30 days' notice. Your continued use of the Services after such changes constitutes acceptance of the modified Terms.",
|
|
||||||
isUppercase: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "14. Governing Law and Disputes",
|
|
||||||
content:
|
|
||||||
"These Terms are governed by the laws of the State of Delaware, without regard to conflict of law principles. Any disputes arising from these Terms shall be resolved through binding arbitration in accordance with the rules of the American Arbitration Association, except that either party may seek injunctive relief in any court of competent jurisdiction.",
|
|
||||||
isUppercase: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "15. General Provisions",
|
|
||||||
content:
|
|
||||||
"These Terms constitute the entire agreement between you and Autumn regarding the Services. If any provision is found unenforceable, the remaining provisions will continue in effect. Our failure to enforce any right or provision does not constitute a waiver. You may not assign these Terms without our prior written consent.",
|
|
||||||
isUppercase: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "16. Contact Information",
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
For questions about these Terms, please contact us at
|
|
||||||
security@useautumn.com or at:
|
|
||||||
<br />
|
|
||||||
Rebase, Inc. (d/b/a Autumn)
|
|
||||||
<br />
|
|
||||||
Email: security@useautumn.com
|
|
||||||
<br />
|
|
||||||
Website: https://useautumn.com
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
isUppercase: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function PrivacyPolicy() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="w-full overflow-x-hidden overflow-y-auto bg-[#000000]"
|
|
||||||
style={{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" }}
|
|
||||||
>
|
|
||||||
<div className="relative z-10 bg-[#000000] min-h-screen">
|
|
||||||
<div className="relative w-full px-4 md:px-(--page-pad) pt-5">
|
|
||||||
<div className="absolute pointer-events-none top-0 bottom-0 left-4 md:left-(--page-pad) border-l border-[#292929] z-50" />
|
|
||||||
<div className="absolute pointer-events-none left-0 border-t border-[#292929] w-full" />
|
|
||||||
<Navbar />
|
|
||||||
<div className="absolute pointer-events-none left-0 border-b border-[#292929] w-full" />
|
|
||||||
<div className="absolute pointer-events-none top-0 bottom-0 right-4 md:right-(--page-pad) border-r border-[#292929] z-50" />
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2.5">
|
|
||||||
<div className="border-t border-[#292929] w-full" />
|
|
||||||
<div className="border-t border-[#292929] w-full hidden md:block" />
|
|
||||||
<div className="border-t border-[#292929] w-full hidden md:block" />
|
|
||||||
<div className="border-t border-[#292929] w-full hidden md:block" />
|
|
||||||
<div className="border-t border-[#292929] w-full hidden md:block" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full flex-col border-b border-[#292929]">
|
|
||||||
<div className="flex w-full border-b border-[#292929]">
|
|
||||||
<div className="hidden md:block w-1/8 lg:w-1/6 border-r bg-[#0F0F0F] border-[#292929]" />
|
|
||||||
<div className="flex-1 bg-[#0F0F0F] px-4 sm:px-8 py-10 md:py-16">
|
|
||||||
<h1 className="text-white text-[40px] font-sans tracking-[-2%] uppercase">
|
|
||||||
Terms of Service
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="hidden md:block w-1/8 lg:w-1/6 border-l bg-[#0F0F0F] border-[#292929]" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full">
|
|
||||||
<div className="hidden md:block w-1/8 lg:w-1/6 border-r border-[#292929]" />
|
|
||||||
<div className="flex-1 px-4 sm:px-8 py-12 md:py-16 text-white text-[16px] font-light leading-[1.6] tracking-[-2%] font-sans pb-32">
|
|
||||||
<p className="mb-10">
|
|
||||||
Autumn (Rebase, Inc.)
|
|
||||||
<br />
|
|
||||||
Effective Date: February 1, 2025
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{privacyTerms.map((term, index) => (
|
|
||||||
<div key={index}>
|
|
||||||
<h3
|
|
||||||
className={`text-white font-normal text-[24px] leading-[30px] tracking-[-2%] mb-2 mt-8`}
|
|
||||||
>
|
|
||||||
{term.title}
|
|
||||||
</h3>
|
|
||||||
<p
|
|
||||||
className={`mb-6 font-light text-[16px] tracking-[-2%] leading-[20px] md:leading-[24px] ${term.isUppercase ? "uppercase" : ""}`}
|
|
||||||
>
|
|
||||||
{term.content}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="hidden md:block w-1/8 lg:w-1/6 border-l border-[#292929]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full flex-col gap-2.5 mt-10.5 hidden md:flex mb-4">
|
|
||||||
<div className="border-t border-[#292929] w-full" />
|
|
||||||
<div className="border-t border-[#292929] w-full" />
|
|
||||||
<div className="border-t border-[#292929] w-full" />
|
|
||||||
<div className="border-t border-[#292929] w-full" />
|
|
||||||
<div className="border-t border-[#292929] w-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full aspect-390/313 sm:aspect-1440/619" />
|
|
||||||
<AnimatedFooterImage />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
201
apps/website/app/privacy/page.tsx
Normal file
201
apps/website/app/privacy/page.tsx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import AnimatedFooterImage from "@/components/animated-footer-image";
|
||||||
|
import Footer from "@/components/footer";
|
||||||
|
import Navbar from "@/components/navbar";
|
||||||
|
import type { PageStyle } from "@/lib/types";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const privacyTerms: Array<{
|
||||||
|
title: string;
|
||||||
|
content: ReactNode;
|
||||||
|
isUppercase: boolean;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
title: "1. Acceptance of Terms",
|
||||||
|
content:
|
||||||
|
'By accessing or using the Autumn platform, APIs, SDKs, or any related services (collectively, the "Services") provided by Rebase, Inc., a Delaware corporation ("Autumn," "we," "us," or "our"), you agree to be bound by these Terms of Service ("Terms"). If you do not agree to these Terms, do not use our Services.',
|
||||||
|
isUppercase: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2. Description of Services",
|
||||||
|
content:
|
||||||
|
"Autumn provides billing infrastructure, including usage-based billing, credits management, pricing configuration, and entitlements management for software applications. Our Services are designed for developers and businesses building products that require flexible billing and pricing capabilities.",
|
||||||
|
isUppercase: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3. Account Registration",
|
||||||
|
content:
|
||||||
|
"To use certain features of our Services, you must create an account. You agree to provide accurate, current, and complete information during registration and to update such information as necessary. You are responsible for maintaining the confidentiality of your account credentials and for all activities that occur under your account.",
|
||||||
|
isUppercase: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4. Acceptable Use",
|
||||||
|
content:
|
||||||
|
"You agree not to use the Services for any unlawful purpose or in violation of any applicable laws; (b) interfere with or disrupt the integrity or performance of the Services; (c) attempt to gain unauthorized access to the Services or related systems; (d) use the Services to transmit malicious code or harmful content; or (e) resell or redistribute the Services without our prior written consent.",
|
||||||
|
isUppercase: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "5. Payment Terms",
|
||||||
|
content:
|
||||||
|
"Fees for the Services are set forth on our pricing page or in a separate agreement. You agree to pay all applicable fees in accordance with the payment terms. All fees are non-refundable except as expressly stated otherwise. We reserve the right to modify our pricing with 30 days' notice.",
|
||||||
|
isUppercase: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "6. Data and Privacy",
|
||||||
|
content:
|
||||||
|
'Your use of the Services is subject to our Privacy Policy. You retain ownership of any data you submit to the Services ("Customer Data"). You grant us a limited license to process Customer Data solely to provide the Services. We implement reasonable security measures to protect Customer Data, but you acknowledge that no system is completely secure.',
|
||||||
|
isUppercase: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "7. Intellectual Property",
|
||||||
|
content:
|
||||||
|
"The Services, including all software, APIs, documentation, and related materials, are owned by Autumn and protected by intellectual property laws. We grant you a limited, non-exclusive, non-transferable license to use the Services in accordance with these Terms. You may not copy, modify, or create derivative works of the Services except as expressly permitted.",
|
||||||
|
isUppercase: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "8. Confidentiality",
|
||||||
|
content:
|
||||||
|
"Each party agrees to maintain the confidentiality of any non-public information disclosed by the other party and to use such information only for the purposes of these Terms. This obligation does not apply to information that is publicly available, independently developed, or rightfully received from a third party.",
|
||||||
|
isUppercase: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "9. Warranties and Disclaimers",
|
||||||
|
content:
|
||||||
|
'THE SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. WE DO NOT WARRANT THAT THE SERVICES WILL BE UNINTERRUPTED OR ERROR-FREE.',
|
||||||
|
isUppercase: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "10. Limitation of Liability",
|
||||||
|
content:
|
||||||
|
"TO THE MAXIMUM EXTENT PERMITTED BY LAW, AUTUMN SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS OR REVENUES. OUR TOTAL LIABILITY FOR ANY CLAIMS ARISING FROM THESE TERMS SHALL NOT EXCEED THE AMOUNTS PAID BY YOU TO AUTUMN IN THE TWELVE MONTHS PRECEDING THE CLAIM.",
|
||||||
|
isUppercase: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "11. Indemnification",
|
||||||
|
content:
|
||||||
|
"You agree to indemnify, defend, and hold harmless Autumn and its officers, directors, employees, and agents from any claims, liabilities, damages, losses, or expenses arising from your use of the Services, your violation of these Terms, or your infringement of any third-party rights.",
|
||||||
|
isUppercase: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "12. Term and Termination",
|
||||||
|
content:
|
||||||
|
"These Terms remain in effect until terminated. Either party may terminate for convenience with 30 days' written notice. We may suspend or terminate your access immediately if you breach these Terms. Upon termination, your right to use the Services ceases, and you must pay any outstanding fees.",
|
||||||
|
isUppercase: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "13. Modifications",
|
||||||
|
content:
|
||||||
|
"We may modify these Terms at any time by posting the revised Terms on our website. Material changes will be communicated with at least 30 days' notice. Your continued use of the Services after such changes constitutes acceptance of the modified Terms.",
|
||||||
|
isUppercase: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "14. Governing Law and Disputes",
|
||||||
|
content:
|
||||||
|
"These Terms are governed by the laws of the State of Delaware, without regard to conflict of law principles. Any disputes arising from these Terms shall be resolved through binding arbitration in accordance with the rules of the American Arbitration Association, except that either party may seek injunctive relief in any court of competent jurisdiction.",
|
||||||
|
isUppercase: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "15. General Provisions",
|
||||||
|
content:
|
||||||
|
"These Terms constitute the entire agreement between you and Autumn regarding the Services. If any provision is found unenforceable, the remaining provisions will continue in effect. Our failure to enforce any right or provision does not constitute a waiver. You may not assign these Terms without our prior written consent.",
|
||||||
|
isUppercase: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "16. Contact Information",
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
For questions about these Terms, please contact us at
|
||||||
|
security@useautumn.com or at:
|
||||||
|
<br />
|
||||||
|
Rebase, Inc. (d/b/a Autumn)
|
||||||
|
<br />
|
||||||
|
Email: security@useautumn.com
|
||||||
|
<br />
|
||||||
|
Website: https://useautumn.com
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
isUppercase: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function PrivacyPolicy() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full overflow-x-hidden overflow-y-auto bg-[#000000]"
|
||||||
|
style={
|
||||||
|
{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" } as PageStyle
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="relative z-10 bg-[#000000] min-h-screen">
|
||||||
|
<div className="relative w-full px-4 md:px-(--page-pad) pt-5">
|
||||||
|
<div className="absolute pointer-events-none top-0 bottom-0 left-4 md:left-(--page-pad) border-l border-[#292929] z-50" />
|
||||||
|
<div className="absolute pointer-events-none left-0 border-t border-[#292929] w-full" />
|
||||||
|
<Navbar />
|
||||||
|
<div className="absolute pointer-events-none left-0 border-b border-[#292929] w-full" />
|
||||||
|
<div className="absolute pointer-events-none top-0 bottom-0 right-4 md:right-(--page-pad) border-r border-[#292929] z-50" />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2.5">
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
<div className="border-t border-[#292929] w-full hidden md:block" />
|
||||||
|
<div className="border-t border-[#292929] w-full hidden md:block" />
|
||||||
|
<div className="border-t border-[#292929] w-full hidden md:block" />
|
||||||
|
<div className="border-t border-[#292929] w-full hidden md:block" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex w-full flex-col border-b border-[#292929]">
|
||||||
|
<div className="flex w-full border-b border-[#292929]">
|
||||||
|
<div className="hidden md:block w-1/8 lg:w-1/6 border-r bg-[#0F0F0F] border-[#292929]" />
|
||||||
|
<div className="flex-1 bg-[#0F0F0F] px-4 sm:px-8 py-10 md:py-16">
|
||||||
|
<h1 className="text-white text-[40px] font-sans tracking-[-2%] uppercase">
|
||||||
|
Terms of Service
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:block w-1/8 lg:w-1/6 border-l bg-[#0F0F0F] border-[#292929]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex w-full">
|
||||||
|
<div className="hidden md:block w-1/8 lg:w-1/6 border-r border-[#292929]" />
|
||||||
|
<div className="flex-1 px-4 sm:px-8 py-12 md:py-16 text-white text-[16px] font-light leading-[1.6] tracking-[-2%] font-sans pb-32">
|
||||||
|
<p className="mb-10">
|
||||||
|
Autumn (Rebase, Inc.)
|
||||||
|
<br />
|
||||||
|
Effective Date: February 1, 2025
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{privacyTerms.map((term, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<h3 className="mb-2 mt-8 text-[24px] leading-[30px] font-normal tracking-[-2%] text-white">
|
||||||
|
{term.title}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"mb-6 text-[16px] leading-[20px] font-light tracking-[-2%] md:leading-[24px]",
|
||||||
|
term.isUppercase && "uppercase",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{term.content}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:block w-1/8 lg:w-1/6 border-l border-[#292929]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full flex-col gap-2.5 mt-10.5 hidden md:flex mb-4">
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full aspect-390/313 sm:aspect-1440/619" />
|
||||||
|
<AnimatedFooterImage />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { forwardRef } from "react";
|
|
||||||
|
|
||||||
const AnimatedFooterImage = forwardRef(function AnimatedFooterImage(_, ref) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className="fixed bottom-0 left-0 w-full z-0 pointer-events-none overflow-hidden h-[420px] md:h-[580px]"
|
|
||||||
>
|
|
||||||
<div className="relative w-full h-full">
|
|
||||||
<Image
|
|
||||||
src="/images/footer/footer.webp"
|
|
||||||
alt="footer background"
|
|
||||||
fill
|
|
||||||
priority
|
|
||||||
sizes="100vw"
|
|
||||||
className="object-cover object-top"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default AnimatedFooterImage;
|
|
||||||
29
apps/website/components/animated-footer-image.tsx
Normal file
29
apps/website/components/animated-footer-image.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
"use client";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { forwardRef } from "react";
|
||||||
|
|
||||||
|
const AnimatedFooterImage = forwardRef<HTMLDivElement>(
|
||||||
|
function AnimatedFooterImage(_props, ref) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="fixed bottom-0 left-0 w-full z-0 pointer-events-none overflow-hidden h-[420px] md:h-[580px]"
|
||||||
|
>
|
||||||
|
<div className="relative w-full h-full">
|
||||||
|
<Image
|
||||||
|
src="/images/footer/footer.webp"
|
||||||
|
alt="footer background"
|
||||||
|
fill
|
||||||
|
priority
|
||||||
|
sizes="100vw"
|
||||||
|
className="object-cover object-top"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
AnimatedFooterImage.displayName = "AnimatedFooterImage";
|
||||||
|
|
||||||
|
export default AnimatedFooterImage;
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Image from "next/image";
|
|
||||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
||||||
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
||||||
import js from "react-syntax-highlighter/dist/esm/languages/hljs/javascript";
|
|
||||||
|
|
||||||
// useLayoutEffect runs synchronously before browser paint on the client,
|
|
||||||
// eliminating the container-query font-size race that causes CLS on reload.
|
|
||||||
const useIsomorphicLayoutEffect =
|
|
||||||
typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
||||||
|
|
||||||
SyntaxHighlighter.registerLanguage("javascript", js);
|
|
||||||
|
|
||||||
const autumnTheme = {
|
|
||||||
hljs: {
|
|
||||||
display: "block",
|
|
||||||
background: "transparent",
|
|
||||||
color: "#BFBFBF",
|
|
||||||
padding: "0",
|
|
||||||
margin: "0",
|
|
||||||
},
|
|
||||||
"hljs-comment": { color: "#6B6B6B" },
|
|
||||||
"hljs-keyword": { color: "#9564ff" },
|
|
||||||
"hljs-built_in": { color: "#BFBFBF" },
|
|
||||||
"hljs-string": { color: "#2B8C3F" },
|
|
||||||
"hljs-number": { color: "#9564ff" },
|
|
||||||
"hljs-literal": { color: "#2B8C3F" },
|
|
||||||
"hljs-attr": { color: "#FF1F12" },
|
|
||||||
"hljs-property": { color: "#FF1F12" },
|
|
||||||
"hljs-variable": { color: "#0161B5" },
|
|
||||||
"hljs-title": { color: "#BFBFBF" },
|
|
||||||
"hljs-params": { color: "#0161B5" },
|
|
||||||
"hljs-punctuation": { color: "#BFBFBF" },
|
|
||||||
};
|
|
||||||
|
|
||||||
const TOTAL_LINES = 21;
|
|
||||||
const LINE_HEIGHT = 24;
|
|
||||||
|
|
||||||
const codeContent = `// Your entire billing integration
|
|
||||||
const { allowed, remaining } = await
|
|
||||||
autumn.check({
|
|
||||||
featureId: "ai_tokens"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (allowed) {
|
|
||||||
await autumn.track({
|
|
||||||
featureId: "ai_tokens",
|
|
||||||
value: 1024
|
|
||||||
});
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const TYPING_SPEED = 10;
|
|
||||||
|
|
||||||
export default function AutumnConfig({
|
|
||||||
lines = TOTAL_LINES,
|
|
||||||
initialDelay = 0,
|
|
||||||
awaitEvent = null,
|
|
||||||
}) {
|
|
||||||
const fullCode = (() => {
|
|
||||||
const lineCount = codeContent.split("\n").length;
|
|
||||||
return codeContent + "\n".repeat(Math.max(0, lines - lineCount));
|
|
||||||
})();
|
|
||||||
const containerRef = useRef(null);
|
|
||||||
const [fontSize, setFontSize] = useState(16);
|
|
||||||
const [displayed, setDisplayed] = useState("");
|
|
||||||
const [cursorVisible, setCursorVisible] = useState(true);
|
|
||||||
const [awaitDone, setAwaitDone] = useState(!awaitEvent);
|
|
||||||
const [started, setStarted] = useState(initialDelay === 0 && !awaitEvent);
|
|
||||||
const done = displayed.length >= fullCode.length;
|
|
||||||
|
|
||||||
// Wait for the signal event before starting the delay countdown
|
|
||||||
useEffect(() => {
|
|
||||||
if (!awaitEvent) return;
|
|
||||||
const handler = () => setAwaitDone(true);
|
|
||||||
window.addEventListener(awaitEvent, handler, { once: true });
|
|
||||||
return () => window.removeEventListener(awaitEvent, handler);
|
|
||||||
}, [awaitEvent]);
|
|
||||||
|
|
||||||
// Start typing after awaitDone, respecting initialDelay
|
|
||||||
useEffect(() => {
|
|
||||||
if (!awaitDone) return;
|
|
||||||
const t = setTimeout(() => setStarted(true), initialDelay);
|
|
||||||
return () => clearTimeout(t);
|
|
||||||
}, [awaitDone, initialDelay]);
|
|
||||||
|
|
||||||
useIsomorphicLayoutEffect(() => {
|
|
||||||
const el = containerRef.current;
|
|
||||||
if (!el) return;
|
|
||||||
const measure = () =>
|
|
||||||
setFontSize(Math.min(Math.max(el.offsetWidth * 0.0385, 10), 20));
|
|
||||||
measure();
|
|
||||||
const ro = new ResizeObserver(measure);
|
|
||||||
ro.observe(el);
|
|
||||||
return () => ro.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const displayedPadded =
|
|
||||||
displayed + "\n".repeat(Math.max(0, lines - displayed.split("\n").length));
|
|
||||||
useEffect(() => {
|
|
||||||
if (!started || done) return;
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setDisplayed(fullCode.slice(0, displayed.length + 1));
|
|
||||||
}, TYPING_SPEED);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [displayed, done, started]);
|
|
||||||
|
|
||||||
// Blinking cursor after done
|
|
||||||
useEffect(() => {
|
|
||||||
if (!done) return;
|
|
||||||
const interval = setInterval(() => setCursorVisible((v) => !v), 530);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [done]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
className="@container w-full max-w-[520px] border border-[#2A2A2A] bg-[#000000]/90"
|
|
||||||
>
|
|
||||||
{/* Title bar */}
|
|
||||||
{/* <div className="flex items-center justify-between border-b border-[#2A2A2A] px-4 py-0.5 gap-3 w-full">
|
|
||||||
<div className="flex-1 min-w-0 overflow-hidden flex justify-end items-center h-[21px]">
|
|
||||||
<Image
|
|
||||||
src="/images/hero/box.svg"
|
|
||||||
width={186}
|
|
||||||
height={21}
|
|
||||||
alt="Box"
|
|
||||||
style={{ width: "auto", height: "auto" }}
|
|
||||||
className="max-w-none w-[186px] h-[21px] object-none object-right shrink-0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="font-mono text-[17.5px] text-[#FFFFFF99] whitespace-nowrap pt-0.5 shrink-0 mx-1">
|
|
||||||
autumn.config.ts
|
|
||||||
</span>
|
|
||||||
<div className="flex-1 min-w-0 overflow-hidden flex justify-start items-center h-[21px]">
|
|
||||||
<Image
|
|
||||||
src="/images/hero/box.svg"
|
|
||||||
width={186}
|
|
||||||
height={21}
|
|
||||||
alt="Box"
|
|
||||||
style={{ width: "auto", height: "auto" }}
|
|
||||||
className="w-[186px] h-[21px] object-none object-left"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="ml-1 border border-[#292929] cursor-pointer select-none flex items-center justify-center shrink-0 p-1.5">
|
|
||||||
<img
|
|
||||||
src="/images/hero/cross.svg"
|
|
||||||
width={11}
|
|
||||||
height={11}
|
|
||||||
alt="cross"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div> */}
|
|
||||||
<div className="flex items-center justify-between border-b border-[#292929] px-2 py-1.5 w-full bg-[#000000]">
|
|
||||||
{/* Left Vent: flex-1 makes it stretch, min-w-0 allows it to shrink below its content if needed */}
|
|
||||||
<div className="flex-1 min-w-[10px] h-[22px] border border-[#292929] flex flex-col justify-evenly px-[2px]">
|
|
||||||
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
|
|
||||||
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
|
|
||||||
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filename: shrink-0 ensures the text never gets squashed */}
|
|
||||||
<span className="font-mono text-[18px] text-[#FFFFFF99] whitespace-nowrap shrink-0 px-2">
|
|
||||||
billing.ts
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Right Vent: Matches the left one */}
|
|
||||||
<div className="flex-1 min-w-[10px] h-[22px] border border-[#2A2A2A] flex flex-col justify-evenly px-[2px]">
|
|
||||||
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
|
|
||||||
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
|
|
||||||
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Close Button Container */}
|
|
||||||
<div className="w-[24px] ml-2 h-[24px] border border-[#2A2A2A] flex items-center justify-center shrink-0 cursor-pointer hover:bg-white/5 transition-colors">
|
|
||||||
<Image
|
|
||||||
src="/images/hero/cross.svg"
|
|
||||||
width={11}
|
|
||||||
height={11}
|
|
||||||
alt="Box"
|
|
||||||
style={{ width: "auto", height: "auto" }}
|
|
||||||
className="w-full h-[11px] object-fill object-left"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Code area — fluidly driven by container query relative font sizes so height never distorts */}
|
|
||||||
<div className="relative px-4 py-3 font-mono text-sm overflow-hidden">
|
|
||||||
{/* Dynamic-height inner box: 20 lines × 1.25em */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: `${lines * LINE_HEIGHT}px`,
|
|
||||||
overflow: "hidden",
|
|
||||||
fontSize: `${fontSize}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SyntaxHighlighter
|
|
||||||
language="javascript"
|
|
||||||
style={autumnTheme}
|
|
||||||
showLineNumbers
|
|
||||||
lineNumberStyle={{
|
|
||||||
color: "#fff",
|
|
||||||
minWidth: "2rem",
|
|
||||||
paddingRight: "1rem",
|
|
||||||
userSelect: "none",
|
|
||||||
}}
|
|
||||||
customStyle={{
|
|
||||||
background: "transparent",
|
|
||||||
padding: 0,
|
|
||||||
margin: 0,
|
|
||||||
fontSize: "inherit",
|
|
||||||
lineHeight: `${LINE_HEIGHT}px`,
|
|
||||||
fontWeight: "300",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{displayedPadded}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ height: 0, overflow: "visible", position: "relative" }}>
|
|
||||||
<span
|
|
||||||
className="absolute -top-3.5 left-14 w-0.5 h-3.5 bg-[#9564ff]"
|
|
||||||
style={{
|
|
||||||
opacity: cursorVisible ? 1 : 0,
|
|
||||||
transition: "opacity 0.1s",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pointer-events-none absolute bottom-0 left-0 right-0 h-28 bg-linear-to-t from-[#000000] via-[#000000]/70 to-transparent z-10" />
|
|
||||||
|
|
||||||
<div className="px-1 sm:px-2">
|
|
||||||
<div className="absolute left-2 right-2 sm:left-3 sm:right-3 bottom-[10px] flex items-center justify-between border border-[#9564ff] bg-[#20143C] px-2 sm:px-4 py-2 sm:py-2.5 font-mono text-[10px] sm:text-sm shadow-[0_4px_20px_rgba(149,100,255,0.1)] z-20">
|
|
||||||
<div className="flex items-center gap-1 sm:gap-2">
|
|
||||||
<span className="text-[#959494]">allowed:</span>
|
|
||||||
<span className="text-[#2B8C3F]">true</span>
|
|
||||||
<span className="text-[#959494] ml-0.5 sm:ml-0">remaining:</span>
|
|
||||||
<span className="text-[#9564ff]">8976</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-[#9564ff] ml-1">92ms</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
261
apps/website/components/autumn-config.tsx
Normal file
261
apps/website/components/autumn-config.tsx
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
|
import { Light as RawSyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
import js from "react-syntax-highlighter/dist/esm/languages/hljs/javascript";
|
||||||
|
|
||||||
|
const SyntaxHighlighter = RawSyntaxHighlighter as unknown as ((props: {
|
||||||
|
children: string;
|
||||||
|
customStyle?: Record<string, string | number>;
|
||||||
|
language: string;
|
||||||
|
lineNumberStyle?: Record<string, string | number>;
|
||||||
|
showLineNumbers?: boolean;
|
||||||
|
style?: Record<string, Record<string, string | number>>;
|
||||||
|
}) => JSX.Element) & {
|
||||||
|
registerLanguage: (name: string, language: unknown) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// useLayoutEffect runs synchronously before browser paint on the client,
|
||||||
|
// eliminating the container-query font-size race that causes CLS on reload.
|
||||||
|
const useIsomorphicLayoutEffect =
|
||||||
|
typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
||||||
|
|
||||||
|
SyntaxHighlighter.registerLanguage("javascript", js);
|
||||||
|
|
||||||
|
const autumnTheme = {
|
||||||
|
hljs: {
|
||||||
|
display: "block",
|
||||||
|
background: "transparent",
|
||||||
|
color: "#BFBFBF",
|
||||||
|
padding: "0",
|
||||||
|
margin: "0",
|
||||||
|
},
|
||||||
|
"hljs-comment": { color: "#6B6B6B" },
|
||||||
|
"hljs-keyword": { color: "#9564ff" },
|
||||||
|
"hljs-built_in": { color: "#BFBFBF" },
|
||||||
|
"hljs-string": { color: "#2B8C3F" },
|
||||||
|
"hljs-number": { color: "#9564ff" },
|
||||||
|
"hljs-literal": { color: "#2B8C3F" },
|
||||||
|
"hljs-attr": { color: "#FF1F12" },
|
||||||
|
"hljs-property": { color: "#FF1F12" },
|
||||||
|
"hljs-variable": { color: "#0161B5" },
|
||||||
|
"hljs-title": { color: "#BFBFBF" },
|
||||||
|
"hljs-params": { color: "#0161B5" },
|
||||||
|
"hljs-punctuation": { color: "#BFBFBF" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const TOTAL_LINES = 16;
|
||||||
|
const LINE_HEIGHT = 24;
|
||||||
|
|
||||||
|
const codeContent = `// Your entire billing integration
|
||||||
|
const { allowed } = await check({
|
||||||
|
featureId: "ai_tokens"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allowed) {
|
||||||
|
await track({
|
||||||
|
featureId: "ai_tokens",
|
||||||
|
value: 1024
|
||||||
|
});
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const TYPING_SPEED = 10;
|
||||||
|
|
||||||
|
export default function AutumnConfig({
|
||||||
|
lines = TOTAL_LINES,
|
||||||
|
initialDelay = 0,
|
||||||
|
awaitEvent = null,
|
||||||
|
}: {
|
||||||
|
awaitEvent?: string | null;
|
||||||
|
initialDelay?: number;
|
||||||
|
lines?: number;
|
||||||
|
}) {
|
||||||
|
const fullCode = (() => {
|
||||||
|
const lineCount = codeContent.split("\n").length;
|
||||||
|
return codeContent + "\n".repeat(Math.max(0, lines - lineCount));
|
||||||
|
})();
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [fontSize, setFontSize] = useState(16);
|
||||||
|
const [displayed, setDisplayed] = useState("");
|
||||||
|
const [cursorVisible, setCursorVisible] = useState(true);
|
||||||
|
const [awaitDone, setAwaitDone] = useState(!awaitEvent);
|
||||||
|
const [started, setStarted] = useState(initialDelay === 0 && !awaitEvent);
|
||||||
|
const done = displayed.length >= fullCode.length;
|
||||||
|
|
||||||
|
// Wait for the signal event before starting the delay countdown
|
||||||
|
useEffect(() => {
|
||||||
|
if (!awaitEvent) return;
|
||||||
|
const handler = () => setAwaitDone(true);
|
||||||
|
window.addEventListener(awaitEvent, handler, { once: true });
|
||||||
|
return () => window.removeEventListener(awaitEvent, handler);
|
||||||
|
}, [awaitEvent]);
|
||||||
|
|
||||||
|
// Start typing after awaitDone, respecting initialDelay
|
||||||
|
useEffect(() => {
|
||||||
|
if (!awaitDone) return;
|
||||||
|
const t = setTimeout(() => setStarted(true), initialDelay);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}, [awaitDone, initialDelay]);
|
||||||
|
|
||||||
|
useIsomorphicLayoutEffect(() => {
|
||||||
|
const el = containerRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const measure = () =>
|
||||||
|
setFontSize(Math.min(Math.max(el.offsetWidth * 0.0385, 10), 20));
|
||||||
|
measure();
|
||||||
|
const ro = new ResizeObserver(measure);
|
||||||
|
ro.observe(el);
|
||||||
|
return () => ro.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const displayedPadded =
|
||||||
|
displayed + "\n".repeat(Math.max(0, lines - displayed.split("\n").length));
|
||||||
|
useEffect(() => {
|
||||||
|
if (!started || done) return;
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setDisplayed(fullCode.slice(0, displayed.length + 1));
|
||||||
|
}, TYPING_SPEED);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [displayed, done, started, fullCode]);
|
||||||
|
|
||||||
|
// Blinking cursor after done
|
||||||
|
useEffect(() => {
|
||||||
|
if (!done) return;
|
||||||
|
const interval = setInterval(() => setCursorVisible((v) => !v), 530);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [done]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="@container w-full max-w-[520px] border border-[#2A2A2A] bg-[#000000]/90"
|
||||||
|
>
|
||||||
|
{/* Title bar */}
|
||||||
|
{/* <div className="flex items-center justify-between border-b border-[#2A2A2A] px-4 py-0.5 gap-3 w-full">
|
||||||
|
<div className="flex-1 min-w-0 overflow-hidden flex justify-end items-center h-[21px]">
|
||||||
|
<Image
|
||||||
|
src="/images/hero/box.svg"
|
||||||
|
width={186}
|
||||||
|
height={21}
|
||||||
|
alt="Box"
|
||||||
|
style={{ width: "auto", height: "auto" }}
|
||||||
|
className="max-w-none w-[186px] h-[21px] object-none object-right shrink-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-[17.5px] text-[#FFFFFF99] whitespace-nowrap pt-0.5 shrink-0 mx-1">
|
||||||
|
autumn.config.ts
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 min-w-0 overflow-hidden flex justify-start items-center h-[21px]">
|
||||||
|
<Image
|
||||||
|
src="/images/hero/box.svg"
|
||||||
|
width={186}
|
||||||
|
height={21}
|
||||||
|
alt="Box"
|
||||||
|
style={{ width: "auto", height: "auto" }}
|
||||||
|
className="w-[186px] h-[21px] object-none object-left"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="ml-1 border border-[#292929] cursor-pointer select-none flex items-center justify-center shrink-0 p-1.5">
|
||||||
|
<img
|
||||||
|
src="/images/hero/cross.svg"
|
||||||
|
width={11}
|
||||||
|
height={11}
|
||||||
|
alt="cross"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
<div className="flex items-center justify-between border-b border-[#292929] px-2 py-1.5 w-full bg-[#000000]">
|
||||||
|
{/* Left Vent: flex-1 makes it stretch, min-w-0 allows it to shrink below its content if needed */}
|
||||||
|
<div className="flex-1 min-w-[10px] h-[22px] border border-[#292929] flex flex-col justify-evenly px-[2px]">
|
||||||
|
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
|
||||||
|
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
|
||||||
|
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filename: shrink-0 ensures the text never gets squashed */}
|
||||||
|
<span className="font-mono text-[18px] text-[#FFFFFF99] whitespace-nowrap shrink-0 px-2">
|
||||||
|
billing.ts
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Right Vent: Matches the left one */}
|
||||||
|
<div className="flex-1 min-w-[10px] h-[22px] border border-[#2A2A2A] flex flex-col justify-evenly px-[2px]">
|
||||||
|
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
|
||||||
|
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
|
||||||
|
<div className="h-[1px] w-full bg-[#2A2A2A]/60" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Close Button Container */}
|
||||||
|
<div className="w-[24px] ml-2 h-[24px] border border-[#2A2A2A] flex items-center justify-center shrink-0 cursor-pointer hover:bg-white/5 transition-colors">
|
||||||
|
<Image
|
||||||
|
src="/images/hero/cross.svg"
|
||||||
|
width={11}
|
||||||
|
height={11}
|
||||||
|
alt="Box"
|
||||||
|
style={{ width: "auto", height: "auto" }}
|
||||||
|
className="w-full h-[11px] object-fill object-left"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Code area — fluidly driven by container query relative font sizes so height never distorts */}
|
||||||
|
<div className="relative px-4 py-3 font-mono text-sm overflow-hidden">
|
||||||
|
{/* Dynamic-height inner box: 20 lines × 1.25em */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: `${lines * LINE_HEIGHT}px`,
|
||||||
|
overflow: "hidden",
|
||||||
|
fontSize: `${fontSize}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
language="javascript"
|
||||||
|
style={autumnTheme}
|
||||||
|
showLineNumbers
|
||||||
|
lineNumberStyle={{
|
||||||
|
color: "#fff",
|
||||||
|
minWidth: "2rem",
|
||||||
|
paddingRight: "1rem",
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
customStyle={{
|
||||||
|
background: "transparent",
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
fontSize: "inherit",
|
||||||
|
lineHeight: `${LINE_HEIGHT}px`,
|
||||||
|
fontWeight: "300",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{displayedPadded}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ height: 0, overflow: "visible", position: "relative" }}>
|
||||||
|
<span
|
||||||
|
className="absolute -top-3.5 left-14 w-0.5 h-3.5 bg-[#9564ff]"
|
||||||
|
style={{
|
||||||
|
opacity: cursorVisible ? 1 : 0,
|
||||||
|
transition: "opacity 0.1s",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pointer-events-none absolute bottom-0 left-0 right-0 h-28 bg-linear-to-t from-[#000000] via-[#000000]/70 to-transparent z-10" />
|
||||||
|
|
||||||
|
<div className="px-1 sm:px-2">
|
||||||
|
<div className="absolute left-2 right-2 sm:left-3 sm:right-3 bottom-[10px] flex items-center justify-between border border-[#9564ff] bg-[#20143C] px-2 sm:px-4 py-2 sm:py-2.5 font-mono text-[10px] sm:text-sm shadow-[0_4px_20px_rgba(149,100,255,0.1)] z-20">
|
||||||
|
<div className="flex items-center gap-1 sm:gap-2">
|
||||||
|
<span className="text-[#959494]">allowed:</span>
|
||||||
|
<span className="text-[#2B8C3F]">true</span>
|
||||||
|
<span className="text-[#959494] ml-0.5 sm:ml-0">remaining:</span>
|
||||||
|
<span className="text-[#9564ff]">8976</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[#9564ff] ml-1">92ms</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
function BlogHeading({ as: Tag, children, ...props }) {
|
|
||||||
return (
|
|
||||||
<Tag {...props} className="scroll-mt-24">
|
|
||||||
{children}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mdxComponents = {
|
|
||||||
h1: (props) => <BlogHeading as="h1" {...props} />,
|
|
||||||
h2: (props) => <BlogHeading as="h2" {...props} />,
|
|
||||||
h3: (props) => <BlogHeading as="h3" {...props} />,
|
|
||||||
h4: (props) => <BlogHeading as="h4" {...props} />,
|
|
||||||
a: ({ href, children, ...props }) => {
|
|
||||||
const isExternal = href?.startsWith("http");
|
|
||||||
if (isExternal) {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-[#9564ff] hover:text-[#b08aff] underline underline-offset-2 transition-colors"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
href={href || "#"}
|
|
||||||
className="text-[#9564ff] hover:text-[#b08aff] underline underline-offset-2 transition-colors"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
pre: ({ children, ...props }) => (
|
|
||||||
<pre
|
|
||||||
className="rounded-lg border border-[#292929] bg-[#141414] p-4 overflow-x-auto text-sm leading-relaxed"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</pre>
|
|
||||||
),
|
|
||||||
code: ({ children, ...props }) => {
|
|
||||||
const isInline = typeof children === "string";
|
|
||||||
if (isInline && !props.className) {
|
|
||||||
return (
|
|
||||||
<code className="rounded bg-[#1c1c1c] border border-[#292929] px-1.5 py-0.5 text-[0.875em] text-[#e0e0e0] font-mono">
|
|
||||||
{children}
|
|
||||||
</code>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <code {...props}>{children}</code>;
|
|
||||||
},
|
|
||||||
blockquote: ({ children, ...props }) => (
|
|
||||||
<blockquote
|
|
||||||
className="border-l-2 border-[#9564ff] pl-4 italic text-[#FFFFFF99]"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</blockquote>
|
|
||||||
),
|
|
||||||
hr: (props) => <hr className="border-[#292929] my-8" {...props} />,
|
|
||||||
table: ({ children, ...props }) => (
|
|
||||||
<div className="overflow-x-auto my-6">
|
|
||||||
<table className="w-full border-collapse text-sm" {...props}>
|
|
||||||
{children}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
th: ({ children, ...props }) => (
|
|
||||||
<th
|
|
||||||
className="border border-[#292929] bg-[#141414] px-4 py-2 text-left font-medium text-white"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</th>
|
|
||||||
),
|
|
||||||
td: ({ children, ...props }) => (
|
|
||||||
<td className="border border-[#292929] px-4 py-2" {...props}>
|
|
||||||
{children}
|
|
||||||
</td>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
110
apps/website/components/blogComponents.tsx
Normal file
110
apps/website/components/blogComponents.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import type { ComponentPropsWithoutRef, ElementType, ReactNode } from "react";
|
||||||
|
|
||||||
|
function BlogHeading({
|
||||||
|
as: Tag,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
as: ElementType;
|
||||||
|
children?: ReactNode;
|
||||||
|
} & ComponentPropsWithoutRef<"h1">) {
|
||||||
|
return (
|
||||||
|
<Tag {...props} className="scroll-mt-24">
|
||||||
|
{children}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mdxComponents = {
|
||||||
|
h1: (props: ComponentPropsWithoutRef<"h1">) => (
|
||||||
|
<BlogHeading as="h1" {...props} />
|
||||||
|
),
|
||||||
|
h2: (props: ComponentPropsWithoutRef<"h2">) => (
|
||||||
|
<BlogHeading as="h2" {...props} />
|
||||||
|
),
|
||||||
|
h3: (props: ComponentPropsWithoutRef<"h3">) => (
|
||||||
|
<BlogHeading as="h3" {...props} />
|
||||||
|
),
|
||||||
|
h4: (props: ComponentPropsWithoutRef<"h4">) => (
|
||||||
|
<BlogHeading as="h4" {...props} />
|
||||||
|
),
|
||||||
|
a: ({ href, children, ...props }: ComponentPropsWithoutRef<"a">) => {
|
||||||
|
const isExternal = href?.startsWith("http");
|
||||||
|
if (isExternal) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-[#9564ff] hover:text-[#b08aff] underline underline-offset-2 transition-colors"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href || "#"}
|
||||||
|
className="text-[#9564ff] hover:text-[#b08aff] underline underline-offset-2 transition-colors"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
pre: ({ children, ...props }: ComponentPropsWithoutRef<"pre">) => (
|
||||||
|
<pre
|
||||||
|
className="rounded-lg border border-[#292929] bg-[#141414] p-4 overflow-x-auto text-sm leading-relaxed"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</pre>
|
||||||
|
),
|
||||||
|
code: ({ children, ...props }: ComponentPropsWithoutRef<"code">) => {
|
||||||
|
const isInline = typeof children === "string";
|
||||||
|
if (isInline && !props.className) {
|
||||||
|
return (
|
||||||
|
<code className="rounded bg-[#1c1c1c] border border-[#292929] px-1.5 py-0.5 text-[0.875em] text-[#e0e0e0] font-mono">
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <code {...props}>{children}</code>;
|
||||||
|
},
|
||||||
|
blockquote: ({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ComponentPropsWithoutRef<"blockquote">) => (
|
||||||
|
<blockquote
|
||||||
|
className="border-l-2 border-[#9564ff] pl-4 italic text-[#FFFFFF99]"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</blockquote>
|
||||||
|
),
|
||||||
|
hr: (props: ComponentPropsWithoutRef<"hr">) => (
|
||||||
|
<hr className="border-[#292929] my-8" {...props} />
|
||||||
|
),
|
||||||
|
table: ({ children, ...props }: ComponentPropsWithoutRef<"table">) => (
|
||||||
|
<div className="overflow-x-auto my-6">
|
||||||
|
<table className="w-full border-collapse text-sm" {...props}>
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
th: ({ children, ...props }: ComponentPropsWithoutRef<"th">) => (
|
||||||
|
<th
|
||||||
|
className="border border-[#292929] bg-[#141414] px-4 py-2 text-left font-medium text-white"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</th>
|
||||||
|
),
|
||||||
|
td: ({ children, ...props }: ComponentPropsWithoutRef<"td">) => (
|
||||||
|
<td className="border border-[#292929] px-4 py-2" {...props}>
|
||||||
|
{children}
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
};
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { forwardRef, useImperativeHandle, useRef, useEffect } from "react";
|
|
||||||
import gsap from "gsap";
|
|
||||||
|
|
||||||
export const DashboardIconPixel = forwardRef(function DashboardIconPixel(
|
|
||||||
{ Icon },
|
|
||||||
ref,
|
|
||||||
) {
|
|
||||||
const iconRef = useRef(null);
|
|
||||||
const tlRef = useRef(null);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
restart: () => tlRef.current?.play(),
|
|
||||||
reverse: () => tlRef.current?.reverse(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const pixelEls = iconRef.current?.querySelectorAll(".icon-pixel-path");
|
|
||||||
if (!pixelEls?.length) return;
|
|
||||||
|
|
||||||
// Sort pixels by visual position: bottom-left → top-right diagonal
|
|
||||||
const pixels = Array.from(pixelEls).sort((a, b) => {
|
|
||||||
const aBox = a.getBBox();
|
|
||||||
const bBox = b.getBBox();
|
|
||||||
return (
|
|
||||||
aBox.x +
|
|
||||||
aBox.width / 2 -
|
|
||||||
(aBox.y + aBox.height / 2) -
|
|
||||||
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
gsap.set(pixels, {
|
|
||||||
opacity: 0.15,
|
|
||||||
scale: 0.8,
|
|
||||||
transformOrigin: "left bottom",
|
|
||||||
fill: "currentColor",
|
|
||||||
});
|
|
||||||
|
|
||||||
tlRef.current = gsap.timeline({ paused: true });
|
|
||||||
|
|
||||||
tlRef.current
|
|
||||||
.to(pixels, {
|
|
||||||
opacity: 1,
|
|
||||||
scale: 1.15,
|
|
||||||
fill: "#FFFFFF",
|
|
||||||
duration: 0.2,
|
|
||||||
stagger: 0.01,
|
|
||||||
ease: "power2.out",
|
|
||||||
})
|
|
||||||
.to(pixels, {
|
|
||||||
scale: 1,
|
|
||||||
duration: 0.01,
|
|
||||||
ease: "back.out(3)",
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => tlRef.current?.kill();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="relative inline-flex items-center justify-center w-[14px] h-[14px]">
|
|
||||||
<Icon ref={iconRef} className="w-full h-full text-white" />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
70
apps/website/components/dashboard-icon-pixel.tsx
Normal file
70
apps/website/components/dashboard-icon-pixel.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
"use client";
|
||||||
|
import gsap from "gsap";
|
||||||
|
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
|
||||||
|
import type { PixelHoverHandle, PixelIconComponent } from "@/lib/types";
|
||||||
|
|
||||||
|
export const DashboardIconPixel = forwardRef<
|
||||||
|
PixelHoverHandle,
|
||||||
|
{ Icon: PixelIconComponent }
|
||||||
|
>(function DashboardIconPixel({ Icon }, ref) {
|
||||||
|
const iconRef = useRef<SVGSVGElement | null>(null);
|
||||||
|
const tlRef = useRef<gsap.core.Timeline | null>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
restart: () => tlRef.current?.play(),
|
||||||
|
reverse: () => tlRef.current?.reverse(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const pixelEls =
|
||||||
|
iconRef.current?.querySelectorAll<SVGGraphicsElement>(".icon-pixel-path");
|
||||||
|
if (!pixelEls?.length) return;
|
||||||
|
|
||||||
|
// Sort pixels by visual position: bottom-left → top-right diagonal
|
||||||
|
const pixels = Array.from(pixelEls).sort((a, b) => {
|
||||||
|
const aBox = a.getBBox();
|
||||||
|
const bBox = b.getBBox();
|
||||||
|
return (
|
||||||
|
aBox.x +
|
||||||
|
aBox.width / 2 -
|
||||||
|
(aBox.y + aBox.height / 2) -
|
||||||
|
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
gsap.set(pixels, {
|
||||||
|
opacity: 0.15,
|
||||||
|
scale: 0.8,
|
||||||
|
transformOrigin: "left bottom",
|
||||||
|
fill: "currentColor",
|
||||||
|
});
|
||||||
|
|
||||||
|
tlRef.current = gsap.timeline({ paused: true });
|
||||||
|
|
||||||
|
tlRef.current
|
||||||
|
.to(pixels, {
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1.15,
|
||||||
|
fill: "#FFFFFF",
|
||||||
|
duration: 0.2,
|
||||||
|
stagger: 0.01,
|
||||||
|
ease: "power2.out",
|
||||||
|
})
|
||||||
|
.to(pixels, {
|
||||||
|
scale: 1,
|
||||||
|
duration: 0.01,
|
||||||
|
ease: "back.out(3)",
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
tlRef.current?.kill();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="relative inline-flex items-center justify-center w-[14px] h-[14px]">
|
||||||
|
<Icon ref={iconRef} className="w-full h-full text-white" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
DashboardIconPixel.displayName = "DashboardIconPixel";
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import AnimatedFooterImage from "./animated-footer-image";
|
|
||||||
|
|
||||||
export default function ElasticRecoil({ children }) {
|
|
||||||
const liftAmount = useMotionValue(0);
|
|
||||||
|
|
||||||
const [showFooter, setShowFooter] = useState(false);
|
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const check = () => setIsMobile(window.innerWidth < 768);
|
|
||||||
check();
|
|
||||||
window.addEventListener("resize", check);
|
|
||||||
return () => window.removeEventListener("resize", check);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const desktopSpring = { stiffness: 200, damping: 15, mass: 0.5 };
|
|
||||||
const mobileSpring = { stiffness: 800, damping: 60, mass: 0.2 };
|
|
||||||
const animatedLift = useSpring(
|
|
||||||
liftAmount,
|
|
||||||
isMobile ? mobileSpring : desktopSpring,
|
|
||||||
);
|
|
||||||
const cappedMax = isMobile ? -420 : -580;
|
|
||||||
const y = useTransform(animatedLift, [0, 400], [0, cappedMax]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return animatedLift.on("change", (v) => {
|
|
||||||
setShowFooter(v > 1);
|
|
||||||
});
|
|
||||||
}, [animatedLift]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let timeout;
|
|
||||||
let recoilFired = false;
|
|
||||||
let isTouching = false; // Track if finger is on screen
|
|
||||||
|
|
||||||
const triggerRebound = () => {
|
|
||||||
liftAmount.set(0);
|
|
||||||
if (!recoilFired) {
|
|
||||||
recoilFired = true;
|
|
||||||
window.dispatchEvent(new CustomEvent("elastic-recoil"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWheel = (e) => {
|
|
||||||
const isAtBottom =
|
|
||||||
window.innerHeight + window.pageYOffset >=
|
|
||||||
document.documentElement.scrollHeight - 5;
|
|
||||||
|
|
||||||
if (isAtBottom && e.deltaY > 0) {
|
|
||||||
const normalizedDelta =
|
|
||||||
e.deltaMode === 1
|
|
||||||
? e.deltaY * 20
|
|
||||||
: e.deltaMode === 2
|
|
||||||
? e.deltaY * 300
|
|
||||||
: e.deltaY;
|
|
||||||
liftAmount.set(Math.min(liftAmount.get() + normalizedDelta * 0.5, 400));
|
|
||||||
recoilFired = false;
|
|
||||||
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
if (!isTouching) {
|
|
||||||
triggerRebound();
|
|
||||||
}
|
|
||||||
}, 1500);
|
|
||||||
} else if (e.deltaY < 0) {
|
|
||||||
if (!isTouching) {
|
|
||||||
liftAmount.set(0);
|
|
||||||
recoilFired = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let lastTouchY = 0;
|
|
||||||
let atBottomOnStart = false;
|
|
||||||
const handleTouchStart = (e) => {
|
|
||||||
isTouching = true;
|
|
||||||
lastTouchY = e.touches[0].clientY;
|
|
||||||
atBottomOnStart =
|
|
||||||
window.innerHeight + window.pageYOffset >=
|
|
||||||
document.documentElement.scrollHeight - 5;
|
|
||||||
recoilFired = false;
|
|
||||||
clearTimeout(timeout);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTouchMove = (e) => {
|
|
||||||
const currentY = e.touches[0].clientY;
|
|
||||||
const delta = lastTouchY - currentY;
|
|
||||||
lastTouchY = currentY;
|
|
||||||
|
|
||||||
if (!atBottomOnStart) {
|
|
||||||
atBottomOnStart =
|
|
||||||
window.innerHeight + window.pageYOffset >=
|
|
||||||
document.documentElement.scrollHeight - 5;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delta > 0) {
|
|
||||||
liftAmount.set(Math.min(liftAmount.get() + delta * 4, 400));
|
|
||||||
recoilFired = false;
|
|
||||||
} else if (liftAmount.get() > 0) {
|
|
||||||
liftAmount.set(Math.max(0, liftAmount.get() + delta * 4));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTouchEnd = () => {
|
|
||||||
isTouching = false;
|
|
||||||
atBottomOnStart = false;
|
|
||||||
if (liftAmount.get() > 0) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
triggerRebound();
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("wheel", handleWheel, { passive: true });
|
|
||||||
window.addEventListener("touchstart", handleTouchStart, { passive: true });
|
|
||||||
window.addEventListener("touchmove", handleTouchMove, { passive: true });
|
|
||||||
window.addEventListener("touchend", handleTouchEnd, { passive: true });
|
|
||||||
window.addEventListener("touchcancel", handleTouchEnd, { passive: true });
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
window.removeEventListener("wheel", handleWheel);
|
|
||||||
window.removeEventListener("touchstart", handleTouchStart);
|
|
||||||
window.removeEventListener("touchmove", handleTouchMove);
|
|
||||||
window.removeEventListener("touchend", handleTouchEnd);
|
|
||||||
window.removeEventListener("touchcancel", handleTouchEnd);
|
|
||||||
};
|
|
||||||
}, [liftAmount]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative w-full overflow-hidden">
|
|
||||||
{showFooter && <AnimatedFooterImage />}
|
|
||||||
<motion.div style={{ y }} className="relative z-10 bg-black">
|
|
||||||
{children}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
144
apps/website/components/elastic-footer.tsx
Normal file
144
apps/website/components/elastic-footer.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
"use client";
|
||||||
|
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import type { LayoutProps } from "@/lib/types";
|
||||||
|
import AnimatedFooterImage from "./animated-footer-image";
|
||||||
|
|
||||||
|
export default function ElasticRecoil({ children }: LayoutProps) {
|
||||||
|
const liftAmount = useMotionValue(0);
|
||||||
|
|
||||||
|
const [showFooter, setShowFooter] = useState(false);
|
||||||
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const check = () => setIsMobile(window.innerWidth < 768);
|
||||||
|
check();
|
||||||
|
window.addEventListener("resize", check);
|
||||||
|
return () => window.removeEventListener("resize", check);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const desktopSpring = { stiffness: 200, damping: 15, mass: 0.5 };
|
||||||
|
const mobileSpring = { stiffness: 800, damping: 60, mass: 0.2 };
|
||||||
|
const animatedLift = useSpring(
|
||||||
|
liftAmount,
|
||||||
|
isMobile ? mobileSpring : desktopSpring,
|
||||||
|
);
|
||||||
|
const cappedMax = isMobile ? -420 : -580;
|
||||||
|
const y = useTransform(animatedLift, [0, 400], [0, cappedMax]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return animatedLift.on("change", (v) => {
|
||||||
|
setShowFooter(v > 1);
|
||||||
|
});
|
||||||
|
}, [animatedLift]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let recoilFired = false;
|
||||||
|
let isTouching = false;
|
||||||
|
|
||||||
|
const triggerRebound = () => {
|
||||||
|
liftAmount.set(0);
|
||||||
|
if (!recoilFired) {
|
||||||
|
recoilFired = true;
|
||||||
|
window.dispatchEvent(new CustomEvent("elastic-recoil"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWheel = (e: WheelEvent) => {
|
||||||
|
const isAtBottom =
|
||||||
|
window.innerHeight + window.pageYOffset >=
|
||||||
|
document.documentElement.scrollHeight - 5;
|
||||||
|
|
||||||
|
if (isAtBottom && e.deltaY > 0) {
|
||||||
|
const normalizedDelta =
|
||||||
|
e.deltaMode === 1
|
||||||
|
? e.deltaY * 20
|
||||||
|
: e.deltaMode === 2
|
||||||
|
? e.deltaY * 300
|
||||||
|
: e.deltaY;
|
||||||
|
liftAmount.set(Math.min(liftAmount.get() + normalizedDelta * 0.5, 400));
|
||||||
|
recoilFired = false;
|
||||||
|
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
if (!isTouching) {
|
||||||
|
triggerRebound();
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
} else if (e.deltaY < 0) {
|
||||||
|
if (!isTouching) {
|
||||||
|
liftAmount.set(0);
|
||||||
|
recoilFired = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let lastTouchY = 0;
|
||||||
|
let atBottomOnStart = false;
|
||||||
|
const handleTouchStart = (e: TouchEvent) => {
|
||||||
|
isTouching = true;
|
||||||
|
lastTouchY = e.touches[0].clientY;
|
||||||
|
atBottomOnStart =
|
||||||
|
window.innerHeight + window.pageYOffset >=
|
||||||
|
document.documentElement.scrollHeight - 5;
|
||||||
|
recoilFired = false;
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchMove = (e: TouchEvent) => {
|
||||||
|
const currentY = e.touches[0].clientY;
|
||||||
|
const delta = lastTouchY - currentY;
|
||||||
|
lastTouchY = currentY;
|
||||||
|
|
||||||
|
if (!atBottomOnStart) {
|
||||||
|
atBottomOnStart =
|
||||||
|
window.innerHeight + window.pageYOffset >=
|
||||||
|
document.documentElement.scrollHeight - 5;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delta > 0) {
|
||||||
|
liftAmount.set(Math.min(liftAmount.get() + delta * 4, 400));
|
||||||
|
recoilFired = false;
|
||||||
|
} else if (liftAmount.get() > 0) {
|
||||||
|
liftAmount.set(Math.max(0, liftAmount.get() + delta * 4));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
isTouching = false;
|
||||||
|
atBottomOnStart = false;
|
||||||
|
if (liftAmount.get() > 0) {
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
triggerRebound();
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("wheel", handleWheel, { passive: true });
|
||||||
|
window.addEventListener("touchstart", handleTouchStart, { passive: true });
|
||||||
|
window.addEventListener("touchmove", handleTouchMove, { passive: true });
|
||||||
|
window.addEventListener("touchend", handleTouchEnd, { passive: true });
|
||||||
|
window.addEventListener("touchcancel", handleTouchEnd, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
|
window.removeEventListener("wheel", handleWheel);
|
||||||
|
window.removeEventListener("touchstart", handleTouchStart);
|
||||||
|
window.removeEventListener("touchmove", handleTouchMove);
|
||||||
|
window.removeEventListener("touchend", handleTouchEnd);
|
||||||
|
window.removeEventListener("touchcancel", handleTouchEnd);
|
||||||
|
};
|
||||||
|
}, [liftAmount]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full overflow-hidden">
|
||||||
|
{showFooter && <AnimatedFooterImage />}
|
||||||
|
<motion.div style={{ y }} className="relative z-10 bg-black">
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { faqData, AnimatedPlusMinus } from "@/app/constant";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
|
|
||||||
export default function FAQ() {
|
|
||||||
const [openId, setOpenId] = useState(3);
|
|
||||||
|
|
||||||
const springConfig = {
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 280,
|
|
||||||
damping: 32,
|
|
||||||
mass: 1,
|
|
||||||
restDelta: 0.01,
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleAccordion = (id) => {
|
|
||||||
setOpenId(openId === id ? null : id);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
className="bg-[#000000] text-white overflow-hidden relative border-b border-[#292929]"
|
|
||||||
style={{
|
|
||||||
width: "calc(100% + var(--page-pad) * 2)",
|
|
||||||
marginLeft: "calc(var(--page-pad) * -1)",
|
|
||||||
paddingLeft: "var(--page-pad)",
|
|
||||||
paddingRight: "var(--page-pad)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 w-full min-h-[500px]">
|
|
||||||
<div className="lg:border-r border-b border-[#292929] pl-4 md:pl-4 xl:pl-[90px] pr-6 lg:pr-12 py-12 md:py-[60px] flex flex-col justify-center">
|
|
||||||
<h2 className="text-[30px] md:text-[40px] font-normal tracking-[-2%] leading-[1.1]">
|
|
||||||
<span className="text-[#FFFFFF99] font-light">
|
|
||||||
Frequently Asked
|
|
||||||
</span>
|
|
||||||
<br />
|
|
||||||
<span className="text-white font-normal">Questions</span>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="hidden lg:block border-b border-[#292929] h-full w-full"></div>
|
|
||||||
|
|
||||||
<div className="hidden lg:block lg:border-r border-[#292929] h-full w-full relative z-0">
|
|
||||||
<div className="absolute bottom-0 w-full h-full pb-32"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col h-full w-full relative z-10 ">
|
|
||||||
{faqData.map((faq) => {
|
|
||||||
const isOpen = openId === faq.id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={faq.id}
|
|
||||||
onClick={() => toggleAccordion(faq.id)}
|
|
||||||
className={`group relative border-b last:border-b-0 border-[#292929] cursor-pointer w-full flex flex-col justify-center transition-colors duration-300 ${isOpen ? "" : "hover:bg-[#080808]"}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`absolute inset-0 z-0 overflow-hidden pointer-events-none transition-opacity duration-500 ${
|
|
||||||
isOpen ? "opacity-100" : "opacity-0"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 bg-white/[0.03] md:group-hover:block hidden" />
|
|
||||||
|
|
||||||
<img
|
|
||||||
src="/images/pricing/FAQ/faqbg.svg"
|
|
||||||
alt="faq background"
|
|
||||||
loading="lazy"
|
|
||||||
className="absolute right-0 top-0 w-full h-[400px] md:h-full object-contain object-top-right md:object-cover md:object-right border-none opacity-60"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-linear-to-r from-[#351B6D]/90 via-[#351B6D]/80 to-black/75 mix-blend-normal" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative z-10 px-4.5 md:px-4 lg:px-[20px] py-[30px]">
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
|
||||||
<h3
|
|
||||||
className={`text-base lg:text-[18px] tracking-[-2%] transition-all duration-400 ${
|
|
||||||
isOpen
|
|
||||||
? "text-white font-normal"
|
|
||||||
: "text-[#FFFFFF66] font-light md:group-hover:text-white"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{faq.question}
|
|
||||||
</h3>
|
|
||||||
<div className="shrink-0 overflow-hidden">
|
|
||||||
<AnimatedPlusMinus
|
|
||||||
isOpen={isOpen}
|
|
||||||
className={`w-5 h-5 transition-colors duration-400 ${
|
|
||||||
isOpen
|
|
||||||
? "text-white"
|
|
||||||
: "text-[#FFFFFF66] md:group-hover:text-white"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`grid transition-all duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] ${
|
|
||||||
isOpen
|
|
||||||
? "grid-rows-[1fr] opacity-100"
|
|
||||||
: "grid-rows-[0fr] opacity-0"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="overflow-hidden">
|
|
||||||
<AnimatePresence initial={false}>
|
|
||||||
{isOpen && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ y: -15, opacity: 0 }}
|
|
||||||
animate={{ y: 0, opacity: 1 }}
|
|
||||||
exit={{ y: -10, opacity: 0 }}
|
|
||||||
transition={{
|
|
||||||
height: springConfig,
|
|
||||||
opacity: { duration: 0.25 },
|
|
||||||
y: springConfig,
|
|
||||||
}}
|
|
||||||
className="pt-5 text-[#ffffff] leading-[16px] md:leading-[20px] font-light text-[12px] md:text-[14px] max-w-[85%] flex flex-col gap-3.5 tracking-[-0.5%]"
|
|
||||||
>
|
|
||||||
{faq.answer.split("\n\n").map((paragraph, idx) => (
|
|
||||||
<p key={idx}>{paragraph}</p>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="relative z-0 px-8 lg:px-[20px] py-[30px] pointer-events-none opacity-0 select-none"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
|
||||||
<h3 className="text-base lg:text-[18px] tracking-[-2%] text-transparent select-none">
|
|
||||||
|
|
||||||
</h3>
|
|
||||||
<div className="shrink-0 w-5 h-5"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
154
apps/website/components/faq.tsx
Normal file
154
apps/website/components/faq.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
"use client";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { AnimatedPlusMinus, faqData } from "@/app/constant";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export default function FAQ() {
|
||||||
|
const [openId, setOpenId] = useState<number | null>(3);
|
||||||
|
|
||||||
|
const springConfig = {
|
||||||
|
type: "spring" as const,
|
||||||
|
stiffness: 280,
|
||||||
|
damping: 32,
|
||||||
|
mass: 1,
|
||||||
|
restDelta: 0.01,
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAccordion = (id: number) => {
|
||||||
|
setOpenId(openId === id ? null : id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="bg-[#000000] text-white overflow-hidden relative border-b border-[#292929]"
|
||||||
|
style={{
|
||||||
|
width: "calc(100% + var(--page-pad) * 2)",
|
||||||
|
marginLeft: "calc(var(--page-pad) * -1)",
|
||||||
|
paddingLeft: "var(--page-pad)",
|
||||||
|
paddingRight: "var(--page-pad)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 w-full min-h-[500px]">
|
||||||
|
<div className="lg:border-r border-b border-[#292929] pl-4 md:pl-4 xl:pl-[90px] pr-6 lg:pr-12 py-12 md:py-[60px] flex flex-col justify-center">
|
||||||
|
<h2 className="text-[30px] md:text-[40px] font-normal tracking-[-2%] leading-[1.1]">
|
||||||
|
<span className="text-[#FFFFFF99] font-light">
|
||||||
|
Frequently Asked
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<span className="text-white font-normal">Questions</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden lg:block border-b border-[#292929] h-full w-full"></div>
|
||||||
|
|
||||||
|
<div className="hidden lg:block lg:border-r border-[#292929] h-full w-full relative z-0">
|
||||||
|
<div className="absolute bottom-0 w-full h-full pb-32"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col h-full w-full relative z-10 ">
|
||||||
|
{faqData.map((faq) => {
|
||||||
|
const isOpen = openId === faq.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={faq.id}
|
||||||
|
onClick={() => toggleAccordion(faq.id)}
|
||||||
|
className={cn(
|
||||||
|
"group relative flex w-full cursor-pointer flex-col justify-center border-b border-[#292929] transition-colors duration-300 last:border-b-0",
|
||||||
|
!isOpen && "hover:bg-[#080808]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 z-0 overflow-hidden pointer-events-none transition-opacity duration-500",
|
||||||
|
isOpen ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-white/[0.03] md:group-hover:block hidden" />
|
||||||
|
|
||||||
|
<img
|
||||||
|
src="/images/pricing/FAQ/faqbg.svg"
|
||||||
|
alt="faq background"
|
||||||
|
loading="lazy"
|
||||||
|
className="absolute right-0 top-0 w-full h-[400px] md:h-full object-contain object-top-right md:object-cover md:object-right border-none opacity-60"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-linear-to-r from-[#351B6D]/90 via-[#351B6D]/80 to-black/75 mix-blend-normal" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10 px-4.5 md:px-4 lg:px-[20px] py-[30px]">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<h3
|
||||||
|
className={cn(
|
||||||
|
"text-base tracking-[-2%] transition-all duration-400 lg:text-[18px]",
|
||||||
|
isOpen
|
||||||
|
? "font-normal text-white"
|
||||||
|
: "font-light text-[#FFFFFF66] md:group-hover:text-white",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{faq.question}
|
||||||
|
</h3>
|
||||||
|
<div className="shrink-0 overflow-hidden">
|
||||||
|
<AnimatedPlusMinus
|
||||||
|
isOpen={isOpen}
|
||||||
|
className={cn(
|
||||||
|
"h-5 w-5 transition-colors duration-400",
|
||||||
|
isOpen
|
||||||
|
? "text-white"
|
||||||
|
: "text-[#FFFFFF66] md:group-hover:text-white",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"grid transition-all duration-500 ease-[cubic-bezier(0.16,1,0.3,1)]",
|
||||||
|
isOpen
|
||||||
|
? "grid-rows-[1fr] opacity-100"
|
||||||
|
: "grid-rows-[0fr] opacity-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: -15, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
exit={{ y: -10, opacity: 0 }}
|
||||||
|
transition={{
|
||||||
|
height: springConfig,
|
||||||
|
opacity: { duration: 0.25 },
|
||||||
|
y: springConfig,
|
||||||
|
}}
|
||||||
|
className="pt-5 text-[#ffffff] leading-[16px] md:leading-[20px] font-light text-[12px] md:text-[14px] max-w-[85%] flex flex-col gap-3.5 tracking-[-0.5%]"
|
||||||
|
>
|
||||||
|
{faq.answer.split("\n\n").map((paragraph, idx) => (
|
||||||
|
<p key={idx}>{paragraph}</p>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="relative z-0 px-8 lg:px-[20px] py-[30px] pointer-events-none opacity-0 select-none"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<h3 className="text-base lg:text-[18px] tracking-[-2%] text-transparent select-none">
|
||||||
|
|
||||||
|
</h3>
|
||||||
|
<div className="shrink-0 w-5 h-5"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { forwardRef, useImperativeHandle, useRef, useEffect } from "react";
|
|
||||||
import gsap from "gsap";
|
|
||||||
|
|
||||||
export const FeatureIconAnimation = forwardRef(({ Icon }, ref) => {
|
|
||||||
const iconRef = useRef(null);
|
|
||||||
const tlRef = useRef(null);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
play: () => tlRef.current?.play(),
|
|
||||||
reverse: () => tlRef.current?.reverse(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const pixelEls = iconRef.current?.querySelectorAll("path");
|
|
||||||
if (!pixelEls?.length) return;
|
|
||||||
|
|
||||||
// Sort pixels by distance from center — center pixels animate first, outer ones last
|
|
||||||
const centers = Array.from(pixelEls).map((el) => {
|
|
||||||
const b = el.getBBox();
|
|
||||||
return { el, cx: b.x + b.width / 2, cy: b.y + b.height / 2 };
|
|
||||||
});
|
|
||||||
const midX = centers.reduce((s, c) => s + c.cx, 0) / centers.length;
|
|
||||||
const midY = centers.reduce((s, c) => s + c.cy, 0) / centers.length;
|
|
||||||
const pixels = centers
|
|
||||||
.sort((a, b) => {
|
|
||||||
const da = (a.cx - midX) ** 2 + (a.cy - midY) ** 2;
|
|
||||||
const db = (b.cx - midX) ** 2 + (b.cy - midY) ** 2;
|
|
||||||
return da - db;
|
|
||||||
})
|
|
||||||
.map((c) => c.el);
|
|
||||||
|
|
||||||
gsap.set(pixels, {
|
|
||||||
opacity: 0.15,
|
|
||||||
scale: 0.8,
|
|
||||||
transformOrigin: "center center",
|
|
||||||
fill: "currentColor",
|
|
||||||
});
|
|
||||||
|
|
||||||
tlRef.current = gsap.timeline({ paused: true });
|
|
||||||
|
|
||||||
tlRef.current
|
|
||||||
.to(pixels, {
|
|
||||||
opacity: 1,
|
|
||||||
scale: 1.15,
|
|
||||||
fill: "#9564ff",
|
|
||||||
duration: 0.2,
|
|
||||||
stagger: 0.04,
|
|
||||||
ease: "power2.out",
|
|
||||||
})
|
|
||||||
.to(pixels, {
|
|
||||||
scale: 1,
|
|
||||||
duration: 0.15,
|
|
||||||
ease: "back.out(3)",
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => tlRef.current?.kill();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex items-center justify-start w-12 h-12 group/icon">
|
|
||||||
<div className="relative w-[24px] h-[24px] flex items-center justify-center">
|
|
||||||
<Icon ref={iconRef} className="w-full h-full text-white" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
FeatureIconAnimation.displayName = "FeatureIconAnimation";
|
|
||||||
75
apps/website/components/feature-icon-animation.tsx
Normal file
75
apps/website/components/feature-icon-animation.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"use client";
|
||||||
|
import gsap from "gsap";
|
||||||
|
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
|
||||||
|
import type { PixelAnimationHandle, PixelIconComponent } from "@/lib/types";
|
||||||
|
|
||||||
|
export const FeatureIconAnimation = forwardRef<
|
||||||
|
PixelAnimationHandle,
|
||||||
|
{ Icon: PixelIconComponent }
|
||||||
|
>(({ Icon }, ref) => {
|
||||||
|
const iconRef = useRef<SVGSVGElement | null>(null);
|
||||||
|
const tlRef = useRef<gsap.core.Timeline | null>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
play: () => tlRef.current?.play(),
|
||||||
|
reverse: () => tlRef.current?.reverse(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const pixelEls =
|
||||||
|
iconRef.current?.querySelectorAll<SVGGraphicsElement>("path");
|
||||||
|
if (!pixelEls?.length) return;
|
||||||
|
|
||||||
|
// Sort pixels by distance from center — center pixels animate first, outer ones last
|
||||||
|
const centers = Array.from(pixelEls).map((el) => {
|
||||||
|
const b = el.getBBox();
|
||||||
|
return { el, cx: b.x + b.width / 2, cy: b.y + b.height / 2 };
|
||||||
|
});
|
||||||
|
const midX = centers.reduce((s, c) => s + c.cx, 0) / centers.length;
|
||||||
|
const midY = centers.reduce((s, c) => s + c.cy, 0) / centers.length;
|
||||||
|
const pixels = centers
|
||||||
|
.sort((a, b) => {
|
||||||
|
const da = (a.cx - midX) ** 2 + (a.cy - midY) ** 2;
|
||||||
|
const db = (b.cx - midX) ** 2 + (b.cy - midY) ** 2;
|
||||||
|
return da - db;
|
||||||
|
})
|
||||||
|
.map((c) => c.el);
|
||||||
|
|
||||||
|
gsap.set(pixels, {
|
||||||
|
opacity: 0.15,
|
||||||
|
scale: 0.8,
|
||||||
|
transformOrigin: "center center",
|
||||||
|
fill: "currentColor",
|
||||||
|
});
|
||||||
|
|
||||||
|
tlRef.current = gsap.timeline({ paused: true });
|
||||||
|
|
||||||
|
tlRef.current
|
||||||
|
.to(pixels, {
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1.15,
|
||||||
|
fill: "#9564ff",
|
||||||
|
duration: 0.2,
|
||||||
|
stagger: 0.04,
|
||||||
|
ease: "power2.out",
|
||||||
|
})
|
||||||
|
.to(pixels, {
|
||||||
|
scale: 1,
|
||||||
|
duration: 0.15,
|
||||||
|
ease: "back.out(3)",
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
tlRef.current?.kill();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex items-center justify-start w-12 h-12 group/icon">
|
||||||
|
<div className="relative w-[24px] h-[24px] flex items-center justify-center">
|
||||||
|
<Icon ref={iconRef} className="w-full h-full text-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
FeatureIconAnimation.displayName = "FeatureIconAnimation";
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { featuresData } from "@/app/constant";
|
|
||||||
import { useRef } from "react";
|
|
||||||
import { FeatureIconAnimation } from "./feature-icon-animation";
|
|
||||||
|
|
||||||
function FeatureCard({ feature }) {
|
|
||||||
const isDesktop =
|
|
||||||
typeof window !== "undefined" &&
|
|
||||||
window.matchMedia("(hover: hover)").matches;
|
|
||||||
const iconRef = useRef(null);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onMouseEnter={() => isDesktop && iconRef.current?.play()}
|
|
||||||
onMouseLeave={() => isDesktop && iconRef.current?.reverse()}
|
|
||||||
className="group relative flex px-4 md:px-4 flex-col justify-between p-6 bg-[#0F0F0F] min-h-[200px] md:min-h-[280px] border-r border-b border-[#292929] overflow-hidden cursor-pointer"
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 opacity-0 translate-y-6 md:group-hover:opacity-100 md:group-hover:translate-y-0 pointer-events-none z-0 hidden md:block">
|
|
||||||
<video
|
|
||||||
src="/images/features/pixel effect.webm"
|
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
muted
|
|
||||||
playsInline
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* Hover Gradient Overlay */}
|
|
||||||
<div className="absolute inset-x-0 bottom-0 h-[70%] bg-[linear-gradient(to_bottom,rgba(10,10,10,0)_0%,rgba(135,82,250,0.15)_40%,rgba(135,82,250,0.45)_70%,rgba(135,82,250,0.85)_90%)] opacity-0 md:group-hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
|
|
||||||
|
|
||||||
<div className="relative z-10 flex flex-col h-full gap-[42px] md:gap-24.5">
|
|
||||||
<feature.Icon className="w-6 h-6 text-white md:hidden" />
|
|
||||||
<div className="hidden md:block">
|
|
||||||
<FeatureIconAnimation Icon={feature.Icon} ref={iconRef} />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2 md:gap-4.5">
|
|
||||||
<h3 className="text-white font-normal tracking-[-5%] leading-6 text-[20px] md:text-[24px] font-sans">
|
|
||||||
{feature.title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-[#FFFFFF99] w-full font-light tracking-[-2%] text-[14px] md:text-[16px] font-sans leading-[18px] md:leading-[20px] pr-2 md:pr-4 text-pretty">
|
|
||||||
{feature.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Features() {
|
|
||||||
return (
|
|
||||||
<section className="bg-[#000000] w-full">
|
|
||||||
<div className="flex flex-col px-4 md:px-4 sm:px-8 py-12 md:py-16 xl:px-22.75 items-start">
|
|
||||||
<h2 className="text-[30px] md:text-[40px] leading-[32px] md:leading-[44px] font-sans tracking-[-4%]">
|
|
||||||
<div className="text-[#FFFFFF99]">Everything you need</div>
|
|
||||||
<div className="text-white">for AI and usage billing.</div>
|
|
||||||
</h2>
|
|
||||||
<div className="mt-4 text-[16px] tracking-[-2%] font-sans font-light text-[#FFFFFF99] leading-[20px] max-w-[420px]">
|
|
||||||
Your entire billing infrastructure, {" "}
|
|
||||||
<span className="text-white">
|
|
||||||
fully managed.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t border-[#292929] w-full" />
|
|
||||||
<div className="grid grid-cols-1 gap-[-3px] md:grid-cols-2 lg:grid-cols-3 xl:px-22.75 border-[#292929] [&>*:last-child]:border-b-0 [&>*:nth-last-child(2)]:border-b-0 md:[&>*:nth-last-child(-n+2)]:border-b-0 lg:[&>*:nth-last-child(-n+3)]:border-b-0 *:border-l md:[&>*:nth-child(2n)]:border-l-0 lg:[&>*:nth-child(3n+1)]:border-l lg:[&>*:nth-child(3n+2)]:border-l-0 lg:[&>*:nth-child(3n)]:border-l-0">
|
|
||||||
{featuresData.map((feature, i) => (
|
|
||||||
<FeatureCard key={i} feature={feature} />
|
|
||||||
))}
|
|
||||||
{/* <div className="bg-[#0f0f0f] w-full h-full min-h-[280px] hidden lg:block border-r border-b border-[#292929]" /> */}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
72
apps/website/components/features.tsx
Executable file
72
apps/website/components/features.tsx
Executable file
@@ -0,0 +1,72 @@
|
|||||||
|
"use client";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { featuresData } from "@/app/constant";
|
||||||
|
import type { PixelAnimationHandle } from "@/lib/types";
|
||||||
|
import { FeatureIconAnimation } from "./feature-icon-animation";
|
||||||
|
|
||||||
|
function FeatureCard({ feature }: { feature: (typeof featuresData)[number] }) {
|
||||||
|
const isDesktop =
|
||||||
|
typeof window !== "undefined" &&
|
||||||
|
window.matchMedia("(hover: hover)").matches;
|
||||||
|
const iconRef = useRef<PixelAnimationHandle | null>(null);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onMouseEnter={() => isDesktop && iconRef.current?.play()}
|
||||||
|
onMouseLeave={() => isDesktop && iconRef.current?.reverse()}
|
||||||
|
className="group relative flex px-4 md:px-4 flex-col justify-between p-6 bg-[#0F0F0F] min-h-[200px] md:min-h-[280px] border-r border-b border-[#292929] overflow-hidden cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 opacity-0 translate-y-6 md:group-hover:opacity-100 md:group-hover:translate-y-0 pointer-events-none z-0 hidden md:block">
|
||||||
|
<video
|
||||||
|
src="/images/features/pixel effect.webm"
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Hover Gradient Overlay */}
|
||||||
|
<div className="absolute inset-x-0 bottom-0 h-[70%] bg-[linear-gradient(to_bottom,rgba(10,10,10,0)_0%,rgba(135,82,250,0.15)_40%,rgba(135,82,250,0.45)_70%,rgba(135,82,250,0.85)_90%)] opacity-0 md:group-hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="relative z-10 flex flex-col h-full gap-[42px] md:gap-24.5">
|
||||||
|
<feature.Icon className="w-6 h-6 text-white md:hidden" />
|
||||||
|
<div className="hidden md:block">
|
||||||
|
<FeatureIconAnimation Icon={feature.Icon} ref={iconRef} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2 md:gap-4.5">
|
||||||
|
<h3 className="text-white font-normal tracking-[-5%] leading-6 text-[20px] md:text-[24px] font-sans">
|
||||||
|
{feature.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-[#FFFFFF99] w-full font-light tracking-[-2%] text-[14px] md:text-[16px] font-sans leading-[18px] md:leading-[20px] pr-2 md:pr-4 text-pretty">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Features() {
|
||||||
|
return (
|
||||||
|
<section className="bg-[#000000] w-full">
|
||||||
|
<div className="flex flex-col px-4 md:px-4 sm:px-8 py-12 md:py-16 xl:px-22.75 items-start">
|
||||||
|
<h2 className="text-[30px] md:text-[40px] leading-[32px] md:leading-[44px] font-sans tracking-[-4%]">
|
||||||
|
<div className="text-[#FFFFFF99]">Everything you need</div>
|
||||||
|
<div className="text-white">for AI and usage billing.</div>
|
||||||
|
</h2>
|
||||||
|
<div className="mt-4 text-[16px] tracking-[-2%] font-sans font-light text-[#FFFFFF99] leading-[20px] max-w-[420px]">
|
||||||
|
Your entire billing infrastructure,{" "}
|
||||||
|
<span className="text-white">fully managed.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
<div className="grid grid-cols-1 gap-[-3px] md:grid-cols-2 lg:grid-cols-3 xl:px-22.75 border-[#292929] [&>*:last-child]:border-b-0 [&>*:nth-last-child(2)]:border-b-0 md:[&>*:nth-last-child(-n+2)]:border-b-0 lg:[&>*:nth-last-child(-n+3)]:border-b-0 *:border-l md:[&>*:nth-child(2n)]:border-l-0 lg:[&>*:nth-child(3n+1)]:border-l lg:[&>*:nth-child(3n+2)]:border-l-0 lg:[&>*:nth-child(3n)]:border-l-0">
|
||||||
|
{featuresData.map((feature, i) => (
|
||||||
|
<FeatureCard key={i} feature={feature} />
|
||||||
|
))}
|
||||||
|
{/* <div className="bg-[#0f0f0f] w-full h-full min-h-[280px] hidden lg:block border-r border-b border-[#292929]" /> */}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const footerColumns = [
|
|
||||||
{
|
|
||||||
title: "PRODUCT",
|
|
||||||
links: [
|
|
||||||
{ label: "FEATURES", href: "#" },
|
|
||||||
{ label: "INTEGRATIONS", href: "#" },
|
|
||||||
{ label: "PRICING", href: "#" },
|
|
||||||
{ label: "CHANGELOG", href: "#" },
|
|
||||||
{ label: "ROADMAP", href: "#" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "COMPANY",
|
|
||||||
links: [
|
|
||||||
{ label: "OUR TEAM", href: "#" },
|
|
||||||
{ label: "OUR VALUES", href: "/privacy" },
|
|
||||||
{ label: "BLOG", href: "/blog" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "RESOURCES",
|
|
||||||
links: [
|
|
||||||
{ label: "DOWNLOADS", href: "https://useautumn.com/" },
|
|
||||||
{ label: "DOCUMENTATION", href: "https://docs.useautumn.com/welcome" },
|
|
||||||
{ label: "CONTACT", href: "https://cal.com/ayrod/a?user=ayrod" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Footer() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<footer
|
|
||||||
style={{
|
|
||||||
width: "calc(100% + var(--page-pad) * 2)",
|
|
||||||
marginLeft: "calc(var(--page-pad) * -1)",
|
|
||||||
paddingLeft: "var(--page-pad)",
|
|
||||||
paddingRight: "var(--page-pad)",
|
|
||||||
}}
|
|
||||||
className="mt-[50px] relative bg-[#000000] border-t border-[#292929] overflow-hidden text-[#FFFFFF99] grid"
|
|
||||||
>
|
|
||||||
<div className="col-start-1 row-start-1 flex flex-col z-10 w-full relative">
|
|
||||||
<div className="relative w-full flex flex-col justify-between min-h-[400px]">
|
|
||||||
<div className="absolute bottom-0 left-0 right-0 h-[300px] sm:h-[550px] lg:h-[400px] pointer-events-none opacity-70 bg-[url('/images/footer/footerbg.svg')] bg-cover sm:bg-contain bg-bottom bg-no-repeat z-0" />
|
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row justify-between pt-[32px] md:pt-24 pb-32 md:pb-16 pl-4.5 xl:pl-[90px] pr-4 sm:pr-8 lg:pr-12 gap-10 md:gap-16 lg:gap-8 relative z-10">
|
|
||||||
{/* Left Side: Logo and Description */}
|
|
||||||
<div className="flex flex-col max-w-sm">
|
|
||||||
<Link href="/" className="mb-3 md:mb-6 inline-block">
|
|
||||||
<Image
|
|
||||||
src="/images/navbar/autumnlogo.svg"
|
|
||||||
width={195}
|
|
||||||
height={48}
|
|
||||||
alt="Autumn"
|
|
||||||
className="brightness-0 invert w-[130px] h-[32px] md:w-[195px] md:h-[48px]"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
<p className="text-[14px] md:text-[16px] leading-[18px] md:leading-[20px] tracking-[-2%] text-[#FFFFFF99] font-light">
|
|
||||||
Autumn is built on top of Stripe Billing (for now), so their
|
|
||||||
fees (0.7%, and 2.9% + 30c) still apply.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Side: Columns */}
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-12 sm:gap-16 lg:gap-24">
|
|
||||||
{footerColumns.map((col, index) => (
|
|
||||||
<div key={index} className="flex flex-col">
|
|
||||||
<h3 className="flex items-center gap-2 text-white font-mono text-[14px] uppercase tracking-[-1%] mb-6">
|
|
||||||
<div className="w-[8px] h-[8px] bg-[#FFFFFF99]"></div>
|
|
||||||
{col.title}
|
|
||||||
</h3>
|
|
||||||
<ul className="flex flex-col gap-1">
|
|
||||||
{col.links.map((link, i) => (
|
|
||||||
<li key={i} className="group/strip">
|
|
||||||
<Link
|
|
||||||
href={link.href}
|
|
||||||
target="_blank"
|
|
||||||
className="group/strip flex items-center gap-3 text-[14px] font-mono uppercase tracking-[-1%] transition-colors duration-300"
|
|
||||||
>
|
|
||||||
<span className="w-1 h-1 bg-[#FFFFFF99] group-hover/strip:bg-white group-active/strip:bg-white transition-colors duration-300 shrink-0"></span>
|
|
||||||
|
|
||||||
<span className="relative">
|
|
||||||
<span className="text-[#FFFFFF99]">
|
|
||||||
{link.label}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="absolute inset-y-0 left-0 w-0 group-hover/strip:w-full group-active/strip:w-full overflow-hidden transition-all duration-300 ease-in-out bg-white text-black font-normal pointer-events-none flex items-center"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<span className="whitespace-nowrap">
|
|
||||||
{link.label}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="relative block z-10 bg-black/40 backdrop-blur-md border-y border-[#292929]"
|
|
||||||
style={{
|
|
||||||
width: "calc(100% + var(--page-pad) * 2)",
|
|
||||||
marginLeft: "calc(var(--page-pad) * -1)",
|
|
||||||
paddingLeft: "var(--page-pad)",
|
|
||||||
paddingRight: "var(--page-pad)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="max-w-[1400px] mx-auto flex flex-col md:flex-row justify-between items-stretch h-auto md:h-14 font-mono text-[10px] sm:text-[11px] tracking-[0.2em]">
|
|
||||||
{/* Social Left Box */}
|
|
||||||
<div className="flex flex-row md:flex-row border-b md:border-b-0 border-[#292929] w-full md:w-auto">
|
|
||||||
<div className="px-4 sm:px-8 py-4.5 md:py-0 border-r border-[#292929] tracking-[-2%] flex items-center justify-center md:justify-start text-[12px] md:text-[14px]">
|
|
||||||
SOCIAL
|
|
||||||
</div>
|
|
||||||
<div className="px-6 sm:px-8 py-4 md:py-0 flex items-center justify-center md:justify-start gap-3 sm:gap-4 md:border-r border-[#292929]">
|
|
||||||
<Link
|
|
||||||
href="https://www.linkedin.com/company/useautumn"
|
|
||||||
className="group/strip flex items-center text-[14px] tracking-[-2%] transition-colors duration-300"
|
|
||||||
>
|
|
||||||
<span className="relative">
|
|
||||||
<span className="text-[#FFFFFF99] group-hover/strip:text-[#ffffff] text-[12px] md:text-[14px] transition-colors duration-300">
|
|
||||||
LINKEDIN
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="absolute inset-y-0 left-0 w-0 group-hover/strip:w-full group-active/strip:w-full overflow-hidden transition-all duration-300 ease-in-out bg-white text-black font-normal pointer-events-none flex items-center"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<span className="whitespace-nowrap">LINKEDIN</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
<div className="w-[5px] h-[5px] bg-[#FFFFFF]"></div>
|
|
||||||
<Link
|
|
||||||
href="https://x.com/autumnpricing"
|
|
||||||
className="group/strip flex items-center text-[14px] tracking-[-2%] transition-colors duration-300"
|
|
||||||
>
|
|
||||||
<span className="relative">
|
|
||||||
<span className="text-[#FFFFFF99] group-hover/strip:text-[#ffffff] transition-colors duration-300 text-[12px] md:text-[14px]">
|
|
||||||
TWITTER
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="absolute inset-y-0 left-0 w-0 group-hover/strip:w-full group-active/strip:w-full overflow-hidden transition-all duration-300 ease-in-out bg-white text-black font-normal pointer-events-none flex items-center"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<span className="whitespace-nowrap">TWITTER</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="px-4.5 py-4 md:py-0 flex items-center justify-start md:justify-end text-center md:text-right text-[#FFFFFF99] text-[12px] md:text-[14px] tracking-[-2%] border-b md:border-b-0 border-[#292929] w-full md:w-auto">
|
|
||||||
Copyright © 2026 Autumn All rights reserved
|
|
||||||
<div className="hidden lg:flex gap-3 ml-[15px] h-[54px]">
|
|
||||||
<div className="border-r border-[#292929]" />
|
|
||||||
<div className="border-r border-[#292929]" />
|
|
||||||
<div className="border-r border-[#292929]" />
|
|
||||||
<div className="border-r border-[#292929]" />
|
|
||||||
<div className="border-r border-[#292929]" />
|
|
||||||
<div className="border-r border-[#292929]" />
|
|
||||||
<div className="border-r border-[#292929]" />
|
|
||||||
<div className="border-r border-[#292929]" />
|
|
||||||
<div className="border-r border-[#292929]" />
|
|
||||||
<div className="border-r border-[#292929]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
180
apps/website/components/footer.tsx
Normal file
180
apps/website/components/footer.tsx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
const footerColumns: any[] = [
|
||||||
|
// {
|
||||||
|
// title: "PRODUCT",
|
||||||
|
// links: [
|
||||||
|
// { label: "FEATURES", href: "#" },
|
||||||
|
// { label: "INTEGRATIONS", href: "#" },
|
||||||
|
// { label: "PRICING", href: "#" },
|
||||||
|
// { label: "CHANGELOG", href: "#" },
|
||||||
|
// { label: "ROADMAP", href: "#" },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "COMPANY",
|
||||||
|
// links: [
|
||||||
|
// { label: "OUR TEAM", href: "#" },
|
||||||
|
// { label: "OUR VALUES", href: "/privacy" },
|
||||||
|
// { label: "BLOG", href: "/blog" },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "RESOURCES",
|
||||||
|
// links: [
|
||||||
|
// { label: "DOWNLOADS", href: "https://useautumn.com/" },
|
||||||
|
// { label: "DOCUMENTATION", href: "https://docs.useautumn.com/welcome" },
|
||||||
|
// { label: "CONTACT", href: "https://cal.com/ayrod/a?user=ayrod" },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<footer
|
||||||
|
style={{
|
||||||
|
width: "calc(100% + var(--page-pad) * 2)",
|
||||||
|
marginLeft: "calc(var(--page-pad) * -1)",
|
||||||
|
paddingLeft: "var(--page-pad)",
|
||||||
|
paddingRight: "var(--page-pad)",
|
||||||
|
}}
|
||||||
|
className="mt-[50px] relative bg-[#000000] border-t border-[#292929] overflow-hidden text-[#FFFFFF99] grid"
|
||||||
|
>
|
||||||
|
<div className="col-start-1 row-start-1 flex flex-col z-10 w-full relative">
|
||||||
|
<div className="relative w-full flex flex-col justify-between min-h-[400px]">
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-[300px] sm:h-[550px] lg:h-[400px] pointer-events-none opacity-70 bg-[url('/images/footer/footerbg.svg')] bg-cover sm:bg-contain bg-bottom bg-no-repeat z-0" />
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-row justify-between pt-[32px] md:pt-24 pb-32 md:pb-16 pl-4.5 xl:pl-[90px] pr-4 sm:pr-8 lg:pr-12 gap-10 md:gap-16 lg:gap-8 relative z-10">
|
||||||
|
{/* Left Side: Logo and Description */}
|
||||||
|
<div className="flex flex-col max-w-sm">
|
||||||
|
<Link href="/" className="mb-3 md:mb-6 inline-block">
|
||||||
|
<Image
|
||||||
|
src="/images/navbar/autumnlogo.svg"
|
||||||
|
width={195}
|
||||||
|
height={48}
|
||||||
|
alt="Autumn"
|
||||||
|
className="brightness-0 invert w-[130px] h-[32px] md:w-[195px] md:h-[48px]"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
<p className="text-[14px] md:text-[16px] leading-[18px] md:leading-[20px] tracking-[-2%] text-[#FFFFFF99] font-light">
|
||||||
|
Autumn is drop-in billing for AI companies.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Side: Columns */}
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-12 sm:gap-16 lg:gap-24">
|
||||||
|
{footerColumns.map((col, index) => (
|
||||||
|
<div key={index} className="flex flex-col">
|
||||||
|
<h3 className="flex items-center gap-2 text-white font-mono text-[14px] uppercase tracking-[-1%] mb-6">
|
||||||
|
<div className="w-[8px] h-[8px] bg-[#FFFFFF99]"></div>
|
||||||
|
{col.title}
|
||||||
|
</h3>
|
||||||
|
<ul className="flex flex-col gap-1">
|
||||||
|
{col.links.map((link, i) => (
|
||||||
|
<li key={i} className="group/strip">
|
||||||
|
<Link
|
||||||
|
href={link.href}
|
||||||
|
target="_blank"
|
||||||
|
className="group/strip flex items-center gap-3 text-[14px] font-mono uppercase tracking-[-1%] transition-colors duration-300"
|
||||||
|
>
|
||||||
|
<span className="w-1 h-1 bg-[#FFFFFF99] group-hover/strip:bg-white group-active/strip:bg-white transition-colors duration-300 shrink-0"></span>
|
||||||
|
|
||||||
|
<span className="relative">
|
||||||
|
<span className="text-[#FFFFFF99]">
|
||||||
|
{link.label}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="absolute inset-y-0 left-0 w-0 group-hover/strip:w-full group-active/strip:w-full overflow-hidden transition-all duration-300 ease-in-out bg-white text-black font-normal pointer-events-none flex items-center"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<span className="whitespace-nowrap">
|
||||||
|
{link.label}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="relative block z-10 bg-black/40 backdrop-blur-md border-y border-[#292929]"
|
||||||
|
style={{
|
||||||
|
width: "calc(100% + var(--page-pad) * 2)",
|
||||||
|
marginLeft: "calc(var(--page-pad) * -1)",
|
||||||
|
paddingLeft: "var(--page-pad)",
|
||||||
|
paddingRight: "var(--page-pad)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="max-w-[1400px] mx-auto flex flex-col md:flex-row justify-between items-stretch h-auto md:h-14 font-mono text-[10px] sm:text-[11px] tracking-[0.2em]">
|
||||||
|
{/* Social Left Box */}
|
||||||
|
<div className="flex flex-row md:flex-row border-b md:border-b-0 border-[#292929] w-full md:w-auto">
|
||||||
|
<div className="px-4 sm:px-8 py-4.5 md:py-0 border-r border-[#292929] tracking-[-2%] flex items-center justify-center md:justify-start text-[12px] md:text-[14px]">
|
||||||
|
SOCIAL
|
||||||
|
</div>
|
||||||
|
<div className="px-6 sm:px-8 py-4 md:py-0 flex items-center justify-center md:justify-start gap-3 sm:gap-4 md:border-r border-[#292929]">
|
||||||
|
<Link
|
||||||
|
href="https://www.linkedin.com/company/useautumn"
|
||||||
|
className="group/strip flex items-center text-[14px] tracking-[-2%] transition-colors duration-300"
|
||||||
|
>
|
||||||
|
<span className="relative">
|
||||||
|
<span className="text-[#FFFFFF99] group-hover/strip:text-[#ffffff] text-[12px] md:text-[14px] transition-colors duration-300">
|
||||||
|
LINKEDIN
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="absolute inset-y-0 left-0 w-0 group-hover/strip:w-full group-active/strip:w-full overflow-hidden transition-all duration-300 ease-in-out bg-white text-black font-normal pointer-events-none flex items-center"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<span className="whitespace-nowrap">LINKEDIN</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
<div className="w-[5px] h-[5px] bg-[#FFFFFF]"></div>
|
||||||
|
<Link
|
||||||
|
href="https://x.com/autumnpricing"
|
||||||
|
className="group/strip flex items-center text-[14px] tracking-[-2%] transition-colors duration-300"
|
||||||
|
>
|
||||||
|
<span className="relative">
|
||||||
|
<span className="text-[#FFFFFF99] group-hover/strip:text-[#ffffff] transition-colors duration-300 text-[12px] md:text-[14px]">
|
||||||
|
TWITTER
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="absolute inset-y-0 left-0 w-0 group-hover/strip:w-full group-active/strip:w-full overflow-hidden transition-all duration-300 ease-in-out bg-white text-black font-normal pointer-events-none flex items-center"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<span className="whitespace-nowrap">TWITTER</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4.5 py-4 md:py-0 flex items-center justify-start md:justify-end text-center md:text-right text-[#FFFFFF99] text-[12px] md:text-[14px] tracking-[-2%] border-b md:border-b-0 border-[#292929] w-full md:w-auto">
|
||||||
|
Copyright © 2026 Autumn All rights reserved
|
||||||
|
<div className="hidden lg:flex gap-3 ml-[15px] h-[54px]">
|
||||||
|
<div className="border-r border-[#292929]" />
|
||||||
|
<div className="border-r border-[#292929]" />
|
||||||
|
<div className="border-r border-[#292929]" />
|
||||||
|
<div className="border-r border-[#292929]" />
|
||||||
|
<div className="border-r border-[#292929]" />
|
||||||
|
<div className="border-r border-[#292929]" />
|
||||||
|
<div className="border-r border-[#292929]" />
|
||||||
|
<div className="border-r border-[#292929]" />
|
||||||
|
<div className="border-r border-[#292929]" />
|
||||||
|
<div className="border-r border-[#292929]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
307
apps/website/components/hero.tsx
Executable file
307
apps/website/components/hero.tsx
Executable file
@@ -0,0 +1,307 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useGSAP } from "@gsap/react";
|
||||||
|
import gsap from "gsap";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { CTALines, IconCTADocs, IconCTAStart } from "@/app/constant";
|
||||||
|
import AutumnConfig from "./autumn-config";
|
||||||
|
|
||||||
|
// import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
// const AutumnConfig = dynamic(() => import("./autumn-config"), { ssr: false });
|
||||||
|
|
||||||
|
const BADGE_TEXT = "// 100% open source";
|
||||||
|
|
||||||
|
const getLoggedInHintCookie = () => {
|
||||||
|
if (typeof window === "undefined") return null;
|
||||||
|
return (
|
||||||
|
document.cookie
|
||||||
|
.split("; ")
|
||||||
|
.find((row) => row.startsWith("logged_in_hint="))
|
||||||
|
?.split("=")[1] === "1"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Hero() {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const heroTlRef = useRef<gsap.core.Timeline | null>(null);
|
||||||
|
const [displayedText, setDisplayedText] = useState("");
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
|
||||||
|
// Read the hint cookie after mount to avoid SSR/CSR hydration mismatch.
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoggedIn(getLoggedInHintCookie() === true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Badge typewriter
|
||||||
|
useEffect(() => {
|
||||||
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*0123456789";
|
||||||
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
const startTypewriter = () => {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
let iteration = 0;
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
setDisplayedText(
|
||||||
|
BADGE_TEXT.split("")
|
||||||
|
.map((char, index) => {
|
||||||
|
if (char === " ") return " ";
|
||||||
|
if (index < Math.floor(iteration)) return char;
|
||||||
|
if (index === Math.floor(iteration))
|
||||||
|
return chars[Math.floor(Math.random() * chars.length)];
|
||||||
|
return "";
|
||||||
|
})
|
||||||
|
.join(""),
|
||||||
|
);
|
||||||
|
|
||||||
|
iteration += 0.4;
|
||||||
|
|
||||||
|
if (iteration >= BADGE_TEXT.length) {
|
||||||
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
setDisplayedText(BADGE_TEXT);
|
||||||
|
}
|
||||||
|
}, 30);
|
||||||
|
}, 150);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("preloader:complete", startTypewriter, {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("preloader:complete", startTypewriter);
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = () => heroTlRef.current?.play();
|
||||||
|
window.addEventListener("preloader:complete", handler, { once: true });
|
||||||
|
return () => window.removeEventListener("preloader:complete", handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useGSAP(
|
||||||
|
() => {
|
||||||
|
gsap.set(".hero-root", { opacity: 0 });
|
||||||
|
|
||||||
|
gsap.set(".hero-bg", {
|
||||||
|
opacity: 0,
|
||||||
|
filter: "blur(6px) brightness(1)",
|
||||||
|
scale: 0.97,
|
||||||
|
transformOrigin: "center top",
|
||||||
|
});
|
||||||
|
|
||||||
|
gsap.set(".hero-reveal", {
|
||||||
|
opacity: 0,
|
||||||
|
y: 25,
|
||||||
|
filter: "blur(12px)",
|
||||||
|
scale: 0.96,
|
||||||
|
transformOrigin: "center bottom",
|
||||||
|
});
|
||||||
|
|
||||||
|
gsap.set(".hero-cta", { opacity: 0, scale: 0.95 });
|
||||||
|
|
||||||
|
const tl = gsap.timeline({
|
||||||
|
paused: true,
|
||||||
|
defaults: { overwrite: "auto" },
|
||||||
|
});
|
||||||
|
heroTlRef.current = tl;
|
||||||
|
|
||||||
|
tl.to(".hero-root", { opacity: 1, duration: 0.3, ease: "none" })
|
||||||
|
|
||||||
|
.to(".hero-bg", {
|
||||||
|
opacity: 1,
|
||||||
|
filter: "blur(0px) brightness(1)",
|
||||||
|
scale: 1,
|
||||||
|
duration: 0.4,
|
||||||
|
ease: "power2.out",
|
||||||
|
})
|
||||||
|
|
||||||
|
.to(".hero-bg", {
|
||||||
|
filter: "blur(0px) brightness(1.6)",
|
||||||
|
duration: 0.125,
|
||||||
|
ease: "power2.in",
|
||||||
|
})
|
||||||
|
|
||||||
|
.to(".hero-bg", {
|
||||||
|
filter: "blur(0px) brightness(1)",
|
||||||
|
duration: 0.125,
|
||||||
|
ease: "power2.out",
|
||||||
|
})
|
||||||
|
|
||||||
|
.to(
|
||||||
|
".hero-reveal",
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
filter: "blur(0px)",
|
||||||
|
scale: 1,
|
||||||
|
duration: 1.1,
|
||||||
|
stagger: 0.1,
|
||||||
|
ease: "power3.out",
|
||||||
|
},
|
||||||
|
"-=0.2",
|
||||||
|
)
|
||||||
|
|
||||||
|
.to(
|
||||||
|
".hero-cta",
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
duration: 0.3,
|
||||||
|
stagger: 0.06,
|
||||||
|
ease: "back.out(1.5)",
|
||||||
|
},
|
||||||
|
"-=0.1",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ scope: containerRef },
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef}>
|
||||||
|
<div className="relative hero-root opacity-0 flex flex-col items-stretch pb-0 md:pb-12 mb-0 bg-[#0F0F0F]">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex flex-col gap-6 px-4 xl:px-22.75 py-8 bg-[#0F0F0F] mt-26">
|
||||||
|
<h4 className="hero-reveal relative uppercase font-mono tracking-[-2%] text-[12px] md:text-sm leading-sm text-white md:text-[#FFFFFF99] bg-[#2c2c2d] w-fit p-2 min-h-[30px] md:min-h-[36px] flex items-center">
|
||||||
|
<span className="invisible select-none" aria-hidden="true">
|
||||||
|
{BADGE_TEXT}
|
||||||
|
</span>
|
||||||
|
<span className="absolute inset-0 flex items-center p-2">
|
||||||
|
{displayedText}
|
||||||
|
</span>
|
||||||
|
</h4>
|
||||||
|
<div className="flex flex-col gap-6 w-full px-0 lg:px-0">
|
||||||
|
<h1 className="hero-reveal text-[44px] md:text-[56px] w-full max-w-sm sm:max-w-[480px] md:max-w-xl leading-[44px] tracking-[-5%] md:leading-14 font-sans">
|
||||||
|
<span className="text-[#FFFFFF99] font-normal">
|
||||||
|
The drop-in billing layer for
|
||||||
|
</span>{" "}
|
||||||
|
<span className="text-white block md:inline">AI startups</span>
|
||||||
|
</h1>
|
||||||
|
<p className="hero-reveal tracking-[-2%] w-full max-w-xs sm:max-w-[480px] md:max-w-xl text-[#FFFFFF99] md:text-[16px] text-[14px] font-light leading-5 font-sans">
|
||||||
|
Stop rebuilding usage limits, credit ledgers and payment logic.{" "}
|
||||||
|
<span className="text-white font-light">
|
||||||
|
Autumn is your customer database
|
||||||
|
</span>{" "}
|
||||||
|
that scales from your first user to your largest contract.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="hero-reveal relative w-[50vw] max-w-[720px] p-16 py-0 mx-auto hidden xl:block">
|
||||||
|
<div className="absolute inset-0 z-0 pointer-events-none">
|
||||||
|
<video
|
||||||
|
src="/images/pricing-models/pricingbg.webm"
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="relative z-10 translate-y-16 w-full flex justify-center">
|
||||||
|
<AutumnConfig
|
||||||
|
initialDelay={200}
|
||||||
|
awaitEvent="preloader:complete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-[#292929]" />
|
||||||
|
<div className="flex flex-nowrap items-center xl:px-22.75 px-4 bg-[#0F0F0F] w-full overflow-hidden">
|
||||||
|
{/* Primary CTA */}
|
||||||
|
<div className="hero-cta w-full md:w-fit md:flex-shrink-0">
|
||||||
|
<Link
|
||||||
|
href={
|
||||||
|
isLoggedIn
|
||||||
|
? "https://app.useautumn.com"
|
||||||
|
: "https://app.useautumn.com/sign-in"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial="initial"
|
||||||
|
whileHover="hover"
|
||||||
|
whileTap="hover"
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
{/* Adjusted px-3 for mobile, md:px-4 for desktop */}
|
||||||
|
<div className="relative overflow-hidden flex items-center gap-1.5 md:gap-2.5 cursor-pointer justify-between py-2 px-3 md:px-4 md:py-3.5 md:w-50 font-sans bg-[#9564ff] hover:bg-[#7D46F4] transition-colors duration-300">
|
||||||
|
<CTALines />
|
||||||
|
<span className="relative z-10 tracking-[-2%] uppercase md:normal-case text-white font-medium text-[12px] md:text-base whitespace-nowrap">
|
||||||
|
{isLoggedIn ? "Dashboard" : "Start for free"}
|
||||||
|
</span>
|
||||||
|
<span className="relative z-10 scale-95 md:scale-100">
|
||||||
|
<IconCTAStart />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Secondary CTA */}
|
||||||
|
<div className="hero-cta w-full md:w-fit md:flex-shrink-0">
|
||||||
|
<Link href={"https://cal.com/ayrod"}>
|
||||||
|
<motion.div
|
||||||
|
initial="initial"
|
||||||
|
whileHover="hover"
|
||||||
|
whileTap="hover"
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<div className="relative overflow-hidden flex items-center gap-1.5 md:gap-2.5 border-r border-[#292929] text-white cursor-pointer justify-between py-2 px-3 md:px-4 md:py-3.5 md:w-50 font-sans bg-[#0F0F0F] hover:bg-[#FFFFFF1F] transition-colors duration-300">
|
||||||
|
<CTALines />
|
||||||
|
<span className="relative z-10 tracking-[-2%] text-[12px] uppercase md:normal-case md:text-[16px] whitespace-nowrap">
|
||||||
|
Book a call
|
||||||
|
</span>
|
||||||
|
<span className="relative z-10 scale-100">
|
||||||
|
<IconCTADocs />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hero-cta hidden md:flex flex-nowrap gap-2 md:gap-3 ml-2 md:ml-3 h-10.5 md:h-12.5 flex-1">
|
||||||
|
<div className="border-r border-[#292929] h-full hidden md:block" />
|
||||||
|
<div className="border-r border-[#292929] h-full hidden md:block" />
|
||||||
|
<div className="border-r border-[#292929] h-full hidden md:block" />
|
||||||
|
|
||||||
|
<div className="border-r border-[#292929] h-full hidden md:block" />
|
||||||
|
<div className="border-r border-[#292929] h-full hidden md:block" />
|
||||||
|
<div className="border-r border-[#292929] h-full hidden md:block" />
|
||||||
|
<div className="border-r border-[#292929] h-full hidden md:block" />
|
||||||
|
<div className="border-r border-[#292929] h-full hidden md:block" />
|
||||||
|
<div className="border-r border-[#292929] h-full hidden md:block" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-b border-[#292929]" />
|
||||||
|
{/* MOBILE VIEW*/}
|
||||||
|
<div className="relative block xl:hidden w-full overflow-hidden bg-[#0F0F0F] mt-12">
|
||||||
|
<div className="relative overflow-hidden w-full p-7 flex items-center justify-center">
|
||||||
|
<video
|
||||||
|
src="/images/pricing-models/pricingbg.webm"
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
className="hero-bg absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="hero-reveal relative z-10 w-[96%] sm:w-[90%] max-w-[520px] flex justify-center items-center">
|
||||||
|
{/* <AutumnConfig lines={16} initialDelay={1000} awaitEvent="preloader:complete" /> */}
|
||||||
|
<Image
|
||||||
|
src={"/images/hero/autumn_mobile.svg"}
|
||||||
|
width={1600}
|
||||||
|
height={1600}
|
||||||
|
alt="xyz"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
apps/website/components/home-sections.tsx
Executable file
46
apps/website/components/home-sections.tsx
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import Hero from "./hero";
|
||||||
|
import SectionDivider from "./section-divider";
|
||||||
|
|
||||||
|
const ProductionScale = dynamic(() => import("@/components/production-scale"), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
const Problem = dynamic(() => import("@/components/problem"), { ssr: false });
|
||||||
|
const Solution = dynamic(() => import("@/components/solution"), { ssr: false });
|
||||||
|
const Features = dynamic(() => import("@/components/features"), { ssr: false });
|
||||||
|
const PricingModels = dynamic(() => import("@/components/pricing-models"), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
const Testimonials = dynamic(() => import("@/components/testimonials"), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
const Pricing = dynamic(() => import("@/components/pricing"), { ssr: false });
|
||||||
|
const FAQ = dynamic(() => import("@/components/faq"), { ssr: false });
|
||||||
|
const Footer = dynamic(() => import("@/components/footer"), { ssr: false });
|
||||||
|
|
||||||
|
export default function HomeSections() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Hero />
|
||||||
|
<SectionDivider title="THE PROBLEM" />
|
||||||
|
<Problem />
|
||||||
|
<SectionDivider title="THE SOLUTION" />
|
||||||
|
<Solution />
|
||||||
|
<SectionDivider title="PRICING MODELS" />
|
||||||
|
<PricingModels />
|
||||||
|
<SectionDivider title="FEATURES" />
|
||||||
|
<Features />
|
||||||
|
<SectionDivider title="TESTIMONIALS" />
|
||||||
|
<Testimonials />
|
||||||
|
<SectionDivider title="PRODUCTION SCALE" />
|
||||||
|
<ProductionScale />
|
||||||
|
<SectionDivider title="PRICING" />
|
||||||
|
<Pricing />
|
||||||
|
<SectionDivider title="FAQ" />
|
||||||
|
<FAQ />
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,421 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
CTALines,
|
|
||||||
IconBlog,
|
|
||||||
IconCTAStart,
|
|
||||||
IconDashboard,
|
|
||||||
IconDiscord,
|
|
||||||
IconDocs,
|
|
||||||
IconPricing,
|
|
||||||
MenuGridIcon,
|
|
||||||
} from "@/app/constant";
|
|
||||||
import { useGSAP } from "@gsap/react";
|
|
||||||
import gsap from "gsap";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
|
||||||
forwardRef,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { DashboardIconPixel } from "./dashboard-icon-pixel";
|
|
||||||
|
|
||||||
const NAV_LINKS = [
|
|
||||||
{ label: "Docs", href: "https://docs.useautumn.com/welcome", Icon: IconDocs },
|
|
||||||
{ label: "Blog", href: "/blog", Icon: IconBlog },
|
|
||||||
{ label: "Pricing", href: "#pricing", Icon: IconPricing },
|
|
||||||
{
|
|
||||||
label: "Discord",
|
|
||||||
href: "https://discord.com/invite/STqxY92zuS",
|
|
||||||
Icon: IconDiscord,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const NavIconPixel = forwardRef(function NavIconPixel({ Icon }, ref) {
|
|
||||||
const iconRef = useRef(null);
|
|
||||||
const tlRef = useRef(null);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
restart: () => tlRef.current?.play(),
|
|
||||||
reverse: () => tlRef.current?.reverse(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const pixelEls = iconRef.current?.querySelectorAll(".icon-pixel-path");
|
|
||||||
if (!pixelEls?.length) return;
|
|
||||||
|
|
||||||
const pixels = Array.from(pixelEls).sort((a, b) => {
|
|
||||||
const aBox = a.getBBox();
|
|
||||||
const bBox = b.getBBox();
|
|
||||||
return (
|
|
||||||
aBox.x +
|
|
||||||
aBox.width / 2 -
|
|
||||||
(aBox.y + aBox.height / 2) -
|
|
||||||
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
gsap.set(pixels, {
|
|
||||||
opacity: 0.15,
|
|
||||||
scale: 0.8,
|
|
||||||
transformOrigin: "left bottom",
|
|
||||||
fill: "currentColor",
|
|
||||||
});
|
|
||||||
|
|
||||||
tlRef.current = gsap.timeline({ paused: true });
|
|
||||||
|
|
||||||
tlRef.current
|
|
||||||
.to(pixels, {
|
|
||||||
opacity: 1,
|
|
||||||
scale: 1.15,
|
|
||||||
fill: "#FFFFFF",
|
|
||||||
duration: 0.01,
|
|
||||||
stagger: 0.025,
|
|
||||||
ease: "power2.out",
|
|
||||||
})
|
|
||||||
.to(pixels, {
|
|
||||||
scale: 1,
|
|
||||||
duration: 0.01,
|
|
||||||
ease: "back.out(3)",
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => tlRef.current?.kill();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="relative inline-flex items-center justify-center w-8 h-8 group/icon">
|
|
||||||
<div className="relative w-[16px] h-[16px] flex items-center justify-center">
|
|
||||||
<Icon ref={iconRef} className="w-full h-full text-white" />
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
function NavLinkItem({ item }) {
|
|
||||||
const iconRef = useRef(null);
|
|
||||||
const isAnchor = item.href.startsWith("#");
|
|
||||||
|
|
||||||
const handleClick = (e) => {
|
|
||||||
if (!isAnchor) return;
|
|
||||||
e.preventDefault();
|
|
||||||
const target = document.querySelector(item.href);
|
|
||||||
if (target) target.scrollIntoView({ behavior: "smooth" });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="nav-link">
|
|
||||||
<Link
|
|
||||||
href={item.href}
|
|
||||||
onClick={handleClick}
|
|
||||||
className="group inline-flex items-center py-2 text-[#FFFFFF99] hover:text-white transition-colors"
|
|
||||||
onMouseEnter={() => iconRef.current?.restart()}
|
|
||||||
onMouseLeave={() => iconRef.current?.reverse()}
|
|
||||||
>
|
|
||||||
<NavIconPixel Icon={item.Icon} ref={iconRef} />
|
|
||||||
<span className="font-mono text-[14px] uppercase tracking-widest transition-colors group-hover:text-white">
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Navbar() {
|
|
||||||
const containerRef = useRef(null);
|
|
||||||
const dashboardIconRef = useRef(null);
|
|
||||||
const RECOIL_DELAY = 1000;
|
|
||||||
const [recoilHidden, setRecoilHidden] = useState(false);
|
|
||||||
const recoilTimerRef = useRef(null);
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
|
||||||
const [scrolled, setScrolled] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleScroll = () => setScrolled(window.scrollY > 10);
|
|
||||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
||||||
return () => window.removeEventListener("scroll", handleScroll);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onRecoil = () => {
|
|
||||||
setRecoilHidden(true);
|
|
||||||
clearTimeout(recoilTimerRef.current);
|
|
||||||
recoilTimerRef.current = setTimeout(
|
|
||||||
() => setRecoilHidden(false),
|
|
||||||
RECOIL_DELAY,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
window.addEventListener("elastic-recoil", onRecoil);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("elastic-recoil", onRecoil);
|
|
||||||
clearTimeout(recoilTimerRef.current);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Lock body scroll while mobile menu is open
|
|
||||||
useEffect(() => {
|
|
||||||
document.documentElement.style.overflow = menuOpen ? "hidden" : "";
|
|
||||||
document.body.style.overflow = menuOpen ? "hidden" : "";
|
|
||||||
return () => {
|
|
||||||
document.documentElement.style.overflow = "";
|
|
||||||
document.body.style.overflow = "";
|
|
||||||
};
|
|
||||||
}, [menuOpen]);
|
|
||||||
|
|
||||||
useGSAP(
|
|
||||||
() => {
|
|
||||||
gsap.set(".nav-root", { opacity: 0 });
|
|
||||||
gsap.set(".nav-logo", {
|
|
||||||
opacity: 0,
|
|
||||||
filter: "blur(6px) brightness(1)",
|
|
||||||
scale: 0.92,
|
|
||||||
transformOrigin: "left center",
|
|
||||||
});
|
|
||||||
gsap.set(".nav-link", { opacity: 0, y: -8 });
|
|
||||||
gsap.set(".nav-dashboard", { opacity: 0, scale: 0.95 });
|
|
||||||
|
|
||||||
const tl = gsap.timeline({ defaults: { overwrite: "auto" } });
|
|
||||||
|
|
||||||
tl.to(".nav-root", { opacity: 1, duration: 0.3, ease: "none" })
|
|
||||||
.to(".nav-logo", {
|
|
||||||
opacity: 1,
|
|
||||||
filter: "blur(0px) brightness(1)",
|
|
||||||
scale: 1,
|
|
||||||
duration: 0.7,
|
|
||||||
ease: "power2.out",
|
|
||||||
})
|
|
||||||
.to(".nav-logo", {
|
|
||||||
filter: "blur(0px) brightness(1.6)",
|
|
||||||
duration: 0.225,
|
|
||||||
ease: "power2.in",
|
|
||||||
})
|
|
||||||
.to(".nav-logo", {
|
|
||||||
filter: "blur(0px) brightness(1)",
|
|
||||||
duration: 0.125,
|
|
||||||
ease: "power2.out",
|
|
||||||
})
|
|
||||||
.to(
|
|
||||||
".nav-link",
|
|
||||||
{
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
duration: 0.25,
|
|
||||||
stagger: 0.06,
|
|
||||||
ease: "power2.out",
|
|
||||||
},
|
|
||||||
"-=0.05",
|
|
||||||
)
|
|
||||||
.to(
|
|
||||||
".nav-dashboard",
|
|
||||||
{
|
|
||||||
opacity: 1,
|
|
||||||
scale: 1,
|
|
||||||
duration: 0.3,
|
|
||||||
ease: "back.out(1.5)",
|
|
||||||
},
|
|
||||||
"-=0.1",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ scope: containerRef },
|
|
||||||
);
|
|
||||||
|
|
||||||
const mobileTl = useRef(null);
|
|
||||||
|
|
||||||
useGSAP(
|
|
||||||
() => {
|
|
||||||
gsap.set(".nav-mobile", {
|
|
||||||
opacity: 0,
|
|
||||||
clipPath: "inset(0% 0 100% 0)",
|
|
||||||
pointerEvents: "none",
|
|
||||||
});
|
|
||||||
|
|
||||||
gsap.set(
|
|
||||||
".nav-mobile a, .nav-mobile img, .nav-mobile button, .nav-mobile .border-b",
|
|
||||||
{
|
|
||||||
opacity: 0,
|
|
||||||
filter: "blur(8px)",
|
|
||||||
scale: 0.95,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
mobileTl.current = gsap.timeline({
|
|
||||||
paused: true,
|
|
||||||
defaults: { overwrite: "auto" },
|
|
||||||
});
|
|
||||||
|
|
||||||
mobileTl.current
|
|
||||||
.to(".nav-mobile", {
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
pointerEvents: "auto",
|
|
||||||
clipPath: "inset(0% 0 0% 0)",
|
|
||||||
duration: 0.65,
|
|
||||||
ease: "power3.inOut",
|
|
||||||
})
|
|
||||||
.to(
|
|
||||||
".nav-mobile a, .nav-mobile img, .nav-mobile button, .nav-mobile .border-b",
|
|
||||||
{
|
|
||||||
opacity: 1,
|
|
||||||
filter: "blur(0px)",
|
|
||||||
scale: 1,
|
|
||||||
duration: 0.4,
|
|
||||||
stagger: 0.02,
|
|
||||||
ease: "power2.out",
|
|
||||||
},
|
|
||||||
"-=0.35",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ scope: containerRef },
|
|
||||||
);
|
|
||||||
|
|
||||||
useGSAP(
|
|
||||||
() => {
|
|
||||||
if (mobileTl.current) {
|
|
||||||
if (menuOpen) {
|
|
||||||
mobileTl.current.timeScale(1).play();
|
|
||||||
} else {
|
|
||||||
mobileTl.current.timeScale(1.8).reverse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ scope: containerRef, dependencies: [menuOpen] },
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" }}
|
|
||||||
ref={containerRef}
|
|
||||||
>
|
|
||||||
<div className="absolute pointer-events-none left-0 border-t border-[#292929] w-screen z-50" />
|
|
||||||
{scrolled && !recoilHidden && (
|
|
||||||
<div className="h-[56px] md:h-[44px] w-fit" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`${scrolled && !recoilHidden
|
|
||||||
? "fixed top-0 left-0 z-80 w-full px-4 md:px-(--page-pad) bg-[#0F0F0F] backdrop-blur-md border-b border-t pt-4 border-[#292929]"
|
|
||||||
: "relative"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{scrolled && !recoilHidden && (
|
|
||||||
<>
|
|
||||||
<div className="absolute pointer-events-none top-4 left-0 w-full border-t border-[#292929]" />
|
|
||||||
<div className="absolute pointer-events-none left-4 md:left-(--page-pad) top-0 bottom-0 border-l border-[#292929]" />
|
|
||||||
<div className="absolute pointer-events-none right-4 md:right-(--page-pad) top-0 bottom-0 border-r border-[#292929]" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<nav className="nav-root bg-[#0F0F0F] flex items-center justify-between font-mono uppercase text-xs pl-2 md:pl-0 h-[44px] md:h-[44px] lg:pb-0 xl:pb-0">
|
|
||||||
<Link href={"/"}>
|
|
||||||
<Image
|
|
||||||
src="/images/navbar/autumnlogo.svg"
|
|
||||||
width={114}
|
|
||||||
height={28}
|
|
||||||
alt="Autumn"
|
|
||||||
loading="lazy"
|
|
||||||
className="nav-logo ml-2 block w-[90px] sm:w-[110px] lg:w-[114px] h-auto"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="hidden lg:flex items-center gap-6">
|
|
||||||
{NAV_LINKS.map((item) => (
|
|
||||||
<NavLinkItem key={item.label} item={item} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="nav-dashboard hidden lg:block">
|
|
||||||
<motion.div
|
|
||||||
initial="initial"
|
|
||||||
whileHover="hover"
|
|
||||||
whileTap={{ scale: 0.97 }}
|
|
||||||
className="relative"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href="https://useautumn.com"
|
|
||||||
target="_blank"
|
|
||||||
className="relative overflow-hidden inline-flex items-center gap-2 bg-[#9564ff] hover:bg-[#7D46F4] active:bg-[#7D46F4] transition-colors duration-300 px-4 py-3.5 text-white cursor-pointer whitespace-nowrap"
|
|
||||||
onMouseEnter={() => dashboardIconRef.current?.restart()}
|
|
||||||
onMouseLeave={() => dashboardIconRef.current?.reverse()}
|
|
||||||
>
|
|
||||||
<CTALines />
|
|
||||||
<div className="relative z-10 flex items-center gap-2">
|
|
||||||
<DashboardIconPixel
|
|
||||||
Icon={IconDashboard}
|
|
||||||
ref={dashboardIconRef}
|
|
||||||
/>
|
|
||||||
<span className="font-sans font-medium tracking-tight">
|
|
||||||
Dashboard
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.button
|
|
||||||
className="lg:hidden mr-2 p-1.5 text-[#FFFFFF99] cursor-pointer"
|
|
||||||
onClick={() => setMenuOpen(!menuOpen)}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
aria-label="Toggle menu"
|
|
||||||
>
|
|
||||||
<MenuGridIcon isOpen={menuOpen} />
|
|
||||||
</motion.button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`fixed nav-mobile inset-x-0 overflow-y-auto overflow-x-hidden ${scrolled && !recoilHidden ? "top-[58px] sm:top-[60px]" : "top-[66px] sm:top-[62px]"} bg-[#000000] flex flex-col font-mono uppercase lg:top-5 h-[calc(100dvh-58px)] z-40 px-4 md:px-(--page-pad) pb-8 transition-all duration-300`}
|
|
||||||
style={{
|
|
||||||
opacity: 0,
|
|
||||||
pointerEvents: "none",
|
|
||||||
clipPath: "inset(0% 0 100% 0)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Nav items */}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
{NAV_LINKS.map((item) => {
|
|
||||||
const isAnchor = item.href.startsWith("#");
|
|
||||||
const isExternal = item.href.startsWith("http");
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={item.label}
|
|
||||||
href={item.href}
|
|
||||||
target={isExternal ? "_blank" : undefined}
|
|
||||||
onClick={isAnchor ? (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setMenuOpen(false);
|
|
||||||
const target = document.querySelector(item.href);
|
|
||||||
if (target) target.scrollIntoView({ behavior: "smooth" });
|
|
||||||
} : undefined}
|
|
||||||
className="flex items-center gap-4 px-4 py-3.5 border-b border-[#292929] active:bg-[#141414ea] text-[#ffffff99] hover:text-white active:text-white transition-colors text-sm tracking-[-1%]"
|
|
||||||
>
|
|
||||||
<item.Icon className="h-3.5 w-3.5 shrink-0" />
|
|
||||||
<span>{item.label}</span>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col mt-auto -mb-2.5">
|
|
||||||
<div className="border-t border-[#292929] py-1.5" />
|
|
||||||
<div className="px-4">
|
|
||||||
<Link
|
|
||||||
href="https://useautumn.com"
|
|
||||||
target="_blank"
|
|
||||||
onClick={() => setMenuOpen(false)}
|
|
||||||
className="flex items-center justify-between gap-4 px-2 py-2.5 bg-[#9564ff] active:bg-[#7D46F4] transition-colors duration-300 text-white text-sm tracking-widest"
|
|
||||||
>
|
|
||||||
<span className="tracking-[-2%] text-sm">Start for free</span>
|
|
||||||
<IconCTAStart className="h-3.5 w-3.5" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
{Array.from({ length: 3 }).map((_, i) => (
|
|
||||||
<div key={i} className="border-b border-[#292929] py-1.5" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!scrolled && (
|
|
||||||
<div className="absolute pointer-events-none left-0 border-b border-[#292929] w-full z-50" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
453
apps/website/components/navbar.tsx
Normal file
453
apps/website/components/navbar.tsx
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useGSAP } from "@gsap/react";
|
||||||
|
import gsap from "gsap";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
forwardRef,
|
||||||
|
type MouseEvent,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
|
CTALines,
|
||||||
|
IconBlog,
|
||||||
|
IconCTAStart,
|
||||||
|
IconDashboard,
|
||||||
|
IconDiscord,
|
||||||
|
IconDocs,
|
||||||
|
IconPricing,
|
||||||
|
MenuGridIcon,
|
||||||
|
} from "@/app/constant";
|
||||||
|
import type {
|
||||||
|
PageStyle,
|
||||||
|
PixelHoverHandle,
|
||||||
|
PixelIconComponent,
|
||||||
|
} from "@/lib/types";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { DashboardIconPixel } from "./dashboard-icon-pixel";
|
||||||
|
|
||||||
|
const NAV_LINKS = [
|
||||||
|
{ label: "Docs", href: "https://docs.useautumn.com/welcome", Icon: IconDocs },
|
||||||
|
{ label: "Blog", href: "/blog", Icon: IconBlog },
|
||||||
|
{ label: "Pricing", href: "#pricing", Icon: IconPricing },
|
||||||
|
{
|
||||||
|
label: "Discord",
|
||||||
|
href: "https://discord.com/invite/STqxY92zuS",
|
||||||
|
Icon: IconDiscord,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const NavIconPixel = forwardRef<PixelHoverHandle, { Icon: PixelIconComponent }>(
|
||||||
|
function NavIconPixel({ Icon }, ref) {
|
||||||
|
const iconRef = useRef<SVGSVGElement | null>(null);
|
||||||
|
const tlRef = useRef<gsap.core.Timeline | null>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
restart: () => tlRef.current?.play(),
|
||||||
|
reverse: () => tlRef.current?.reverse(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const pixelEls =
|
||||||
|
iconRef.current?.querySelectorAll<SVGPathElement>(".icon-pixel-path");
|
||||||
|
if (!pixelEls?.length) return;
|
||||||
|
|
||||||
|
const pixels = Array.from(pixelEls).sort((a, b) => {
|
||||||
|
const aBox = a.getBBox();
|
||||||
|
const bBox = b.getBBox();
|
||||||
|
return (
|
||||||
|
aBox.x +
|
||||||
|
aBox.width / 2 -
|
||||||
|
(aBox.y + aBox.height / 2) -
|
||||||
|
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
gsap.set(pixels, {
|
||||||
|
opacity: 0.15,
|
||||||
|
scale: 0.8,
|
||||||
|
transformOrigin: "left bottom",
|
||||||
|
fill: "currentColor",
|
||||||
|
});
|
||||||
|
|
||||||
|
tlRef.current = gsap.timeline({ paused: true });
|
||||||
|
|
||||||
|
tlRef.current
|
||||||
|
.to(pixels, {
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1.15,
|
||||||
|
fill: "#FFFFFF",
|
||||||
|
duration: 0.01,
|
||||||
|
stagger: 0.025,
|
||||||
|
ease: "power2.out",
|
||||||
|
})
|
||||||
|
.to(pixels, {
|
||||||
|
scale: 1,
|
||||||
|
duration: 0.01,
|
||||||
|
ease: "back.out(3)",
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
tlRef.current?.kill();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="group/icon relative inline-flex h-8 w-8 items-center justify-center">
|
||||||
|
<div className="relative flex h-[16px] w-[16px] items-center justify-center">
|
||||||
|
<Icon ref={iconRef} className="h-full w-full text-white" />
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
NavIconPixel.displayName = "NavIconPixel";
|
||||||
|
|
||||||
|
function NavLinkItem({ item }: { item: (typeof NAV_LINKS)[number] }) {
|
||||||
|
const iconRef = useRef<PixelHoverHandle | null>(null);
|
||||||
|
const isAnchor = item.href.startsWith("#");
|
||||||
|
|
||||||
|
const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {
|
||||||
|
if (!isAnchor) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const target = document.querySelector(item.href);
|
||||||
|
if (target) target.scrollIntoView({ behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="nav-link">
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
onClick={handleClick}
|
||||||
|
className="group inline-flex items-center py-2 text-[#FFFFFF99] hover:text-white transition-colors"
|
||||||
|
onMouseEnter={() => iconRef.current?.restart()}
|
||||||
|
onMouseLeave={() => iconRef.current?.reverse()}
|
||||||
|
>
|
||||||
|
<NavIconPixel Icon={item.Icon} ref={iconRef} />
|
||||||
|
<span className="font-mono text-[14px] uppercase tracking-widest transition-colors group-hover:text-white">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Navbar({
|
||||||
|
animateIntro = true,
|
||||||
|
}: {
|
||||||
|
animateIntro?: boolean;
|
||||||
|
}) {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const dashboardIconRef = useRef<PixelHoverHandle | null>(null);
|
||||||
|
const RECOIL_DELAY = 1000;
|
||||||
|
const [recoilHidden, setRecoilHidden] = useState(false);
|
||||||
|
const recoilTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => setScrolled(window.scrollY > 10);
|
||||||
|
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||||
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onRecoil = () => {
|
||||||
|
setRecoilHidden(true);
|
||||||
|
if (recoilTimerRef.current) clearTimeout(recoilTimerRef.current);
|
||||||
|
recoilTimerRef.current = setTimeout(
|
||||||
|
() => setRecoilHidden(false),
|
||||||
|
RECOIL_DELAY,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
window.addEventListener("elastic-recoil", onRecoil);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("elastic-recoil", onRecoil);
|
||||||
|
if (recoilTimerRef.current) clearTimeout(recoilTimerRef.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Lock body scroll while mobile menu is open
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.style.overflow = menuOpen ? "hidden" : "";
|
||||||
|
document.body.style.overflow = menuOpen ? "hidden" : "";
|
||||||
|
return () => {
|
||||||
|
document.documentElement.style.overflow = "";
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
};
|
||||||
|
}, [menuOpen]);
|
||||||
|
|
||||||
|
useGSAP(
|
||||||
|
() => {
|
||||||
|
if (!animateIntro) return;
|
||||||
|
gsap.set(".nav-root", { opacity: 0 });
|
||||||
|
gsap.set(".nav-logo", {
|
||||||
|
opacity: 0,
|
||||||
|
filter: "blur(6px) brightness(1)",
|
||||||
|
scale: 0.92,
|
||||||
|
transformOrigin: "left center",
|
||||||
|
});
|
||||||
|
gsap.set(".nav-link", { opacity: 0, y: -8 });
|
||||||
|
gsap.set(".nav-dashboard", { opacity: 0, scale: 0.95 });
|
||||||
|
|
||||||
|
const tl = gsap.timeline({ defaults: { overwrite: "auto" } });
|
||||||
|
|
||||||
|
tl.to(".nav-root", { opacity: 1, duration: 0.3, ease: "none" })
|
||||||
|
.to(".nav-logo", {
|
||||||
|
opacity: 1,
|
||||||
|
filter: "blur(0px) brightness(1)",
|
||||||
|
scale: 1,
|
||||||
|
duration: 0.7,
|
||||||
|
ease: "power2.out",
|
||||||
|
})
|
||||||
|
.to(".nav-logo", {
|
||||||
|
filter: "blur(0px) brightness(1.6)",
|
||||||
|
duration: 0.225,
|
||||||
|
ease: "power2.in",
|
||||||
|
})
|
||||||
|
.to(".nav-logo", {
|
||||||
|
filter: "blur(0px) brightness(1)",
|
||||||
|
duration: 0.125,
|
||||||
|
ease: "power2.out",
|
||||||
|
})
|
||||||
|
.to(
|
||||||
|
".nav-link",
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
duration: 0.25,
|
||||||
|
stagger: 0.06,
|
||||||
|
ease: "power2.out",
|
||||||
|
},
|
||||||
|
"-=0.05",
|
||||||
|
)
|
||||||
|
.to(
|
||||||
|
".nav-dashboard",
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "back.out(1.5)",
|
||||||
|
},
|
||||||
|
"-=0.1",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ scope: containerRef, dependencies: [animateIntro] },
|
||||||
|
);
|
||||||
|
|
||||||
|
const mobileTl = useRef<gsap.core.Timeline | null>(null);
|
||||||
|
|
||||||
|
useGSAP(
|
||||||
|
() => {
|
||||||
|
gsap.set(".nav-mobile", {
|
||||||
|
opacity: 0,
|
||||||
|
clipPath: "inset(0% 0 100% 0)",
|
||||||
|
pointerEvents: "none",
|
||||||
|
});
|
||||||
|
|
||||||
|
gsap.set(
|
||||||
|
".nav-mobile a, .nav-mobile img, .nav-mobile button, .nav-mobile .border-b",
|
||||||
|
{
|
||||||
|
opacity: 0,
|
||||||
|
filter: "blur(8px)",
|
||||||
|
scale: 0.95,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
mobileTl.current = gsap.timeline({
|
||||||
|
paused: true,
|
||||||
|
defaults: { overwrite: "auto" },
|
||||||
|
});
|
||||||
|
|
||||||
|
mobileTl.current
|
||||||
|
.to(".nav-mobile", {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
pointerEvents: "auto",
|
||||||
|
clipPath: "inset(0% 0 0% 0)",
|
||||||
|
duration: 0.65,
|
||||||
|
ease: "power3.inOut",
|
||||||
|
})
|
||||||
|
.to(
|
||||||
|
".nav-mobile a, .nav-mobile img, .nav-mobile button, .nav-mobile .border-b",
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
filter: "blur(0px)",
|
||||||
|
scale: 1,
|
||||||
|
duration: 0.4,
|
||||||
|
stagger: 0.02,
|
||||||
|
ease: "power2.out",
|
||||||
|
},
|
||||||
|
"-=0.35",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ scope: containerRef },
|
||||||
|
);
|
||||||
|
|
||||||
|
useGSAP(
|
||||||
|
() => {
|
||||||
|
if (mobileTl.current) {
|
||||||
|
if (menuOpen) {
|
||||||
|
mobileTl.current.timeScale(1).play();
|
||||||
|
} else {
|
||||||
|
mobileTl.current.timeScale(1.8).reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ scope: containerRef, dependencies: [menuOpen] },
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" } as PageStyle
|
||||||
|
}
|
||||||
|
ref={containerRef}
|
||||||
|
>
|
||||||
|
<div className="absolute pointer-events-none left-0 border-t border-[#292929] w-screen z-50" />
|
||||||
|
{scrolled && !recoilHidden && (
|
||||||
|
<div className="h-[56px] md:h-[44px] w-fit" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
scrolled && !recoilHidden
|
||||||
|
? "fixed top-0 left-0 z-80 w-full border-t border-b border-[#292929] bg-[#0F0F0F] px-4 pt-4 backdrop-blur-md md:px-(--page-pad)"
|
||||||
|
: "relative",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{scrolled && !recoilHidden && (
|
||||||
|
<>
|
||||||
|
<div className="absolute pointer-events-none top-4 left-0 w-full border-t border-[#292929]" />
|
||||||
|
<div className="absolute pointer-events-none left-4 md:left-(--page-pad) top-0 bottom-0 border-l border-[#292929]" />
|
||||||
|
<div className="absolute pointer-events-none right-4 md:right-(--page-pad) top-0 bottom-0 border-r border-[#292929]" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<nav className="nav-root bg-[#0F0F0F] flex items-center justify-between font-mono uppercase text-xs pl-2 md:pl-0 h-[44px] md:h-[44px] lg:pb-0 xl:pb-0">
|
||||||
|
<Link href={"/"}>
|
||||||
|
<Image
|
||||||
|
src="/images/navbar/autumnlogo.svg"
|
||||||
|
width={114}
|
||||||
|
height={28}
|
||||||
|
alt="Autumn"
|
||||||
|
loading="lazy"
|
||||||
|
className="nav-logo ml-2 block w-[90px] sm:w-[110px] lg:w-[114px] h-auto"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="hidden lg:flex items-center gap-6">
|
||||||
|
{NAV_LINKS.map((item) => (
|
||||||
|
<NavLinkItem key={item.label} item={item} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="nav-dashboard hidden lg:block">
|
||||||
|
<motion.div
|
||||||
|
initial="initial"
|
||||||
|
whileHover="hover"
|
||||||
|
whileTap={{ scale: 0.97 }}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="https://app.useautumn.com"
|
||||||
|
target="_blank"
|
||||||
|
className="relative overflow-hidden inline-flex items-center gap-2 bg-[#9564ff] hover:bg-[#7D46F4] active:bg-[#7D46F4] transition-colors duration-300 px-4 py-3.5 text-white cursor-pointer whitespace-nowrap"
|
||||||
|
onMouseEnter={() => dashboardIconRef.current?.restart()}
|
||||||
|
onMouseLeave={() => dashboardIconRef.current?.reverse()}
|
||||||
|
>
|
||||||
|
<CTALines />
|
||||||
|
<div className="relative z-10 flex items-center gap-2">
|
||||||
|
<DashboardIconPixel
|
||||||
|
Icon={IconDashboard}
|
||||||
|
ref={dashboardIconRef}
|
||||||
|
/>
|
||||||
|
<span className="font-sans font-medium tracking-tight">
|
||||||
|
Dashboard
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className="lg:hidden mr-2 p-1.5 text-[#FFFFFF99] cursor-pointer"
|
||||||
|
onClick={() => setMenuOpen(!menuOpen)}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
>
|
||||||
|
<MenuGridIcon isOpen={menuOpen} />
|
||||||
|
</motion.button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"fixed nav-mobile inset-x-0 z-40 flex h-[calc(100dvh-58px)] flex-col overflow-x-hidden overflow-y-auto bg-[#000000] px-4 pb-8 font-mono uppercase transition-all duration-300 md:px-(--page-pad) lg:top-5",
|
||||||
|
scrolled && !recoilHidden
|
||||||
|
? "top-[58px] sm:top-[60px]"
|
||||||
|
: "top-[66px] sm:top-[62px]",
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
opacity: 0,
|
||||||
|
pointerEvents: "none",
|
||||||
|
clipPath: "inset(0% 0 100% 0)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Nav items */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{NAV_LINKS.map((item) => {
|
||||||
|
const isAnchor = item.href.startsWith("#");
|
||||||
|
const isExternal = item.href.startsWith("http");
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.label}
|
||||||
|
href={item.href}
|
||||||
|
target={isExternal ? "_blank" : undefined}
|
||||||
|
onClick={
|
||||||
|
isAnchor
|
||||||
|
? (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setMenuOpen(false);
|
||||||
|
const target = document.querySelector(item.href);
|
||||||
|
if (target)
|
||||||
|
target.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
className="flex items-center gap-4 px-4 py-3.5 border-b border-[#292929] active:bg-[#141414ea] text-[#ffffff99] hover:text-white active:text-white transition-colors text-sm tracking-[-1%]"
|
||||||
|
>
|
||||||
|
<item.Icon className="h-3.5 w-3.5 shrink-0" />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col mt-auto -mb-2.5">
|
||||||
|
<div className="border-t border-[#292929] py-1.5" />
|
||||||
|
<div className="px-4">
|
||||||
|
<Link
|
||||||
|
href="https://useautumn.com"
|
||||||
|
target="_blank"
|
||||||
|
onClick={() => setMenuOpen(false)}
|
||||||
|
className="flex items-center justify-between gap-4 px-2 py-2.5 bg-[#9564ff] active:bg-[#7D46F4] transition-colors duration-300 text-white text-sm tracking-widest"
|
||||||
|
>
|
||||||
|
<span className="tracking-[-2%] text-sm">Start for free</span>
|
||||||
|
<IconCTAStart className="h-3.5 w-3.5" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-b border-[#292929] py-1.5" />
|
||||||
|
<div className="border-b border-[#292929] py-1.5" />
|
||||||
|
<div className="border-b border-[#292929] py-1.5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!scrolled && (
|
||||||
|
<div className="absolute pointer-events-none left-0 border-b border-[#292929] w-full z-50" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import gsap from "gsap";
|
|
||||||
import {
|
|
||||||
PRELOADER_LOGO_PATH,
|
|
||||||
PRELOADER_LOGO_VIEWBOX,
|
|
||||||
PreloaderLogo,
|
|
||||||
} from "@/app/constant";
|
|
||||||
|
|
||||||
export default function Preloader() {
|
|
||||||
const wrapperRef = useRef(null);
|
|
||||||
const logoWrapRef = useRef(null);
|
|
||||||
const gridWrapRef = useRef(null);
|
|
||||||
const blackCoverRef = useRef(null);
|
|
||||||
const fallbackRef = useRef(null);
|
|
||||||
const [done, setDone] = useState(false);
|
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
window.history.scrollRestoration = "manual";
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
document.documentElement.scrollTop = 0;
|
|
||||||
document.body.scrollTop = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
document.documentElement.scrollTop = 0;
|
|
||||||
document.body.scrollTop = 0;
|
|
||||||
|
|
||||||
if (fallbackRef.current) {
|
|
||||||
fallbackRef.current.style.opacity = "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
const preventScroll = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("wheel", preventScroll, {
|
|
||||||
passive: false,
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
window.addEventListener("touchmove", preventScroll, {
|
|
||||||
passive: false,
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const wrapper = wrapperRef.current;
|
|
||||||
const logoWrap = logoWrapRef.current;
|
|
||||||
const gridWrap = gridWrapRef.current;
|
|
||||||
const blackCover = blackCoverRef.current;
|
|
||||||
|
|
||||||
gsap.set(gridWrap, {
|
|
||||||
clipPath: "inset(0 100% 0 0)",
|
|
||||||
willChange: "clip-path",
|
|
||||||
force3D: true,
|
|
||||||
});
|
|
||||||
gsap.set(blackCover, { opacity: 0, willChange: "opacity" });
|
|
||||||
|
|
||||||
const logoSvg = logoWrap.querySelector("svg");
|
|
||||||
gsap.set(logoSvg, { opacity: 0 });
|
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
Object.assign(canvas.style, {
|
|
||||||
position: "absolute",
|
|
||||||
top: "50%",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translate(-50%, -50%)",
|
|
||||||
pointerEvents: "none",
|
|
||||||
});
|
|
||||||
wrapper.appendChild(canvas);
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
document.documentElement.style.overflow = "";
|
|
||||||
document.body.style.overflow = "";
|
|
||||||
document.body.style.paddingRight = "";
|
|
||||||
window.removeEventListener("wheel", preventScroll, { capture: true });
|
|
||||||
window.removeEventListener("touchmove", preventScroll, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
|
|
||||||
};
|
|
||||||
|
|
||||||
const runExitSequence = () => {
|
|
||||||
const tl = gsap.timeline({ defaults: { overwrite: "auto" } });
|
|
||||||
|
|
||||||
tl.to(canvas, { opacity: 0, duration: 0.35, ease: "power2.out" })
|
|
||||||
.to(
|
|
||||||
gridWrap,
|
|
||||||
{
|
|
||||||
clipPath: "inset(0 0% 0 0)",
|
|
||||||
duration: 0.35,
|
|
||||||
ease: "power2.out",
|
|
||||||
force3D: true,
|
|
||||||
},
|
|
||||||
"<",
|
|
||||||
)
|
|
||||||
.to(blackCover, {
|
|
||||||
opacity: 1,
|
|
||||||
duration: 0.45,
|
|
||||||
ease: "power2.inOut",
|
|
||||||
})
|
|
||||||
.to(wrapper, {
|
|
||||||
opacity: 0,
|
|
||||||
duration: 0.45,
|
|
||||||
ease: "power2.inOut",
|
|
||||||
onStart: () => {
|
|
||||||
window.dispatchEvent(new CustomEvent("preloader:complete"));
|
|
||||||
},
|
|
||||||
onComplete: () => {
|
|
||||||
cleanup();
|
|
||||||
setDone(true);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
gsap.to(gridWrap, {
|
|
||||||
clipPath: "inset(0 12% 0 0)",
|
|
||||||
duration: 0.8,
|
|
||||||
ease: "power2.in",
|
|
||||||
force3D: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const W = PRELOADER_LOGO_VIEWBOX.width;
|
|
||||||
const H = PRELOADER_LOGO_VIEWBOX.height;
|
|
||||||
|
|
||||||
const offscreen = document.createElement("canvas");
|
|
||||||
offscreen.width = W;
|
|
||||||
offscreen.height = H;
|
|
||||||
const octx = offscreen.getContext("2d");
|
|
||||||
const path2d = new Path2D(PRELOADER_LOGO_PATH);
|
|
||||||
octx.fillStyle = "white";
|
|
||||||
octx.fill(path2d);
|
|
||||||
const { data } = octx.getImageData(0, 0, W, H);
|
|
||||||
|
|
||||||
const dpr = window.devicePixelRatio || 1;
|
|
||||||
const displayW =
|
|
||||||
logoSvg.offsetWidth || logoSvg.getBoundingClientRect().width || 54;
|
|
||||||
const displayH = Math.round((displayW / W) * H);
|
|
||||||
const isMobile = displayW < 70;
|
|
||||||
|
|
||||||
const STEP = isMobile ? 6 : 4;
|
|
||||||
const DOT_R = isMobile
|
|
||||||
? Math.max(0.6, (displayW / W) * STEP * 0.3)
|
|
||||||
: Math.max(1, (displayW / W) * STEP * 0.45);
|
|
||||||
|
|
||||||
const dots = [];
|
|
||||||
for (let y = 0; y < H; y += STEP) {
|
|
||||||
for (let x = 0; x < W; x += STEP) {
|
|
||||||
const i = (y * W + x) * 4;
|
|
||||||
if (data[i + 3] > 40) {
|
|
||||||
dots.push({ nx: x / W, ny: y / H });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = dots.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
[dots[i], dots[j]] = [dots[j], dots[i]];
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.width = displayW * dpr;
|
|
||||||
canvas.height = displayH * dpr;
|
|
||||||
canvas.style.width = `${displayW}px`;
|
|
||||||
canvas.style.height = `${displayH}px`;
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
ctx.scale(dpr, dpr);
|
|
||||||
|
|
||||||
let painted = 0;
|
|
||||||
const total = dots.length;
|
|
||||||
const progress = { value: 0 };
|
|
||||||
|
|
||||||
gsap.to(progress, {
|
|
||||||
value: 1,
|
|
||||||
duration: 1.05,
|
|
||||||
ease: "power1.inOut",
|
|
||||||
onUpdate() {
|
|
||||||
const target = Math.floor(progress.value * total);
|
|
||||||
while (painted < target) {
|
|
||||||
const d = dots[painted];
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(d.nx * displayW, d.ny * displayH, DOT_R, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = "#9564FF";
|
|
||||||
ctx.fill();
|
|
||||||
painted++;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onComplete() {
|
|
||||||
gsap.delayedCall(0.2, runExitSequence);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, 120);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cleanup();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (done) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={wrapperRef}
|
|
||||||
className="fixed inset-0 z-[9999] bg-black"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={fallbackRef}
|
|
||||||
className="absolute inset-0 flex items-center justify-center z-10 pointer-events-none transition-opacity duration-300"
|
|
||||||
>
|
|
||||||
<PreloaderLogo className="w-[53.57px] md:w-[97.94px] h-auto opacity-20" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<div ref={logoWrapRef}>
|
|
||||||
<PreloaderLogo className="w-[53.57px] md:w-[97.94px] h-auto" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ref={gridWrapRef} className="absolute bottom-0 left-0 w-full">
|
|
||||||
<img
|
|
||||||
src="/images/preloader/grid.png"
|
|
||||||
width={1440}
|
|
||||||
height={217}
|
|
||||||
alt=""
|
|
||||||
className="w-full h-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
ref={blackCoverRef}
|
|
||||||
className="absolute inset-0 bg-black pointer-events-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
244
apps/website/components/preloader.tsx
Normal file
244
apps/website/components/preloader.tsx
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import gsap from "gsap";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
PRELOADER_LOGO_PATH,
|
||||||
|
PRELOADER_LOGO_VIEWBOX,
|
||||||
|
PreloaderLogo,
|
||||||
|
} from "@/app/constant";
|
||||||
|
|
||||||
|
export default function Preloader() {
|
||||||
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const logoWrapRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const gridWrapRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const blackCoverRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const fallbackRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [done, setDone] = useState(false);
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.history.scrollRestoration = "manual";
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
document.documentElement.scrollTop = 0;
|
||||||
|
document.body.scrollTop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
document.documentElement.scrollTop = 0;
|
||||||
|
document.body.scrollTop = 0;
|
||||||
|
|
||||||
|
if (fallbackRef.current) {
|
||||||
|
fallbackRef.current.style.opacity = "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
const preventScroll = (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("wheel", preventScroll, {
|
||||||
|
passive: false,
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
window.addEventListener("touchmove", preventScroll, {
|
||||||
|
passive: false,
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper = wrapperRef.current;
|
||||||
|
const logoWrap = logoWrapRef.current;
|
||||||
|
const gridWrap = gridWrapRef.current;
|
||||||
|
const blackCover = blackCoverRef.current;
|
||||||
|
if (!wrapper || !logoWrap || !gridWrap || !blackCover) return;
|
||||||
|
|
||||||
|
gsap.set(gridWrap, {
|
||||||
|
clipPath: "inset(0 100% 0 0)",
|
||||||
|
willChange: "clip-path",
|
||||||
|
force3D: true,
|
||||||
|
});
|
||||||
|
gsap.set(blackCover, { opacity: 0, willChange: "opacity" });
|
||||||
|
|
||||||
|
const logoSvg = logoWrap.querySelector<SVGSVGElement>("svg");
|
||||||
|
if (!logoSvg) return;
|
||||||
|
gsap.set(logoSvg, { opacity: 0 });
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
Object.assign(canvas.style, {
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
pointerEvents: "none",
|
||||||
|
});
|
||||||
|
wrapper.appendChild(canvas);
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
document.documentElement.style.overflow = "";
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
document.body.style.paddingRight = "";
|
||||||
|
window.removeEventListener("wheel", preventScroll, { capture: true });
|
||||||
|
window.removeEventListener("touchmove", preventScroll, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
|
||||||
|
};
|
||||||
|
|
||||||
|
const runExitSequence = () => {
|
||||||
|
const tl = gsap.timeline({ defaults: { overwrite: "auto" } });
|
||||||
|
|
||||||
|
tl.to(canvas, { opacity: 0, duration: 0.35, ease: "power2.out" })
|
||||||
|
.to(
|
||||||
|
gridWrap,
|
||||||
|
{
|
||||||
|
clipPath: "inset(0 0% 0 0)",
|
||||||
|
duration: 0.35,
|
||||||
|
ease: "power2.out",
|
||||||
|
force3D: true,
|
||||||
|
},
|
||||||
|
"<",
|
||||||
|
)
|
||||||
|
.to(blackCover, {
|
||||||
|
opacity: 1,
|
||||||
|
duration: 0.45,
|
||||||
|
ease: "power2.inOut",
|
||||||
|
})
|
||||||
|
.to(wrapper, {
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.45,
|
||||||
|
ease: "power2.inOut",
|
||||||
|
onStart: () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("preloader:complete"));
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
cleanup();
|
||||||
|
setDone(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
gsap.to(gridWrap, {
|
||||||
|
clipPath: "inset(0 12% 0 0)",
|
||||||
|
duration: 0.8,
|
||||||
|
ease: "power2.in",
|
||||||
|
force3D: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const W = PRELOADER_LOGO_VIEWBOX.width;
|
||||||
|
const H = PRELOADER_LOGO_VIEWBOX.height;
|
||||||
|
|
||||||
|
const offscreen = document.createElement("canvas");
|
||||||
|
offscreen.width = W;
|
||||||
|
offscreen.height = H;
|
||||||
|
const octx = offscreen.getContext("2d");
|
||||||
|
if (!octx) return;
|
||||||
|
const path2d = new Path2D(PRELOADER_LOGO_PATH);
|
||||||
|
octx.fillStyle = "white";
|
||||||
|
octx.fill(path2d);
|
||||||
|
const { data } = octx.getImageData(0, 0, W, H);
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const displayW =
|
||||||
|
logoWrap.getBoundingClientRect().width ||
|
||||||
|
logoSvg.getBoundingClientRect().width ||
|
||||||
|
54;
|
||||||
|
const displayH = Math.round((displayW / W) * H);
|
||||||
|
const isMobile = displayW < 70;
|
||||||
|
|
||||||
|
const STEP = isMobile ? 6 : 4;
|
||||||
|
const DOT_R = isMobile
|
||||||
|
? Math.max(0.6, (displayW / W) * STEP * 0.3)
|
||||||
|
: Math.max(1, (displayW / W) * STEP * 0.45);
|
||||||
|
|
||||||
|
const dots: Array<{ nx: number; ny: number }> = [];
|
||||||
|
for (let y = 0; y < H; y += STEP) {
|
||||||
|
for (let x = 0; x < W; x += STEP) {
|
||||||
|
const i = (y * W + x) * 4;
|
||||||
|
if (data[i + 3] > 40) {
|
||||||
|
dots.push({ nx: x / W, ny: y / H });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = dots.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[dots[i], dots[j]] = [dots[j], dots[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = displayW * dpr;
|
||||||
|
canvas.height = displayH * dpr;
|
||||||
|
canvas.style.width = `${displayW}px`;
|
||||||
|
canvas.style.height = `${displayH}px`;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
let painted = 0;
|
||||||
|
const total = dots.length;
|
||||||
|
const progress = { value: 0 };
|
||||||
|
|
||||||
|
gsap.to(progress, {
|
||||||
|
value: 1,
|
||||||
|
duration: 1.05,
|
||||||
|
ease: "power1.inOut",
|
||||||
|
onUpdate() {
|
||||||
|
const target = Math.floor(progress.value * total);
|
||||||
|
while (painted < target) {
|
||||||
|
const d = dots[painted];
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(d.nx * displayW, d.ny * displayH, DOT_R, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = "#9564FF";
|
||||||
|
ctx.fill();
|
||||||
|
painted++;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onComplete() {
|
||||||
|
gsap.delayedCall(0.2, runExitSequence);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 120);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (done) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={wrapperRef}
|
||||||
|
className="fixed inset-0 z-[9999] bg-black"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={fallbackRef}
|
||||||
|
className="absolute inset-0 flex items-center justify-center z-10 pointer-events-none transition-opacity duration-300"
|
||||||
|
>
|
||||||
|
<PreloaderLogo className="w-[53.57px] md:w-[97.94px] h-auto opacity-20" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div ref={logoWrapRef}>
|
||||||
|
<PreloaderLogo className="w-[53.57px] md:w-[97.94px] h-auto" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={gridWrapRef} className="absolute bottom-0 left-0 w-full">
|
||||||
|
<img
|
||||||
|
src="/images/preloader/grid.png"
|
||||||
|
width={1440}
|
||||||
|
height={217}
|
||||||
|
alt=""
|
||||||
|
className="w-full h-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={blackCoverRef}
|
||||||
|
className="absolute inset-0 bg-black pointer-events-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { CTALines, IconCTAStart } from "@/app/constant";
|
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
const sidebarItems = [
|
|
||||||
{
|
|
||||||
id: "subscriptions",
|
|
||||||
label: "Subscriptions",
|
|
||||||
model: 0,
|
|
||||||
desc: "Monthly or yearly plans with feature gating. Upgrades and downgrades handled automatically. Proration included.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "free-trials",
|
|
||||||
label: "Free Trials",
|
|
||||||
model: 2,
|
|
||||||
desc: "Card-required or card-optional trials. Auto-convert to paid. Configurable trial lengths per plan.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "credits",
|
|
||||||
label: "Credits & Top-ups",
|
|
||||||
model: 1,
|
|
||||||
desc: "Prepaid credits that features draw from. One-time purchases or auto-refill. Set minimum thresholds.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "usage",
|
|
||||||
label: "Usage-Based",
|
|
||||||
model: 4,
|
|
||||||
desc: "Pay for what you use. Real-time metering, overage handling, usage resets. Combine with base\u00a0subscriptions.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "seat",
|
|
||||||
label: "Seat-Based",
|
|
||||||
model: 6,
|
|
||||||
desc: "Per-user pricing with seat limits. Add or remove seats dynamically. Automatic proration.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "hybrid",
|
|
||||||
label: "Hybrid Models",
|
|
||||||
model: 5,
|
|
||||||
desc: "Mix subscriptions, usage, credits, and seats in one plan. Example: $50/month + $0.02/token + 5 seats included.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "rollovers",
|
|
||||||
label: "Rollovers & Expirations",
|
|
||||||
model: 7,
|
|
||||||
desc: "Roll credits to next period or expire after X days. Configure per plan or per feature.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "enterprise",
|
|
||||||
label: "Custom Enterprise",
|
|
||||||
model: 3,
|
|
||||||
desc: "One-off pricing for large customers. Unique limits, custom billing cycles, manual overrides—all in the dashboard.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function PricingModels() {
|
|
||||||
const [activeTab, setActiveTab] = useState(sidebarItems[0]);
|
|
||||||
|
|
||||||
const images = {
|
|
||||||
0: "/images/pricing-models/Subscriptions.webp",
|
|
||||||
1: "/images/pricing-models/Subscriptions (1).webp",
|
|
||||||
2: "/images/pricing-models/Trial Configuration.webp",
|
|
||||||
3: "/images/pricing-models/Hybrid Plan Builder.webp",
|
|
||||||
4: "/images/pricing-models/Usage Metering.webp",
|
|
||||||
5: "/images/pricing-models/Hybrid Plan Builder (1).webp",
|
|
||||||
6: "/images/pricing-models/Subscriptions (2).webp",
|
|
||||||
7: "/images/pricing-models/Subscriptions (3).webp",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="bg-[#000000] w-full overflow-hidden flex flex-col">
|
|
||||||
<div className="hidden lg:flex w-full items-center justify-between py-12 xl:py-24 relative z-10 px-4 xl:px-22.75">
|
|
||||||
<div className="flex w-full">
|
|
||||||
<div className="">
|
|
||||||
<h2 className="text-[40px] leading-[1.1] font-sans tracking-[-2%] font-normal">
|
|
||||||
<span className="text-[#FFFFFF99]">Any pricing model.</span>{" "}
|
|
||||||
<span className="text-white">Seriously.</span>
|
|
||||||
</h2>
|
|
||||||
<div className="mt-4 text-[16px] font-sans leading-relaxed tracking-[-1%] font-light">
|
|
||||||
<span className="text-[#FFFFFF99]">
|
|
||||||
Configure in the dashboard or CLI.
|
|
||||||
</span>{" "}
|
|
||||||
<span className="text-[#FFFFFF99]">Rollout to all customers, or create custom plans for your largest customers.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="hero-cta">
|
|
||||||
<Link href={"https://docs.useautumn.com/examples/monetary-credits"} target="_blank">
|
|
||||||
<motion.div
|
|
||||||
initial="initial"
|
|
||||||
whileHover="hover"
|
|
||||||
whileTap="tap"
|
|
||||||
className="relative"
|
|
||||||
>
|
|
||||||
<div className="relative overflow-hidden flex items-center cursor-pointer justify-between px-4 py-3.5 md:w-50 font-sans bg-[#9564ff] hover:bg-[#7D46F4] active:bg-[#7D46F4] transition-colors duration-300 whitespace-nowrap">
|
|
||||||
<CTALines />
|
|
||||||
<span className="relative z-10 tracking-tight text-white font-medium">
|
|
||||||
View templates
|
|
||||||
</span>
|
|
||||||
<span className="relative z-10">
|
|
||||||
<IconCTAStart />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex lg:hidden flex-col px-4 py-12 relative z-10">
|
|
||||||
<h2 className="text-[28px] sm:text-[32px] leading-[1.1] font-sans tracking-[-2%] font-normal">
|
|
||||||
<span className="text-[#FFFFFF99]">Any pricing model.</span>{" "}
|
|
||||||
<span className="text-white">Seriously.</span>
|
|
||||||
</h2>
|
|
||||||
<div className="mt-4 text-[15px] font-sans leading-relaxed tracking-[-1%] font-light">
|
|
||||||
<span className="text-[#FFFFFF99]">
|
|
||||||
Configure in the dashboard or CLI.
|
|
||||||
</span>{" "}
|
|
||||||
<span className="text-[#FFFFFF99]">Rollout to all customers, or create custom plans for your largest customers.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t-0 lg:border-t border-[#292929] w-full relative grid grid-cols-1 lg:grid-cols-[60px_220px_220px_1fr] xl:grid-cols-[90px_291px_300px_1fr] auto-rows-auto lg:grid-rows-[200px_260px] xl:grid-rows-[260px_340px]">
|
|
||||||
|
|
||||||
<div className="hidden lg:block border-l border-r border-b border-[#292929] min-h-[120px] lg:min-h-[260px]"></div>
|
|
||||||
<div className="hidden lg:block border-r border-b border-[#292929]"></div>
|
|
||||||
<div className="hidden z-20 lg:block border-r border-b bg-[#0F0F0F] border-[#292929]"></div>
|
|
||||||
<div className="hidden lg:flex lg:row-span-2 items-end justify-center lg:pl-4 lg:pr-4 relative z-10 w-full lg:h-full order-first lg:order-0 mt-0 xl:mt-10.5">
|
|
||||||
<div className="relative z-10 w-full lg:max-w-120 xl:max-w-150 h-auto overflow-hidden">
|
|
||||||
<div className="relative w-full aspect-square">
|
|
||||||
{Object.entries(images).map(([key, src]) => {
|
|
||||||
const isActive = activeTab.model === Number(key);
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
key={key}
|
|
||||||
initial={false}
|
|
||||||
animate={{
|
|
||||||
opacity: isActive ? 1 : 0,
|
|
||||||
scale: isActive ? 1 : 0.98,
|
|
||||||
}}
|
|
||||||
transition={{ duration: 0.25 }}
|
|
||||||
className="absolute inset-0"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={src}
|
|
||||||
alt="Pricing Model"
|
|
||||||
fill
|
|
||||||
sizes="(max-width: 768px) 100vw, 50vw"
|
|
||||||
className="object-contain"
|
|
||||||
priority={key === "0"}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="hidden lg:block border-l border-r border-[#292929]"></div>
|
|
||||||
<div
|
|
||||||
className={`border-r border-[#292929] flex flex-col py-0 lg:py-[32px] z-10 relative bg-[#000000]`}
|
|
||||||
>
|
|
||||||
<ul className="flex flex-col">
|
|
||||||
{sidebarItems.map((item) => {
|
|
||||||
const isActive = activeTab.id === item.id;
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={item.id}
|
|
||||||
onClick={() => setActiveTab(item)}
|
|
||||||
className={`flex flex-col cursor-pointer transition-colors border-b last:border-b-0 lg:border-none border-[#292929] ${
|
|
||||||
isActive ? "lg:bg-transparent bg-[#0f0f0f]" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{/* Mobile Accordion Image */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{isActive && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ height: 0, opacity: 0 }}
|
|
||||||
animate={{ height: "auto", opacity: 1 }}
|
|
||||||
exit={{ height: 0, opacity: 0 }}
|
|
||||||
className="block lg:hidden w-full relative overflow-hidden"
|
|
||||||
>
|
|
||||||
<div className="relative w-full aspect-3/2.5 sm:aspect-square">
|
|
||||||
<div className="absolute bottom-0 left-0 right-0 z-10 flex items-center justify-center px-3 pt-2">
|
|
||||||
<img
|
|
||||||
src={images[item.model]}
|
|
||||||
alt="Pricing Model"
|
|
||||||
className="w-full h-auto max-h-full object-contain object-bottom"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`text-[20px] lg:text-[20px] tracking-[-5%] leading-none lg:leading-[20px] flex items-center gap-2 font-sans px-4 lg:px-[14px] py-5 lg:py-1.5 ${
|
|
||||||
isActive
|
|
||||||
? "text-white lg:text-[#FFFFFF99]"
|
|
||||||
: "text-[#FFFFFF99] lg:text-[#FFFFFF99] lg:opacity-50"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="w-[3px] h-[24px] hidden lg:block">
|
|
||||||
{isActive && (
|
|
||||||
<motion.div
|
|
||||||
layoutId="activeTabIndicator"
|
|
||||||
className="w-[3px] h-[24px] bg-[#9564FF]"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{item.label}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Accordion Description */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{isActive && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ height: 0, opacity: 0 }}
|
|
||||||
animate={{ height: "auto", opacity: 1 }}
|
|
||||||
exit={{ height: 0, opacity: 0 }}
|
|
||||||
className="lg:hidden px-4 border-b border-[#8752FA] overflow-hidden text-[#FFFFFF99] text-[14px] md:text-[16px] lg:text-[14px] leading-[1.4] tracking-[-2%] font-light text-pretty"
|
|
||||||
>
|
|
||||||
<div className="pb-6">{item.desc}</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="hidden lg:flex border-b border-r lg:border-b-0 bg-[#0F0F0F] border-[#292929] flex-col justify-end p-6 z-10 relative">
|
|
||||||
<div className="text-[#FFFFFF99] text-[16px] font-sans leading-relaxed tracking-[-2%] font-light text-pretty">
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
<motion.div
|
|
||||||
key={activeTab.id}
|
|
||||||
initial={{ opacity: 0, y: 5 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: -5 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
{activeTab.desc}
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
263
apps/website/components/pricing-models.tsx
Executable file
263
apps/website/components/pricing-models.tsx
Executable file
@@ -0,0 +1,263 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { CTALines, IconCTAStart } from "@/app/constant";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const sidebarItems = [
|
||||||
|
{
|
||||||
|
id: "subscriptions",
|
||||||
|
label: "Subscriptions",
|
||||||
|
model: 0,
|
||||||
|
desc: "Monthly or yearly plans with feature gating. Upgrades and downgrades handled automatically. Proration included.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "free-trials",
|
||||||
|
label: "Free Trials",
|
||||||
|
model: 2,
|
||||||
|
desc: "Card-required or card-optional trials. Auto-convert to paid. Configurable trial lengths per plan.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "credits",
|
||||||
|
label: "Credits & Top-ups",
|
||||||
|
model: 1,
|
||||||
|
desc: "Prepaid credits that features draw from. One-time purchases or auto-refill. Set minimum thresholds.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "usage",
|
||||||
|
label: "Usage-Based",
|
||||||
|
model: 4,
|
||||||
|
desc: "Pay for what you use. Real-time metering, overage handling, usage resets. Combine with base\u00a0subscriptions.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "seat",
|
||||||
|
label: "Seat-Based",
|
||||||
|
model: 6,
|
||||||
|
desc: "Per-user pricing with seat limits. Add or remove seats dynamically. Automatic proration.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "hybrid",
|
||||||
|
label: "Hybrid Models",
|
||||||
|
model: 5,
|
||||||
|
desc: "Mix subscriptions, usage, credits, and seats in one plan. Example: $50/month + $0.02/token + 5 seats included.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "rollovers",
|
||||||
|
label: "Rollovers & Expirations",
|
||||||
|
model: 7,
|
||||||
|
desc: "Roll credits to next period or expire after X days. Configure per plan or per feature.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "enterprise",
|
||||||
|
label: "Custom Enterprise",
|
||||||
|
model: 3,
|
||||||
|
desc: "One-off pricing for large customers. Unique limits, custom billing cycles, manual overrides—all in the dashboard.",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const images: Record<(typeof sidebarItems)[number]["model"], string> = {
|
||||||
|
0: "/images/pricing-models/Subscriptions.webp",
|
||||||
|
1: "/images/pricing-models/Subscriptions (1).webp",
|
||||||
|
2: "/images/pricing-models/Trial Configuration.webp",
|
||||||
|
3: "/images/pricing-models/Hybrid Plan Builder.webp",
|
||||||
|
4: "/images/pricing-models/Usage Metering.webp",
|
||||||
|
5: "/images/pricing-models/Hybrid Plan Builder (1).webp",
|
||||||
|
6: "/images/pricing-models/Subscriptions (2).webp",
|
||||||
|
7: "/images/pricing-models/Subscriptions (3).webp",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PricingModels() {
|
||||||
|
const [activeTab, setActiveTab] = useState<(typeof sidebarItems)[number]>(
|
||||||
|
sidebarItems[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-[#000000] w-full overflow-hidden flex flex-col">
|
||||||
|
<div className="hidden lg:flex w-full items-center justify-between py-12 xl:py-24 relative z-10 px-4 xl:px-22.75">
|
||||||
|
<div className="flex w-full">
|
||||||
|
<div className="">
|
||||||
|
<h2 className="text-[40px] leading-[1.1] font-sans tracking-[-2%] font-normal">
|
||||||
|
<span className="text-[#FFFFFF99]">Any pricing model.</span>{" "}
|
||||||
|
<span className="text-white">Seriously.</span>
|
||||||
|
</h2>
|
||||||
|
<div className="mt-4 text-[16px] font-sans leading-relaxed tracking-[-1%] font-light">
|
||||||
|
<span className="text-[#FFFFFF99]">
|
||||||
|
Configure in the dashboard or CLI.
|
||||||
|
</span>{" "}
|
||||||
|
<span className="text-[#FFFFFF99]">
|
||||||
|
Rollout to all customers, or create custom plans for your
|
||||||
|
largest customers.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="hero-cta">
|
||||||
|
<Link
|
||||||
|
href={"https://docs.useautumn.com/examples/monetary-credits"}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial="initial"
|
||||||
|
whileHover="hover"
|
||||||
|
whileTap="tap"
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<div className="relative overflow-hidden flex items-center cursor-pointer justify-between px-4 py-3.5 md:w-50 font-sans bg-[#9564ff] hover:bg-[#7D46F4] active:bg-[#7D46F4] transition-colors duration-300 whitespace-nowrap">
|
||||||
|
<CTALines />
|
||||||
|
<span className="relative z-10 tracking-tight text-white font-medium">
|
||||||
|
View templates
|
||||||
|
</span>
|
||||||
|
<span className="relative z-10">
|
||||||
|
<IconCTAStart />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex lg:hidden flex-col px-4 py-12 relative z-10">
|
||||||
|
<h2 className="text-[28px] sm:text-[32px] leading-[1.1] font-sans tracking-[-2%] font-normal">
|
||||||
|
<span className="text-[#FFFFFF99]">Any pricing model.</span>{" "}
|
||||||
|
<span className="text-white">Seriously.</span>
|
||||||
|
</h2>
|
||||||
|
<div className="mt-4 text-[15px] font-sans leading-relaxed tracking-[-1%] font-light">
|
||||||
|
<span className="text-[#FFFFFF99]">
|
||||||
|
Configure in the dashboard or CLI.
|
||||||
|
</span>{" "}
|
||||||
|
<span className="text-[#FFFFFF99]">
|
||||||
|
Rollout to all customers, or create custom plans for your largest
|
||||||
|
customers.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t-0 lg:border-t border-[#292929] w-full relative grid grid-cols-1 lg:grid-cols-[60px_220px_220px_1fr] xl:grid-cols-[90px_291px_300px_1fr] auto-rows-auto lg:grid-rows-[200px_260px] xl:grid-rows-[260px_340px]">
|
||||||
|
<div className="hidden lg:block border-l border-r border-b border-[#292929] min-h-[120px] lg:min-h-[260px]"></div>
|
||||||
|
<div className="hidden lg:block border-r border-b border-[#292929]"></div>
|
||||||
|
<div className="hidden z-20 lg:block border-r border-b bg-[#0F0F0F] border-[#292929]"></div>
|
||||||
|
<div className="hidden lg:flex lg:row-span-2 items-end justify-center lg:pl-4 lg:pr-4 relative z-10 w-full lg:h-full order-first lg:order-0 mt-0 xl:mt-10.5">
|
||||||
|
<div className="relative z-10 w-full lg:max-w-120 xl:max-w-150 h-auto overflow-hidden">
|
||||||
|
<div className="relative w-full aspect-square">
|
||||||
|
{Object.entries(images).map(([key, src]) => {
|
||||||
|
const isActive = activeTab.model === Number(key);
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={key}
|
||||||
|
initial={false}
|
||||||
|
animate={{
|
||||||
|
opacity: isActive ? 1 : 0,
|
||||||
|
scale: isActive ? 1 : 0.98,
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.25 }}
|
||||||
|
className="absolute inset-0"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt="Pricing Model"
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 768px) 100vw, 50vw"
|
||||||
|
className="object-contain"
|
||||||
|
priority={key === "0"}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="hidden lg:block border-l border-r border-[#292929]"></div>
|
||||||
|
<div className="relative z-10 flex flex-col border-r border-[#292929] bg-[#000000] py-0 lg:py-[32px]">
|
||||||
|
<ul className="flex flex-col">
|
||||||
|
{sidebarItems.map((item) => {
|
||||||
|
const isActive = activeTab.id === item.id;
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => setActiveTab(item)}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-pointer flex-col border-b border-[#292929] transition-colors last:border-b-0 lg:border-none",
|
||||||
|
isActive && "bg-[#0f0f0f] lg:bg-transparent",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Mobile Accordion Image */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{isActive && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: "auto", opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
className="block lg:hidden w-full relative overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="relative w-full aspect-3/2.5 sm:aspect-square">
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 z-10 flex items-center justify-center px-3 pt-2">
|
||||||
|
<img
|
||||||
|
src={images[item.model]}
|
||||||
|
alt="Pricing Model"
|
||||||
|
className="w-full h-auto max-h-full object-contain object-bottom"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 px-4 py-5 font-sans text-[20px] leading-none tracking-[-5%] lg:px-[14px] lg:py-1.5 lg:text-[20px] lg:leading-[20px]",
|
||||||
|
isActive
|
||||||
|
? "text-white lg:text-[#FFFFFF99]"
|
||||||
|
: "text-[#FFFFFF99] lg:text-[#FFFFFF99] lg:opacity-50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="w-[3px] h-[24px] hidden lg:block">
|
||||||
|
{isActive && (
|
||||||
|
<motion.div
|
||||||
|
layoutId="activeTabIndicator"
|
||||||
|
className="w-[3px] h-[24px] bg-[#9564FF]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Accordion Description */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{isActive && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: "auto", opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
className="lg:hidden px-4 border-b border-[#8752FA] overflow-hidden text-[#FFFFFF99] text-[14px] md:text-[16px] lg:text-[14px] leading-[1.4] tracking-[-2%] font-light text-pretty"
|
||||||
|
>
|
||||||
|
<div className="pb-6">{item.desc}</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="hidden lg:flex border-b border-r lg:border-b-0 bg-[#0F0F0F] border-[#292929] flex-col justify-end p-6 z-10 relative">
|
||||||
|
<div className="text-[#FFFFFF99] text-[16px] font-sans leading-relaxed tracking-[-2%] font-light text-pretty">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={activeTab.id}
|
||||||
|
initial={{ opacity: 0, y: 5 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -5 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
{activeTab.desc}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { IconArrowRightSmall, IconTick } from "@/app/constant";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const plans = [
|
|
||||||
{
|
|
||||||
name: "FREE",
|
|
||||||
price: "0",
|
|
||||||
description: "Perfect while finding PMF. Everything you need to start.",
|
|
||||||
features: [
|
|
||||||
"Up to 8K monthly revenue",
|
|
||||||
"All core features",
|
|
||||||
"Community support",
|
|
||||||
],
|
|
||||||
buttonText: "Get started",
|
|
||||||
href: "https://app.useautumn.com/sign-in",
|
|
||||||
isPro: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "PRO",
|
|
||||||
price: "375",
|
|
||||||
description: "For teams scaling with real usage-based pricing.",
|
|
||||||
features: [
|
|
||||||
"Up to 50K monthly revenue",
|
|
||||||
"Priority support",
|
|
||||||
"Custom plans",
|
|
||||||
"Usage analytics",
|
|
||||||
],
|
|
||||||
buttonText: "Start with Pro",
|
|
||||||
href: "https://app.useautumn.com/sign-in",
|
|
||||||
isPro: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ENTERPRISE",
|
|
||||||
price: "Custom",
|
|
||||||
description: "For compliance, scale, or custom requirements.",
|
|
||||||
features: [
|
|
||||||
"Dedicated support",
|
|
||||||
"Multi-region",
|
|
||||||
"Compliance assistance",
|
|
||||||
],
|
|
||||||
buttonText: "Book a call",
|
|
||||||
href: "https://cal.com/ayrod/a?user=ayrod",
|
|
||||||
isPro: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Pricing() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div id="pricing" className="min-h-screen relative flex w-full lg:w-[calc(100%+calc(var(--page-pad)*2))] lg:-ml-(--page-pad) items-center justify-center lg:py-24 pb-3">
|
|
||||||
{/* Desktop Background */}
|
|
||||||
<Image
|
|
||||||
src="/images/pricing/pricing.webp"
|
|
||||||
alt="pricing background desktop"
|
|
||||||
fill
|
|
||||||
className="object-cover absolute z-10 lg:z-50 hidden md:block"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
{/* Mobile Background */}
|
|
||||||
<Image
|
|
||||||
src="/images/pricing/pricing-mob.webp"
|
|
||||||
alt="pricing background mobile"
|
|
||||||
fill
|
|
||||||
className="object-cover absolute z-10 lg:z-50 block md:hidden"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<div className="relative z-20 lg:z-60 w-full pt-0 lg:pt-8 pl-4 lg:pl-[calc(var(--page-pad)+22px)] xl:pl-[calc(var(--page-pad)+90px)] pr-4 lg:pr-[calc(var(--page-pad)+22px)] xl:pr-[calc(var(--page-pad)+90px)]">
|
|
||||||
<div className="lg:bg-black text-white lg:border lg:border-[#292929] flex flex-col gap-6 lg:gap-0 border-none">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="bg-black lg:bg-transparent -mx-4 px-4 lg:mx-0 lg:px-8 py-10 lg:py-8 border-b-0 lg:border-b border-[#292929]">
|
|
||||||
<h1 className="text-[30px] leading-[32px] md:leading-[40px] md:text-3xl lg:text-[40px] tracking-[-4%] text-white font-normal w-[85%] md:w-full font-sans">
|
|
||||||
Start free. Scale with confidence.
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pricing Columns */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 relative z-60 gap-8 lg:gap-0">
|
|
||||||
{plans.map((plan, index) => (
|
|
||||||
<div
|
|
||||||
key={plan.name}
|
|
||||||
className={`relative flex flex-col bg-black lg:bg-transparent border lg:border-0 border-[#292929] ${index === 0 ? "lg:border-r" : ""
|
|
||||||
} ${index === 1 ? "lg:border-r" : ""
|
|
||||||
} ${index === 2 ? "md:col-span-2 lg:col-span-1 md:w-[calc(50%-16px)] md:justify-self-center lg:w-full lg:justify-self-auto" : ""}`}
|
|
||||||
>
|
|
||||||
{plan.isPro && (
|
|
||||||
<div className="hidden lg:block absolute -inset-px z-20 pointer-events-none border border-transparent [border-image:linear-gradient(to_bottom,#A175FF,#000000)_1]"></div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{plan.isPro && (
|
|
||||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 z-30 bg-black text-[14px] font-mono tracking-[-1%] text-white px-3 py-1.5 md:p-2.5 border border-[#A175FF]">
|
|
||||||
RECOMMENDED
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
"flex flex-col h-full px-4 md:px-5 lg:px-8 pt-10 md:pt-10 lg:pt-15 pb-6 md:pb-0 relative z-10 w-full overflow-hidden"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="mb-6">
|
|
||||||
<span className="inline-block px-3 py-1 bg-[#8752FA]/20 text-[#9564FF] text-[12px] md:text-[13px] lg:text-[16px] font-mono tracking-[-2%] uppercase mb-3 md:mb-2">
|
|
||||||
{plan.name}
|
|
||||||
</span>
|
|
||||||
<div className="flex items-start gap-1 mb-2">
|
|
||||||
{plan.price !== "Custom" && (
|
|
||||||
<span className="text-sm md:text-base lg:text-xl text-white mt-1.5 md:mt-0">
|
|
||||||
$
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="text-[30px] md:text-[40px] leading-[44px] lg:text-5xl text-white font-normal tracking-[-2%] font-sans">
|
|
||||||
{plan.price}
|
|
||||||
</span>
|
|
||||||
{plan.price !== "Custom" && (
|
|
||||||
<span className="text-[#FFFFFF99] font-light self-end text-[13px] md:text-sm lg:text-base tracking-[-2%] mb-1.5 md:mb-0">
|
|
||||||
/month
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="md:text-white font-light md:font-extralight text-[13px] md:text-[16px] tracking-[-2%] leading-[18px] md:leading-5 w-full md:w-[95%] text-pretty">
|
|
||||||
{plan.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t border-[#27272A] w-full mb-6"></div>
|
|
||||||
|
|
||||||
<ul className="flex flex-col gap-3 md:gap-4 grow mb-10 md:mb-16.5">
|
|
||||||
{plan.features.map((feature, i) => (
|
|
||||||
<li key={i} className="flex items-start gap-3">
|
|
||||||
<IconTick className="w-4 h-4 mt-[3px] shrink-0" />
|
|
||||||
<span className="md:text-white font-light text-[14px] md:text-[16px]">
|
|
||||||
{feature}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div className="flex w-full mb-0 md:mb-8 mt-auto mx-auto pt-4 md:pt-0">
|
|
||||||
<motion.div
|
|
||||||
initial="initial"
|
|
||||||
whileHover="hover"
|
|
||||||
whileTap="hover"
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={plan.href || "#"}
|
|
||||||
target="_blank"
|
|
||||||
className="group cursor-pointer w-full flex items-center md:items-stretch justify-between transition-colors duration-300 border bg-transparent py-1 md:py-0 hover:bg-[#7641E8] active:bg-[#7641E8] border-[#292929]"
|
|
||||||
>
|
|
||||||
<span className="text-white text-[16px] md:text-[18px] pl-4 flex items-center tracking-[-1%] font-sans">
|
|
||||||
{plan.buttonText}
|
|
||||||
</span>
|
|
||||||
<div className="flex items-center justify-center w-8 h-8 md:h-auto md:w-[26px] aspect-square transition-colors duration-300 m-1.5 md:m-2.5 bg-[#514D5A] text-white group-hover:bg-white group-hover:text-[#8752FA] group-active:bg-white group-active:text-[#8752FA]">
|
|
||||||
<IconArrowRightSmall className="w-4 h-4" />
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="px-6 md:px-8 py-5 md:py-6 lg:mt-[32px] border border-[#292929] lg:border-x-0 lg:border-b-0 text-left md:text-center bg-black lg:bg-transparent">
|
|
||||||
<p className="text-white text-pretty font-light leading-[18px] tracking-[-2%] md:font-extralight text-[16px] md:leading-[1.6] text-wrap-balance">
|
|
||||||
Autumn is built on top of Stripe billing, so Stripe fees (0.7%
|
|
||||||
and 2.9% + 30¢) still apply.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
178
apps/website/components/pricing.tsx
Executable file
178
apps/website/components/pricing.tsx
Executable file
@@ -0,0 +1,178 @@
|
|||||||
|
"use client";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { IconArrowRightSmall, IconTick } from "@/app/constant";
|
||||||
|
|
||||||
|
const plans = [
|
||||||
|
{
|
||||||
|
name: "FREE",
|
||||||
|
price: "0",
|
||||||
|
description: "Perfect while finding PMF. Everything you need to start.",
|
||||||
|
features: [
|
||||||
|
"Up to 8K monthly revenue",
|
||||||
|
"All core features",
|
||||||
|
"Community support",
|
||||||
|
],
|
||||||
|
buttonText: "Get started",
|
||||||
|
href: "https://app.useautumn.com/sign-in",
|
||||||
|
isPro: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PRO",
|
||||||
|
price: "375",
|
||||||
|
description: "For teams scaling with real usage-based pricing.",
|
||||||
|
features: [
|
||||||
|
"Up to 50K monthly revenue",
|
||||||
|
"Priority support",
|
||||||
|
"Custom plans",
|
||||||
|
"Usage analytics",
|
||||||
|
],
|
||||||
|
buttonText: "Start with Pro",
|
||||||
|
href: "https://app.useautumn.com/sign-in",
|
||||||
|
isPro: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ENTERPRISE",
|
||||||
|
price: "Custom",
|
||||||
|
description: "For compliance, scale, or custom requirements.",
|
||||||
|
features: ["Dedicated support", "Multi-region", "Compliance assistance"],
|
||||||
|
buttonText: "Book a call",
|
||||||
|
href: "https://cal.com/ayrod/a?user=ayrod",
|
||||||
|
isPro: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Pricing() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
id="pricing"
|
||||||
|
className="min-h-screen relative flex w-full lg:w-[calc(100%+calc(var(--page-pad)*2))] lg:-ml-(--page-pad) items-center justify-center lg:py-24 pb-3"
|
||||||
|
>
|
||||||
|
{/* Desktop Background */}
|
||||||
|
<Image
|
||||||
|
src="/images/pricing/pricing.webp"
|
||||||
|
alt="pricing background desktop"
|
||||||
|
fill
|
||||||
|
className="object-cover absolute z-10 lg:z-50 hidden md:block"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
{/* Mobile Background */}
|
||||||
|
<Image
|
||||||
|
src="/images/pricing/pricing-mob.webp"
|
||||||
|
alt="pricing background mobile"
|
||||||
|
fill
|
||||||
|
className="object-cover absolute z-10 lg:z-50 block md:hidden"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<div className="relative z-20 lg:z-60 w-full pt-0 lg:pt-8 pl-4 lg:pl-[calc(var(--page-pad)+22px)] xl:pl-[calc(var(--page-pad)+90px)] pr-4 lg:pr-[calc(var(--page-pad)+22px)] xl:pr-[calc(var(--page-pad)+90px)]">
|
||||||
|
<div className="lg:bg-black text-white lg:border lg:border-[#292929] flex flex-col gap-6 lg:gap-0 border-none">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-black lg:bg-transparent -mx-4 px-4 lg:mx-0 lg:px-8 py-10 lg:py-8 border-b-0 lg:border-b border-[#292929]">
|
||||||
|
<h1 className="text-[30px] leading-[32px] md:leading-[40px] md:text-3xl lg:text-[40px] tracking-[-4%] text-white font-normal w-[85%] md:w-full font-sans">
|
||||||
|
Start free. Scale with confidence.
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pricing Columns */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 relative z-60 gap-8 lg:gap-0">
|
||||||
|
{plans.map((plan, index) => (
|
||||||
|
<div
|
||||||
|
key={plan.name}
|
||||||
|
className={`relative flex flex-col bg-black lg:bg-transparent border lg:border-0 border-[#292929] ${
|
||||||
|
index === 0 ? "lg:border-r" : ""
|
||||||
|
} ${
|
||||||
|
index === 1 ? "lg:border-r" : ""
|
||||||
|
} ${index === 2 ? "md:col-span-2 lg:col-span-1 md:w-[calc(50%-16px)] md:justify-self-center lg:w-full lg:justify-self-auto" : ""}`}
|
||||||
|
>
|
||||||
|
{plan.isPro && (
|
||||||
|
<div className="hidden lg:block absolute -inset-px z-20 pointer-events-none border border-transparent [border-image:linear-gradient(to_bottom,#A175FF,#000000)_1]"></div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{plan.isPro && (
|
||||||
|
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 z-30 bg-black text-[14px] font-mono tracking-[-1%] text-white px-3 py-1.5 md:p-2.5 border border-[#A175FF]">
|
||||||
|
RECOMMENDED
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"flex flex-col h-full px-4 md:px-5 lg:px-8 pt-10 md:pt-10 lg:pt-15 pb-6 md:pb-0 relative z-10 w-full overflow-hidden"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="mb-6">
|
||||||
|
<span className="inline-block px-3 py-1 bg-[#8752FA]/20 text-[#9564FF] text-[12px] md:text-[13px] lg:text-[16px] font-mono tracking-[-2%] uppercase mb-3 md:mb-2">
|
||||||
|
{plan.name}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-start gap-1 mb-2">
|
||||||
|
{plan.price !== "Custom" && (
|
||||||
|
<span className="text-sm md:text-base lg:text-xl text-white mt-1.5 md:mt-0">
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-[30px] md:text-[40px] leading-[44px] lg:text-5xl text-white font-normal tracking-[-2%] font-sans">
|
||||||
|
{plan.price}
|
||||||
|
</span>
|
||||||
|
{plan.price !== "Custom" && (
|
||||||
|
<span className="text-[#FFFFFF99] font-light self-end text-[13px] md:text-sm lg:text-base tracking-[-2%] mb-1.5 md:mb-0">
|
||||||
|
/month
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="md:text-white font-light md:font-extralight text-[13px] md:text-[16px] tracking-[-2%] leading-[18px] md:leading-5 w-full md:w-[95%] text-pretty">
|
||||||
|
{plan.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-[#27272A] w-full mb-6"></div>
|
||||||
|
|
||||||
|
<ul className="flex flex-col gap-3 md:gap-4 grow mb-10 md:mb-16.5">
|
||||||
|
{plan.features.map((feature, i) => (
|
||||||
|
<li key={i} className="flex items-start gap-3">
|
||||||
|
<IconTick className="w-4 h-4 mt-[3px] shrink-0" />
|
||||||
|
<span className="md:text-white font-light text-[14px] md:text-[16px]">
|
||||||
|
{feature}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="flex w-full mb-0 md:mb-8 mt-auto mx-auto pt-4 md:pt-0">
|
||||||
|
<motion.div
|
||||||
|
initial="initial"
|
||||||
|
whileHover="hover"
|
||||||
|
whileTap="hover"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={plan.href || "#"}
|
||||||
|
target="_blank"
|
||||||
|
className="group cursor-pointer w-full flex items-center md:items-stretch justify-between transition-colors duration-300 border bg-transparent py-1 md:py-0 hover:bg-[#7641E8] active:bg-[#7641E8] border-[#292929]"
|
||||||
|
>
|
||||||
|
<span className="text-white text-[16px] md:text-[18px] pl-4 flex items-center tracking-[-1%] font-sans">
|
||||||
|
{plan.buttonText}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center justify-center w-8 h-8 md:h-auto md:w-[26px] aspect-square transition-colors duration-300 m-1.5 md:m-2.5 bg-[#514D5A] text-white group-hover:bg-white group-hover:text-[#8752FA] group-active:bg-white group-active:text-[#8752FA]">
|
||||||
|
<IconArrowRightSmall className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-6 md:px-8 py-5 md:py-6 lg:mt-[32px] border border-[#292929] lg:border-x-0 lg:border-b-0 text-left md:text-center bg-black lg:bg-transparent">
|
||||||
|
<p className="text-white text-pretty font-light leading-[18px] tracking-[-2%] md:font-extralight text-[16px] md:leading-[1.6] text-wrap-balance">
|
||||||
|
Autumn is built on top of Stripe billing, so Stripe fees (0.7%
|
||||||
|
and 2.9% + 30¢) still apply.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
const IMAGE_SOURCES = [
|
|
||||||
"/images/issues/Slack.svg",
|
|
||||||
"/images/issues/Frame%202147243001.svg",
|
|
||||||
"/images/issues/Frame%202147242729.svg",
|
|
||||||
"/images/issues/Slack-1.svg",
|
|
||||||
"/images/issues/Frame%202147243134.svg",
|
|
||||||
"/images/issues/Slack-2.svg",
|
|
||||||
"/images/issues/Frame%202147243146.svg",
|
|
||||||
"/images/issues/Stripe.svg",
|
|
||||||
];
|
|
||||||
|
|
||||||
const ITEM_COUNT = IMAGE_SOURCES.length;
|
|
||||||
const PAUSE_SECONDS = 2;
|
|
||||||
const SCROLL_SECONDS = 0.5;
|
|
||||||
const TOTAL_SECONDS = ITEM_COUNT * (PAUSE_SECONDS + SCROLL_SECONDS);
|
|
||||||
|
|
||||||
function buildKeyframes() {
|
|
||||||
const stepPct = 100 / ITEM_COUNT;
|
|
||||||
const pauseRatio = PAUSE_SECONDS / (PAUSE_SECONDS + SCROLL_SECONDS);
|
|
||||||
const perItemTranslate = 50 / ITEM_COUNT;
|
|
||||||
|
|
||||||
const frames = ["0% { transform: translateY(0); }"];
|
|
||||||
|
|
||||||
for (let i = 0; i < ITEM_COUNT; i++) {
|
|
||||||
const holdY = i * perItemTranslate;
|
|
||||||
const nextY = (i + 1) * perItemTranslate;
|
|
||||||
const pauseEndPct = (i * stepPct + stepPct * pauseRatio).toFixed(4);
|
|
||||||
const stepEndPct = ((i + 1) * stepPct).toFixed(4);
|
|
||||||
|
|
||||||
const holdTransform =
|
|
||||||
holdY === 0 ? "translateY(0)" : `translateY(-${holdY.toFixed(4)}%)`;
|
|
||||||
|
|
||||||
frames.push(`${pauseEndPct}% { transform: ${holdTransform}; }`);
|
|
||||||
frames.push(
|
|
||||||
`${stepEndPct}% { transform: translateY(-${nextY.toFixed(4)}%); }`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return frames.join("\n ");
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyframesCSS = buildKeyframes();
|
|
||||||
|
|
||||||
export default function ProblemAnimation() {
|
|
||||||
return (
|
|
||||||
<div className="relative w-full h-140 md:h-175 lg:h-175 xl:h-full bg-[#080808] overflow-hidden">
|
|
||||||
<style>{`
|
|
||||||
@keyframes carouselUp {
|
|
||||||
${keyframesCSS}
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
|
|
||||||
<div className="pointer-events-none absolute inset-x-0 top-0 h-20 z-10 bg-gradient-to-b from-[#080808] to-transparent" />
|
|
||||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-20 z-10 bg-gradient-to-t from-[#080808] to-transparent" />
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="flex flex-col items-center will-change-transform"
|
|
||||||
style={{
|
|
||||||
animation: `carouselUp ${TOTAL_SECONDS}s ease-in-out infinite`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{[...IMAGE_SOURCES, ...IMAGE_SOURCES].map((src, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="flex items-center justify-center shrink-0 w-full h-[80px] md:h-[140px]"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={src}
|
|
||||||
alt=""
|
|
||||||
className="block max-h-full max-w-[90%] object-contain"
|
|
||||||
draggable={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
80
apps/website/components/problem-animation.tsx
Executable file
80
apps/website/components/problem-animation.tsx
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
const IMAGE_SOURCES = [
|
||||||
|
"/images/issues/Slack.svg",
|
||||||
|
"/images/issues/Frame%202147243001.svg",
|
||||||
|
"/images/issues/Frame%202147242729.svg",
|
||||||
|
"/images/issues/Slack-1.svg",
|
||||||
|
"/images/issues/Frame%202147243134.svg",
|
||||||
|
"/images/issues/Slack-2.svg",
|
||||||
|
"/images/issues/Frame%202147243146.svg",
|
||||||
|
"/images/issues/Stripe.svg",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ITEM_COUNT = IMAGE_SOURCES.length;
|
||||||
|
const PAUSE_SECONDS = 2;
|
||||||
|
const SCROLL_SECONDS = 0.5;
|
||||||
|
const TOTAL_SECONDS = ITEM_COUNT * (PAUSE_SECONDS + SCROLL_SECONDS);
|
||||||
|
|
||||||
|
function buildKeyframes() {
|
||||||
|
const stepPct = 100 / ITEM_COUNT;
|
||||||
|
const pauseRatio = PAUSE_SECONDS / (PAUSE_SECONDS + SCROLL_SECONDS);
|
||||||
|
const perItemTranslate = 50 / ITEM_COUNT;
|
||||||
|
|
||||||
|
const frames = ["0% { transform: translateY(0); }"];
|
||||||
|
|
||||||
|
for (let i = 0; i < ITEM_COUNT; i++) {
|
||||||
|
const holdY = i * perItemTranslate;
|
||||||
|
const nextY = (i + 1) * perItemTranslate;
|
||||||
|
const pauseEndPct = (i * stepPct + stepPct * pauseRatio).toFixed(4);
|
||||||
|
const stepEndPct = ((i + 1) * stepPct).toFixed(4);
|
||||||
|
|
||||||
|
const holdTransform =
|
||||||
|
holdY === 0 ? "translateY(0)" : `translateY(-${holdY.toFixed(4)}%)`;
|
||||||
|
|
||||||
|
frames.push(`${pauseEndPct}% { transform: ${holdTransform}; }`);
|
||||||
|
frames.push(
|
||||||
|
`${stepEndPct}% { transform: translateY(-${nextY.toFixed(4)}%); }`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames.join("\n ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyframesCSS = buildKeyframes();
|
||||||
|
|
||||||
|
export default function ProblemAnimation() {
|
||||||
|
return (
|
||||||
|
<div className="relative w-full h-140 md:h-175 lg:h-175 xl:h-full bg-[#080808] overflow-hidden">
|
||||||
|
<style>{`
|
||||||
|
@keyframes carouselUp {
|
||||||
|
${keyframesCSS}
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
<div className="pointer-events-none absolute inset-x-0 top-0 h-20 z-10 bg-gradient-to-b from-[#080808] to-transparent" />
|
||||||
|
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-20 z-10 bg-gradient-to-t from-[#080808] to-transparent" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-center will-change-transform"
|
||||||
|
style={{
|
||||||
|
animation: `carouselUp ${TOTAL_SECONDS}s ease-in-out infinite`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[...IMAGE_SOURCES, ...IMAGE_SOURCES].map((src, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="flex items-center justify-center shrink-0 w-full h-[80px] md:h-[140px]"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt=""
|
||||||
|
className="block max-h-full max-w-[90%] object-contain"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
100
apps/website/components/problem.tsx
Executable file
100
apps/website/components/problem.tsx
Executable file
@@ -0,0 +1,100 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { ProblemBgSvg } from "../app/constant";
|
||||||
|
|
||||||
|
const ProblemAnimation = dynamic(() => import("./problem-animation"), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Problem() {
|
||||||
|
return (
|
||||||
|
<section className="bg-[#000000]">
|
||||||
|
{/* <div
|
||||||
|
className="w-full py-5 border-l border-r border-[#292929]"
|
||||||
|
style={{ backgroundColor: "rgba(229, 46, 185, 0.12)" }}
|
||||||
|
>
|
||||||
|
<div className="flex items-start md:items-center gap-2 pl-4 xl:pl-[90px]">
|
||||||
|
<Image
|
||||||
|
src="/images/problems/warning.svg"
|
||||||
|
width={18}
|
||||||
|
height={16}
|
||||||
|
alt="warning"
|
||||||
|
className="shrink-0 mt-1 md:mt-0"
|
||||||
|
style={{ width: "auto", height: "auto" }}
|
||||||
|
/>
|
||||||
|
<p className="text-[#D942B5] font-light tracking-[-2%] text-[14px] md:text-[16px] xl:text-[16px] leading-[18px] md:leading-[20px]">
|
||||||
|
<span className="font-light mr-1">The hidden complexity:</span>
|
||||||
|
Payment processors move money. They don'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,229 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useGSAP } from "@gsap/react";
|
|
||||||
import gsap from "gsap";
|
|
||||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
gsap.registerPlugin(ScrollTrigger);
|
|
||||||
|
|
||||||
const SCRAMBLE_CHARS =
|
|
||||||
"!@#$%^&*()_+-=[]{}|;:,.<>?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
||||||
|
|
||||||
function scrambleText(
|
|
||||||
el,
|
|
||||||
finalText,
|
|
||||||
{ charDuration = 60, cycleSpeed = 30 } = {},
|
|
||||||
) {
|
|
||||||
const len = finalText.length;
|
|
||||||
let settled = 0;
|
|
||||||
const timeouts = [];
|
|
||||||
|
|
||||||
const cycleInterval = setInterval(() => {
|
|
||||||
const display = finalText
|
|
||||||
.split("")
|
|
||||||
.map((ch, i) =>
|
|
||||||
i < settled
|
|
||||||
? ch
|
|
||||||
: SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)],
|
|
||||||
)
|
|
||||||
.join("");
|
|
||||||
el.textContent = display;
|
|
||||||
}, cycleSpeed);
|
|
||||||
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
const t = setTimeout(
|
|
||||||
() => {
|
|
||||||
settled = i + 1;
|
|
||||||
if (settled === len) {
|
|
||||||
clearInterval(cycleInterval);
|
|
||||||
el.textContent = finalText;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(i + 1) * charDuration,
|
|
||||||
);
|
|
||||||
timeouts.push(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
return function cleanup() {
|
|
||||||
clearInterval(cycleInterval);
|
|
||||||
timeouts.forEach(clearTimeout);
|
|
||||||
el.textContent = finalText;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const cards = [
|
|
||||||
{
|
|
||||||
bg: "#A175FF",
|
|
||||||
icon: "/images/production/uptime2.svg",
|
|
||||||
metric: "1 Billion +",
|
|
||||||
label: "Monthly events",
|
|
||||||
description:
|
|
||||||
"Autumn handles billions of billing events monthly for some of your favorite apps.",
|
|
||||||
clipart: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bg: "#FFE8FA",
|
|
||||||
icon: "/images/production/latency.svg",
|
|
||||||
metric: "<50ms",
|
|
||||||
label: "US latency",
|
|
||||||
description:
|
|
||||||
"Every billing check resolves in under 50ms. Your users never wait for a gate.",
|
|
||||||
clipart: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bg: "#D698FF",
|
|
||||||
icon: "/images/production/uptiime.svg",
|
|
||||||
metric: "10 minutes",
|
|
||||||
label: "Support SLA",
|
|
||||||
description:
|
|
||||||
"Billing is critical. We're loved for our rapid response times.",
|
|
||||||
clipart: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bg: "#F55DD0",
|
|
||||||
icon: "/images/production/churn.svg",
|
|
||||||
metric: "Zero",
|
|
||||||
label: "Churn rate",
|
|
||||||
description: "Unless their company shuts down, our customers stay with us.",
|
|
||||||
clipart: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function ProductionScale() {
|
|
||||||
const containerRef = useRef(null);
|
|
||||||
const scrambleCleanups = useRef([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const cleanups = scrambleCleanups.current;
|
|
||||||
return () => {
|
|
||||||
cleanups.forEach((fn) => fn());
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useGSAP(
|
|
||||||
() => {
|
|
||||||
const isMobile = window.innerWidth < 768;
|
|
||||||
const cardY = isMobile ? 16 : 30;
|
|
||||||
|
|
||||||
gsap.set(".ps-card", { opacity: 0, y: cardY, scale: 0.97 });
|
|
||||||
|
|
||||||
const tl = gsap.timeline({
|
|
||||||
scrollTrigger: {
|
|
||||||
trigger: ".ps-section",
|
|
||||||
start: "top 75%",
|
|
||||||
},
|
|
||||||
defaults: { overwrite: "auto" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const cardEls = gsap.utils.toArray(".ps-card");
|
|
||||||
cardEls.forEach((card, i) => {
|
|
||||||
tl.to(
|
|
||||||
card,
|
|
||||||
{
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
scale: 1,
|
|
||||||
duration: 0.9,
|
|
||||||
ease: "power3.out",
|
|
||||||
onComplete() {
|
|
||||||
const metricEl = card.querySelector(".ps-metric");
|
|
||||||
|
|
||||||
if (!metricEl) return;
|
|
||||||
const finalText = metricEl.dataset.final;
|
|
||||||
const delayTimer = setTimeout(() => {
|
|
||||||
const cleanup = scrambleText(metricEl, finalText);
|
|
||||||
scrambleCleanups.current.push(cleanup);
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
scrambleCleanups.current.push(() => clearTimeout(delayTimer));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
0.3 + i * 0.15,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ scope: containerRef },
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={containerRef} className="overflow-hidden">
|
|
||||||
<section className="ps-section flex flex-col lg:flex-row items-start justify-between py-12 lg:py-16 gap-12 lg:gap-0 bg-[#0F0F0F]">
|
|
||||||
<div className="flex px-4 xl:pl-22.5 lg:pr-0 flex-col my-auto gap-4 lg:gap-6 pt-2 w-full lg:w-auto">
|
|
||||||
<div className="leading-none lg:leading-10">
|
|
||||||
<p className="text-[#FFFFFF99] tracking-[-4%] text-[30px] lg:text-[40px] font-normal">
|
|
||||||
You're in
|
|
||||||
</p>
|
|
||||||
<h2 className="text-white tracking-[-4%] text-[30px] lg:text-[40px] font-normal mt-1 lg:mt-0">
|
|
||||||
good hands
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<p className="text-[#FFFFFF99] font-light text-[16px] lg:text-sm lg:w-sm leading-[20px] lg:leading-5">
|
|
||||||
Autumn is trusted by some of fastest-growing teams. Open source core,
|
|
||||||
self-host ready.{" "}
|
|
||||||
<span className="text-white">
|
|
||||||
We'll help you go live quickly
|
|
||||||
<br className="hidden lg:block" />{" "}
|
|
||||||
and get back to what's important.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-end gap-3 lg:gap-4 w-full pl-6 lg:pl-0 lg:w-[50%] [--card-step:24px] lg:[--card-step:52px]">
|
|
||||||
{cards.map((card, i) => (
|
|
||||||
<div
|
|
||||||
key={card.metric}
|
|
||||||
className="ps-card relative flex items-center justify-between pl-4 pr-3 py-4 lg:pl-6 lg:pr-10 lg:py-3.5 gap-2 lg:gap-6"
|
|
||||||
style={{
|
|
||||||
width: `calc(100% - (var(--card-step) * ${i}))`,
|
|
||||||
backgroundColor: card.bg,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-1 min-w-[95px] lg:min-w-auto shrink-0">
|
|
||||||
<div className="flex items-center gap-1.5 lg:gap-2">
|
|
||||||
<Image
|
|
||||||
src={card.icon}
|
|
||||||
width={18}
|
|
||||||
height={18}
|
|
||||||
alt={card.label}
|
|
||||||
className="w-[14px] h-[14px] lg:w-[18px] lg:h-[18px]"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className="ps-metric text-xl lg:text-2xl font-medium tracking-[-5%] text-[#1A0A2E]"
|
|
||||||
data-final={card.metric}
|
|
||||||
>
|
|
||||||
{card.metric}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-[11px] lg:text-[14px] leading-[1] lg:leading-4.5 tracking-[-2%] font-normal text-[#1A0A2E]/60">
|
|
||||||
{card.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-[10.5px] lg:text-[14px] text-[#1A0A2E]/80 font-normal leading-[1.3] lg:leading-4.5 tracking-[0] lg:tracking-[-2%] flex-1 lg:flex-none lg:w-66 lg:shrink-0 text-left">
|
|
||||||
{card.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{card.badge && (
|
|
||||||
<span className="absolute top-0 right-0 bg-[#1A0A2E] text-white font-mono text-xs px-2.5 py-1">
|
|
||||||
{card.badge}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{card.clipart && (
|
|
||||||
<Image
|
|
||||||
src="/images/production/clipart.svg"
|
|
||||||
width={12}
|
|
||||||
height={12}
|
|
||||||
alt="clipart"
|
|
||||||
className="absolute bottom-0 right-0 max-lg:w-[8px] max-lg:h-[8px]"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
223
apps/website/components/production-scale.tsx
Executable file
223
apps/website/components/production-scale.tsx
Executable file
@@ -0,0 +1,223 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useGSAP } from "@gsap/react";
|
||||||
|
import gsap from "gsap";
|
||||||
|
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
|
|
||||||
|
const SCRAMBLE_CHARS =
|
||||||
|
"!@#$%^&*()_+-=[]{}|;:,.<>?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
function scrambleText(
|
||||||
|
el: HTMLElement,
|
||||||
|
finalText: string,
|
||||||
|
{ charDuration = 60, cycleSpeed = 30 } = {},
|
||||||
|
) {
|
||||||
|
const len = finalText.length;
|
||||||
|
let settled = 0;
|
||||||
|
const timeouts: Array<ReturnType<typeof setTimeout>> = [];
|
||||||
|
|
||||||
|
const cycleInterval = setInterval(() => {
|
||||||
|
const display = finalText
|
||||||
|
.split("")
|
||||||
|
.map((ch, i) =>
|
||||||
|
i < settled
|
||||||
|
? ch
|
||||||
|
: SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)],
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
el.textContent = display;
|
||||||
|
}, cycleSpeed);
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const t = setTimeout(
|
||||||
|
() => {
|
||||||
|
settled = i + 1;
|
||||||
|
if (settled === len) {
|
||||||
|
clearInterval(cycleInterval);
|
||||||
|
el.textContent = finalText;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(i + 1) * charDuration,
|
||||||
|
);
|
||||||
|
timeouts.push(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function cleanup() {
|
||||||
|
clearInterval(cycleInterval);
|
||||||
|
timeouts.forEach(clearTimeout);
|
||||||
|
el.textContent = finalText;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{
|
||||||
|
bg: "#A175FF",
|
||||||
|
icon: "/images/production/uptime2.svg",
|
||||||
|
metric: "1 Billion +",
|
||||||
|
label: "Monthly events",
|
||||||
|
description:
|
||||||
|
"Autumn handles billions of billing events monthly for some of your favorite apps.",
|
||||||
|
clipart: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bg: "#FFE8FA",
|
||||||
|
icon: "/images/production/latency.svg",
|
||||||
|
metric: "<50ms",
|
||||||
|
label: "US latency",
|
||||||
|
description:
|
||||||
|
"Every billing check resolves in under 50ms. Your users never wait for a gate.",
|
||||||
|
clipart: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bg: "#D698FF",
|
||||||
|
icon: "/images/production/uptiime.svg",
|
||||||
|
metric: "10 minutes",
|
||||||
|
label: "Support SLA",
|
||||||
|
description:
|
||||||
|
"Billing is critical. We're loved for our rapid response times.",
|
||||||
|
clipart: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bg: "#F55DD0",
|
||||||
|
icon: "/images/production/churn.svg",
|
||||||
|
metric: "Zero",
|
||||||
|
label: "Churn rate",
|
||||||
|
description: "Unless their company shuts down, our customers stay with us.",
|
||||||
|
clipart: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ProductionScale() {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const scrambleCleanups = useRef<Array<() => void>>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cleanups = scrambleCleanups.current;
|
||||||
|
return () => {
|
||||||
|
cleanups.forEach((fn) => fn());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useGSAP(
|
||||||
|
() => {
|
||||||
|
const isMobile = window.innerWidth < 768;
|
||||||
|
const cardY = isMobile ? 16 : 30;
|
||||||
|
|
||||||
|
gsap.set(".ps-card", { opacity: 0, y: cardY, scale: 0.97 });
|
||||||
|
|
||||||
|
const tl = gsap.timeline({
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: ".ps-section",
|
||||||
|
start: "top 75%",
|
||||||
|
},
|
||||||
|
defaults: { overwrite: "auto" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const cardEls = gsap.utils.toArray<HTMLElement>(".ps-card");
|
||||||
|
cardEls.forEach((card, i) => {
|
||||||
|
tl.to(
|
||||||
|
card,
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
scale: 1,
|
||||||
|
duration: 0.9,
|
||||||
|
ease: "power3.out",
|
||||||
|
onComplete() {
|
||||||
|
const metricEl = card.querySelector<HTMLElement>(".ps-metric");
|
||||||
|
if (!metricEl) return;
|
||||||
|
const finalText = metricEl.dataset.final;
|
||||||
|
if (!finalText) return;
|
||||||
|
const delayTimer = setTimeout(() => {
|
||||||
|
const cleanup = scrambleText(metricEl, finalText);
|
||||||
|
scrambleCleanups.current.push(cleanup);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
scrambleCleanups.current.push(() => clearTimeout(delayTimer));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
0.3 + i * 0.15,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ scope: containerRef },
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className="overflow-hidden">
|
||||||
|
<section className="ps-section flex flex-col lg:flex-row items-start justify-between py-12 lg:py-16 gap-12 lg:gap-0 bg-[#0F0F0F]">
|
||||||
|
<div className="flex px-4 xl:pl-22.5 lg:pr-0 flex-col my-auto gap-4 lg:gap-6 pt-2 w-full lg:w-auto">
|
||||||
|
<div className="leading-none lg:leading-10">
|
||||||
|
<p className="text-[#FFFFFF99] tracking-[-4%] text-[30px] lg:text-[40px] font-normal">
|
||||||
|
You're in
|
||||||
|
</p>
|
||||||
|
<h2 className="text-white tracking-[-4%] text-[30px] lg:text-[40px] font-normal mt-1 lg:mt-0">
|
||||||
|
good hands
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-[#FFFFFF99] font-light text-[16px] lg:text-sm lg:w-sm leading-[20px] lg:leading-5">
|
||||||
|
Autumn is trusted by some of fastest-growing teams. Open source
|
||||||
|
core, self-host ready.{" "}
|
||||||
|
<span className="text-white">
|
||||||
|
We'll help you go live quickly
|
||||||
|
<br className="hidden lg:block" /> and get back to what's
|
||||||
|
important.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-end gap-3 lg:gap-4 w-full pl-6 lg:pl-0 lg:w-[50%] [--card-step:24px] lg:[--card-step:52px]">
|
||||||
|
{cards.map((card, i) => (
|
||||||
|
<div
|
||||||
|
key={card.metric}
|
||||||
|
className="ps-card relative flex items-center justify-between pl-4 pr-3 py-4 lg:pl-6 lg:pr-10 lg:py-3.5 gap-2 lg:gap-6"
|
||||||
|
style={{
|
||||||
|
width: `calc(100% - (var(--card-step) * ${i}))`,
|
||||||
|
backgroundColor: card.bg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1 min-w-[95px] lg:min-w-auto shrink-0">
|
||||||
|
<div className="flex items-center gap-1.5 lg:gap-2">
|
||||||
|
<Image
|
||||||
|
src={card.icon}
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
alt={card.label}
|
||||||
|
className="w-[14px] h-[14px] lg:w-[18px] lg:h-[18px]"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="ps-metric text-xl lg:text-2xl font-medium tracking-[-5%] text-[#1A0A2E]"
|
||||||
|
data-final={card.metric}
|
||||||
|
>
|
||||||
|
{card.metric}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] lg:text-[14px] leading-[1] lg:leading-4.5 tracking-[-2%] font-normal text-[#1A0A2E]/60">
|
||||||
|
{card.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[10.5px] lg:text-[14px] text-[#1A0A2E]/80 font-normal leading-[1.3] lg:leading-4.5 tracking-[0] lg:tracking-[-2%] flex-1 lg:flex-none lg:w-66 lg:shrink-0 text-left">
|
||||||
|
{card.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{card.clipart && (
|
||||||
|
<Image
|
||||||
|
src="/images/production/clipart.svg"
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
alt="clipart"
|
||||||
|
className="absolute bottom-0 right-0 max-lg:w-[8px] max-lg:h-[8px]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
export default function SectionDivider({ title }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col gap-2.5">
|
|
||||||
<div className="border-t border-[#292929] w-full" />
|
|
||||||
<div className="border-t border-[#292929] w-full" />
|
|
||||||
<div className="border-t border-[#292929] w-full" />
|
|
||||||
<div className="border-t border-[#292929] w-full" />
|
|
||||||
</div>
|
|
||||||
<div className="w-[calc(100%+calc(var(--page-pad)*2))] -ml-(--page-pad) flex border-y border-[#292929] bg-[#000000] mt-2.5">
|
|
||||||
<div className="flex-1 py-6.5 pl-[calc(var(--page-pad)+16px)] xl:pl-[calc(var(--page-pad)+90px)] flex items-center">
|
|
||||||
<span className="font-mono text-[#FFFFFF99] text-[14px] tracking-[-2%] leading-[14px] uppercase">
|
|
||||||
// {title}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
19
apps/website/components/section-divider.tsx
Normal file
19
apps/website/components/section-divider.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export default function SectionDivider({ title }: { title: string }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-2.5">
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
<div className="border-t border-[#292929] w-full" />
|
||||||
|
</div>
|
||||||
|
<div className="w-[calc(100%+calc(var(--page-pad)*2))] -ml-(--page-pad) flex border-y border-[#292929] bg-[#000000] mt-2.5">
|
||||||
|
<div className="flex-1 py-6.5 pl-[calc(var(--page-pad)+16px)] xl:pl-[calc(var(--page-pad)+90px)] flex items-center">
|
||||||
|
<span className="font-mono text-[#FFFFFF99] text-[14px] tracking-[-2%] leading-[14px] uppercase">
|
||||||
|
// {title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import lottie, { AnimationItem } from "lottie-web";
|
|
||||||
import gsap from "gsap";
|
|
||||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
||||||
import desktopAnimationData from "@/public/animation/solution-desktop.json";
|
|
||||||
import mobileAnimationData from "@/public/animation/solution-mobile.json";
|
|
||||||
|
|
||||||
gsap.registerPlugin(ScrollTrigger);
|
|
||||||
|
|
||||||
export default function SolutionAnimation() {
|
|
||||||
const containerRef = useRef(null);
|
|
||||||
const animRef = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!containerRef.current) return;
|
|
||||||
|
|
||||||
const isMobile = window.innerWidth < 768;
|
|
||||||
const animationData = isMobile ? mobileAnimationData : desktopAnimationData;
|
|
||||||
|
|
||||||
const anim = lottie.loadAnimation({
|
|
||||||
container: containerRef.current,
|
|
||||||
renderer: "svg",
|
|
||||||
loop: false,
|
|
||||||
autoplay: false,
|
|
||||||
animationData,
|
|
||||||
});
|
|
||||||
|
|
||||||
animRef.current = anim;
|
|
||||||
|
|
||||||
const totalFrames = anim.totalFrames;
|
|
||||||
|
|
||||||
const loopStart = Math.floor(totalFrames * 0.2);
|
|
||||||
|
|
||||||
anim.addEventListener("complete", () => {
|
|
||||||
anim.loop = true;
|
|
||||||
anim.playSegments([loopStart, totalFrames], true);
|
|
||||||
});
|
|
||||||
|
|
||||||
const st = ScrollTrigger.create({
|
|
||||||
trigger: containerRef.current,
|
|
||||||
start: "top 75%", // Plays when the top of the animation reaches 75% viewport height
|
|
||||||
onEnter: () => anim.play(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
st.kill();
|
|
||||||
anim.destroy();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <div ref={containerRef} style={{ width: "100%" }} />;
|
|
||||||
}
|
|
||||||
54
apps/website/components/solution-animation.tsx
Normal file
54
apps/website/components/solution-animation.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import gsap from "gsap";
|
||||||
|
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||||
|
import lottie, { type AnimationItem } from "lottie-web";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import desktopAnimationData from "@/public/animation/solution-desktop.json";
|
||||||
|
import mobileAnimationData from "@/public/animation/solution-mobile.json";
|
||||||
|
|
||||||
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
|
|
||||||
|
export default function SolutionAnimation() {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const animRef = useRef<AnimationItem | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
const isMobile = window.innerWidth < 768;
|
||||||
|
const animationData = isMobile ? mobileAnimationData : desktopAnimationData;
|
||||||
|
|
||||||
|
const anim = lottie.loadAnimation({
|
||||||
|
container: containerRef.current,
|
||||||
|
renderer: "svg",
|
||||||
|
loop: false,
|
||||||
|
autoplay: false,
|
||||||
|
animationData,
|
||||||
|
});
|
||||||
|
|
||||||
|
animRef.current = anim;
|
||||||
|
|
||||||
|
const totalFrames = anim.totalFrames;
|
||||||
|
|
||||||
|
const loopStart = Math.floor(totalFrames * 0.2);
|
||||||
|
|
||||||
|
anim.addEventListener("complete", () => {
|
||||||
|
anim.loop = true;
|
||||||
|
anim.playSegments([loopStart, totalFrames], true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const st = ScrollTrigger.create({
|
||||||
|
trigger: containerRef.current,
|
||||||
|
start: "top 75%", // Plays when the top of the animation reaches 75% viewport height
|
||||||
|
onEnter: () => anim.play(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
st.kill();
|
||||||
|
anim.destroy();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div ref={containerRef} style={{ width: "100%" }} />;
|
||||||
|
}
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import React from "react";
|
|
||||||
import SolutionAnimation from "./solution-animation";
|
|
||||||
|
|
||||||
const dbItems = [
|
|
||||||
{ text: "SUBSCRIPTION STATE", active: true },
|
|
||||||
{ text: "CREDIT BALANCES & ROLLOVERS", active: false },
|
|
||||||
{ text: "FEATURE ENTITLEMENTS", active: false },
|
|
||||||
{ text: "WEBHOOK HANDLING", active: false },
|
|
||||||
{ text: "USAGE RESETS & PRORATION", active: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
const appItems = [
|
|
||||||
{ text: "PRODUCT FEATURES", icon: "grid" },
|
|
||||||
{ text: "USER INTERFACE", icon: "crossfade" },
|
|
||||||
{ text: "BUSINESS LOGIC", icon: "box-grid" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const stripeItems = [
|
|
||||||
{ text: "MOVES MONEY", icon: "grid" },
|
|
||||||
{ text: "INVOICING", icon: "crossfade" },
|
|
||||||
{ text: "CARD PROCESSING", icon: "box-grid" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const iconSrc = {
|
|
||||||
grid: "/images/solutions/grid.svg",
|
|
||||||
crossfade: "/images/solutions/crossfade.svg",
|
|
||||||
"box-grid": "/images/solutions/box-grid.svg",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Solution() {
|
|
||||||
return (
|
|
||||||
<section className="relative w-full bg-[#000000] border-t border-[#292929] overflow-hidden pt-24">
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 z-0 pointer-events-none"
|
|
||||||
style={{
|
|
||||||
backgroundImage:
|
|
||||||
"linear-gradient(to right, rgba(128,128,128,0.07) 1px, transparent 1px), linear-gradient(to bottom, rgba(128,128,128,0.07) 1px, transparent 1px)",
|
|
||||||
backgroundSize: "40px 40px",
|
|
||||||
maskImage:
|
|
||||||
"linear-gradient(to bottom, transparent 0%, black 18%, black 78%, transparent 100%)",
|
|
||||||
WebkitMaskImage:
|
|
||||||
"linear-gradient(to bottom, transparent 0%, black 18%, black 78%, transparent 100%)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative z-10 max-w-[1400px] mx-auto px-4 flex flex-col items-center">
|
|
||||||
{/* Heading */}
|
|
||||||
<div className="text-center mb-16 lg:mb-4 flex flex-col items-center">
|
|
||||||
<h2 className="text-[30px] leading-[30px] md:text-[40px] md:leading-[40px] font-normal tracking-tight mb-6">
|
|
||||||
<span className="text-[#A3A3A3]">Replace it all with </span>
|
|
||||||
<span className="text-white">Autumn</span>
|
|
||||||
</h2>
|
|
||||||
<p className="text-[#A3A3A3] text-[14px] md:text-[16px] sm:text-base max-w-2xl mx-auto font-light leading-[20px] tracking-[-2%]">
|
|
||||||
Autumn is a database purpose-built for billing state. Configure your
|
|
||||||
pricing
|
|
||||||
<br className="hidden sm:block" />
|
|
||||||
in the dashboard.{" "}
|
|
||||||
<span className="text-white">
|
|
||||||
Three API calls handle everything else.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<SolutionAnimation />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
68
apps/website/components/solution.tsx
Normal file
68
apps/website/components/solution.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import React from "react";
|
||||||
|
import SolutionAnimation from "./solution-animation";
|
||||||
|
|
||||||
|
const dbItems = [
|
||||||
|
{ text: "SUBSCRIPTION STATE", active: true },
|
||||||
|
{ text: "CREDIT BALANCES & ROLLOVERS", active: false },
|
||||||
|
{ text: "FEATURE ENTITLEMENTS", active: false },
|
||||||
|
{ text: "WEBHOOK HANDLING", active: false },
|
||||||
|
{ text: "USAGE RESETS & PRORATION", active: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const appItems = [
|
||||||
|
{ text: "PRODUCT FEATURES", icon: "grid" },
|
||||||
|
{ text: "USER INTERFACE", icon: "crossfade" },
|
||||||
|
{ text: "BUSINESS LOGIC", icon: "box-grid" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const stripeItems = [
|
||||||
|
{ text: "MOVES MONEY", icon: "grid" },
|
||||||
|
{ text: "INVOICING", icon: "crossfade" },
|
||||||
|
{ text: "CARD PROCESSING", icon: "box-grid" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const iconSrc = {
|
||||||
|
grid: "/images/solutions/grid.svg",
|
||||||
|
crossfade: "/images/solutions/crossfade.svg",
|
||||||
|
"box-grid": "/images/solutions/box-grid.svg",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Solution() {
|
||||||
|
return (
|
||||||
|
<section className="relative w-full bg-[#000000] border-t border-[#292929] overflow-hidden pt-24">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-0 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
"linear-gradient(to right, rgba(128,128,128,0.07) 1px, transparent 1px), linear-gradient(to bottom, rgba(128,128,128,0.07) 1px, transparent 1px)",
|
||||||
|
backgroundSize: "40px 40px",
|
||||||
|
maskImage:
|
||||||
|
"linear-gradient(to bottom, transparent 0%, black 18%, black 78%, transparent 100%)",
|
||||||
|
WebkitMaskImage:
|
||||||
|
"linear-gradient(to bottom, transparent 0%, black 18%, black 78%, transparent 100%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative z-10 max-w-[1400px] mx-auto px-4 flex flex-col items-center">
|
||||||
|
{/* Heading */}
|
||||||
|
<div className="text-center mb-16 lg:mb-4 flex flex-col items-center">
|
||||||
|
<h2 className="text-[30px] leading-[30px] md:text-[40px] md:leading-[40px] font-normal tracking-tight mb-6">
|
||||||
|
<span className="text-[#A3A3A3]">Replace it all with </span>
|
||||||
|
<span className="text-white">Autumn</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-[#A3A3A3] text-[14px] md:text-[16px] sm:text-base max-w-2xl mx-auto font-light leading-[20px] tracking-[-2%]">
|
||||||
|
Autumn is a database purpose-built for billing state. Configure your
|
||||||
|
pricing
|
||||||
|
<br className="hidden sm:block" />
|
||||||
|
in the dashboard.{" "}
|
||||||
|
<span className="text-white">
|
||||||
|
Three API calls handle everything else.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<SolutionAnimation />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import {
|
|
||||||
IconArrowLeft,
|
|
||||||
IconArrowRight,
|
|
||||||
IconQuotes,
|
|
||||||
PixelatedPattern,
|
|
||||||
} from "@/app/constant";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
|
|
||||||
const testimonialsData = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
quote:
|
|
||||||
"Literally cannot imagine going without it. Thank you. We had some pretty crazy usage-based limitations for different features, as well as a free trial.",
|
|
||||||
author: "DANIEL EDRISIAR",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
quote: "Amazing product. Amazing founders.",
|
|
||||||
author: "NIZZY",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
quote:
|
|
||||||
"Autumn is awesome. We've been happy customers since the very beginning - it was a no-brainer to be honest. The founders are in true founder mode.",
|
|
||||||
author: "MAX PRILUTSKIY",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
quote: "What migrating to Autumn does (scroll!)",
|
|
||||||
author: "Ben Y",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
quote:
|
|
||||||
"Autumn fixed stripe. Trust me. Save you at least a week and potentially months of Stripe integration time. I wish we could have discovered Autumn earlier.",
|
|
||||||
author: "Benny Kok",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
quote:
|
|
||||||
"@autumnpricing is so good it ruined every other tool for me. nothing else even feels right anymore",
|
|
||||||
author: "Can Vardar",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const Testimonials = () => {
|
|
||||||
const videoRef = useRef(null);
|
|
||||||
const scrollRef = useRef(null);
|
|
||||||
const progressRef = useRef(null);
|
|
||||||
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
|
||||||
const [canScrollRight, setCanScrollRight] = useState(true);
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (scrollRef.current) {
|
|
||||||
const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current;
|
|
||||||
setCanScrollLeft(scrollLeft > 0);
|
|
||||||
setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 1);
|
|
||||||
|
|
||||||
if (progressRef.current) {
|
|
||||||
const maxScroll = scrollWidth - clientWidth;
|
|
||||||
const progress = maxScroll > 0 ? (scrollLeft / maxScroll) * 100 : 0;
|
|
||||||
progressRef.current.style.transform = `translateX(${progress}%)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
if (videoRef.current) {
|
|
||||||
videoRef.current.playbackRate = 0.4;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleScroll();
|
|
||||||
window.addEventListener("resize", handleScroll);
|
|
||||||
return () => window.removeEventListener("resize", handleScroll);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const scrollByAmount = (amount) => {
|
|
||||||
if (scrollRef.current) {
|
|
||||||
scrollRef.current.scrollBy({ left: amount, behavior: "smooth" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="w-full bg-[#000000] text-white overflow-hidden">
|
|
||||||
<div className=" mx-auto">
|
|
||||||
<div className="px-4 sm:px-6 md:px-4 lg:px-4 xl:px-22.75 pt-[48px] xl:pt-32 pb-[48px] flex flex-row items-start justify-between">
|
|
||||||
<h2 className="w-full text-center md:w-auto md:text-left text-[30px] leading-[32px] sm:text-5xl md:text-[40px] font-normal tracking-[-5%]">
|
|
||||||
<span className="text-[#FFFFFF99]">Built for </span>
|
|
||||||
<span className="text-white">teams</span>
|
|
||||||
<br className="sm:hidden" />
|
|
||||||
<span className="text-white"> that move fast</span>
|
|
||||||
</h2>
|
|
||||||
<div className="hidden md:flex items-center space-x-4">
|
|
||||||
<button
|
|
||||||
onClick={() => scrollByAmount(-400)}
|
|
||||||
disabled={!canScrollLeft}
|
|
||||||
className="group p-1.5 bg-transparent cursor-pointer flex items-center justify-center transition-all duration-300 border border-[#292929]"
|
|
||||||
aria-label="Previous testimonials"
|
|
||||||
>
|
|
||||||
<IconArrowLeft
|
|
||||||
disabled={!canScrollLeft}
|
|
||||||
className="w-6 h-6 text-gray-400 hover:text-white"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => scrollByAmount(400)}
|
|
||||||
disabled={!canScrollRight}
|
|
||||||
className="group p-1.5 bg-transparent cursor-pointer flex items-center justify-center transition-all duration-300 border border-[#292929]"
|
|
||||||
aria-label="Next testimonials"
|
|
||||||
>
|
|
||||||
<IconArrowRight
|
|
||||||
disabled={!canScrollRight}
|
|
||||||
className="w-6 h-6 text-gray-400 hover:text-white"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t border-[#1A1A1A] w-full"></div>
|
|
||||||
|
|
||||||
<div className="px-0 xl:pl-22.75">
|
|
||||||
<div
|
|
||||||
ref={scrollRef}
|
|
||||||
onScroll={handleScroll}
|
|
||||||
className="flex overflow-x-auto snap-x snap-mandatory hide-scrollbar group/track"
|
|
||||||
style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
|
|
||||||
>
|
|
||||||
{testimonialsData.map((testimonial) => (
|
|
||||||
<div
|
|
||||||
key={testimonial.id}
|
|
||||||
className="group cursor-pointer shrink-0 w-[300px] sm:w-[300px] md:w-[360px] snap-start min-h-[360px] flex flex-col justify-between p-4 sm:p-10 border-l border-r border-b border-[#1A1A1A] transition-all duration-300 relative overflow-hidden"
|
|
||||||
>
|
|
||||||
{/* Hover Pixelated Pattern (Masked) */}
|
|
||||||
<div className="absolute inset-x-0 bottom-0 h-[120%] pointer-events-none z-0 overflow-hidden">
|
|
||||||
<div className="absolute inset-0 opacity-0 translate-y-6 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-500 ease-out pointer-events-none z-0 hidden md:block">
|
|
||||||
<video
|
|
||||||
ref={videoRef}
|
|
||||||
src="/images/testimonials/testimonial section.webm"
|
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
muted
|
|
||||||
playsInline
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Purple Glow Gradient Overlay */}
|
|
||||||
<div className="absolute inset-x-0 bottom-0 h-[70%] bg-[linear-gradient(to_bottom,rgba(10,10,10,0)_0%,rgba(135,82,250,0.15)_40%,rgba(135,82,250,0.45)_70%,rgba(135,82,250,0.85)_90%)] opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none z-0" />
|
|
||||||
<div className="relative z-10 flex flex-col h-full justify-start">
|
|
||||||
<IconQuotes className="w-8 h-8 text-[#4C4C4C] opacity-60 group-hover:text-[#9564FF] group-hover:opacity-100 transition-colors duration-500 mb-6" />
|
|
||||||
<p className="text-white text-[14px] leading-[18px] sm:text-xl sm:leading-6 font-extralight tracking-[-2%] transition-colors duration-500">
|
|
||||||
{testimonial.quote}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="relative z-10 font-mono text-sm tracking-[-2%] uppercase text-white opacity-60 group-hover:opacity-100 transition-colors duration-500 mt-33.5">
|
|
||||||
{testimonial.author}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="border-t border-[#1A1A1A] w-full"></div>
|
|
||||||
|
|
||||||
<div className="flex md:hidden justify-end px-4 mt-6">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<button
|
|
||||||
onClick={() => scrollByAmount(-400)}
|
|
||||||
disabled={!canScrollLeft}
|
|
||||||
className="p-1.5 bg-transparent flex items-center justify-center border border-[#292929]"
|
|
||||||
aria-label="Previous testimonials"
|
|
||||||
>
|
|
||||||
<IconArrowLeft
|
|
||||||
disabled={!canScrollLeft}
|
|
||||||
className="w-6 h-6 text-white"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => scrollByAmount(400)}
|
|
||||||
disabled={!canScrollRight}
|
|
||||||
className="p-1.5 bg-transparent flex items-center justify-center border border-[#292929]"
|
|
||||||
aria-label="Next testimonials"
|
|
||||||
>
|
|
||||||
<IconArrowRight
|
|
||||||
disabled={!canScrollRight}
|
|
||||||
className="w-6 h-6 text-white"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-center mt-5 md:mt-10 pb-0 md:pb-10">
|
|
||||||
<div className="w-[168px] hidden md:block h-1 bg-[#1A1A1A] rounded-full overflow-hidden relative">
|
|
||||||
<div
|
|
||||||
ref={progressRef}
|
|
||||||
className="absolute left-0 top-0 h-full bg-[#8752FA] w-[84px] rounded-full"
|
|
||||||
style={{ transform: `translateX(0%)` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style jsx global>{`
|
|
||||||
.hide-scrollbar::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.arrow-corners {
|
|
||||||
transform-box: fill-box;
|
|
||||||
transform-origin: center;
|
|
||||||
transition: transform 0.25s ease;
|
|
||||||
}
|
|
||||||
.group:hover .arrow-corners {
|
|
||||||
transform: scale(1.4);
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Testimonials;
|
|
||||||
222
apps/website/components/testimonials.tsx
Executable file
222
apps/website/components/testimonials.tsx
Executable file
@@ -0,0 +1,222 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
IconArrowLeft,
|
||||||
|
IconArrowRight,
|
||||||
|
IconQuotes,
|
||||||
|
PixelatedPattern,
|
||||||
|
} from "@/app/constant";
|
||||||
|
|
||||||
|
const testimonialsData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
quote:
|
||||||
|
"Literally cannot imagine going without it. Thank you. We had some pretty crazy usage-based limitations for different features, as well as a free trial.",
|
||||||
|
author: "DANIEL EDRISIAR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
quote: "Amazing product. Amazing founders.",
|
||||||
|
author: "NIZZY",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
quote:
|
||||||
|
"Autumn is awesome. We've been happy customers since the very beginning - it was a no-brainer to be honest. The founders are in true founder mode.",
|
||||||
|
author: "MAX PRILUTSKIY",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
quote: "What migrating to Autumn does (scroll!)",
|
||||||
|
author: "Ben Y",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
quote:
|
||||||
|
"Autumn fixed stripe. Trust me. Save you at least a week and potentially months of Stripe integration time. I wish we could have discovered Autumn earlier.",
|
||||||
|
author: "Benny Kok",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
quote:
|
||||||
|
"@autumnpricing is so good it ruined every other tool for me. nothing else even feels right anymore",
|
||||||
|
author: "Can Vardar",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Testimonials = () => {
|
||||||
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const progressRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
||||||
|
const [canScrollRight, setCanScrollRight] = useState(true);
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current;
|
||||||
|
setCanScrollLeft(scrollLeft > 0);
|
||||||
|
setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 1);
|
||||||
|
|
||||||
|
if (progressRef.current) {
|
||||||
|
const maxScroll = scrollWidth - clientWidth;
|
||||||
|
const progress = maxScroll > 0 ? (scrollLeft / maxScroll) * 100 : 0;
|
||||||
|
progressRef.current.style.transform = `translateX(${progress}%)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (videoRef.current) {
|
||||||
|
videoRef.current.playbackRate = 0.4;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleScroll();
|
||||||
|
window.addEventListener("resize", handleScroll);
|
||||||
|
return () => window.removeEventListener("resize", handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scrollByAmount = (amount: number) => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollBy({ left: amount, behavior: "smooth" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="w-full bg-[#000000] text-white overflow-hidden">
|
||||||
|
<div className=" mx-auto">
|
||||||
|
<div className="px-4 sm:px-6 md:px-4 lg:px-4 xl:px-22.75 pt-[48px] xl:pt-32 pb-[48px] flex flex-row items-start justify-between">
|
||||||
|
<h2 className="w-full text-center md:w-auto md:text-left text-[30px] leading-[32px] sm:text-5xl md:text-[40px] font-normal tracking-[-5%]">
|
||||||
|
<span className="text-[#FFFFFF99]">Built for </span>
|
||||||
|
<span className="text-white">teams</span>
|
||||||
|
<br className="sm:hidden" />
|
||||||
|
<span className="text-white"> that move fast</span>
|
||||||
|
</h2>
|
||||||
|
<div className="hidden md:flex items-center space-x-4">
|
||||||
|
<button
|
||||||
|
onClick={() => scrollByAmount(-400)}
|
||||||
|
disabled={!canScrollLeft}
|
||||||
|
className="group p-1.5 bg-transparent cursor-pointer flex items-center justify-center transition-all duration-300 border border-[#292929]"
|
||||||
|
aria-label="Previous testimonials"
|
||||||
|
>
|
||||||
|
<IconArrowLeft
|
||||||
|
disabled={!canScrollLeft}
|
||||||
|
className="w-6 h-6 text-gray-400 hover:text-white"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => scrollByAmount(400)}
|
||||||
|
disabled={!canScrollRight}
|
||||||
|
className="group p-1.5 bg-transparent cursor-pointer flex items-center justify-center transition-all duration-300 border border-[#292929]"
|
||||||
|
aria-label="Next testimonials"
|
||||||
|
>
|
||||||
|
<IconArrowRight
|
||||||
|
disabled={!canScrollRight}
|
||||||
|
className="w-6 h-6 text-gray-400 hover:text-white"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-[#1A1A1A] w-full"></div>
|
||||||
|
|
||||||
|
<div className="px-0 xl:pl-22.75">
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
className="flex overflow-x-auto snap-x snap-mandatory hide-scrollbar group/track"
|
||||||
|
style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
|
||||||
|
>
|
||||||
|
{testimonialsData.map((testimonial) => (
|
||||||
|
<div
|
||||||
|
key={testimonial.id}
|
||||||
|
className="group cursor-pointer shrink-0 w-[300px] sm:w-[300px] md:w-[360px] snap-start min-h-[360px] flex flex-col justify-between p-4 sm:p-10 border-l border-r border-b border-[#1A1A1A] transition-all duration-300 relative overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* Hover Pixelated Pattern (Masked) */}
|
||||||
|
<div className="absolute inset-x-0 bottom-0 h-[120%] pointer-events-none z-0 overflow-hidden">
|
||||||
|
<div className="absolute inset-0 opacity-0 translate-y-6 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-500 ease-out pointer-events-none z-0 hidden md:block">
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
src="/images/testimonials/testimonial section.webm"
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Purple Glow Gradient Overlay */}
|
||||||
|
<div className="absolute inset-x-0 bottom-0 h-[70%] bg-[linear-gradient(to_bottom,rgba(10,10,10,0)_0%,rgba(135,82,250,0.15)_40%,rgba(135,82,250,0.45)_70%,rgba(135,82,250,0.85)_90%)] opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none z-0" />
|
||||||
|
<div className="relative z-10 flex flex-col h-full justify-start">
|
||||||
|
<IconQuotes className="w-8 h-8 text-[#4C4C4C] opacity-60 group-hover:text-[#9564FF] group-hover:opacity-100 transition-colors duration-500 mb-6" />
|
||||||
|
<p className="text-white text-[14px] leading-[18px] sm:text-xl sm:leading-6 font-extralight tracking-[-2%] transition-colors duration-500">
|
||||||
|
{testimonial.quote}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="relative z-10 font-mono text-sm tracking-[-2%] uppercase text-white opacity-60 group-hover:opacity-100 transition-colors duration-500 mt-33.5">
|
||||||
|
{testimonial.author}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-[#1A1A1A] w-full"></div>
|
||||||
|
|
||||||
|
<div className="flex md:hidden justify-end px-4 mt-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => scrollByAmount(-400)}
|
||||||
|
disabled={!canScrollLeft}
|
||||||
|
className="p-1.5 bg-transparent flex items-center justify-center border border-[#292929]"
|
||||||
|
aria-label="Previous testimonials"
|
||||||
|
>
|
||||||
|
<IconArrowLeft
|
||||||
|
disabled={!canScrollLeft}
|
||||||
|
className="w-6 h-6 text-white"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => scrollByAmount(400)}
|
||||||
|
disabled={!canScrollRight}
|
||||||
|
className="p-1.5 bg-transparent flex items-center justify-center border border-[#292929]"
|
||||||
|
aria-label="Next testimonials"
|
||||||
|
>
|
||||||
|
<IconArrowRight
|
||||||
|
disabled={!canScrollRight}
|
||||||
|
className="w-6 h-6 text-white"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center mt-5 md:mt-10 pb-0 md:pb-10">
|
||||||
|
<div className="w-[168px] hidden md:block h-1 bg-[#1A1A1A] rounded-full overflow-hidden relative">
|
||||||
|
<div
|
||||||
|
ref={progressRef}
|
||||||
|
className="absolute left-0 top-0 h-full bg-[#8752FA] w-[84px] rounded-full"
|
||||||
|
style={{ transform: `translateX(0%)` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style jsx global>{`
|
||||||
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.arrow-corners {
|
||||||
|
transform-box: fill-box;
|
||||||
|
transform-origin: center;
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
}
|
||||||
|
.group:hover .arrow-corners {
|
||||||
|
transform: scale(1.4);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Testimonials;
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import matter from "gray-matter";
|
|
||||||
|
|
||||||
const CONTENT_DIR = path.join(process.cwd(), "content", "blog");
|
|
||||||
|
|
||||||
export function getAllPosts() {
|
|
||||||
if (!fs.existsSync(CONTENT_DIR)) return [];
|
|
||||||
|
|
||||||
const files = fs
|
|
||||||
.readdirSync(CONTENT_DIR)
|
|
||||||
.filter((file) => file.endsWith(".mdx"));
|
|
||||||
|
|
||||||
const posts = files.map((filename) => {
|
|
||||||
const filePath = path.join(CONTENT_DIR, filename);
|
|
||||||
const raw = fs.readFileSync(filePath, "utf-8");
|
|
||||||
const { data } = matter(raw);
|
|
||||||
|
|
||||||
return {
|
|
||||||
slug: data.slug || filename.replace(/\.mdx$/, ""),
|
|
||||||
title: data.title || "Untitled",
|
|
||||||
description: data.description || "",
|
|
||||||
date: data.date || null,
|
|
||||||
author: data.author || "Autumn Team",
|
|
||||||
image: data.image || null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return posts.sort((a, b) => {
|
|
||||||
if (!a.date || !b.date) return 0;
|
|
||||||
return new Date(b.date) - new Date(a.date);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPostBySlug({ slug }) {
|
|
||||||
if (!fs.existsSync(CONTENT_DIR)) return null;
|
|
||||||
|
|
||||||
const files = fs
|
|
||||||
.readdirSync(CONTENT_DIR)
|
|
||||||
.filter((file) => file.endsWith(".mdx"));
|
|
||||||
|
|
||||||
for (const filename of files) {
|
|
||||||
const filePath = path.join(CONTENT_DIR, filename);
|
|
||||||
const raw = fs.readFileSync(filePath, "utf-8");
|
|
||||||
const { data, content } = matter(raw);
|
|
||||||
const fileSlug = data.slug || filename.replace(/\.mdx$/, "");
|
|
||||||
|
|
||||||
if (fileSlug === slug) {
|
|
||||||
return {
|
|
||||||
slug: fileSlug,
|
|
||||||
title: data.title || "Untitled",
|
|
||||||
description: data.description || "",
|
|
||||||
date: data.date || null,
|
|
||||||
author: data.author || "Autumn Team",
|
|
||||||
image: data.image || null,
|
|
||||||
source: content,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
75
apps/website/lib/blogUtils.ts
Normal file
75
apps/website/lib/blogUtils.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import matter from "gray-matter";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const CONTENT_DIR = path.join(process.cwd(), "content", "blog");
|
||||||
|
|
||||||
|
export type BlogPostSummary = {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
date: string | null;
|
||||||
|
author: string;
|
||||||
|
image: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BlogPost = BlogPostSummary & {
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getAllPosts(): BlogPostSummary[] {
|
||||||
|
if (!fs.existsSync(CONTENT_DIR)) return [];
|
||||||
|
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(CONTENT_DIR)
|
||||||
|
.filter((file) => file.endsWith(".mdx"));
|
||||||
|
|
||||||
|
const posts = files.map((filename) => {
|
||||||
|
const filePath = path.join(CONTENT_DIR, filename);
|
||||||
|
const raw = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const { data } = matter(raw);
|
||||||
|
|
||||||
|
return {
|
||||||
|
slug: data.slug || filename.replace(/\.mdx$/, ""),
|
||||||
|
title: data.title || "Untitled",
|
||||||
|
description: data.description || "",
|
||||||
|
date: data.date || null,
|
||||||
|
author: data.author || "Autumn Team",
|
||||||
|
image: data.image || null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return posts.sort((a, b) => {
|
||||||
|
if (!a.date || !b.date) return 0;
|
||||||
|
return new Date(b.date).getTime() - new Date(a.date).getTime();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPostBySlug({ slug }: { slug: string }): BlogPost | null {
|
||||||
|
if (!fs.existsSync(CONTENT_DIR)) return null;
|
||||||
|
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(CONTENT_DIR)
|
||||||
|
.filter((file) => file.endsWith(".mdx"));
|
||||||
|
|
||||||
|
for (const filename of files) {
|
||||||
|
const filePath = path.join(CONTENT_DIR, filename);
|
||||||
|
const raw = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const { data, content } = matter(raw);
|
||||||
|
const fileSlug = data.slug || filename.replace(/\.mdx$/, "");
|
||||||
|
|
||||||
|
if (fileSlug === slug) {
|
||||||
|
return {
|
||||||
|
slug: fileSlug,
|
||||||
|
title: data.title || "Untitled",
|
||||||
|
description: data.description || "",
|
||||||
|
date: data.date || null,
|
||||||
|
author: data.author || "Autumn Team",
|
||||||
|
image: data.image || null,
|
||||||
|
source: content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
34
apps/website/lib/types.ts
Normal file
34
apps/website/lib/types.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type {
|
||||||
|
ComponentPropsWithoutRef,
|
||||||
|
ComponentType,
|
||||||
|
CSSProperties,
|
||||||
|
PropsWithChildren,
|
||||||
|
RefAttributes,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
export type PageStyle = CSSProperties & {
|
||||||
|
"--page-pad"?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SvgIconProps = ComponentPropsWithoutRef<"svg">;
|
||||||
|
export type ImgProps = ComponentPropsWithoutRef<"img">;
|
||||||
|
|
||||||
|
export type PixelIconComponent = ComponentType<
|
||||||
|
SvgIconProps & Partial<RefAttributes<SVGSVGElement>>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type PixelAnimationHandle = {
|
||||||
|
play: () => void;
|
||||||
|
reverse: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PixelHoverHandle = {
|
||||||
|
restart: () => void;
|
||||||
|
reverse: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LayoutProps = PropsWithChildren;
|
||||||
|
|
||||||
|
export type BlogParams = Promise<{
|
||||||
|
slug: string;
|
||||||
|
}>;
|
||||||
2
apps/website/lib/utils.ts
Normal file
2
apps/website/lib/utils.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const cn = (...inputs: Array<string | false | null | undefined>) =>
|
||||||
|
inputs.filter(Boolean).join(" ");
|
||||||
7
apps/website/tailwind.config.mjs
Normal file
7
apps/website/tailwind.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import typography from "@tailwindcss/typography";
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
plugins: [typography],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
50
apps/website/tsconfig.json
Normal file
50
apps/website/tsconfig.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"typeRoots": [
|
||||||
|
"../../node_modules/@types"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
"react",
|
||||||
|
"react-dom"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"allowJs": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts",
|
||||||
|
"**/*.mts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -50,14 +50,10 @@ export const auth = betterAuth({
|
|||||||
session: {
|
session: {
|
||||||
create: {
|
create: {
|
||||||
before: beforeSessionCreated,
|
before: beforeSessionCreated,
|
||||||
after: (session, context) => {
|
after: afterSessionCreated,
|
||||||
return afterSessionCreated(session, context);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
after: (session, context) => {
|
after: afterSessionDeleted,
|
||||||
return afterSessionDeleted(session, context);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const afterSessionCreated = async (
|
|||||||
session: Session,
|
session: Session,
|
||||||
context: GenericEndpointContext<BetterAuthOptions> | null,
|
context: GenericEndpointContext<BetterAuthOptions> | null,
|
||||||
) => {
|
) => {
|
||||||
|
console.log("Running afterSessionCreated for user ", session.userId);
|
||||||
try {
|
try {
|
||||||
if (!context) return;
|
if (!context) return;
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ export const afterSessionCreated = async (
|
|||||||
path: "/",
|
path: "/",
|
||||||
maxAge: 60 * 60 * 24 * 7, // 7 days
|
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||||
secure: true,
|
secure: true,
|
||||||
sameSite: "none",
|
sameSite: "lax",
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
});
|
});
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const afterSessionDeleted = async (
|
|||||||
path: "/",
|
path: "/",
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
secure: true,
|
secure: true,
|
||||||
sameSite: "none",
|
sameSite: "lax",
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
});
|
});
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|||||||
@@ -52,15 +52,12 @@ export default function App() {
|
|||||||
org_id: data.session.activeOrganizationId ?? "unknown_org",
|
org_id: data.session.activeOrganizationId ?? "unknown_org",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set a non-httpOnly hint cookie so the landing page can detect login state.
|
const isLocal = window.location.hostname === "localhost";
|
||||||
// In production this is handled server-side on .useautumn.com domain.
|
const extras = isLocal ? "" : "; domain=.useautumn.com; Secure";
|
||||||
if (window.location.hostname === "localhost") {
|
if (data?.user) {
|
||||||
if (data?.user) {
|
document.cookie = `logged_in_hint=1; path=/; max-age=604800; SameSite=Lax${extras}`;
|
||||||
document.cookie =
|
} else {
|
||||||
"logged_in_hint=1; path=/; max-age=604800; SameSite=Lax";
|
document.cookie = `logged_in_hint=; path=/; max-age=0; SameSite=Lax${extras}`;
|
||||||
} else {
|
|
||||||
document.cookie = "logged_in_hint=; path=/; max-age=0; SameSite=Lax";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|||||||
Reference in New Issue
Block a user