Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 56 additions & 38 deletions app/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import type {
} from "@/lib/types"
import { CodeExample } from "@/lib/interfaces"

import ABTestWrapper from "@/components/AB/TestWrapper"
import ActivityStats from "@/components/ActivityStats"
import { ChevronNext } from "@/components/Chevron"
import HomeHero from "@/components/Hero/HomeHero"
import BentoCard from "@/components/Homepage/BentoCard"
import CodeExamples from "@/components/Homepage/CodeExamples"
import HomepageSectionImage from "@/components/Homepage/HomepageSectionImage"
import PersonaModalCTA from "@/components/Homepage/PersonaModalCTA"
import { getBentoBoxItems } from "@/components/Homepage/utils"
import ValuesMarqueeFallback from "@/components/Homepage/ValuesMarquee/Fallback"
import BlockHeap from "@/components/icons/block-heap.svg"
Expand Down Expand Up @@ -450,44 +452,60 @@ const Page = async ({ params }: { params: PageParams }) => {
<MainArticle className="flex w-full flex-col items-center" dir={dir}>
<HomeHero />
<div className="w-full space-y-32 px-4 md:mx-6 lg:space-y-48">
<div className="-mb-8 grid w-full grid-cols-2 gap-x-4 gap-y-8 border-b py-20 md:grid-cols-4 md:gap-x-10 lg:-mb-12">
{subHeroCTAs.map(
({ label, description, href, className, Svg }, idx) => {
const Link = (
props: Omit<
SvgButtonLinkProps,
"Svg" | "href" | "label" | "children"
>
) => (
<SvgButtonLink
Svg={Svg}
href={href}
label={label}
customEventOptions={{
eventCategory,
eventAction: "Top 4 CTAs",
eventName: subHeroCTAs[idx].eventName,
}}
{...props}
>
<p className="text-body">{description}</p>
</SvgButtonLink>
)
return (
<Fragment key={label}>
<Link
className={cn("xl:hidden", className)}
variant="col"
/>
<Link
className={cn("hidden xl:block", className)}
variant="row"
/>
</Fragment>
)
}
)}
</div>
<ABTestWrapper
testKey="HomepagePersonaCTAs"
variants={[
// Original: 4 CTAs grid
<div
key="four-ctas"
className="-mb-8 grid w-full grid-cols-2 gap-x-4 gap-y-8 border-b py-20 md:grid-cols-4 md:gap-x-10 lg:-mb-12"
>
{subHeroCTAs.map(
({ label, description, href, className, Svg }, idx) => {
const Link = (
props: Omit<
SvgButtonLinkProps,
"Svg" | "href" | "label" | "children"
>
) => (
<SvgButtonLink
Svg={Svg}
href={href}
label={label}
customEventOptions={{
eventCategory,
eventAction: "Top 4 CTAs",
eventName: subHeroCTAs[idx].eventName,
}}
{...props}
>
<p className="text-body">{description}</p>
</SvgButtonLink>
)
return (
<Fragment key={label}>
<Link
className={cn("xl:hidden", className)}
variant="col"
/>
<Link
className={cn("hidden xl:block", className)}
variant="row"
/>
</Fragment>
)
}
)}
</div>,
// Variation1: "Start here" button with persona modal
<div
key="persona-modal"
className="flex w-full items-center justify-center border-b pb-10"
>
<PersonaModalCTA eventCategory={eventCategory} />
</div>,
]}
/>

{/* What is Ethereum */}
<Section
Expand Down
15 changes: 13 additions & 2 deletions src/components/AB/TestDebugPanel.tsx
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe out of scope but this is setting the debug panel to render in a portal to avoid pending an extra hidden node in the place where the ABTestWrapper is used.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client"

import { useRef, useState } from "react"
import { useEffect, useRef, useState } from "react"
import { createPortal } from "react-dom"

import { cn } from "@/lib/utils/cn"

Expand All @@ -19,6 +20,7 @@ export const ABTestDebugPanel = ({
availableVariants,
}: ABTestDebugPanelProps) => {
const [isOpen, setIsOpen] = useState(false)
const [mounted, setMounted] = useState(false)
const [selectedVariant, setSelectedVariant] = useLocalStorage<number | null>(
`ab-test-${testKey}`,
null
Expand All @@ -27,10 +29,14 @@ export const ABTestDebugPanel = ({

useOnClickOutside(panelRef, () => setIsOpen(false))

useEffect(() => {
setMounted(true)
}, [])

const forceVariant = (variantIndex: number) =>
setSelectedVariant(variantIndex)

return (
const panelContent = (
<div
ref={panelRef}
className="fixed bottom-5 right-5 z-modal rounded-lg border-2 bg-background-low p-2.5 font-mono text-xs"
Expand Down Expand Up @@ -69,4 +75,9 @@ export const ABTestDebugPanel = ({
)}
</div>
)

// Only render portal on client side after mount
if (!mounted) return null

return createPortal(panelContent, document.body)
}
46 changes: 39 additions & 7 deletions src/components/Hero/HomeHero/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getLocale, getTranslations } from "next-intl/server"

import type { ClassNameProp } from "@/lib/types"

import ABTestWrapper from "@/components/AB/TestWrapper"
import LanguageMorpher from "@/components/Homepage/LanguageMorpher"

import { cn } from "@/lib/utils/cn"
Expand Down Expand Up @@ -68,13 +69,44 @@ const HomeHero = async ({
</picture>
</div>
<div className="flex flex-col items-center border-t-[3px] border-primary-low-contrast px-4 py-10 text-center">
<LanguageMorpher />
<div className="flex flex-col items-center gap-y-5 lg:max-w-2xl">
<h1 className="font-black">{t("page-index-title")}</h1>
<p className="max-w-96 text-md text-body-medium lg:text-lg">
{t("page-index-description")}
</p>
</div>
<ABTestWrapper
testKey="HomepagePersonaCTAs"
variants={[
// Original: LanguageMorpher + existing title/description
<div
key="original-hero-content"
className="flex flex-col items-center"
>
<LanguageMorpher />
<div className="flex flex-col items-center gap-y-5 lg:max-w-2xl">
<h1 className="font-black">{t("page-index-title")}</h1>
<p className="max-w-96 text-md text-body-medium lg:text-lg">
{t("page-index-description")}
</p>
</div>
</div>,
// Variation1: New title/subtitle for persona modal
<div
key="persona-hero-content"
className="flex flex-col items-center gap-y-5 lg:max-w-2xl"
>
<LanguageMorpher />
<div className="flex flex-col items-center gap-y-5 lg:max-w-2xl">
<h1 className="font-black">
The internet
<br />
that belongs to you
</h1>
<p className="max-w-lg text-md text-body-medium lg:text-lg">
Create, own, build, connect, and transact.
<br />
Ethereum is a network that everyone can use and anyone can
build on.
</p>
</div>
</div>,
]}
/>
</div>
</div>
)
Expand Down
173 changes: 173 additions & 0 deletions src/components/Homepage/PersonaModalCTA.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"use client"

import { useState } from "react"
import { BookOpen, Building2, Code, ExternalLink } from "lucide-react"

import { ChevronNext } from "@/components/Chevron"
import { Button } from "@/components/ui/buttons/Button"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog-modal"
import { BaseLink } from "@/components/ui/Link"

import { cn } from "@/lib/utils/cn"
import { trackCustomEvent } from "@/lib/utils/matomo"

type PersonaLink = {
label: string
href: string
isExternal?: boolean
}

type PersonaCategory = {
id: string
label: string
Icon: React.FC<{ className?: string }>
iconBgClass: string
iconColorClass: string
links: PersonaLink[]
}

const categories: PersonaCategory[] = [
{
id: "beginners",
label: "For beginners",
Icon: BookOpen,
iconBgClass: "bg-accent-a/20",
iconColorClass: "text-accent-a",
links: [
{ label: "What is Ethereum?", href: "/what-is-ethereum/" },
{ label: "Get a wallet", href: "/wallets/find-wallet/" },
],
},
{
id: "developers",
label: "For developers",
Icon: Code,
iconBgClass: "bg-primary-low-contrast",
iconColorClass: "text-primary",
links: [
{ label: "Developer Hub", href: "/developers/" },
{ label: "Docs", href: "/developers/docs/" },
],
},
{
id: "enterprise",
label: "For enterprise",
Icon: Building2,
iconBgClass: "bg-accent-c/20",
iconColorClass: "text-accent-c",
links: [
{ label: "Founders", href: "/founders/" },
{
label: "Institutions",
href: "https://institutions.ethereum.org/",
isExternal: true,
},
],
},
]

type PersonaModalCTAProps = {
eventCategory: string
}

const PersonaModalCTA = ({ eventCategory }: PersonaModalCTAProps) => {
const [isOpen, setIsOpen] = useState(false)

const handleOpenChange = (open: boolean) => {
if (open) {
trackCustomEvent({
eventCategory,
eventAction: "start here",
eventName: "start here",
})
}
setIsOpen(open)
}

const handleLinkClick = (label: string) => {
trackCustomEvent({
eventCategory,
eventAction: "modal",
eventName: label,
})
setIsOpen(false)
}

return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button variant="solid" size="lg" className="gap-2">
Start here
<ChevronNext className="size-5" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-[1440px] p-4 md:rounded-[32px] md:p-8">
<DialogHeader className="pe-0 pt-8 md:pt-0">
<DialogTitle className="text-center text-2xl font-bold md:text-4xl">
What brings you here?
</DialogTitle>
</DialogHeader>
<div className="mt-4 grid gap-4 md:mt-6 md:grid-cols-3 md:gap-6">
{categories.map(
({ id, label, Icon, iconBgClass, iconColorClass, links }) => (
<div
key={id}
className="border-border-default flex flex-col rounded-3xl border p-6 md:p-10"
>
{/* Icon and Category Label */}
<div className="mb-6 flex flex-col gap-2 md:mb-8 md:gap-4">
<div
className={cn(
"grid size-8 place-items-center rounded-lg md:size-16 md:rounded-2xl",
iconBgClass
)}
>
<Icon className={cn("size-4 md:size-8", iconColorClass)} />
</div>
<p className="text-sm font-bold uppercase tracking-wider">
{label}
</p>
</div>

{/* Links */}
<div className="mt-auto flex flex-col gap-2 md:gap-4">
{links.map(({ label: linkLabel, href, isExternal }, idx) => (
<div key={linkLabel}>
{idx > 0 && <div className="mb-2 border-t md:mb-4" />}
<BaseLink
href={href}
onClick={() => handleLinkClick(linkLabel)}
hideArrow
className="group flex items-center justify-between text-xl font-bold text-primary no-underline transition-colors hover:text-primary-hover md:text-3xl"
{...(isExternal && {
target: "_blank",
rel: "noopener noreferrer",
})}
>
<span className="flex items-center gap-1">
{linkLabel}
{isExternal && (
<ExternalLink className="size-3 text-body-medium md:size-4" />
)}
</span>
<ChevronNext className="size-5 text-primary transition-transform group-hover:translate-x-1" />
</BaseLink>
</div>
))}
</div>
</div>
)
)}
</div>
</DialogContent>
</Dialog>
)
}

export default PersonaModalCTA