Rebuild the About page from the current marketing frames, and leave the unapproved sentences off the page.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
3dee3a587b
commit
2101676429
56 files changed
+1515
-238
No files matched your search
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Figma: "Section / Centered Header" (24214:25892)
|
||||
*/
|
||||
|
||||
import { memo } from "react";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import CenteredHeaderView from "./CenteredHeader.view";
|
||||
import type { CenteredHeaderSegment } from "./CenteredHeader.types";
|
||||
|
||||
const CenteredHeaderContainer = memo(() => {
|
||||
const copy = useMessages().pages.about.centeredHeader;
|
||||
return (
|
||||
<CenteredHeaderView
|
||||
accessibleName={copy.accessibleName}
|
||||
subhead={copy.subhead}
|
||||
credit={copy.credit}
|
||||
segments={copy.segments as CenteredHeaderSegment[]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
CenteredHeaderContainer.displayName = "CenteredHeader";
|
||||
|
||||
export default CenteredHeaderContainer;
|
||||
@@ -0,0 +1,12 @@
|
||||
export type CenteredHeaderIcon = "arrow" | "ring";
|
||||
|
||||
export type CenteredHeaderSegment =
|
||||
| { type: "word"; text: string }
|
||||
| { type: "icon"; icon: CenteredHeaderIcon };
|
||||
|
||||
export interface CenteredHeaderViewProps {
|
||||
accessibleName: string;
|
||||
segments: CenteredHeaderSegment[];
|
||||
subhead: string;
|
||||
credit: string;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import { memo, type ReactNode } from "react";
|
||||
import { getAssetPath } from "../../../../lib/assetUtils";
|
||||
import type {
|
||||
CenteredHeaderIcon,
|
||||
CenteredHeaderSegment,
|
||||
CenteredHeaderViewProps,
|
||||
} from "./CenteredHeader.types";
|
||||
|
||||
const ICON_SRC: Record<CenteredHeaderIcon, string> = {
|
||||
arrow: "assets/shapes/about-header-arrow.svg",
|
||||
ring: "assets/shapes/about-header-ring.svg",
|
||||
};
|
||||
|
||||
const ICON_CLASS: Record<CenteredHeaderIcon, string> = {
|
||||
arrow: "size-10 md:size-[54px] lg:size-20",
|
||||
ring: "size-8 md:size-[54px] lg:size-20",
|
||||
};
|
||||
|
||||
/** Poster lines at 1024+, matching the desktop frame. Narrower widths wrap on their own. */
|
||||
const DESKTOP_LINES = [[0], [1], [2, 3], [4, 5], [6, 7, 8]] as const;
|
||||
|
||||
function SegmentView({ segment }: { segment: CenteredHeaderSegment }) {
|
||||
if (segment.type === "word") {
|
||||
return (
|
||||
<span className="font-bricolage-grotesque text-display-m whitespace-nowrap text-[var(--color-content-default-primary)] md:text-display-xl lg:text-display-xxl">
|
||||
{segment.text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex shrink-0 items-center justify-center ${ICON_CLASS[segment.icon]}`}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- decorative Figma mark */}
|
||||
<img
|
||||
src={getAssetPath(ICON_SRC[segment.icon])}
|
||||
alt=""
|
||||
width={80}
|
||||
height={80}
|
||||
className="size-full"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function CenteredHeaderView({
|
||||
accessibleName,
|
||||
segments,
|
||||
subhead,
|
||||
credit,
|
||||
}: CenteredHeaderViewProps) {
|
||||
const desktopLines: ReactNode = DESKTOP_LINES.map((line) => (
|
||||
<div key={line.join("-")} className="flex items-center justify-center gap-4">
|
||||
{line.map((index) => {
|
||||
const segment = segments[index];
|
||||
return segment ? <SegmentView key={index} segment={segment} /> : null;
|
||||
})}
|
||||
</div>
|
||||
));
|
||||
|
||||
return (
|
||||
<section
|
||||
data-figma-node="24214:25892"
|
||||
className="flex flex-col items-center gap-[var(--space-600)] overflow-hidden bg-black px-[var(--space-600)] py-[var(--space-3000)] md:px-[var(--space-1800)]"
|
||||
aria-labelledby="about-centered-header"
|
||||
>
|
||||
<h1 id="about-centered-header" className="sr-only">
|
||||
{accessibleName}
|
||||
</h1>
|
||||
<div
|
||||
aria-hidden
|
||||
className="flex w-full flex-wrap items-center justify-center gap-x-3 gap-y-2 lg:hidden"
|
||||
>
|
||||
{segments.map((segment, index) => (
|
||||
<SegmentView key={index} segment={segment} />
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
aria-hidden
|
||||
className="hidden w-full max-w-[32rem] flex-col items-center gap-4 lg:flex"
|
||||
>
|
||||
{desktopLines}
|
||||
</div>
|
||||
<p className="max-w-[720px] text-center font-inter text-paragraph-m text-[var(--color-content-default-secondary)] md:text-paragraph-xl lg:font-bricolage-grotesque lg:text-display-s">
|
||||
{subhead}
|
||||
</p>
|
||||
<div
|
||||
className="h-px w-[295px] bg-[var(--color-border-default-secondary)]"
|
||||
aria-hidden
|
||||
/>
|
||||
<p className="max-w-[720px] text-center font-inter text-label-xs text-[var(--color-content-default-tertiary)] md:text-label-s">
|
||||
{credit}
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
CenteredHeaderView.displayName = "CenteredHeaderView";
|
||||
|
||||
export default memo(CenteredHeaderView);
|
||||
@@ -0,0 +1,6 @@
|
||||
export { default } from "./CenteredHeader.container";
|
||||
export type {
|
||||
CenteredHeaderIcon,
|
||||
CenteredHeaderSegment,
|
||||
CenteredHeaderViewProps,
|
||||
} from "./CenteredHeader.types";
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Figma: "Section / Credits" (24240:1547)
|
||||
*/
|
||||
|
||||
import { memo } from "react";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import CreditsView from "./Credits.view";
|
||||
|
||||
const CreditsContainer = memo(() => {
|
||||
const copy = useMessages().pages.about.credits;
|
||||
return <CreditsView heading={copy.heading} rows={copy.rows} />;
|
||||
});
|
||||
|
||||
CreditsContainer.displayName = "Credits";
|
||||
|
||||
export default CreditsContainer;
|
||||
@@ -0,0 +1,9 @@
|
||||
export type CreditRow = {
|
||||
role: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export interface CreditsViewProps {
|
||||
heading: string;
|
||||
rows: CreditRow[];
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import type { CreditsViewProps } from "./Credits.types";
|
||||
|
||||
function CreditsView({ heading, rows }: CreditsViewProps) {
|
||||
return (
|
||||
<section
|
||||
data-figma-node="24240:1547"
|
||||
className="flex flex-col items-center bg-black px-[var(--space-400)] py-16 md:px-16 lg:px-20 lg:py-[var(--space-3000)]"
|
||||
aria-labelledby="about-credits-heading"
|
||||
>
|
||||
<div className="flex w-full max-w-[800px] flex-col items-center gap-12">
|
||||
<h2
|
||||
id="about-credits-heading"
|
||||
className="text-center font-bricolage-grotesque text-heading-l text-[var(--color-content-default-primary)]"
|
||||
>
|
||||
{heading}
|
||||
</h2>
|
||||
<dl className="m-0 flex w-full flex-col gap-5">
|
||||
{rows.map((row) => (
|
||||
<div
|
||||
key={row.role}
|
||||
className="flex flex-col gap-1 border-b border-[var(--color-border-default-tertiary)] pb-4"
|
||||
>
|
||||
<dt className="font-inter text-label-caps-xxs text-[var(--color-content-default-tertiary)] uppercase">
|
||||
{row.role}
|
||||
</dt>
|
||||
<dd className="m-0 font-inter text-paragraph-s text-[var(--color-content-default-primary)]">
|
||||
{row.name}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
CreditsView.displayName = "CreditsView";
|
||||
|
||||
export default memo(CreditsView);
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./Credits.container";
|
||||
export type { CreditRow, CreditsViewProps } from "./Credits.types";
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Figma: "Section / Four Questions" (24230:852)
|
||||
*/
|
||||
|
||||
import { memo } from "react";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import FourQuestionsView from "./FourQuestions.view";
|
||||
import type { FourQuestionCard } from "./FourQuestions.types";
|
||||
|
||||
const FourQuestionsContainer = memo(() => {
|
||||
const copy = useMessages().pages.about.fourQuestions;
|
||||
return (
|
||||
<FourQuestionsView
|
||||
heading={copy.heading}
|
||||
cards={copy.cards as FourQuestionCard[]}
|
||||
wishHeading={copy.wishHeading}
|
||||
wishParagraphs={copy.wishParagraphs}
|
||||
statsHeading={copy.statsHeading}
|
||||
statsSource={copy.statsSource}
|
||||
stats={copy.stats}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
FourQuestionsContainer.displayName = "FourQuestions";
|
||||
|
||||
export default FourQuestionsContainer;
|
||||
@@ -0,0 +1,29 @@
|
||||
export const FOUR_QUESTION_TONE_OPTIONS = [
|
||||
"yellow",
|
||||
"teal",
|
||||
"lavender",
|
||||
"red",
|
||||
] as const;
|
||||
|
||||
export type FourQuestionTone = (typeof FOUR_QUESTION_TONE_OPTIONS)[number];
|
||||
|
||||
export type FourQuestionCard = {
|
||||
tone: FourQuestionTone;
|
||||
question: string;
|
||||
line: string;
|
||||
};
|
||||
|
||||
export type FourQuestionStat = {
|
||||
value: string;
|
||||
line: string;
|
||||
};
|
||||
|
||||
export interface FourQuestionsViewProps {
|
||||
heading: string;
|
||||
cards: FourQuestionCard[];
|
||||
wishHeading: string;
|
||||
wishParagraphs: string[];
|
||||
statsHeading: string;
|
||||
statsSource: string;
|
||||
stats: FourQuestionStat[];
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import type {
|
||||
FourQuestionTone,
|
||||
FourQuestionsViewProps,
|
||||
} from "./FourQuestions.types";
|
||||
|
||||
const TONE_CLASS: Record<FourQuestionTone, string> = {
|
||||
yellow: "bg-[var(--color-surface-invert-brand-secondary)]",
|
||||
teal: "bg-[var(--color-surface-invert-brand-teal)]",
|
||||
lavender: "bg-[var(--color-surface-invert-brand-lavender)]",
|
||||
red: "bg-[var(--color-surface-invert-brand-red)]",
|
||||
};
|
||||
|
||||
function FourQuestionsView({
|
||||
heading,
|
||||
cards,
|
||||
wishHeading,
|
||||
wishParagraphs,
|
||||
statsHeading,
|
||||
statsSource,
|
||||
stats,
|
||||
}: FourQuestionsViewProps) {
|
||||
return (
|
||||
<section
|
||||
data-figma-node="24230:852"
|
||||
className="flex flex-col gap-11 border-b border-[var(--color-border-default-secondary)] bg-black px-[var(--space-400)] py-16 lg:gap-14 lg:px-[var(--space-2400)] lg:py-20"
|
||||
aria-labelledby="about-four-questions-heading"
|
||||
>
|
||||
<h2
|
||||
id="about-four-questions-heading"
|
||||
className="max-w-[768px] font-bricolage-grotesque text-heading-l text-[var(--color-content-default-primary)] lg:text-heading-xxl"
|
||||
>
|
||||
{heading}
|
||||
</h2>
|
||||
<ul className="m-0 grid list-none grid-cols-1 gap-4 p-0 md:grid-cols-2 md:gap-6">
|
||||
{cards.map((card) => (
|
||||
<li
|
||||
key={card.question}
|
||||
className={`flex flex-col gap-3 rounded-[var(--radius-300)] p-6 lg:gap-4 lg:rounded-[var(--radius-500)] lg:p-8 ${TONE_CLASS[card.tone]}`}
|
||||
>
|
||||
<h3 className="font-bricolage-grotesque text-display-xs text-[var(--color-content-invert-primary)]">
|
||||
{card.question}
|
||||
</h3>
|
||||
<p className="font-inter text-paragraph-s text-[var(--color-content-invert-tertiary)] lg:text-paragraph-m">
|
||||
{card.line}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex max-w-[736px] flex-col gap-6">
|
||||
<h2 className="font-bricolage-grotesque text-heading-l text-[var(--color-content-default-primary)] lg:text-heading-xxl">
|
||||
{wishHeading}
|
||||
</h2>
|
||||
<div className="flex flex-col gap-[1lh]">
|
||||
{wishParagraphs.map((paragraph) => (
|
||||
<p
|
||||
key={paragraph}
|
||||
className="font-inter text-paragraph-m text-[var(--color-content-default-secondary)] lg:text-paragraph-l"
|
||||
>
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-2 lg:gap-3">
|
||||
<h3 className="font-bricolage-grotesque text-display-s text-[var(--color-content-default-primary)]">
|
||||
{statsHeading}
|
||||
</h3>
|
||||
<p className="font-inter text-paragraph-s text-[var(--color-content-default-tertiary)] lg:text-paragraph-m">
|
||||
{statsSource}
|
||||
</p>
|
||||
</div>
|
||||
<dl className="m-0 flex flex-col pt-0 lg:flex-row lg:gap-16 lg:pt-10">
|
||||
{stats.map((stat) => (
|
||||
<div
|
||||
key={stat.value}
|
||||
className="flex flex-col gap-2.5 border-t border-[var(--color-border-default-secondary)] py-6 lg:flex-1 lg:gap-4 lg:border-t-0 lg:py-0"
|
||||
>
|
||||
<dt className="font-bricolage-grotesque text-display-s text-[var(--color-content-default-primary)] lg:text-display-xl">
|
||||
{stat.value}
|
||||
</dt>
|
||||
<dd className="m-0 font-inter text-paragraph-s text-[var(--color-content-default-secondary)] lg:text-paragraph-m">
|
||||
{stat.line}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
FourQuestionsView.displayName = "FourQuestionsView";
|
||||
|
||||
export default memo(FourQuestionsView);
|
||||
@@ -0,0 +1,8 @@
|
||||
export { default } from "./FourQuestions.container";
|
||||
export type {
|
||||
FourQuestionCard,
|
||||
FourQuestionStat,
|
||||
FourQuestionTone,
|
||||
FourQuestionsViewProps,
|
||||
} from "./FourQuestions.types";
|
||||
export { FOUR_QUESTION_TONE_OPTIONS } from "./FourQuestions.types";
|
||||
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Figma: "Section / History" (24220:30635)
|
||||
*/
|
||||
|
||||
import { memo } from "react";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import HistoryView from "./History.view";
|
||||
import type { HistoryItem } from "./History.types";
|
||||
|
||||
const HistoryContainer = memo(() => {
|
||||
const copy = useMessages().pages.about.history;
|
||||
return (
|
||||
<HistoryView heading={copy.heading} items={copy.items as HistoryItem[]} />
|
||||
);
|
||||
});
|
||||
|
||||
HistoryContainer.displayName = "History";
|
||||
|
||||
export default HistoryContainer;
|
||||
@@ -0,0 +1,24 @@
|
||||
export type HistoryAccent = "brand" | "positive";
|
||||
|
||||
export type HistoryRun = {
|
||||
text: string;
|
||||
emphasis?: boolean;
|
||||
};
|
||||
|
||||
export type HistoryItem =
|
||||
| {
|
||||
kind: "entry";
|
||||
date: string;
|
||||
accent: HistoryAccent;
|
||||
body: HistoryRun[];
|
||||
}
|
||||
| {
|
||||
kind: "callout";
|
||||
title: string;
|
||||
body: string;
|
||||
};
|
||||
|
||||
export interface HistoryViewProps {
|
||||
heading: string;
|
||||
items: HistoryItem[];
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import type { HistoryAccent, HistoryItem, HistoryViewProps } from "./History.types";
|
||||
|
||||
const ACCENT_CLASS: Record<HistoryAccent, string> = {
|
||||
brand: "bg-[var(--color-content-default-brand-primary)]",
|
||||
positive: "bg-[var(--color-surface-default-positive-primary)]",
|
||||
};
|
||||
|
||||
const DATE_CLASS =
|
||||
"font-bricolage-grotesque text-display-xxs text-[var(--color-content-default-primary)]";
|
||||
|
||||
const BODY_CLASS =
|
||||
"font-inter text-paragraph-m text-[var(--color-content-default-secondary)]";
|
||||
|
||||
function DateMark({ date, accent }: { date: string; accent: HistoryAccent }) {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-1 md:w-[140px] md:shrink-0">
|
||||
<p className={`${DATE_CLASS} whitespace-nowrap`}>{date}</p>
|
||||
<span className={`h-0.5 w-8 ${ACCENT_CLASS[accent]}`} aria-hidden />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HistoryItemView({ item }: { item: HistoryItem }) {
|
||||
if (item.kind === "callout") {
|
||||
return (
|
||||
<aside className="flex flex-col gap-5 border-l-[3px] border-[var(--color-content-default-brand-primary)] pl-6">
|
||||
<p className={DATE_CLASS}>{item.title}</p>
|
||||
<p className={BODY_CLASS}>{item.body}</p>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col gap-4 md:flex-row md:gap-8 ${
|
||||
item.accent === "positive" ? "pt-6" : ""
|
||||
}`}
|
||||
>
|
||||
<DateMark date={item.date} accent={item.accent} />
|
||||
<p className={`min-w-0 flex-1 ${BODY_CLASS}`}>
|
||||
{item.body.map((run, index) =>
|
||||
run.emphasis ? (
|
||||
<em key={index} className="italic">
|
||||
{run.text}
|
||||
</em>
|
||||
) : (
|
||||
<span key={index}>{run.text}</span>
|
||||
),
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HistoryView({ heading, items }: HistoryViewProps) {
|
||||
return (
|
||||
<section
|
||||
data-figma-node="24220:30635"
|
||||
className="flex flex-col items-center gap-10 bg-black px-[var(--space-400)] py-12 md:gap-16 md:px-[var(--space-1800)] md:py-[88px]"
|
||||
aria-labelledby="about-history-heading"
|
||||
>
|
||||
<h2
|
||||
id="about-history-heading"
|
||||
className="w-full max-w-[800px] text-center font-bricolage-grotesque text-heading-l text-[var(--color-content-default-primary)]"
|
||||
>
|
||||
{heading}
|
||||
</h2>
|
||||
<div className="flex w-full max-w-[800px] flex-col gap-8 md:gap-10">
|
||||
{items.map((item, index) => (
|
||||
<HistoryItemView key={item.kind === "entry" ? item.date : index} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
HistoryView.displayName = "HistoryView";
|
||||
|
||||
export default memo(HistoryView);
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./History.container";
|
||||
export type { HistoryItem, HistoryViewProps } from "./History.types";
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Figma: "Section / Narrative Block" (24237:1241)
|
||||
*/
|
||||
|
||||
import { memo, useCallback, useState } from "react";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import AskOrganizerInquiryModal from "../../modals/AskOrganizerInquiry";
|
||||
import NarrativeBlockView from "./NarrativeBlock.view";
|
||||
|
||||
const NarrativeBlockContainer = memo(() => {
|
||||
const copy = useMessages().pages.about.narrativeBlock;
|
||||
const [inquiryOpen, setInquiryOpen] = useState(false);
|
||||
const openInquiry = useCallback(() => setInquiryOpen(true), []);
|
||||
const closeInquiry = useCallback(() => setInquiryOpen(false), []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NarrativeBlockView
|
||||
heading={copy.heading}
|
||||
paragraphs={copy.paragraphs}
|
||||
addHeading={copy.addHeading}
|
||||
addLine={copy.addLine}
|
||||
shareExperiences={copy.shareExperiences}
|
||||
helpBuild={copy.helpBuild}
|
||||
helpBuildHref={copy.helpBuildHref}
|
||||
onShareExperiences={openInquiry}
|
||||
/>
|
||||
<AskOrganizerInquiryModal isOpen={inquiryOpen} onClose={closeInquiry} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
NarrativeBlockContainer.displayName = "NarrativeBlock";
|
||||
|
||||
export default NarrativeBlockContainer;
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface NarrativeBlockViewProps {
|
||||
heading: string;
|
||||
paragraphs: string[];
|
||||
addHeading: string;
|
||||
addLine: string;
|
||||
shareExperiences: string;
|
||||
helpBuild: string;
|
||||
helpBuildHref: string;
|
||||
onShareExperiences: () => void;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import Button from "../../buttons/Button";
|
||||
import type { NarrativeBlockViewProps } from "./NarrativeBlock.types";
|
||||
|
||||
function NarrativeBlockView({
|
||||
heading,
|
||||
paragraphs,
|
||||
addHeading,
|
||||
addLine,
|
||||
shareExperiences,
|
||||
helpBuild,
|
||||
helpBuildHref,
|
||||
onShareExperiences,
|
||||
}: NarrativeBlockViewProps) {
|
||||
return (
|
||||
<section
|
||||
data-figma-node="24237:1241"
|
||||
className="flex flex-col items-center gap-10 border-b border-[var(--color-border-default-secondary)] bg-black px-[var(--space-400)] py-16 lg:gap-14 lg:px-[var(--space-2400)] lg:py-[var(--space-3000)]"
|
||||
aria-labelledby="about-narrative-heading"
|
||||
>
|
||||
<div className="flex w-full max-w-[800px] flex-col gap-8 lg:gap-10">
|
||||
<h2
|
||||
id="about-narrative-heading"
|
||||
className="font-bricolage-grotesque text-heading-l text-[var(--color-content-default-primary)] lg:text-heading-xxl"
|
||||
>
|
||||
{heading}
|
||||
</h2>
|
||||
<div className="flex flex-col gap-8">
|
||||
{paragraphs.map((paragraph) => (
|
||||
<p
|
||||
key={paragraph}
|
||||
className="font-inter text-paragraph-l text-[var(--color-content-default-secondary)] lg:text-paragraph-xl"
|
||||
>
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="font-bricolage-grotesque text-display-xs text-[var(--color-content-default-primary)]">
|
||||
{addHeading}
|
||||
</h3>
|
||||
<p className="font-inter text-paragraph-s text-[var(--color-content-default-secondary)]">
|
||||
{addLine}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
size="medium"
|
||||
onClick={onShareExperiences}
|
||||
>
|
||||
{shareExperiences}
|
||||
</Button>
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
size="medium"
|
||||
href={helpBuildHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{helpBuild}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
NarrativeBlockView.displayName = "NarrativeBlockView";
|
||||
|
||||
export default memo(NarrativeBlockView);
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./NarrativeBlock.container";
|
||||
export type { NarrativeBlockViewProps } from "./NarrativeBlock.types";
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Figma: "Section / Participant Selection" (24227:30762)
|
||||
*/
|
||||
|
||||
import { memo } from "react";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import ParticipantSelectionView from "./ParticipantSelection.view";
|
||||
|
||||
const ParticipantSelectionContainer = memo(() => {
|
||||
const copy = useMessages().pages.about.participantSelection;
|
||||
return <ParticipantSelectionView {...copy} />;
|
||||
});
|
||||
|
||||
ParticipantSelectionContainer.displayName = "ParticipantSelection";
|
||||
|
||||
export default ParticipantSelectionContainer;
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface ParticipantSelectionViewProps {
|
||||
heading: string;
|
||||
body: string;
|
||||
groups: string[];
|
||||
footnote: string;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { getAssetPath } from "../../../../lib/assetUtils";
|
||||
import type { ParticipantSelectionViewProps } from "./ParticipantSelection.types";
|
||||
|
||||
function ParticipantArt() {
|
||||
return (
|
||||
<div
|
||||
className="relative mx-auto aspect-[540/555] w-full max-w-[420px] pr-12 lg:mx-0 lg:w-[540px] lg:max-w-none lg:shrink-0 lg:pr-0"
|
||||
aria-hidden
|
||||
>
|
||||
{/* eslint-disable @next/next/no-img-element -- decorative Figma shapes */}
|
||||
<img
|
||||
src={getAssetPath("assets/shapes/about-participant-circles.svg")}
|
||||
alt=""
|
||||
width={282}
|
||||
height={282}
|
||||
className="absolute top-0 left-0 h-auto w-[52.2%]"
|
||||
/>
|
||||
<img
|
||||
src={getAssetPath("assets/shapes/about-participant-rings.svg")}
|
||||
alt=""
|
||||
width={334}
|
||||
height={334}
|
||||
className="absolute top-[6.3%] left-[38.15%] h-auto w-[61.85%]"
|
||||
/>
|
||||
<img
|
||||
src={getAssetPath("assets/shapes/about-participant-clover.svg")}
|
||||
alt=""
|
||||
width={200}
|
||||
height={200}
|
||||
className="absolute top-[57.4%] left-[25.7%] h-auto w-[37%] rotate-[60deg]"
|
||||
/>
|
||||
{/* eslint-enable @next/next/no-img-element */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ParticipantSelectionView({
|
||||
heading,
|
||||
body,
|
||||
groups,
|
||||
footnote,
|
||||
}: ParticipantSelectionViewProps) {
|
||||
return (
|
||||
<section
|
||||
data-figma-node="24227:30762"
|
||||
className="flex flex-col gap-4 border-b border-[var(--color-border-default-secondary)] bg-black px-[var(--space-400)] py-20 lg:flex-row lg:items-center lg:justify-center lg:gap-8 lg:px-[var(--space-2400)]"
|
||||
aria-labelledby="about-participant-heading"
|
||||
>
|
||||
<ParticipantArt />
|
||||
<div className="flex w-full min-w-0 flex-col gap-12 lg:max-w-[928px] lg:flex-1">
|
||||
<div className="flex flex-col gap-4">
|
||||
<h2
|
||||
id="about-participant-heading"
|
||||
className="font-bricolage-grotesque text-display-s text-[var(--color-content-default-primary)] lg:text-heading-xxl"
|
||||
>
|
||||
{heading}
|
||||
</h2>
|
||||
<p className="font-inter text-paragraph-s text-[var(--color-content-default-tertiary)] lg:text-paragraph-l">
|
||||
{body}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<ul className="m-0 list-none p-0">
|
||||
{groups.map((group, index) => (
|
||||
<li
|
||||
key={group}
|
||||
className={`border-b border-[var(--color-border-default-secondary)] py-4 font-inter text-paragraph-xs text-[var(--color-content-default-primary)] lg:text-paragraph-m ${
|
||||
index === 0 ? "border-t" : ""
|
||||
}`}
|
||||
>
|
||||
{group}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="pt-6 font-inter text-paragraph-xxs text-[var(--color-content-default-tertiary)] lg:text-paragraph-s">
|
||||
{footnote}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
ParticipantSelectionView.displayName = "ParticipantSelectionView";
|
||||
|
||||
export default memo(ParticipantSelectionView);
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./ParticipantSelection.container";
|
||||
export type { ParticipantSelectionViewProps } from "./ParticipantSelection.types";
|
||||
@@ -13,6 +13,7 @@ const QuoteBlockContainer = memo<QuoteBlockProps>(
|
||||
className = "",
|
||||
quote = "The rules of decision-making must be open and available to everyone, and this can happen only if they are formalized.",
|
||||
quoteSecondary,
|
||||
statementStacked = false,
|
||||
author = "Jo Freeman",
|
||||
source = "The Tyranny of Structurelessness",
|
||||
avatarSrc = getAssetPath(ASSETS.QUOTE_AVATAR),
|
||||
@@ -151,6 +152,7 @@ const QuoteBlockContainer = memo<QuoteBlockProps>(
|
||||
className={className}
|
||||
quote={quote}
|
||||
quoteSecondary={quoteSecondary}
|
||||
statementStacked={statementStacked}
|
||||
author={author}
|
||||
source={source}
|
||||
quoteId={quoteId}
|
||||
|
||||
@@ -11,6 +11,11 @@ export interface QuoteBlockProps {
|
||||
quote?: string;
|
||||
/** Second paragraph for **`statement`** (Section/Quote); merged into one `<p>` from **`lg`**. */
|
||||
quoteSecondary?: string;
|
||||
/**
|
||||
* Keep the two statement paragraphs stacked at every breakpoint.
|
||||
* Default joins them into one paragraph from `lg`.
|
||||
*/
|
||||
statementStacked?: boolean;
|
||||
author?: string;
|
||||
source?: string;
|
||||
avatarSrc?: string;
|
||||
@@ -46,6 +51,7 @@ export interface QuoteBlockViewProps {
|
||||
className: string;
|
||||
quote: string;
|
||||
quoteSecondary?: string;
|
||||
statementStacked: boolean;
|
||||
author: string;
|
||||
source?: string;
|
||||
quoteId: string;
|
||||
|
||||
@@ -11,6 +11,7 @@ function QuoteBlockView({
|
||||
className,
|
||||
quote,
|
||||
quoteSecondary,
|
||||
statementStacked,
|
||||
author,
|
||||
source,
|
||||
quoteId,
|
||||
@@ -41,13 +42,25 @@ function QuoteBlockView({
|
||||
<div className="relative z-10 flex w-full min-w-0 shrink-0 flex-col items-center text-center">
|
||||
<p
|
||||
id={quoteId}
|
||||
className={`${statementTextClass} mb-0 flex w-full min-w-0 flex-col gap-9 text-center md:gap-[length:var(--text-xx-large-heading--line-height)] lg:block lg:gap-0`}
|
||||
className={
|
||||
statementStacked
|
||||
? `${statementTextClass} mb-0 flex w-full min-w-0 flex-col gap-[1lh] text-center`
|
||||
: `${statementTextClass} mb-0 flex w-full min-w-0 flex-col gap-9 text-center md:gap-[length:var(--text-xx-large-heading--line-height)] lg:block lg:gap-0`
|
||||
}
|
||||
>
|
||||
<span className="block lg:inline">{quote}</span>
|
||||
<span className={statementStacked ? "block" : "block lg:inline"}>
|
||||
{quote}
|
||||
</span>
|
||||
{quoteSecondary ? (
|
||||
<>
|
||||
<span className="hidden lg:inline">{" "}</span>
|
||||
<span className="block lg:inline">{quoteSecondary}</span>
|
||||
{statementStacked ? (
|
||||
" "
|
||||
) : (
|
||||
<span className="hidden lg:inline">{" "}</span>
|
||||
)}
|
||||
<span className={statementStacked ? "block" : "block lg:inline"}>
|
||||
{quoteSecondary}
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Figma: "Section / Rebuild Process" (24237:1085)
|
||||
*/
|
||||
|
||||
import { memo } from "react";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import RebuildProcessView from "./RebuildProcess.view";
|
||||
|
||||
const RebuildProcessContainer = memo(() => {
|
||||
const copy = useMessages().pages.about.rebuildProcess;
|
||||
return <RebuildProcessView {...copy} />;
|
||||
});
|
||||
|
||||
RebuildProcessContainer.displayName = "RebuildProcess";
|
||||
|
||||
export default RebuildProcessContainer;
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface RebuildProcessViewProps {
|
||||
heading: string;
|
||||
body: string;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import type { RebuildProcessViewProps } from "./RebuildProcess.types";
|
||||
|
||||
function RebuildProcessView({ heading, body }: RebuildProcessViewProps) {
|
||||
return (
|
||||
<section
|
||||
data-figma-node="24237:1085"
|
||||
className="flex flex-col gap-6 border-b border-white/15 bg-black px-[var(--space-400)] py-16 lg:flex-row lg:items-start lg:justify-center lg:gap-12 lg:px-[var(--space-2400)] lg:py-[var(--space-3000)]"
|
||||
aria-labelledby="about-rebuild-heading"
|
||||
>
|
||||
<h2
|
||||
id="about-rebuild-heading"
|
||||
className="font-bricolage-grotesque text-heading-l text-[var(--color-content-default-primary)] lg:w-[287px] lg:shrink-0"
|
||||
>
|
||||
{heading}
|
||||
</h2>
|
||||
<p className="max-w-[800px] font-inter text-paragraph-l text-[var(--color-content-default-tertiary)] lg:flex-1 lg:text-paragraph-xl">
|
||||
{body}
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
RebuildProcessView.displayName = "RebuildProcessView";
|
||||
|
||||
export default memo(RebuildProcessView);
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./RebuildProcess.container";
|
||||
export type { RebuildProcessViewProps } from "./RebuildProcess.types";
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Figma: "Section / Your Turn" (24239:1397)
|
||||
*/
|
||||
|
||||
import { memo, useCallback, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import { fetchAuthSession } from "../../../../lib/create/api";
|
||||
import { prepareFreshCreateFlowEntrySync } from "../../../(app)/create/utils/prepareFreshCreateFlowEntry";
|
||||
import AskOrganizerInquiryModal from "../../modals/AskOrganizerInquiry";
|
||||
import YourTurnView from "./YourTurn.view";
|
||||
|
||||
const YourTurnContainer = memo(() => {
|
||||
const router = useRouter();
|
||||
const copy = useMessages().pages.about.yourTurn;
|
||||
const [signedIn, setSignedIn] = useState(false);
|
||||
const [inquiryOpen, setInquiryOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void fetchAuthSession()
|
||||
.then(({ user }) => {
|
||||
if (!cancelled) setSignedIn(Boolean(user));
|
||||
})
|
||||
.catch(() => {
|
||||
/* guest fallback */
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onCreateRule = useCallback(() => {
|
||||
prepareFreshCreateFlowEntrySync({ signedIn });
|
||||
router.push("/create/informational");
|
||||
}, [router, signedIn]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<YourTurnView
|
||||
heading={copy.heading}
|
||||
body={copy.body}
|
||||
createRule={copy.createRule}
|
||||
facilitator={copy.facilitator}
|
||||
facilitatorHref={copy.facilitatorHref}
|
||||
questionsLead={copy.questionsLead}
|
||||
faq={copy.faq}
|
||||
faqHref={copy.faqHref}
|
||||
or={copy.or}
|
||||
askOrganizer={copy.askOrganizer}
|
||||
illustrationCaption={copy.illustrationCaption}
|
||||
onCreateRule={onCreateRule}
|
||||
onAskOrganizer={() => setInquiryOpen(true)}
|
||||
/>
|
||||
<AskOrganizerInquiryModal
|
||||
isOpen={inquiryOpen}
|
||||
onClose={() => setInquiryOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
YourTurnContainer.displayName = "YourTurn";
|
||||
|
||||
export default YourTurnContainer;
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface YourTurnViewProps {
|
||||
heading: string;
|
||||
body: string;
|
||||
createRule: string;
|
||||
facilitator: string;
|
||||
facilitatorHref: string;
|
||||
questionsLead: string;
|
||||
faq: string;
|
||||
faqHref: string;
|
||||
or: string;
|
||||
askOrganizer: string;
|
||||
illustrationCaption: string;
|
||||
onCreateRule: () => void;
|
||||
onAskOrganizer: () => void;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import Button from "../../buttons/Button";
|
||||
import Link from "../../navigation/Link";
|
||||
import type { YourTurnViewProps } from "./YourTurn.types";
|
||||
|
||||
const COLOR_BARS = [
|
||||
"bg-[var(--color-lavender-lavender200)]",
|
||||
"bg-[var(--color-surface-invert-brand-teal)]",
|
||||
"bg-[var(--color-surface-invert-brand-secondary)]",
|
||||
"bg-[var(--color-surface-invert-brand-red)]",
|
||||
] as const;
|
||||
|
||||
function YourTurnView({
|
||||
heading,
|
||||
body,
|
||||
createRule,
|
||||
facilitator,
|
||||
facilitatorHref,
|
||||
questionsLead,
|
||||
faq,
|
||||
faqHref,
|
||||
or,
|
||||
askOrganizer,
|
||||
illustrationCaption,
|
||||
onCreateRule,
|
||||
onAskOrganizer,
|
||||
}: YourTurnViewProps) {
|
||||
return (
|
||||
<section
|
||||
data-figma-node="24239:1397"
|
||||
className="border-b border-[var(--color-border-default-secondary)] bg-[var(--color-surface-default-tertiary)] px-[var(--space-600)] py-12 lg:px-[var(--space-2400)] lg:py-[var(--space-3000)]"
|
||||
aria-labelledby="about-your-turn-heading"
|
||||
>
|
||||
<div className="mx-auto flex w-full max-w-[1920px] flex-col gap-10 lg:flex-row lg:items-start lg:gap-12">
|
||||
<div className="flex w-full flex-col gap-8 lg:max-w-[472px] lg:shrink-0 lg:gap-12">
|
||||
<div className="flex flex-col gap-5 lg:gap-6">
|
||||
<div className="flex h-1 w-[184px] gap-1.5 lg:h-3 lg:w-[400px]" aria-hidden>
|
||||
{COLOR_BARS.map((bar) => (
|
||||
<span key={bar} className={`h-full flex-1 rounded-sm ${bar}`} />
|
||||
))}
|
||||
</div>
|
||||
<h2
|
||||
id="about-your-turn-heading"
|
||||
className="font-bricolage-grotesque text-heading-l text-[var(--color-content-default-primary)]"
|
||||
>
|
||||
{heading}
|
||||
</h2>
|
||||
<p className="font-inter text-paragraph-m text-[var(--color-content-default-secondary)]">
|
||||
{body}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 md:flex-row md:gap-4">
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
size="large"
|
||||
className="w-full justify-center md:w-auto"
|
||||
onClick={onCreateRule}
|
||||
>
|
||||
{createRule}
|
||||
</Button>
|
||||
<Button
|
||||
buttonType="outline"
|
||||
palette="default"
|
||||
size="large"
|
||||
className="w-full justify-center md:w-auto"
|
||||
href={facilitatorHref}
|
||||
>
|
||||
{facilitator}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="font-inter text-paragraph-s text-[var(--color-content-default-secondary)]">
|
||||
{questionsLead}{" "}
|
||||
<Link href={faqHref} variant="paragraph" theme="dark" type="primary">
|
||||
{faq}
|
||||
</Link>{" "}
|
||||
{or}{" "}
|
||||
<Link
|
||||
variant="paragraph"
|
||||
theme="dark"
|
||||
type="primary"
|
||||
onClick={onAskOrganizer}
|
||||
>
|
||||
{askOrganizer}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-[246px] w-full flex-col items-start justify-end rounded-[var(--radius-300)] border border-dashed border-[var(--color-border-default-tertiary)] bg-[var(--color-surface-default-secondary)] p-6 lg:h-[340px] lg:flex-1">
|
||||
<p className="font-inter text-paragraph-xxs text-[var(--color-content-default-tertiary)]">
|
||||
{illustrationCaption}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
YourTurnView.displayName = "YourTurnView";
|
||||
|
||||
export default memo(YourTurnView);
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./YourTurn.container";
|
||||
export type { YourTurnViewProps } from "./YourTurn.types";
|
||||
Reference in new issue
Block a user