diff --git a/app/[locale]/assets/_components/assets.tsx b/app/[locale]/assets/_components/assets.tsx index 7b4fd39e058..b6802faef41 100644 --- a/app/[locale]/assets/_components/assets.tsx +++ b/app/[locale]/assets/_components/assets.tsx @@ -54,10 +54,10 @@ import finance from "@/public/images/finance_transparent.png" import future from "@/public/images/future_transparent.png" import hackathon from "@/public/images/hackathon_transparent.png" import communityHero from "@/public/images/heroes/community-hero.png" -import developersHero from "@/public/images/heroes/developers-hub-hero.jpg" +import developersHero from "@/public/images/heroes/developers-hub-hero.png" import garden from "@/public/images/heroes/garden.jpg" import guidesHero from "@/public/images/heroes/guides-hub-hero.jpg" -import layer2Hero from "@/public/images/heroes/layer-2-hub-hero.jpg" +import layer2Hero from "@/public/images/heroes/layer-2-hub-hero.png" import learnHero from "@/public/images/heroes/learn-hub-hero.png" import quizzesHub from "@/public/images/heroes/quizzes-hub-hero.png" import roadmapHero from "@/public/images/heroes/roadmap-hub-hero.jpg" diff --git a/app/[locale]/empty/page.tsx b/app/[locale]/empty/page.tsx new file mode 100644 index 00000000000..d24ea68a82a --- /dev/null +++ b/app/[locale]/empty/page.tsx @@ -0,0 +1,24 @@ +import { getMetadata } from "@/lib/utils/metadata" + +import { routing } from "@/i18n/routing" + +const Page = async () => { + return

ethereum.org

+} + +export async function generateStaticParams() { + return routing.locales.map((locale) => ({ + locale, + })) +} + +export async function generateMetadata() { + return await getMetadata({ + locale: "en", + slug: [""], + title: "ethereum.org", + description: "Empty page test", + }) +} + +export default Page diff --git a/app/[locale]/hero/page.tsx b/app/[locale]/hero/page.tsx new file mode 100644 index 00000000000..a149e4205ab --- /dev/null +++ b/app/[locale]/hero/page.tsx @@ -0,0 +1,276 @@ +"use client" + +import { useEffect, useState } from "react" +import Image from "next/image" +import { useTranslations } from "next-intl" + +import { Heading2 } from "@/components/MdComponents" + +import { breakpointAsNumber } from "@/lib/utils/screen" + +import heroImage from "@/public/images/home/hero.png" + +export default function HeroQualityDemo() { + const t = useTranslations("page-index") + const [selectedQualities, setSelectedQualities] = useState([100, 5]) + const [customHeight, setCustomHeight] = useState(300) // Default height in px + const [isMobile, setIsMobile] = useState(false) + const [useCustomHeight, setUseCustomHeight] = useState(false) // Toggle for custom height + + // Check if mobile on mount + useEffect(() => { + const checkMobile = () => setIsMobile(window.innerWidth < 768) + checkMobile() + window.addEventListener("resize", checkMobile) + return () => window.removeEventListener("resize", checkMobile) + }, []) + + const alt = t("page-index-hero-image-alt") + + // Generate quality values from 100% to 5% in 5% decrements + const qualityValues = Array.from({ length: 20 }, (_, i) => 100 - i * 5) + + const toggleQuality = (quality: number) => { + setSelectedQualities( + (prev) => + prev.includes(quality) + ? prev.filter((q) => q !== quality) + : [...prev, quality].sort((a, b) => b - a) // Keep sorted descending + ) + } + + const handleMouseResize = ( + e: React.MouseEvent, + direction: "top" | "bottom" + ) => { + if (isMobile) return + + const startY = e.clientY + const startHeight = customHeight + + const handleMouseMove = (moveEvent: MouseEvent) => { + const deltaY = + direction === "top" + ? startY - moveEvent.clientY + : moveEvent.clientY - startY + const newHeight = Math.max(100, Math.min(600, startHeight + deltaY)) + setCustomHeight(newHeight) + } + + const handleMouseUp = () => { + document.removeEventListener("mousemove", handleMouseMove) + document.removeEventListener("mouseup", handleMouseUp) + document.body.style.cursor = "default" + document.body.style.userSelect = "auto" + } + + document.body.style.cursor = direction === "top" ? "n-resize" : "s-resize" + document.body.style.userSelect = "none" + document.addEventListener("mousemove", handleMouseMove) + document.addEventListener("mouseup", handleMouseUp) + } + + const ImageContainer = ({ + quality, + isComparison = false, + }: { + quality: number + isComparison?: boolean + }) => ( +
+ {!isMobile && useCustomHeight && ( + <> +
handleMouseResize(e, "top")} + /> +
handleMouseResize(e, "bottom")} + /> + + )} +
+ {`${alt}= 80} + /> +
+
+ ) + + return ( +
+
+

+ Hero Image Quality Comparison +

+

+ Comparing the homepage hero image at different quality settings (100% + to 5% in 5% decrements). Check the boxes below to compare selected + qualities side-by-side. +

+
+
+ {/* Custom Height Toggle */} +
+ +
+ + {useCustomHeight && ( +
+ + Height: {customHeight}px + + {!isMobile && ( + + (Hover top/bottom edges to resize) + + )} +
+ )} + + {!useCustomHeight && ( +
+ Using responsive breakpoint heights: 240px (mobile) • 380px (tablet) + • 480px (desktop) +
+ )} +
+ + {/* Comparison Section */} + {selectedQualities.length > 0 && ( +
+

+ Selected Qualities Comparison ({selectedQualities.length} selected) +

+
+ {selectedQualities.map((quality) => ( +
+
+
+ + Quality: {quality}% + +
+ +
+
+ ))} +
+
+
+ )} + +
+ {qualityValues.map((quality) => ( +
+
+ toggleQuality(quality)} + className="-ms-4 h-4 w-4 flex-shrink-0 rounded border-gray-300 text-primary focus:ring-primary" + /> +
+ + Quality: {quality}% + +
+
+ +
+ ))} +
+ + {/* Mobile Height Slider */} + {isMobile && useCustomHeight && ( +
+
+ + setCustomHeight(Number(e.target.value))} + className="flex-1" + /> + + {customHeight}px + +
+
+ )} + +
+
+

Usage Notes:

+
    +
  • + • Lower quality (5-20%) = smaller file size, faster loading, but + visible compression artifacts +
  • +
  • + • Medium quality (25-50%) = balanced file size and visual quality +
  • +
  • + • Higher quality (55-100%) = larger file size, slower loading, but + better visual fidelity +
  • +
  • + • The optimal quality depends on your performance vs visual + quality requirements +
  • +
  • • Use the height controls to test different viewport sizes
  • +
+
+
+
+ ) +} diff --git a/app/[locale]/layer-2/_components/layer-2.tsx b/app/[locale]/layer-2/_components/layer-2.tsx index 7216a7c88db..ea7c4fdcc3f 100644 --- a/app/[locale]/layer-2/_components/layer-2.tsx +++ b/app/[locale]/layer-2/_components/layer-2.tsx @@ -14,7 +14,7 @@ import InlineLink from "@/components/ui/Link" import { Rollups } from "@/data/networks/networks" import useTranslation from "@/hooks/useTranslation" -import HeroImage from "@/public/images/heroes/layer-2-hub-hero.jpg" +import HeroImage from "@/public/images/heroes/layer-2-hub-hero.png" import EthereumLogo from "@/public/images/layer-2/ethereum.png" import WalkingImage from "@/public/images/layer-2/layer-2-walking.png" import ExploreImage from "@/public/images/layer-2/learn-hero.png" diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 581adcfec38..31b00eb3fcb 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -17,6 +17,7 @@ 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 { getBentoBoxItems } from "@/components/Homepage/utils" import ValuesMarqueeFallback from "@/components/Homepage/ValuesMarquee/Fallback" import BlockHeap from "@/components/icons/block-heap.svg" @@ -91,11 +92,6 @@ import { fetchRSS } from "@/lib/api/fetchRSS" import { fetchTotalEthStaked } from "@/lib/api/fetchTotalEthStaked" import { fetchTotalValueLocked } from "@/lib/api/fetchTotalValueLocked" import EventFallback from "@/public/images/events/event-placeholder.png" -import BuildersImage from "@/public/images/heroes/developers-hub-hero.jpg" -import ActivityImage from "@/public/images/heroes/layer-2-hub-hero.jpg" -import LearnImage from "@/public/images/heroes/learn-hub-hero.png" -import CommunityImage from "@/public/images/heroes/quizzes-hub-hero.png" -import Hero from "@/public/images/home/hero.png" const BentoCardSwiper = dynamic( () => import("@/components/Homepage/BentoCardSwiper"), @@ -437,7 +433,7 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => {

- +
{subHeroCTAs.map( @@ -517,7 +513,7 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => { {/* Activity - The strongest ecosystem */}
- + @@ -566,7 +562,7 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => { className="md:flex-row-reverse" > - + @@ -647,7 +643,7 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => { {/* Builders - Blockchain's biggest builder community */}
- + @@ -700,7 +696,7 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => { className="md:flex-row-reverse" > - + diff --git a/app/[locale]/start/page.tsx b/app/[locale]/start/page.tsx index 51bf42a9b7d..4ae641ec845 100644 --- a/app/[locale]/start/page.tsx +++ b/app/[locale]/start/page.tsx @@ -10,7 +10,7 @@ import ShareModal from "@/components/StartWithEthereumFlow/ShareModal" import { getMetadata } from "@/lib/utils/metadata" import { getNewToCryptoWallets } from "@/lib/utils/wallets" -import HeroImage from "@/public/images/heroes/developers-hub-hero.jpg" +import HeroImage from "@/public/images/heroes/developers-hub-hero.png" import ManDogeImage from "@/public/images/start-with-ethereum/man-doge-playing.png" const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => { @@ -79,7 +79,7 @@ export async function generateMetadata({ slug: ["start"], title: t("page-start-meta-title"), description: t("page-start-meta-description"), - image: "/images/heroes/developers-hub-hero.jpg", + image: "/images/heroes/developers-hub-hero.png", }) } diff --git a/public/images/heroes/developers-hub-hero-portrait.png b/public/images/heroes/developers-hub-hero-portrait.png new file mode 100644 index 00000000000..76a6e8a00f6 Binary files /dev/null and b/public/images/heroes/developers-hub-hero-portrait.png differ diff --git a/public/images/heroes/developers-hub-hero.jpg b/public/images/heroes/developers-hub-hero.jpg deleted file mode 100644 index abdf671a49f..00000000000 Binary files a/public/images/heroes/developers-hub-hero.jpg and /dev/null differ diff --git a/public/images/heroes/developers-hub-hero.png b/public/images/heroes/developers-hub-hero.png new file mode 100644 index 00000000000..46c26c8de88 Binary files /dev/null and b/public/images/heroes/developers-hub-hero.png differ diff --git a/public/images/heroes/layer-2-hub-hero-portrait.png b/public/images/heroes/layer-2-hub-hero-portrait.png new file mode 100644 index 00000000000..7069d1a78fb Binary files /dev/null and b/public/images/heroes/layer-2-hub-hero-portrait.png differ diff --git a/public/images/heroes/layer-2-hub-hero.jpg b/public/images/heroes/layer-2-hub-hero.jpg deleted file mode 100644 index 9554c185756..00000000000 Binary files a/public/images/heroes/layer-2-hub-hero.jpg and /dev/null differ diff --git a/public/images/heroes/layer-2-hub-hero.png b/public/images/heroes/layer-2-hub-hero.png new file mode 100644 index 00000000000..77a18fcab4c Binary files /dev/null and b/public/images/heroes/layer-2-hub-hero.png differ diff --git a/public/images/heroes/learn-hub-hero-portrait.png b/public/images/heroes/learn-hub-hero-portrait.png new file mode 100644 index 00000000000..cf26aa0ec7c Binary files /dev/null and b/public/images/heroes/learn-hub-hero-portrait.png differ diff --git a/public/images/heroes/learn-hub-hero.png b/public/images/heroes/learn-hub-hero.png index 41b03361cda..42417c0b9df 100644 Binary files a/public/images/heroes/learn-hub-hero.png and b/public/images/heroes/learn-hub-hero.png differ diff --git a/public/images/heroes/quizzes-hub-hero-portrait.png b/public/images/heroes/quizzes-hub-hero-portrait.png new file mode 100644 index 00000000000..f2b41fbd1ff Binary files /dev/null and b/public/images/heroes/quizzes-hub-hero-portrait.png differ diff --git a/public/images/heroes/quizzes-hub-hero.png b/public/images/heroes/quizzes-hub-hero.png index 41163b0bc7f..0e07cb21343 100644 Binary files a/public/images/heroes/quizzes-hub-hero.png and b/public/images/heroes/quizzes-hub-hero.png differ diff --git a/public/images/home/hero-2xl.png b/public/images/home/hero-2xl.png new file mode 100644 index 00000000000..42c97e3c7a9 Binary files /dev/null and b/public/images/home/hero-2xl.png differ diff --git a/public/images/home/hero.png b/public/images/home/hero.png index ae5670ad9ef..38817ab859d 100644 Binary files a/public/images/home/hero.png and b/public/images/home/hero.png differ diff --git a/src/components/Hero/HomeHero/HomeHero.stories.tsx b/src/components/Hero/HomeHero/HomeHero.stories.tsx new file mode 100644 index 00000000000..cc51a78dff1 --- /dev/null +++ b/src/components/Hero/HomeHero/HomeHero.stories.tsx @@ -0,0 +1,24 @@ +import { Meta, StoryObj } from "@storybook/react" + +import { langViewportModes } from "@/storybook/modes" + +import HomeHeroComponent from "." + +const meta = { + title: "Organisms / Layouts / Hero", + component: HomeHeroComponent, + parameters: { + layout: "none", + chromatic: { + modes: { + ...langViewportModes, + }, + }, + }, +} satisfies Meta + +export default meta + +export const HomeHero: StoryObj = { + render: () => , +} diff --git a/src/components/Hero/HomeHero/index.tsx b/src/components/Hero/HomeHero/index.tsx index 8d41eb1d20a..11d683f0787 100644 --- a/src/components/Hero/HomeHero/index.tsx +++ b/src/components/Hero/HomeHero/index.tsx @@ -1,29 +1,58 @@ -import { getTranslations } from "next-intl/server" +import { getImageProps } from "next/image" +import { getLocale, getTranslations } from "next-intl/server" -import type { ClassNameProp, CommonHeroProps, Lang } from "@/lib/types" +import type { ClassNameProp } from "@/lib/types" import LanguageMorpher from "@/components/Homepage/LanguageMorpher" -import { Image } from "@/components/Image" -export type HomeHeroProps = Pick & - ClassNameProp & { - locale: Lang - } +import { cn } from "@/lib/utils/cn" +import { breakpointAsNumber } from "@/lib/utils/screen" + +import heroBase from "@/public/images/home/hero.png" +import hero2xl from "@/public/images/home/hero-2xl.png" -const HomeHero = async ({ heroImg, className, locale }: HomeHeroProps) => { +const HomeHero = async ({ className }: ClassNameProp) => { + const locale = getLocale() const t = await getTranslations({ locale, namespace: "page-index" }) + const alt = t("page-index-hero-image-alt") + + const common = { + alt, + sizes: `(max-width: ${breakpointAsNumber["2xl"]}px) 100vw, ${breakpointAsNumber["2xl"]}px`, + priority: true, + } + + const { + props: { srcSet: srcSet2xl }, + } = getImageProps({ ...common, ...hero2xl, quality: 20 }) + + const { + props: { srcSet: srcSetMd }, + } = getImageProps({ ...common, ...heroBase, quality: 10 }) + + const { + props: { srcSet: srcSetBase, ...rest }, + } = getImageProps({ ...common, ...heroBase, quality: 5 }) + return ( -
-
- {t("page-index-hero-image-alt")} +
+
+ + + + + {alt} +
diff --git a/src/components/Hero/index.ts b/src/components/Hero/index.ts index 63d0131a664..c3e14baf76d 100644 --- a/src/components/Hero/index.ts +++ b/src/components/Hero/index.ts @@ -1,4 +1,4 @@ export { default as ContentHero, type ContentHeroProps } from "./ContentHero" -export { default as HomeHero, type HomeHeroProps } from "./HomeHero" +export { default as HomeHero } from "./HomeHero" export { default as HubHero } from "./HubHero" export { default as MdxHero, type MdxHeroProps } from "./MdxHero" diff --git a/src/components/Homepage/HomepageSectionImage.tsx b/src/components/Homepage/HomepageSectionImage.tsx new file mode 100644 index 00000000000..0eb616b4c10 --- /dev/null +++ b/src/components/Homepage/HomepageSectionImage.tsx @@ -0,0 +1,97 @@ +import type { StaticImageData } from "next/image" +import { getImageProps } from "next/image" + +import { breakpointAsNumber } from "@/lib/utils/screen" + +// Import all landscape images (all PNG now) +import developersHubHero from "@/public/images/heroes/developers-hub-hero.png" +// Import all portrait images (all PNG) +import developersHubHeroPortrait from "@/public/images/heroes/developers-hub-hero-portrait.png" +import layerTwoHubHero from "@/public/images/heroes/layer-2-hub-hero.png" +import layerTwoHubHeroPortrait from "@/public/images/heroes/layer-2-hub-hero-portrait.png" +import learnHubHero from "@/public/images/heroes/learn-hub-hero.png" +import learnHubHeroPortrait from "@/public/images/heroes/learn-hub-hero-portrait.png" +import quizzesHubHero from "@/public/images/heroes/quizzes-hub-hero.png" +import quizzesHubHeroPortrait from "@/public/images/heroes/quizzes-hub-hero-portrait.png" + +const imageMap: Record< + string, + { + mobile: StaticImageData + desktop: StaticImageData + } +> = { + activity: { + mobile: layerTwoHubHero, + desktop: layerTwoHubHeroPortrait, + }, + learn: { + mobile: learnHubHero, + desktop: learnHubHeroPortrait, + }, + builders: { + mobile: developersHubHero, + desktop: developersHubHeroPortrait, + }, + community: { + mobile: quizzesHubHero, + desktop: quizzesHubHeroPortrait, + }, +} + +type HomepageSectionImageProps = { + sectionId: string // e.g. "activity", "learn", "builders", "community" + alt: string + className?: string +} + +export default function HomepageSectionImage({ + sectionId, + alt, + className, +}: HomepageSectionImageProps) { + const images = imageMap[sectionId] + + if (!images) { + throw new Error( + `Image not found for section: ${sectionId}. Available sections: ${Object.keys(imageMap).join(", ")}` + ) + } + + const common = { + alt, + // Be very specific: Desktop max 512px, Mobile 100vw + sizes: `(max-width: ${breakpointAsNumber.md}px) 100vw, 512px`, + priority: false, + } + + const { + props: { srcSet: desktop }, + } = getImageProps({ + ...common, + ...images.desktop, + quality: 25, + }) + + const { + props: { srcSet: mobile, ...rest }, + } = getImageProps({ + ...common, + ...images.mobile, + quality: 40, + }) + + return ( + + + + {alt} + + ) +} diff --git a/src/data/roadmap/releases.tsx b/src/data/roadmap/releases.tsx index 70d57f6ffc5..ba7c5b4b64d 100644 --- a/src/data/roadmap/releases.tsx +++ b/src/data/roadmap/releases.tsx @@ -1,8 +1,8 @@ import { StaticImageData } from "next/image" -import DevelopersHubHeroImage from "@/public/images/heroes/developers-hub-hero.jpg" +import DevelopersHubHeroImage from "@/public/images/heroes/developers-hub-hero.png" import GuidesHubHeroImage from "@/public/images/heroes/guides-hub-hero.jpg" -import Layer2HubHeroImage from "@/public/images/heroes/layer-2-hub-hero.jpg" +import Layer2HubHeroImage from "@/public/images/heroes/layer-2-hub-hero.png" import QuizzesHubHeroImage from "@/public/images/heroes/quizzes-hub-hero.png" import FusakaImage from "@/public/images/roadmap/roadmap-fusaka.png" import PectraImage from "@/public/images/roadmap/roadmap-pectra.png" diff --git a/src/layouts/BaseLayout.tsx b/src/layouts/BaseLayout.tsx index cd984372b02..e35840211fd 100644 --- a/src/layouts/BaseLayout.tsx +++ b/src/layouts/BaseLayout.tsx @@ -4,7 +4,7 @@ import dynamic from "next/dynamic" import type { Root } from "@/lib/types" import Footer from "@/components/Footer" -import Nav from "@/components/Nav" +// import Nav from "@/components/Nav" import { SkipLink } from "@/components/SkipLink" // import TranslationBanner from "@/components/TranslationBanner" @@ -52,8 +52,6 @@ export const BaseLayout = async ({ */}
-