Implement how it works page
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Figma: "A Guide to CommunityRule" body ornaments (22078:791901)
|
||||
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22078-791901
|
||||
*
|
||||
* - 19003:23575 — concentric circles, right (`how-shape-2.svg`)
|
||||
* - 19003:23576 — loop mark, left (`how-shape-1.svg`)
|
||||
*/
|
||||
import {
|
||||
getAssetPath,
|
||||
howItWorksOrnamentLeftPath,
|
||||
howItWorksOrnamentRightPath,
|
||||
} from "../../../../lib/assetUtils";
|
||||
|
||||
export default function HowItWorksDecorativeShapes() {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute z-0 hidden aspect-square md:block left-[84.86%] right-[-9.28%] top-[clamp(200px,20vw,255px)]"
|
||||
data-node-id="19003:23575"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={getAssetPath(howItWorksOrnamentRightPath())}
|
||||
alt=""
|
||||
className="pointer-events-none size-full max-w-none object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute z-0 hidden aspect-square md:block left-[-1.66%] right-[88.38%] top-[clamp(520px,55vw,811px)]"
|
||||
data-node-id="19003:23576"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={getAssetPath(howItWorksOrnamentLeftPath())}
|
||||
alt=""
|
||||
className="pointer-events-none size-full max-w-none object-cover"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Figma: "How Community Rule works" (22078:806964)
|
||||
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22078-806964
|
||||
*/
|
||||
import type { Metadata } from "next";
|
||||
import dynamic from "next/dynamic";
|
||||
import messages from "../../../messages/en/index";
|
||||
import { getAllBlogPosts } from "../../../lib/content";
|
||||
import {
|
||||
buildHowItWorksSyntheticPost,
|
||||
HOW_IT_WORKS_SENTINEL_SLUG,
|
||||
} from "../../../lib/howItWorksSyntheticPost";
|
||||
import ContentBanner from "../../components/sections/ContentBanner";
|
||||
import HowItWorksDecorativeShapes from "./_components/HowItWorksDecorativeShapes";
|
||||
import AskOrganizer from "../../components/sections/AskOrganizer";
|
||||
import "../blog/blog.css";
|
||||
|
||||
const RelatedArticles = dynamic(
|
||||
() => import("../../components/sections/RelatedArticles"),
|
||||
{
|
||||
loading: () => (
|
||||
<section className="py-[var(--spacing-scale-032)] min-h-[400px]" />
|
||||
),
|
||||
ssr: true,
|
||||
},
|
||||
);
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const meta = messages.metadata.howItWorks;
|
||||
const page = messages.pages.howItWorks;
|
||||
|
||||
return {
|
||||
title: meta.title,
|
||||
description: meta.description,
|
||||
keywords: meta.keywords,
|
||||
openGraph: {
|
||||
title: page.banner.title,
|
||||
description: page.banner.description,
|
||||
type: "website",
|
||||
siteName: "CommunityRule",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function HowItWorksPage() {
|
||||
const page = messages.pages.howItWorks;
|
||||
const syntheticPost = buildHowItWorksSyntheticPost(page);
|
||||
|
||||
const allPosts = getAllBlogPosts();
|
||||
const relatedPosts = allPosts.slice(0, 8);
|
||||
const slugOrder = allPosts.map((post) => post.slug);
|
||||
|
||||
const askOrganizerData = {
|
||||
title: messages.pages.home.askOrganizer.title,
|
||||
subtitle: messages.pages.home.askOrganizer.subtitle,
|
||||
buttonText: messages.pages.home.askOrganizer.buttonText,
|
||||
};
|
||||
|
||||
const structuredData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
name: page.banner.title,
|
||||
description: page.banner.description,
|
||||
url: "https://communityrule.com/how-it-works",
|
||||
publisher: {
|
||||
"@type": "Organization",
|
||||
name: "CommunityRule",
|
||||
url: "https://communityrule.com",
|
||||
},
|
||||
};
|
||||
|
||||
const breadcrumbData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 1,
|
||||
name: "Home",
|
||||
item: "https://communityrule.com",
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 2,
|
||||
name: page.banner.title,
|
||||
item: "https://communityrule.com/how-it-works",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(structuredData),
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(breadcrumbData),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative min-h-screen overflow-x-hidden bg-transparent">
|
||||
<ContentBanner post={syntheticPost} variant="guide" />
|
||||
|
||||
<div className="relative w-full">
|
||||
<HowItWorksDecorativeShapes />
|
||||
|
||||
<article className="relative z-10 p-[var(--spacing-scale-024)] sm:py-[var(--spacing-scale-032)]">
|
||||
<div
|
||||
className="post-body -mt-[var(--spacing-scale-048)] text-[var(--color-content-default-primary)] text-[16px] leading-[24px] sm:text-[18px] sm:leading-[130%] lg:text-[24px] lg:leading-[32px] xl:text-[32px] xl:leading-[40px] sm:mx-auto sm:max-w-[390px] md:max-w-[472px] lg:max-w-[700px] xl:max-w-[904px]"
|
||||
dangerouslySetInnerHTML={{ __html: syntheticPost.htmlContent }}
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<RelatedArticles
|
||||
relatedPosts={relatedPosts}
|
||||
currentPostSlug={HOW_IT_WORKS_SENTINEL_SLUG}
|
||||
slugOrder={slugOrder}
|
||||
headingSurface="onLight"
|
||||
heading={page.relatedArticles.title}
|
||||
/>
|
||||
|
||||
<AskOrganizer {...askOrganizerData} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -76,6 +76,7 @@ export default function Page() {
|
||||
iconColor: "orange",
|
||||
},
|
||||
],
|
||||
seeHowItWorksHref: t("cardSteps.buttons.seeHowItWorksHref"),
|
||||
};
|
||||
|
||||
const featureGridData = {
|
||||
|
||||
@@ -6,7 +6,13 @@ import ContentContainerView from "./ContentContainer.view";
|
||||
import type { ContentContainerProps } from "./ContentContainer.types";
|
||||
|
||||
const ContentContainerContainer = memo<ContentContainerProps>(
|
||||
({ post, width = "200px", size: sizeProp = "responsive" }) => {
|
||||
({
|
||||
post,
|
||||
width = "200px",
|
||||
size: sizeProp = "responsive",
|
||||
leadingImageSrc,
|
||||
leadingImageAlt,
|
||||
}) => {
|
||||
const size = sizeProp;
|
||||
// Get the corresponding icon based on the same logic as background images
|
||||
const getIconImage = (slug: string): string => {
|
||||
@@ -30,7 +36,9 @@ const ContentContainerContainer = memo<ContentContainerProps>(
|
||||
return icons[finalIndex];
|
||||
};
|
||||
|
||||
const iconImage = getIconImage(post.slug);
|
||||
const iconImage = leadingImageSrc ?? getIconImage(post.slug);
|
||||
const iconAlt =
|
||||
leadingImageAlt ?? `Icon for ${post.frontmatter.title}`;
|
||||
|
||||
// Format date
|
||||
const formattedDate = new Date(post.frontmatter.date).toLocaleDateString(
|
||||
@@ -83,6 +91,7 @@ const ContentContainerContainer = memo<ContentContainerProps>(
|
||||
width={width}
|
||||
size={size}
|
||||
iconImage={iconImage}
|
||||
iconAlt={iconAlt}
|
||||
containerClasses={containerClasses}
|
||||
contentGapClasses={contentGapClasses}
|
||||
textGapClasses={textGapClasses}
|
||||
|
||||
@@ -9,6 +9,10 @@ export interface ContentContainerProps {
|
||||
* Content container size.
|
||||
*/
|
||||
size?: ContentContainerSizeValue;
|
||||
/** When set, replaces the default slug-based thumbnail icon. */
|
||||
leadingImageSrc?: string;
|
||||
/** Alt text for `leadingImageSrc`; defaults to post title. */
|
||||
leadingImageAlt?: string;
|
||||
}
|
||||
|
||||
export interface ContentContainerViewProps {
|
||||
@@ -16,6 +20,7 @@ export interface ContentContainerViewProps {
|
||||
width: string;
|
||||
size: "xs" | "responsive";
|
||||
iconImage: string;
|
||||
iconAlt: string;
|
||||
containerClasses: string;
|
||||
contentGapClasses: string;
|
||||
textGapClasses: string;
|
||||
|
||||
@@ -6,6 +6,7 @@ function ContentContainerView({
|
||||
width,
|
||||
size,
|
||||
iconImage,
|
||||
iconAlt,
|
||||
containerClasses,
|
||||
contentGapClasses,
|
||||
textGapClasses,
|
||||
@@ -27,7 +28,7 @@ function ContentContainerView({
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={iconImage}
|
||||
alt={`Icon for ${post.frontmatter.title}`}
|
||||
alt={iconAlt}
|
||||
className="w-[60px] h-[30px] object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { CardStepsProps } from "./CardSteps.types";
|
||||
* Composes **`cards/Step`** (Figma Card / Step), not **`progress/Stepper`**.
|
||||
*/
|
||||
const CardStepsContainer = memo<CardStepsProps>(
|
||||
({ title, subtitle, steps, headingDesktopLines }) => {
|
||||
({ title, subtitle, steps, headingDesktopLines, seeHowItWorksHref }) => {
|
||||
const schemaData = useSchemaData({
|
||||
type: "HowTo",
|
||||
name: title,
|
||||
@@ -29,6 +29,7 @@ const CardStepsContainer = memo<CardStepsProps>(
|
||||
subtitle={subtitle}
|
||||
steps={steps}
|
||||
headingDesktopLines={headingDesktopLines}
|
||||
seeHowItWorksHref={seeHowItWorksHref}
|
||||
schemaJson={schemaJson}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface CardStepsProps {
|
||||
steps: CardStepsItem[];
|
||||
/** Large-screen heading split: line 1–3 (e.g. How / CommunityRule / helps). */
|
||||
headingDesktopLines?: readonly [string, string, string];
|
||||
/** When set, the section CTA renders as a link. */
|
||||
seeHowItWorksHref?: string;
|
||||
}
|
||||
|
||||
export interface CardStepsViewProps extends CardStepsProps {
|
||||
|
||||
@@ -11,6 +11,7 @@ function CardStepsView({
|
||||
subtitle,
|
||||
steps,
|
||||
headingDesktopLines,
|
||||
seeHowItWorksHref,
|
||||
schemaJson,
|
||||
}: CardStepsViewProps) {
|
||||
const t = useTranslation();
|
||||
@@ -47,7 +48,12 @@ function CardStepsView({
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Button buttonType="outline" palette="default" size="large">
|
||||
<Button
|
||||
buttonType="outline"
|
||||
palette="default"
|
||||
size="large"
|
||||
href={seeHowItWorksHref}
|
||||
>
|
||||
{t("cardSteps.buttons.seeHowItWorks")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { getAssetPath } from "../../../lib/assetUtils";
|
||||
import ContentContainer from "../content/ContentContainer";
|
||||
import type { BlogPost } from "../../../lib/content";
|
||||
|
||||
interface ContentBannerProps {
|
||||
post: BlogPost;
|
||||
}
|
||||
|
||||
const ContentBanner = memo<ContentBannerProps>(({ post }) => {
|
||||
// Get article-specific horizontal thumbnail (small) and banner (md+)
|
||||
const getBackgroundImage = (post: BlogPost): string => {
|
||||
if (post.frontmatter?.thumbnail?.horizontal) {
|
||||
return `/content/blog/${post.frontmatter.thumbnail.horizontal}`;
|
||||
}
|
||||
// Fallback to default image
|
||||
return getAssetPath("assets/Content_Banner.svg");
|
||||
};
|
||||
|
||||
const getBannerImageMd = (post: BlogPost): string => {
|
||||
// Use banner.horizontal when provided; fallback to horizontal thumbnail
|
||||
if (post.frontmatter?.banner?.horizontal) {
|
||||
return `/content/blog/${post.frontmatter.banner.horizontal}`;
|
||||
}
|
||||
// Fallback to horizontal thumbnail, then default banner
|
||||
if (post.frontmatter?.thumbnail?.horizontal) {
|
||||
return `/content/blog/${post.frontmatter.thumbnail.horizontal}`;
|
||||
}
|
||||
return getAssetPath("assets/Content_Banner_2.svg");
|
||||
};
|
||||
|
||||
const backgroundImage = getBackgroundImage(post);
|
||||
const bannerImageMd = getBannerImageMd(post);
|
||||
|
||||
return (
|
||||
<div className="pt-[var(--measures-spacing-016)] md:pt-[var(--measures-spacing-008)] lg:pt-[50px] xl:pt-[112px] h-[275px] sm:h-[326px] md:h-[224px] lg:h-[358.4px] xl:h-[504px] relative w-full sm:overflow-hidden">
|
||||
{/* Background SVG - Default to sm breakpoint */}
|
||||
<div
|
||||
className="absolute inset-0 w-full h-full bg-cover bg-no-repeat aspect-[320/225.5]"
|
||||
style={{
|
||||
backgroundImage: `url(${backgroundImage})`,
|
||||
backgroundPosition: "center bottom",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Background SVG - md breakpoint and above (article banner image) */}
|
||||
<div
|
||||
className="absolute inset-0 w-full h-full bg-cover bg-no-repeat aspect-[640/224] md:block hidden"
|
||||
style={{
|
||||
backgroundImage: `url(${bannerImageMd})`,
|
||||
backgroundPosition: "center bottom",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Content Container */}
|
||||
<div
|
||||
className="
|
||||
relative z-10 h-full
|
||||
flex flex-col
|
||||
pl-[var(--measures-spacing-016)] md:pl-[var(--measures-spacing-024)] lg:pl-[var(--measures-spacing-064)]
|
||||
pr-[96px] md:pr-[350px]
|
||||
|
||||
/* default: normal flow, top-aligned */
|
||||
justify-start
|
||||
|
||||
/* only at md: take out of flow and center vertically */
|
||||
md:absolute md:inset-x-0 md:top-1/2 md:-translate-y-1/2 md:w-full md:h-auto
|
||||
|
||||
/* after md (lg+): snap back to normal flow/top align */
|
||||
lg:static lg:translate-y-0 lg:top-auto lg:h-full lg:justify-start
|
||||
"
|
||||
>
|
||||
{/* ContentContainer with post data */}
|
||||
<ContentContainer post={post} size="responsive" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ContentBanner.displayName = "ContentBanner";
|
||||
|
||||
export default ContentBanner;
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { getAssetPath } from "../../../../lib/assetUtils";
|
||||
import type { BlogPost } from "../../../../lib/content";
|
||||
import ContentBannerView from "./ContentBanner.view";
|
||||
import type { ContentBannerProps } from "./ContentBanner.types";
|
||||
|
||||
/** Figma: Section / ContentBanner — article (blog) and guide (content page template 22078:791901). */
|
||||
const ContentBannerContainer = memo<ContentBannerProps>(
|
||||
({
|
||||
post,
|
||||
variant: variantProp = "article",
|
||||
leadingImageSrc,
|
||||
leadingImageAlt,
|
||||
}) => {
|
||||
const variant = variantProp;
|
||||
|
||||
const getBackgroundImage = (blogPost: BlogPost): string => {
|
||||
if (blogPost.frontmatter?.thumbnail?.horizontal) {
|
||||
return `/content/blog/${blogPost.frontmatter.thumbnail.horizontal}`;
|
||||
}
|
||||
return getAssetPath("assets/Content_Banner.svg");
|
||||
};
|
||||
|
||||
const getBannerImageMd = (blogPost: BlogPost): string => {
|
||||
if (blogPost.frontmatter?.banner?.horizontal) {
|
||||
return `/content/blog/${blogPost.frontmatter.banner.horizontal}`;
|
||||
}
|
||||
if (blogPost.frontmatter?.thumbnail?.horizontal) {
|
||||
return `/content/blog/${blogPost.frontmatter.thumbnail.horizontal}`;
|
||||
}
|
||||
return getAssetPath("assets/Content_Banner_2.svg");
|
||||
};
|
||||
|
||||
const backgroundImageSm =
|
||||
variant === "article" ? getBackgroundImage(post) : undefined;
|
||||
const backgroundImageMd =
|
||||
variant === "article" ? getBannerImageMd(post) : undefined;
|
||||
|
||||
return (
|
||||
<ContentBannerView
|
||||
variant={variant}
|
||||
post={post}
|
||||
leadingImageSrc={leadingImageSrc}
|
||||
leadingImageAlt={leadingImageAlt}
|
||||
backgroundImageSm={backgroundImageSm}
|
||||
backgroundImageMd={backgroundImageMd}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ContentBannerContainer.displayName = "ContentBanner";
|
||||
|
||||
export default ContentBannerContainer;
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { BlogPost } from "../../../../lib/content";
|
||||
|
||||
export type ContentBannerVariant = "article" | "guide";
|
||||
|
||||
export interface ContentBannerProps {
|
||||
post: BlogPost;
|
||||
/**
|
||||
* `article` — blog post hero with thumbnail/banner imagery and metadata.
|
||||
* `guide` — static guide pages (Figma ContentBanner on content page template).
|
||||
*/
|
||||
variant?: ContentBannerVariant;
|
||||
/** Article variant only: replaces slug-based thumbnail icon in ContentContainer. */
|
||||
leadingImageSrc?: string;
|
||||
leadingImageAlt?: string;
|
||||
}
|
||||
|
||||
export interface ContentBannerViewProps {
|
||||
variant: ContentBannerVariant;
|
||||
post: BlogPost;
|
||||
leadingImageSrc?: string;
|
||||
leadingImageAlt?: string;
|
||||
backgroundImageSm?: string;
|
||||
backgroundImageMd?: string;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import ContentContainer from "../../content/ContentContainer";
|
||||
import {
|
||||
getAssetPath,
|
||||
guideBannerLogoArrowPath,
|
||||
} from "../../../../lib/assetUtils";
|
||||
import type { ContentBannerViewProps } from "./ContentBanner.types";
|
||||
|
||||
/**
|
||||
* Figma: ContentBanner on content page template (22078:791901) — left column
|
||||
* title + description; logo mark (22078:806960) in right column.
|
||||
*/
|
||||
function ContentBannerGuideView({
|
||||
post,
|
||||
}: Pick<ContentBannerViewProps, "post">) {
|
||||
const { title, description } = post.frontmatter;
|
||||
|
||||
return (
|
||||
<section
|
||||
className="relative w-full overflow-clip px-[var(--spacing-scale-020)] py-[var(--spacing-scale-024)] sm:px-[var(--spacing-scale-032)] sm:py-[var(--spacing-scale-032)] lg:px-[var(--spacing-scale-048)] lg:py-[var(--spacing-scale-040)]"
|
||||
aria-labelledby="content-banner-title"
|
||||
>
|
||||
<div
|
||||
className="mx-auto flex w-full max-w-[1024px] flex-col items-start gap-[var(--spacing-scale-024)] md:flex-row md:items-center md:gap-[var(--spacing-scale-032)]"
|
||||
data-node-id="19189:9358"
|
||||
>
|
||||
<div
|
||||
className="flex w-full max-w-[365px] shrink-0 flex-col items-start justify-center gap-[var(--spacing-scale-024)]"
|
||||
data-node-id="19189:9171"
|
||||
>
|
||||
<div className="flex w-full flex-col items-start gap-[var(--measures-spacing-016)]">
|
||||
<div className="flex w-full flex-col items-start gap-[var(--measures-spacing-004)] text-left text-[var(--color-content-default-primary)]">
|
||||
<h1
|
||||
id="content-banner-title"
|
||||
className="w-full font-bricolage font-medium text-[32px] leading-[110%] sm:text-[40px] lg:text-[44px]"
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
{description ? (
|
||||
<p className="w-full font-inter font-normal text-[16px] leading-[130%] sm:text-[18px]">
|
||||
{description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex w-full shrink-0 items-center justify-center md:flex-1"
|
||||
data-node-id="22078:806960"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={getAssetPath(guideBannerLogoArrowPath())}
|
||||
alt=""
|
||||
aria-hidden
|
||||
className="h-[clamp(120px,20vw,171px)] w-[clamp(120px,20vw,172px)] max-w-none shrink-0 object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ContentBannerArticleView({
|
||||
post,
|
||||
leadingImageSrc,
|
||||
leadingImageAlt,
|
||||
backgroundImageSm,
|
||||
backgroundImageMd,
|
||||
}: ContentBannerViewProps) {
|
||||
if (!backgroundImageSm || !backgroundImageMd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-[275px] w-full pt-[var(--measures-spacing-016)] sm:h-[326px] sm:overflow-hidden md:h-[224px] md:pt-[var(--measures-spacing-008)] lg:h-[358.4px] lg:pt-[50px] xl:h-[504px] xl:pt-[112px]">
|
||||
<div
|
||||
className="absolute inset-0 aspect-[320/225.5] h-full w-full bg-cover bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url(${backgroundImageSm})`,
|
||||
backgroundPosition: "center bottom",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="absolute inset-0 hidden aspect-[640/224] h-full w-full bg-cover bg-no-repeat md:block"
|
||||
style={{
|
||||
backgroundImage: `url(${backgroundImageMd})`,
|
||||
backgroundPosition: "center bottom",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="
|
||||
relative z-10 flex h-full flex-col
|
||||
pl-[var(--measures-spacing-016)] pr-[96px]
|
||||
justify-start
|
||||
md:absolute md:inset-x-0 md:top-1/2 md:h-auto md:w-full md:-translate-y-1/2
|
||||
md:pl-[var(--measures-spacing-024)] md:pr-[350px]
|
||||
lg:static lg:top-auto lg:h-full lg:translate-y-0 lg:justify-start
|
||||
lg:pl-[var(--measures-spacing-064)]
|
||||
"
|
||||
>
|
||||
<ContentContainer
|
||||
post={post}
|
||||
size="responsive"
|
||||
leadingImageSrc={leadingImageSrc}
|
||||
leadingImageAlt={leadingImageAlt}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ContentBannerView(props: ContentBannerViewProps) {
|
||||
if (props.variant === "guide") {
|
||||
return <ContentBannerGuideView post={props.post} />;
|
||||
}
|
||||
|
||||
return <ContentBannerArticleView {...props} />;
|
||||
}
|
||||
|
||||
ContentBannerView.displayName = "ContentBannerView";
|
||||
|
||||
export default memo(ContentBannerView);
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default } from "./ContentBanner.container";
|
||||
export type {
|
||||
ContentBannerProps,
|
||||
ContentBannerVariant,
|
||||
} from "./ContentBanner.types";
|
||||
@@ -12,6 +12,8 @@ const RelatedArticlesContainer = memo<RelatedArticlesProps>(
|
||||
currentPostSlug,
|
||||
slugOrder = [],
|
||||
variant = "default",
|
||||
headingSurface = "onDark",
|
||||
heading,
|
||||
}) => {
|
||||
const messages = useMessages();
|
||||
// Memoize filtered posts to prevent unnecessary re-computations
|
||||
@@ -116,6 +118,8 @@ const RelatedArticlesContainer = memo<RelatedArticlesProps>(
|
||||
getProgressStyle={getProgressStyle}
|
||||
onMouseDown={handleMouseDown}
|
||||
variant={variant}
|
||||
headingSurface={headingSurface}
|
||||
heading={heading}
|
||||
useCasesHeadingLines={useCasesHeadingLines}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,9 @@ import type { BlogPost } from "../../../../lib/content";
|
||||
|
||||
export type RelatedArticlesVariant = "default" | "useCases";
|
||||
|
||||
/** Heading contrast when the section sits on a dark vs light page background. */
|
||||
export type RelatedArticlesHeadingSurface = "onDark" | "onLight";
|
||||
|
||||
export interface RelatedArticlesProps {
|
||||
relatedPosts: BlogPost[];
|
||||
currentPostSlug: string;
|
||||
@@ -12,6 +15,10 @@ export interface RelatedArticlesProps {
|
||||
* **`lg`** [**20711-14231**](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=20711-14231&m=dev) (shell + card row gutter / padding).
|
||||
*/
|
||||
variant?: RelatedArticlesVariant;
|
||||
/** Default `onDark` (blog). Use `onLight` on transparent / light marketing pages. */
|
||||
headingSurface?: RelatedArticlesHeadingSurface;
|
||||
/** Overrides the default “Related Articles” heading. */
|
||||
heading?: string;
|
||||
}
|
||||
|
||||
export interface RelatedArticlesViewProps {
|
||||
@@ -22,6 +29,8 @@ export interface RelatedArticlesViewProps {
|
||||
getProgressStyle: (_index: number) => React.CSSProperties;
|
||||
onMouseDown?: (_e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
variant?: RelatedArticlesVariant;
|
||||
headingSurface?: RelatedArticlesHeadingSurface;
|
||||
heading?: string;
|
||||
/** Stacked title lines (`pages.useCases.relatedArticles.title`) when `variant="useCases"`. */
|
||||
useCasesHeadingLines?: readonly string[];
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ export function RelatedArticlesView({
|
||||
getProgressStyle,
|
||||
onMouseDown,
|
||||
variant = "default",
|
||||
headingSurface = "onDark",
|
||||
heading = "Related Articles",
|
||||
useCasesHeadingLines,
|
||||
}: RelatedArticlesViewProps) {
|
||||
if (filteredPosts.length === 0) {
|
||||
@@ -49,8 +51,14 @@ export function RelatedArticlesView({
|
||||
</span>
|
||||
</h2>
|
||||
) : (
|
||||
<h2 className="text-center text-[32px] font-medium leading-[110%] text-[var(--color-content-inverse-primary)] lg:text-[44px]">
|
||||
Related Articles
|
||||
<h2
|
||||
className={`text-center text-[32px] font-medium leading-[110%] lg:text-[44px] ${
|
||||
headingSurface === "onLight"
|
||||
? "text-[var(--color-content-default-primary)]"
|
||||
: "text-[var(--color-content-inverse-primary)]"
|
||||
}`}
|
||||
>
|
||||
{heading}
|
||||
</h2>
|
||||
)}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ const ContentLockupContainer = memo<ContentLockupProps>(
|
||||
subtitle,
|
||||
description,
|
||||
ctaText,
|
||||
ctaHref,
|
||||
buttonClassName = "",
|
||||
variant: variantProp = "hero",
|
||||
linkText,
|
||||
@@ -166,6 +167,7 @@ const ContentLockupContainer = memo<ContentLockupProps>(
|
||||
subtitle={subtitle}
|
||||
description={description}
|
||||
ctaText={ctaText}
|
||||
ctaHref={ctaHref}
|
||||
buttonClassName={buttonClassName}
|
||||
variant={variant}
|
||||
linkText={linkText}
|
||||
|
||||
@@ -10,6 +10,7 @@ function ContentLockupView({
|
||||
subtitle,
|
||||
description,
|
||||
ctaText,
|
||||
ctaHref,
|
||||
buttonClassName,
|
||||
variant,
|
||||
linkText,
|
||||
@@ -111,6 +112,7 @@ function ContentLockupView({
|
||||
buttonType="filled"
|
||||
palette={variant === "hero" ? "default" : "inverse"}
|
||||
size="small"
|
||||
href={ctaHref}
|
||||
>
|
||||
{ctaText}
|
||||
</Button>
|
||||
@@ -122,6 +124,7 @@ function ContentLockupView({
|
||||
palette={variant === "hero" ? "default" : "inverse"}
|
||||
size="large"
|
||||
className={buttonClassName}
|
||||
href={ctaHref}
|
||||
>
|
||||
{ctaText}
|
||||
</Button>
|
||||
@@ -132,6 +135,7 @@ function ContentLockupView({
|
||||
buttonType="filled"
|
||||
palette={variant === "hero" ? "default" : "inverse"}
|
||||
size="xlarge"
|
||||
href={ctaHref}
|
||||
>
|
||||
{ctaText}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user