Match Home, top nav, hero, and logo wall to the current marketing frames.
Puts Workshops in the header, pairs the hero with a static rule card and breakpoint CTAs, and shows the three pilot marks with no wall label. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
e12e06f7ce
commit
33cdaf135c
47 files changed
+728
-527
No files matched your search
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Figma: "Section / Hero Banner" (18477:15473)
|
||||
*/
|
||||
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { fetchAuthSession } from "../../../../lib/create/api";
|
||||
import { prepareFreshCreateFlowEntrySync } from "../../../(app)/create/utils/prepareFreshCreateFlowEntry";
|
||||
import type { Category } from "../../cards/Rule";
|
||||
import type { ChipOption } from "../../controls/MultiSelect/MultiSelect.types";
|
||||
import HeroBannerView from "./HeroBanner.view";
|
||||
import type { HeroBannerProps } from "./HeroBanner.types";
|
||||
|
||||
function chipsFromLabels(labels: string[]): ChipOption[] {
|
||||
return labels.map((label) => ({
|
||||
id: label.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
|
||||
label,
|
||||
state: "unselected" as const,
|
||||
}));
|
||||
}
|
||||
|
||||
const HeroBannerContainer = memo<HeroBannerProps>(
|
||||
({ title, subtitle, description }) => {
|
||||
const router = useRouter();
|
||||
const messages = useMessages();
|
||||
const tHome = useTranslation("pages.home.heroBanner");
|
||||
const tHero = useTranslation("heroBanner");
|
||||
const [signedIn, setSignedIn] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void fetchAuthSession()
|
||||
.then(({ user }) => {
|
||||
if (!cancelled) setSignedIn(Boolean(user));
|
||||
})
|
||||
.catch(() => {
|
||||
/* guest fallback */
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCreateRule = useCallback(() => {
|
||||
prepareFreshCreateFlowEntrySync({ signedIn });
|
||||
router.push("/create/informational");
|
||||
}, [router, signedIn]);
|
||||
|
||||
const ruleCategories = useMemo<Category[]>(() => {
|
||||
const groups = messages.heroBanner.ruleCard.categories;
|
||||
return [
|
||||
groups.values,
|
||||
groups.communication,
|
||||
groups.membership,
|
||||
groups.decisionMaking,
|
||||
groups.conflictManagement,
|
||||
].map((group) => ({
|
||||
name: group.name,
|
||||
chipOptions: chipsFromLabels(group.chips),
|
||||
}));
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<HeroBannerView
|
||||
title={title ?? tHome("title")}
|
||||
subtitle={subtitle ?? tHome("subtitle")}
|
||||
description={description ?? tHome("description")}
|
||||
facilitatorCta={tHero("facilitatorCta")}
|
||||
facilitatorHref={tHero("facilitatorHref")}
|
||||
createRuleCta={tHero("createRuleCta")}
|
||||
onCreateRule={handleCreateRule}
|
||||
ruleTitle={tHero("ruleCard.title")}
|
||||
ruleDescription={tHero("ruleCard.description")}
|
||||
ruleCategories={ruleCategories}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
HeroBannerContainer.displayName = "HeroBanner";
|
||||
|
||||
export default HeroBannerContainer;
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* Figma: "Sections / Hero" (see registry)
|
||||
*/
|
||||
|
||||
import { memo } from "react";
|
||||
import Image from "next/image";
|
||||
import ContentLockup from "../../type/ContentLockup";
|
||||
import HeroDecor from "./HeroDecor";
|
||||
import { ASSETS, getAssetPath } from "../../../../lib/assetUtils";
|
||||
|
||||
/**
|
||||
* Intrinsic dimensions of `public/assets/marketing/hero-image.png` (2560×1600,
|
||||
* 16:10). Passed to `next/image` to reserve aspect ratio + drive responsive
|
||||
* srcset generation. Actual rendered size is governed by `sizes`.
|
||||
*/
|
||||
const HERO_IMAGE_WIDTH = 2560;
|
||||
const HERO_IMAGE_HEIGHT = 1600;
|
||||
|
||||
interface HeroBannerProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
ctaText?: string;
|
||||
ctaHref?: string;
|
||||
imageAlt?: string;
|
||||
}
|
||||
|
||||
const HeroBanner = memo<HeroBannerProps>(
|
||||
({
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
ctaText,
|
||||
ctaHref,
|
||||
imageAlt = "Hero illustration",
|
||||
}) => {
|
||||
return (
|
||||
<section className="bg-transparent px-[var(--space-200)] sm:px-[var(--space-250)] md:px-[var(--space-400)] lg:px-[var(--space-600)] xl:px-[var(--space-1200)]">
|
||||
<div className="flex flex-col gap-[var(--space-250)]">
|
||||
{/* Frame container for content */}
|
||||
<div className="bg-[var(--color-surface-invert-brand-primary)] p-[var(--space-300)] sm:p-[var(--space-400)] md:p-[var(--space-1800)] lg:py-[var(--space-2400)] lg:px-[var(--space-1800)] rounded-tl-none rounded-tr-[var(--radius-400)] rounded-br-[var(--radius-400)] rounded-bl-[var(--radius-400)] flex flex-col gap-[var(--space-600)] sm:gap-[var(--space-600)] md:flex-row md:gap-[var(--space-1200)] relative overflow-hidden">
|
||||
{/* DECORATIONS (behind content) */}
|
||||
<HeroDecor
|
||||
className="pointer-events-none absolute z-0
|
||||
left-0 top-0
|
||||
translate-x-[-72px] translate-y-[26px] sm:translate-x-[-78px] sm:translate-y-[24px] md:translate-x-[-86px] md:translate-y-[16px] lg:translate-x-[-88px] lg:translate-y-[16px]
|
||||
w-[1540px] h-[645px] scale-[1.04]"
|
||||
/>
|
||||
|
||||
{/* Content lockup - Large variant */}
|
||||
<div className="md:flex-1">
|
||||
<ContentLockup
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
description={description}
|
||||
ctaText={ctaText}
|
||||
ctaHref={ctaHref}
|
||||
buttonClassName="shrink-0 whitespace-nowrap min-w-[280px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Hero Image Container */}
|
||||
<div className="relative z-10 flex w-full items-center justify-center overflow-hidden rounded-[8px] aspect-[16/10] md:flex-1">
|
||||
<Image
|
||||
src={getAssetPath(ASSETS.HERO_IMAGE)}
|
||||
alt={imageAlt}
|
||||
width={HERO_IMAGE_WIDTH}
|
||||
height={HERO_IMAGE_HEIGHT}
|
||||
priority
|
||||
sizes="(min-width: 768px) 50vw, 100vw"
|
||||
className="size-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
HeroBanner.displayName = "HeroBanner";
|
||||
|
||||
export default HeroBanner;
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Category } from "../../cards/Rule";
|
||||
|
||||
export interface HeroBannerProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface HeroBannerViewProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description: string;
|
||||
facilitatorCta: string;
|
||||
facilitatorHref: string;
|
||||
createRuleCta: string;
|
||||
onCreateRule: () => void;
|
||||
ruleTitle: string;
|
||||
ruleDescription: string;
|
||||
ruleCategories: Category[];
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import Button from "../../buttons/Button";
|
||||
import Rule from "../../cards/Rule";
|
||||
import ContentLockup from "../../type/ContentLockup";
|
||||
import { ASSETS, getAssetPath } from "../../../../lib/assetUtils";
|
||||
import HeroDecor from "./HeroDecor";
|
||||
import type { HeroBannerViewProps } from "./HeroBanner.types";
|
||||
|
||||
function HeroBannerView({
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
facilitatorCta,
|
||||
facilitatorHref,
|
||||
createRuleCta,
|
||||
onCreateRule,
|
||||
ruleTitle,
|
||||
ruleDescription,
|
||||
ruleCategories,
|
||||
}: HeroBannerViewProps) {
|
||||
return (
|
||||
<section
|
||||
className="bg-transparent px-[var(--space-200)] sm:px-[var(--space-250)] md:px-[var(--space-400)] lg:px-[var(--space-600)] xl:px-[var(--space-1200)]"
|
||||
data-figma-node="18477-15473"
|
||||
>
|
||||
<div className="flex flex-col gap-[var(--space-250)]">
|
||||
<div className="relative flex flex-col gap-[var(--space-600)] overflow-hidden rounded-tl-none rounded-tr-[var(--radius-400)] rounded-br-[var(--radius-400)] rounded-bl-[var(--radius-400)] bg-[var(--color-surface-invert-brand-primary)] p-[var(--space-300)] sm:p-[var(--space-400)] md:gap-[var(--space-1200)] md:p-[var(--space-1800)] lg:flex-row lg:items-center lg:gap-[var(--space-1200)] lg:px-[var(--space-1800)] lg:py-[var(--space-1200)] xl:py-[var(--space-1800)]">
|
||||
<HeroDecor
|
||||
className="pointer-events-none absolute z-0
|
||||
left-0 top-0
|
||||
translate-x-[-72px] translate-y-[26px] sm:translate-x-[-78px] sm:translate-y-[24px] md:translate-x-[-86px] md:translate-y-[16px] lg:translate-x-[-88px] lg:translate-y-[16px]
|
||||
w-[1540px] h-[645px] scale-[1.04]"
|
||||
/>
|
||||
|
||||
<div className="relative z-10 flex w-full flex-col gap-[var(--space-150)] sm:gap-[var(--space-300)] md:gap-[var(--space-500)] lg:flex-1">
|
||||
<ContentLockup
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
description={description}
|
||||
/>
|
||||
<div className="relative z-10 flex justify-start">
|
||||
<div className="lg:hidden">
|
||||
<div className="md:hidden">
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="inverse"
|
||||
size="small"
|
||||
href={facilitatorHref}
|
||||
>
|
||||
{facilitatorCta}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="hidden md:block">
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="inverse"
|
||||
size="large"
|
||||
href={facilitatorHref}
|
||||
>
|
||||
{facilitatorCta}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden lg:block">
|
||||
<div className="xl:hidden">
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="inverse"
|
||||
size="large"
|
||||
onClick={onCreateRule}
|
||||
>
|
||||
{createRuleCta}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="hidden xl:block">
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="inverse"
|
||||
size="xlarge"
|
||||
onClick={onCreateRule}
|
||||
>
|
||||
{createRuleCta}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex w-full justify-center lg:w-[568px] lg:shrink-0">
|
||||
<Rule
|
||||
title={ruleTitle}
|
||||
description={ruleDescription}
|
||||
expanded
|
||||
size="L"
|
||||
fluidWidth
|
||||
interactive={false}
|
||||
hideCategoryAddButton
|
||||
descriptionAtBottom
|
||||
backgroundColor="bg-[var(--color-surface-invert-brand-primary)]"
|
||||
className="w-full max-w-[568px] gap-[12px] rounded-[var(--radius-300)] lg:gap-[16px] lg:rounded-[var(--radius-500)]"
|
||||
categories={ruleCategories}
|
||||
icon={
|
||||
// eslint-disable-next-line @next/next/no-img-element -- decorative brand mark beside the title
|
||||
<img
|
||||
src={getAssetPath(ASSETS.LOGO)}
|
||||
alt=""
|
||||
width={88}
|
||||
height={88}
|
||||
className="size-full object-contain brightness-0"
|
||||
role="presentation"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
HeroBannerView.displayName = "HeroBannerView";
|
||||
|
||||
export default memo(HeroBannerView);
|
||||
@@ -1 +1,2 @@
|
||||
export { default } from "./HeroBanner";
|
||||
export { default } from "./HeroBanner.container";
|
||||
export type { HeroBannerProps } from "./HeroBanner.types";
|
||||
Reference in new issue
Block a user