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:
adilalloandCursor committed 2026-11-02 18:50:32 -07:00
1 parent e12e06f7ce
commit 33cdaf135c
47 files changed
+728 -527

No files matched your search

@@ -44,6 +44,7 @@ function AskOrganizerView({
variant={lockupVariant}
alignment={lockupAlignment}
titleId={labelledBy}
headingLevel="h2"
/>
<div
@@ -30,13 +30,14 @@ function FeatureGridView({
<div className="mx-auto flex w-full flex-col gap-[var(--space-1000)] md:gap-[var(--space-1200)] lg:flex-row lg:items-center lg:gap-[var(--space-1800)]">
<div className="lg:min-w-0 lg:flex-1">
<ContentLockup
title={title}
subtitle={subtitle}
variant="feature"
linkText={linkText}
linkHref={linkHref}
titleId={labelledBy}
/>
title={title}
subtitle={subtitle}
variant="feature"
linkText={linkText}
linkHref={linkHref}
titleId={labelledBy}
headingLevel="h2"
/>
</div>
<div className="grid w-full grid-cols-1 gap-[var(--space-200)] md:grid-cols-2 md:gap-[var(--space-400)] lg:flex-1">
@@ -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);
+2 -1
View File
@@ -1 +1,2 @@
export { default } from "./HeroBanner";
export { default } from "./HeroBanner.container";
export type { HeroBannerProps } from "./HeroBanner.types";
@@ -1,9 +1,8 @@
"use client";
/**
* Figma: "Section / Logo Wall" (18847:22366)
* Index frames: 320–429 2×3 grid, 430–639 3×2 grid, 640+ space-between row.
* Logo heights from those instances (1× / 1.25 at lg / 1.5625 at xl).
* Figma: "Section / Logo Wall" (18483:18425)
* Home uses the three pilot marks (street medics, Food Not Bombs, Mutual Aid Colorado).
*/
import { memo, useMemo } from "react";
@@ -17,41 +16,20 @@ const LogoWallContainer = memo<LogoWallProps>(({ logos, className = "" }) => {
const defaultLogos = useMemo(
() => [
{
src: getAssetPath(partnerLogoPath("boulder-street-medics")),
alt: t("partners.boulderStreetMedics"),
size: "h-[48px] lg:h-[60px] xl:h-[74.6px]",
},
{
src: getAssetPath(partnerLogoPath("food-not-bombs")),
alt: t("partners.foodNotBombs"),
size: "h-[48px] lg:h-[60px] xl:h-[74.6px]",
order: "sm:order-4 md:order-4",
},
{
src: getAssetPath(partnerLogoPath("start-coop")),
alt: t("partners.startCoop"),
size: "h-[51.4px] lg:h-[64.2px] xl:h-[80.3px]",
order: "sm:order-1 md:order-3",
},
{
src: getAssetPath(partnerLogoPath("metagov")),
alt: t("partners.metagov"),
size: "h-[26.2px] lg:h-[33px] xl:h-[41px]",
order: "sm:order-6 md:order-1",
},
{
src: getAssetPath(partnerLogoPath("open-civics")),
alt: t("partners.openCivics"),
size: "h-[50.1px] lg:h-[61.1px] xl:h-[72px]",
order: "sm:order-5 md:order-2",
},
{
src: getAssetPath(partnerLogoPath("mutual-aid-co")),
alt: t("partners.mutualAidCo"),
size: "h-[51.1px] lg:h-[63.6px] xl:h-[79.5px]",
order: "sm:order-2 md:order-5",
},
{
src: getAssetPath(partnerLogoPath("cu-boulder")),
alt: t("partners.cuBoulder"),
size: "h-[44.6px] lg:h-[55.8px] xl:h-[69.7px]",
order: "sm:order-3 md:order-6",
},
],
[t],
@@ -63,11 +41,7 @@ const LogoWallContainer = memo<LogoWallProps>(({ logos, className = "" }) => {
);
return (
<LogoWallView
heading={t("heading")}
displayLogos={displayLogos}
className={className}
/>
<LogoWallView displayLogos={displayLogos} className={className} />
);
});
@@ -5,20 +5,17 @@ export interface LogoWallProps {
width?: number;
height?: number;
size?: string;
order?: string;
}>;
className?: string;
}
export interface LogoWallViewProps {
heading: string;
displayLogos: Array<{
src: string;
alt: string;
width?: number;
height?: number;
size?: string;
order?: string;
}>;
className: string;
}
@@ -4,41 +4,31 @@ import { memo } from "react";
import Image from "next/image";
import type { LogoWallViewProps } from "./LogoWall.types";
function LogoWallView({
heading,
displayLogos,
className,
}: LogoWallViewProps) {
function LogoWallView({ displayLogos, className }: LogoWallViewProps) {
return (
<section
className={`bg-[var(--color-surface-default-primary)] p-[var(--space-800)] md:px-[var(--space-600)] md:py-[var(--space-800)] lg:px-[var(--space-1800)] lg:py-[var(--space-1200)] xl:px-[160px] xl:py-[var(--space-1800)] ${className}`}
data-figma-node="18483-18425"
>
<div className="flex flex-col items-center gap-[var(--space-800)] md:gap-[var(--space-600)] xl:gap-[var(--space-800)]">
<p className="font-inter text-center text-[10px] font-medium uppercase leading-[12px] text-[var(--color-content-default-primary)] xl:text-[14px] xl:leading-[12px]">
{heading}
</p>
<div className="w-full opacity-60">
<div className="grid grid-cols-2 grid-rows-3 sm:grid-cols-3 sm:grid-rows-2 md:flex md:w-full md:flex-nowrap md:items-center md:justify-between gap-[38.4px] lg:gap-[var(--space-1200)]">
{displayLogos.map((logo, index) => (
<div
key={index}
className={`flex items-center justify-center md:shrink-0 ${
logo.order || ""
}`}
>
<Image
src={logo.src}
alt={logo.alt}
className={`${logo.size || "h-8"} w-auto object-contain`}
priority={index < 2} // Prioritize first 2 logos for above-the-fold loading
unoptimized // Skip optimization for local images
width={0}
height={0}
sizes="100vw"
/>
</div>
))}
</div>
<div className="w-full opacity-60">
<div className="flex w-full flex-col items-center gap-[38.4px] sm:flex-row sm:flex-wrap sm:justify-around md:flex-nowrap md:justify-between lg:gap-[var(--space-1200)]">
{displayLogos.map((logo, index) => (
<div
key={logo.alt || index}
className="flex items-center justify-center md:shrink-0"
>
<Image
src={logo.src}
alt={logo.alt}
className={`${logo.size || "h-8"} w-auto object-contain`}
priority={index < 2}
unoptimized
width={0}
height={0}
sizes="100vw"
/>
</div>
))}
</div>
</div>
</section>