Backend / staging cleanup, performance substrate, and create-flow polish #60
@@ -49,6 +49,7 @@ export default function Page() {
|
|||||||
description: t("pages.home.heroBanner.description"),
|
description: t("pages.home.heroBanner.description"),
|
||||||
ctaText: t("pages.home.heroBanner.ctaText"),
|
ctaText: t("pages.home.heroBanner.ctaText"),
|
||||||
ctaHref: t("pages.home.heroBanner.ctaHref"),
|
ctaHref: t("pages.home.heroBanner.ctaHref"),
|
||||||
|
imageAlt: t("heroBanner.imageAlt"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const cardStepsData = {
|
const cardStepsData = {
|
||||||
|
|||||||
@@ -45,19 +45,22 @@ function CaseStudyView({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-figma-node="21993-32352"
|
data-figma-node="21993-32352"
|
||||||
className={`relative flex h-[305px] w-[305px] shrink-0 overflow-hidden ${CASE_TILE_RADIUS_CLASS} ${SURFACE_CLASS[surface]} ${className}`.trim()}
|
className={`relative h-[305px] w-[305px] shrink-0 overflow-hidden ${CASE_TILE_RADIUS_CLASS} ${SURFACE_CLASS[surface]} ${className}`.trim()}
|
||||||
>
|
>
|
||||||
{visual ? (
|
{visual ? (
|
||||||
<div className="flex size-full items-center justify-center p-2">{visual}</div>
|
<div className="flex size-full items-center justify-center p-2">{visual}</div>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="absolute inset-0">
|
||||||
<Art
|
<Art
|
||||||
role="img"
|
role="img"
|
||||||
aria-label={imageAlt}
|
aria-label={imageAlt}
|
||||||
data-case-study-art={SURFACE_ART_DATA_KEY[surface]}
|
data-case-study-art={SURFACE_ART_DATA_KEY[surface]}
|
||||||
width={305}
|
width="100%"
|
||||||
height={305}
|
height="100%"
|
||||||
className="pointer-events-none size-full select-none object-contain object-center"
|
className="pointer-events-none block select-none"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Figma: "Sections / Hero" (see registry)
|
* Figma: "Sections / Hero" (see registry)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
|
||||||
import ContentLockup from "../../type/ContentLockup";
|
import ContentLockup from "../../type/ContentLockup";
|
||||||
import HeroDecor from "./HeroDecor";
|
import HeroDecor from "./HeroDecor";
|
||||||
import { ASSETS, getAssetPath } from "../../../../lib/assetUtils";
|
import { ASSETS, getAssetPath } from "../../../../lib/assetUtils";
|
||||||
@@ -25,13 +22,18 @@ interface HeroBannerProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
ctaText?: string;
|
ctaText?: string;
|
||||||
ctaHref?: string;
|
ctaHref?: string;
|
||||||
|
imageAlt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeroBanner = memo<HeroBannerProps>(
|
const HeroBanner = memo<HeroBannerProps>(
|
||||||
({ title, subtitle, description, ctaText, ctaHref }) => {
|
({
|
||||||
const t = useTranslation();
|
title,
|
||||||
const imageAlt = t("heroBanner.imageAlt");
|
subtitle,
|
||||||
|
description,
|
||||||
|
ctaText,
|
||||||
|
ctaHref,
|
||||||
|
imageAlt = "Hero illustration",
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<section className="bg-transparent px-[var(--spacing-scale-008)] sm:px-[var(--spacing-scale-010)] md:px-[var(--spacing-scale-016)] lg:px-[var(--spacing-scale-024)] xl:px-[var(--spacing-scale-048)]">
|
<section className="bg-transparent px-[var(--spacing-scale-008)] sm:px-[var(--spacing-scale-010)] md:px-[var(--spacing-scale-016)] lg:px-[var(--spacing-scale-024)] xl:px-[var(--spacing-scale-048)]">
|
||||||
<div className="flex flex-col gap-[var(--spacing-scale-010)]">
|
<div className="flex flex-col gap-[var(--spacing-scale-010)]">
|
||||||
@@ -58,7 +60,7 @@ const HeroBanner = memo<HeroBannerProps>(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hero Image Container */}
|
{/* Hero Image Container */}
|
||||||
<div className="w-full h-full md:flex-1 rounded-[8px] overflow-hidden relative z-10 flex items-center justify-center">
|
<div className="relative z-10 flex w-full items-center justify-center overflow-hidden rounded-[8px] aspect-[16/10] md:flex-1">
|
||||||
<Image
|
<Image
|
||||||
src={getAssetPath(ASSETS.HERO_IMAGE)}
|
src={getAssetPath(ASSETS.HERO_IMAGE)}
|
||||||
alt={imageAlt}
|
alt={imageAlt}
|
||||||
@@ -66,7 +68,7 @@ const HeroBanner = memo<HeroBannerProps>(
|
|||||||
height={HERO_IMAGE_HEIGHT}
|
height={HERO_IMAGE_HEIGHT}
|
||||||
priority
|
priority
|
||||||
sizes="(min-width: 768px) 50vw, 100vw"
|
sizes="(min-width: 768px) 50vw, 100vw"
|
||||||
className="w-full h-auto"
|
className="size-full object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { memo } from "react";
|
import { memo, useEffect, useState } from "react";
|
||||||
|
|
||||||
interface HeroDecorProps {
|
interface HeroDecorProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeroDecor = memo<HeroDecorProps>(({ className = "" }) => {
|
const HeroDecor = memo<HeroDecorProps>(({ className = "" }) => {
|
||||||
|
const [grainEnabled, setGrainEnabled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// feTurbulence forces tiled rasterization that reads as top-down segments on
|
||||||
|
// first paint. Flat shapes render immediately; grain applies after paint.
|
||||||
|
const frame = requestAnimationFrame(() => {
|
||||||
|
setGrainEnabled(true);
|
||||||
|
});
|
||||||
|
return () => cancelAnimationFrame(frame);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className={`text-[var(--color-surface-default-brand-lighter-accent)] opacity-50 ${className}`}
|
className={`text-[var(--color-surface-default-brand-lighter-accent)] opacity-50 ${className}`}
|
||||||
@@ -59,7 +70,7 @@ const HeroDecor = memo<HeroDecorProps>(({ className = "" }) => {
|
|||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
{/* apply filter only to the decoration paths */}
|
{/* apply filter only to the decoration paths */}
|
||||||
<g fill="currentColor" filter="url(#grain)">
|
<g fill="currentColor" filter={grainEnabled ? "url(#grain)" : undefined}>
|
||||||
<path d="M1441.54 226.758C1495.92 226.758 1540 320.385 1540 435.879C1540 551.373 1495.92 645 1441.54 645C1387.16 645 1343.08 551.373 1343.08 435.879C1343.08 320.385 1387.16 226.758 1441.54 226.758Z" />
|
<path d="M1441.54 226.758C1495.92 226.758 1540 320.385 1540 435.879C1540 551.373 1495.92 645 1441.54 645C1387.16 645 1343.08 551.373 1343.08 435.879C1343.08 320.385 1387.16 226.758 1441.54 226.758Z" />
|
||||||
<path d="M1441.54 226.758C1495.92 226.758 1540 320.385 1540 435.879C1540 551.373 1495.92 645 1441.54 645C1387.16 645 1343.08 551.373 1343.08 435.879C1343.08 320.385 1387.16 226.758 1441.54 226.758Z" />
|
<path d="M1441.54 226.758C1495.92 226.758 1540 320.385 1540 435.879C1540 551.373 1495.92 645 1441.54 645C1387.16 645 1343.08 551.373 1343.08 435.879C1343.08 320.385 1387.16 226.758 1441.54 226.758Z" />
|
||||||
<path d="M674.066 209.121C728.443 209.121 772.525 302.748 772.525 418.242C772.525 533.737 728.443 627.363 674.066 627.363C619.688 627.363 575.607 533.737 575.607 418.242C575.607 302.748 619.688 209.121 674.066 209.121Z" />
|
<path d="M674.066 209.121C728.443 209.121 772.525 302.748 772.525 418.242C772.525 533.737 728.443 627.363 674.066 627.363C619.688 627.363 575.607 533.737 575.607 418.242C575.607 302.748 619.688 209.121 674.066 209.121Z" />
|
||||||
|
|||||||
+24
-2
@@ -1,6 +1,23 @@
|
|||||||
import createMDX from "@next/mdx";
|
import createMDX from "@next/mdx";
|
||||||
|
|
||||||
/* eslint-env node */
|
/* eslint-env node */
|
||||||
|
|
||||||
|
/** Keep viewBox so inline SVGR art can scale/center like `object-contain`. */
|
||||||
|
const svgrLoaderOptions = {
|
||||||
|
svgoConfig: {
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: "preset-default",
|
||||||
|
params: {
|
||||||
|
overrides: {
|
||||||
|
removeViewBox: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
@@ -22,7 +39,12 @@ const nextConfig = {
|
|||||||
rules: {
|
rules: {
|
||||||
"*.svg": {
|
"*.svg": {
|
||||||
condition: { not: "foreign" },
|
condition: { not: "foreign" },
|
||||||
loaders: ["@svgr/webpack"],
|
loaders: [
|
||||||
|
{
|
||||||
|
loader: "@svgr/webpack",
|
||||||
|
options: svgrLoaderOptions,
|
||||||
|
},
|
||||||
|
],
|
||||||
as: "*.js",
|
as: "*.js",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -104,7 +126,7 @@ const nextConfig = {
|
|||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
issuer: /\.[jt]sx?$/,
|
issuer: /\.[jt]sx?$/,
|
||||||
use: ["@svgr/webpack"],
|
use: [{ loader: "@svgr/webpack", options: svgrLoaderOptions }],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bundle analysis - only in production builds
|
// Bundle analysis - only in production builds
|
||||||
|
|||||||
Reference in New Issue
Block a user