Backend / staging cleanup, performance substrate, and create-flow polish #60

Merged
an.di merged 16 commits from adilallo/Backend/StagingCleanup into main 2026-05-26 15:11:47 +00:00
5 changed files with 61 additions and 22 deletions
Showing only changes of commit 2871df27b2 - Show all commits
+1
View File
@@ -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
View File
@@ -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