Files
cfw-autumn/apps/website/components/hero.tsx
Ayush Rodrigues 11ff47c57e Redesign logo wall with responsive layout and updated styling
Update Browser Use SVG color to match other logos, rework mobile layout
to show 3+2 centered grid, match background and typography to hero section,
and add per-logo scale overrides for mobile/desktop.

Made-with: Cursor
2026-04-29 13:55:23 +01:00

291 lines
11 KiB
TypeScript
Executable File

"use client";
import { motion } from "motion/react";
import dynamic from "next/dynamic";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { CTALines, IconCTADocs, IconCTAStart } from "@/app/constant";
import { getGsap } from "@/lib/lazyGsap";
// `AutumnConfig` pulls in `react-syntax-highlighter` + `highlight.js` (~100KB
// gzipped + meaningful parse cost on mobile). It only renders on `xl+`
// viewports, so keep it out of the critical path for mobile users entirely.
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 [displayedText, setDisplayedText] = useState("");
const [isLoggedIn, setIsLoggedIn] = useState(false);
// Gate the xl-only hero visual (background video + AutumnConfig syntax
// highlighter) behind an actual viewport check so mobile never downloads
// ~100KB of react-syntax-highlighter or 640KB of video that `hidden
// xl:block` would otherwise still fetch.
const [isXl, setIsXl] = useState(false);
// Read the hint cookie after mount to avoid SSR/CSR hydration mismatch.
useEffect(() => {
setIsLoggedIn(getLoggedInHintCookie() === true);
}, []);
useEffect(() => {
const mq = window.matchMedia("(min-width: 1280px)");
const update = () => setIsXl(mq.matches);
update();
mq.addEventListener("change", update);
return () => mq.removeEventListener("change", update);
}, []);
// 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);
};
startTypewriter();
return () => {
if (timeoutId) clearTimeout(timeoutId);
if (intervalId) clearInterval(intervalId);
};
}, []);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
let ctx: { revert: () => void } | null = null;
let cancelled = false;
// Mobile: CSS animations in globals.css handle the hero reveal.
// Guard here so mobile never initiates the ~60KB GSAP dynamic import.
if (window.innerWidth < 1024) return;
getGsap().then((gsap) => {
if (cancelled) return;
ctx = gsap.context(() => {
gsap.set(".hero-reveal", {
opacity: 0,
y: 25,
scale: 0.96,
transformOrigin: "center bottom",
});
gsap.set(".hero-cta", { opacity: 0, scale: 0.95 });
const tl = gsap.timeline({ defaults: { overwrite: "auto" } });
// hero-bg is intentionally NOT hidden — it is the LCP element and
// must be visible from first paint. The brightness flash still runs.
tl.to(".hero-bg", { filter: "brightness(1.6)", duration: 0.125, ease: "power2.in" })
.to(".hero-bg", { filter: "brightness(1)", duration: 0.125, ease: "power2.out" })
.to(".hero-reveal", { opacity: 1, y: 0, 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");
}, container);
});
return () => {
cancelled = true;
ctx?.revert();
};
}, []);
return (
<div ref={containerRef}>
<div className="relative hero-root flex flex-col items-stretch pb-0 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 lg:opacity-0 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 lg:opacity-0 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">
Drop-in credits and billing for
</span>{" "}
<span className="text-white block md:inline">AI agents</span>
</h1>
<p className="hero-reveal lg:opacity-0 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 the customer database
</span>{" "}
that scales from your first user to your largest contract.
</p>
</div>
</div>
{/*
Keep the wrapper itself in the SSR HTML with `hidden xl:block`
so desktop gets the correct half-width hero layout from the
first paint (no post-hydration layout shift). Only the heavy
children (video + AutumnConfig) are client-gated on `isXl`
so mobile never downloads them, and desktop fills the
already-reserved space once it hydrates.
*/}
<div className="hero-reveal lg:opacity-0 relative w-[50vw] max-w-[720px] min-h-[525px] p-16 py-0 mx-auto hidden xl:block">
{isXl && (
<>
<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} />
</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 lg:opacity-0 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 lg:opacity-0 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">
Get a demo
</span>
<span className="relative z-10 scale-100">
<IconCTADocs />
</span>
</div>
</motion.div>
</Link>
</div>
<div className="hero-cta lg:opacity-0 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">
{/*
Mobile hero backdrop: a still frame of the desktop webm
baked into a 63KB webp. The full 625KB looping video
competes with the hero SVG + critical JS for bandwidth on
slow mobile radios, and because this sits behind the
dominant `autumn_mobile.svg` with `mix-blend-screen`, a
static frame reads virtually identically.
*/}
<Image
src="/images/pricing-models/pricingbg-mobile.webp"
alt=""
aria-hidden="true"
fill
priority
fetchPriority="high"
sizes="100vw"
className="hero-bg absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 pointer-events-none select-none"
/>
<div className="hero-reveal lg:opacity-0 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>
);
}