Cleanup pass 2

This commit is contained in:
adilallo
2026-05-22 13:30:47 -06:00
parent b7c804bac8
commit 753220f97b
76 changed files with 1338 additions and 1020 deletions
@@ -1,6 +1,7 @@
"use client";
import { memo } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import { RuleView } from "./Rule.view";
import type { RuleProps } from "./Rule.types";
@@ -49,6 +50,9 @@ const RuleContainer = memo<RuleProps>(
fluidWidth = false,
}) => {
const size = sizeProp ?? "L";
const t = useTranslation("ruleCard");
const cardAriaLabel = t("ariaLabel")?.replace("{title}", title) || title;
const recommendedLabel = t("recommendedLabel");
const handleClick = () => {
if (hasBottomLinks) return;
@@ -106,6 +110,8 @@ const RuleContainer = memo<RuleProps>(
recommended={recommended}
templateGridFigmaShell={templateGridFigmaShell}
fluidWidth={fluidWidth}
cardAriaLabel={cardAriaLabel}
recommendedLabel={recommendedLabel}
/>
);
},
+3
View File
@@ -107,4 +107,7 @@ export interface RuleViewProps {
recommended?: boolean;
templateGridFigmaShell?: boolean;
fluidWidth?: boolean;
/** Interactive card aria-label; supplied by the container from `ruleCard` messages. */
cardAriaLabel: string;
recommendedLabel: string;
}
+4 -4
View File
@@ -1,7 +1,6 @@
"use client";
import Image from "next/image";
import { useTranslation } from "../../../contexts/MessagesContext";
import MultiSelect from "../../controls/MultiSelect";
import InlineTextButton from "../../buttons/InlineTextButton";
import NavigationLink from "../../navigation/Link";
@@ -34,9 +33,10 @@ export function RuleView({
recommended = false,
templateGridFigmaShell = false,
fluidWidth = false,
cardAriaLabel,
recommendedLabel,
}: RuleViewProps) {
const t = useTranslation("ruleCard");
const ariaLabel = t("ariaLabel")?.replace("{title}", title) || title;
const ariaLabel = cardAriaLabel;
const interactiveCard = !hasBottomLinks;
// Size-based styling
@@ -306,7 +306,7 @@ export function RuleView({
>
{showRecommendedTag ? (
<Tag variant="templateRecommended">
{t("recommendedLabel")}
{recommendedLabel}
</Tag>
) : null}
{onTitleClick ? (
@@ -1,22 +0,0 @@
"use client";
/**
* Figma: "localization/LanguageSwitcher" (see registry)
*/
import { memo } from "react";
import LanguageSwitcherView from "./LanguageSwitcher.view";
import type { LanguageSwitcherProps } from "./LanguageSwitcher.types";
const LanguageSwitcherContainer = memo<LanguageSwitcherProps>(
({ className }) => {
// Future: Add language switching logic here
// For now, this is just a UI component
return <LanguageSwitcherView className={className} />;
},
);
LanguageSwitcherContainer.displayName = "LanguageSwitcher";
export default LanguageSwitcherContainer;
@@ -1,9 +0,0 @@
export interface LanguageSwitcherProps {
className?: string;
}
export interface Language {
code: string;
name: string;
nativeName: string;
}
@@ -1,42 +0,0 @@
"use client";
import { memo } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import type { LanguageSwitcherProps, Language } from "./LanguageSwitcher.types";
function LanguageSwitcherView({ className = "" }: LanguageSwitcherProps) {
const t = useTranslation("languageSwitcher");
const AVAILABLE_LANGUAGES: Language[] = [
{
code: "en",
name: t("languages.english.name"),
nativeName: t("languages.english.nativeName"),
},
];
return (
<div className={className}>
<label htmlFor="language-select" className="sr-only">
{t("label")}
</label>
<select
id="language-select"
className="bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] font-inter text-sm leading-5 font-normal border border-[var(--color-surface-default-secondary)] rounded-[var(--radius-measures-radius-small)] px-[var(--spacing-scale-012)] py-[var(--spacing-scale-008)] focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 cursor-pointer"
aria-label={t("ariaLabel")}
disabled
>
{AVAILABLE_LANGUAGES.map((language) => (
<option key={language.code} value={language.code}>
{language.nativeName}
</option>
))}
</select>
<p className="text-[var(--color-content-default-secondary)] font-inter text-xs leading-4 font-normal mt-[var(--spacing-scale-008)]">
{t("comingSoonMessage")}
</p>
</div>
);
}
export default memo(LanguageSwitcherView);
@@ -1,2 +0,0 @@
export { default } from "./LanguageSwitcher.container";
export type { LanguageSwitcherProps, Language } from "./LanguageSwitcher.types";
@@ -5,7 +5,7 @@
* File: agv0VBLiBlcnSAaiAORgPR, node 22078-587823
*/
import { memo, useCallback, useEffect, useState, type FormEvent } from "react";
import { memo, useCallback, useEffect, useMemo, useState, type FormEvent } from "react";
import { AskOrganizerInquiryModalView } from "./AskOrganizerInquiryModal.view";
import type { AskOrganizerInquiryModalProps } from "./AskOrganizerInquiryModal.types";
import { ORGANIZER_INQUIRY_HONEYPOT_FIELD } from "../../../../lib/organizerInquiryConstants";
@@ -14,6 +14,23 @@ import { useTranslation } from "../../../contexts/MessagesContext";
const AskOrganizerInquiryModalContainer = memo<AskOrganizerInquiryModalProps>(
({ isOpen, onClose }) => {
const t = useTranslation("modals.askOrganizerInquiry");
const copy = useMemo(
() => ({
title: t("title"),
description: t("description"),
emailLabel: t("emailLabel"),
emailPlaceholder: t("emailPlaceholder"),
questionLabel: t("questionLabel"),
questionPlaceholder: t("questionPlaceholder"),
submitButton: t("submitButton"),
closeAfterSuccess: t("closeAfterSuccess"),
successTitle: t("successTitle"),
successDescription: t("successDescription"),
ariaDialog: t("ariaDialog"),
honeypotLabel: t("honeypotLabel"),
}),
[t],
);
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
const [honeypot, setHoneypot] = useState("");
@@ -102,6 +119,7 @@ const AskOrganizerInquiryModalContainer = memo<AskOrganizerInquiryModalProps>(
<AskOrganizerInquiryModalView
isOpen={isOpen}
onClose={onClose}
copy={copy}
email={email}
message={message}
honeypot={honeypot}
@@ -2,3 +2,35 @@ export interface AskOrganizerInquiryModalProps {
isOpen: boolean;
onClose: () => void;
}
export interface AskOrganizerInquiryModalCopy {
title: string;
description: string;
emailLabel: string;
emailPlaceholder: string;
questionLabel: string;
questionPlaceholder: string;
submitButton: string;
closeAfterSuccess: string;
successTitle: string;
successDescription: string;
ariaDialog: string;
honeypotLabel: string;
}
export interface AskOrganizerInquiryModalViewProps
extends AskOrganizerInquiryModalProps {
copy: AskOrganizerInquiryModalCopy;
email: string;
message: string;
honeypot: string;
submitting: boolean;
success: boolean;
formError: string | null;
emailError: boolean;
questionError: boolean;
onEmailChange: (_v: string) => void;
onMessageChange: (_v: string) => void;
onHoneypotChange: (_v: string) => void;
onSubmit: (_e: import("react").FormEvent<HTMLFormElement>) => void;
}
@@ -1,31 +1,14 @@
"use client";
import type { FormEvent } from "react";
import Create from "../Create";
import TextInput from "../../controls/TextInput";
import TextArea from "../../controls/TextArea";
import Button from "../../buttons/Button";
import { useTranslation } from "../../../contexts/MessagesContext";
import {
ASK_ORGANIZER_INQUIRY_FORM_ID,
ORGANIZER_INQUIRY_HONEYPOT_FIELD,
} from "../../../../lib/organizerInquiryConstants";
import type { AskOrganizerInquiryModalProps } from "./AskOrganizerInquiryModal.types";
export type AskOrganizerInquiryModalViewProps = AskOrganizerInquiryModalProps & {
email: string;
message: string;
honeypot: string;
submitting: boolean;
success: boolean;
formError: string | null;
emailError: boolean;
questionError: boolean;
onEmailChange: (_v: string) => void;
onMessageChange: (_v: string) => void;
onHoneypotChange: (_v: string) => void;
onSubmit: (_e: FormEvent<HTMLFormElement>) => void;
};
import type { AskOrganizerInquiryModalViewProps } from "./AskOrganizerInquiryModal.types";
/**
* Figma: Community Rule System — Modal / Ask an Organizer (22078-587823)
@@ -33,6 +16,7 @@ export type AskOrganizerInquiryModalViewProps = AskOrganizerInquiryModalProps &
export function AskOrganizerInquiryModalView({
isOpen,
onClose,
copy,
email,
message,
honeypot,
@@ -46,8 +30,6 @@ export function AskOrganizerInquiryModalView({
onHoneypotChange,
onSubmit,
}: AskOrganizerInquiryModalViewProps) {
const t = useTranslation("modals.askOrganizerInquiry");
const footer = success ? (
<div className="w-full px-1">
<Button
@@ -58,7 +40,7 @@ export function AskOrganizerInquiryModalView({
className="w-full !justify-center"
onClick={onClose}
>
{t("closeAfterSuccess")}
{copy.closeAfterSuccess}
</Button>
</div>
) : (
@@ -72,7 +54,7 @@ export function AskOrganizerInquiryModalView({
className="w-full !justify-center"
disabled={submitting}
>
{t("submitButton")}
{copy.submitButton}
</Button>
</div>
);
@@ -82,22 +64,22 @@ export function AskOrganizerInquiryModalView({
isOpen={isOpen}
onClose={onClose}
backdropVariant="blurredYellow"
title={t("title")}
description={t("description")}
title={copy.title}
description={copy.description}
showBackButton={false}
showNextButton={false}
stepper={false}
ariaLabel={t("ariaDialog")}
ariaLabel={copy.ariaDialog}
footerContent={footer}
footerClassName="!h-auto min-h-[112px] shrink-0 flex flex-col justify-end pb-8 pt-3 px-4"
>
{success ? (
<div className="flex flex-col gap-3 py-2">
<p className="font-inter text-[18px] font-semibold leading-[24px] text-[var(--color-content-default-primary)]">
{t("successTitle")}
{copy.successTitle}
</p>
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)]">
{t("successDescription")}
{copy.successDescription}
</p>
</div>
) : (
@@ -120,8 +102,8 @@ export function AskOrganizerInquiryModalView({
type="email"
name="email"
autoComplete="email"
label={t("emailLabel")}
placeholder={t("emailPlaceholder")}
label={copy.emailLabel}
placeholder={copy.emailPlaceholder}
value={email}
onChange={(e) => onEmailChange(e.target.value)}
error={emailError}
@@ -131,8 +113,8 @@ export function AskOrganizerInquiryModalView({
<TextArea
name="message"
label={t("questionLabel")}
placeholder={t("questionPlaceholder")}
label={copy.questionLabel}
placeholder={copy.questionPlaceholder}
value={message}
onChange={(e) => onMessageChange(e.target.value)}
error={questionError}
@@ -146,7 +128,7 @@ export function AskOrganizerInquiryModalView({
className="pointer-events-none absolute left-0 top-0 h-px w-px overflow-hidden opacity-0"
>
<label htmlFor={`${ASK_ORGANIZER_INQUIRY_FORM_ID}-${ORGANIZER_INQUIRY_HONEYPOT_FIELD}`}>
{t("honeypotLabel")}
{copy.honeypotLabel}
</label>
<input
id={`${ASK_ORGANIZER_INQUIRY_FORM_ID}-${ORGANIZER_INQUIRY_HONEYPOT_FIELD}`}
+2 -2
View File
@@ -262,14 +262,14 @@ export default function LoginForm({
<p className="text-center font-inter text-[14px] leading-[20px] text-[var(--color-content-default-tertiary)]">
{t("legalPrefix")}
<Link
href="#"
href={tFooter("legal.termsOfServiceHref")}
className="text-[var(--color-content-default-tertiary)] underline decoration-solid underline-offset-2"
>
{tFooter("legal.termsOfService")}
</Link>
{t("legalAnd")}
<Link
href="#"
href={tFooter("legal.privacyPolicyHref")}
className="text-[var(--color-content-default-tertiary)] underline decoration-solid underline-offset-2"
>
{tFooter("legal.privacyPolicy")}
@@ -1,6 +1,7 @@
"use client";
import { memo } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import { ModalFooterView } from "./ModalFooter.view";
import type { ModalFooterProps } from "./ModalFooter.types";
@@ -10,7 +11,17 @@ import type { ModalFooterProps } from "./ModalFooter.types";
* primary/secondary actions.
*/
const ModalFooterContainer = memo<ModalFooterProps>((props) => {
return <ModalFooterView {...props} />;
const t = useTranslation("common");
const resolvedBackText = props.backButtonText ?? t("buttons.back");
const resolvedNextText = props.nextButtonText ?? t("buttons.next");
return (
<ModalFooterView
{...props}
backButtonText={resolvedBackText}
nextButtonText={resolvedNextText}
/>
);
});
ModalFooterContainer.displayName = "ModalFooter";
@@ -1,6 +1,5 @@
"use client";
import { useTranslation } from "../../../contexts/MessagesContext";
import Button from "../../buttons/Button";
import Stepper from "../../progress/Stepper";
import type { ModalFooterProps } from "./ModalFooter.types";
@@ -19,14 +18,6 @@ export function ModalFooterView({
footerContent,
className = "",
}: ModalFooterProps) {
const t = useTranslation("common");
// Use localized defaults if text not provided
const defaultBackText = backButtonText || t("buttons.back");
const defaultNextText = nextButtonText || t("buttons.next");
// Determine if stepper should be shown
// Defaults to true if currentStep and totalSteps are provided, unless explicitly set to false
const shouldShowStepper =
stepperProp !== undefined
? stepperProp
@@ -36,7 +27,6 @@ export function ModalFooterView({
<div
className={`h-[64px] bg-[var(--color-surface-default-primary)] rounded-bl-[var(--radius-300,12px)] rounded-br-[var(--radius-300,12px)] shrink-0 relative ${className}`}
>
{/* Back Button - Absolutely positioned bottom left */}
{showBackButton && (
<div className="absolute left-[16px] top-[12px]">
<Button
@@ -45,19 +35,17 @@ export function ModalFooterView({
size="medium"
onClick={onBack}
>
{defaultBackText}
{backButtonText}
</Button>
</div>
)}
{/* Stepper (Centered) */}
{shouldShowStepper && currentStep && totalSteps && (
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
<Stepper active={currentStep} totalSteps={totalSteps} />
</div>
)}
{/* Next Button - Absolutely positioned bottom right */}
{showNextButton && (
<div className="absolute right-[16px] top-[12px]">
<Button
@@ -67,12 +55,11 @@ export function ModalFooterView({
onClick={onNext}
disabled={nextButtonDisabled}
>
{defaultNextText}
{nextButtonText}
</Button>
</div>
)}
{/* Custom Footer Content */}
{footerContent}
</div>
);
@@ -237,6 +237,16 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
exportPopoverMarkdownLabel={tPopover("downloadMarkdown")}
moreOptionsAriaLabel={t("moreOptionsAriaLabel")}
actionsMenuAriaLabel={t("actionsMenuAriaLabel")}
shareLabel={t("share")}
exportLabel={t("export")}
editLabel={t("edit")}
manageStakeholdersLabel={t("manageStakeholders")}
shareAriaLabel={t("shareAriaLabel")}
exportAriaLabel={t("exportAriaLabel")}
editAriaLabel={t("editAriaLabel")}
manageStakeholdersAriaLabel={t("manageStakeholdersAriaLabel")}
bannerAriaLabel={t("bannerAriaLabel")}
navAriaLabel={t("navAriaLabel")}
/>
);
},
@@ -110,4 +110,14 @@ export type CreateFlowTopNavViewProps = CreateFlowTopNavProps & {
exportPopoverMarkdownLabel: string;
moreOptionsAriaLabel: string;
actionsMenuAriaLabel: string;
shareLabel: string;
exportLabel: string;
editLabel: string;
manageStakeholdersLabel: string;
shareAriaLabel: string;
exportAriaLabel: string;
editAriaLabel: string;
manageStakeholdersAriaLabel: string;
bannerAriaLabel: string;
navAriaLabel: string;
};
@@ -4,7 +4,6 @@ import Logo from "../../asset/Logo";
import Button from "../../buttons/Button";
import ListItem from "../../layout/ListItem";
import Popover from "../../modals/Popover";
import { useTranslation } from "../../../contexts/MessagesContext";
import type { CreateFlowTopNavViewProps } from "./CreateFlowTopNav.types";
const outlineButtonClass =
@@ -65,9 +64,17 @@ export function CreateFlowTopNavView({
exportPopoverMarkdownLabel,
moreOptionsAriaLabel,
actionsMenuAriaLabel,
shareLabel,
exportLabel,
editLabel,
manageStakeholdersLabel,
shareAriaLabel,
exportAriaLabel,
editAriaLabel,
manageStakeholdersAriaLabel,
bannerAriaLabel,
navAriaLabel,
}: CreateFlowTopNavViewProps) {
const t = useTranslation("create.topNav");
const hasSecondaryActions =
hasShare ||
hasExport ||
@@ -83,10 +90,10 @@ export function CreateFlowTopNavView({
palette={buttonPalette}
size="xsmall"
onClick={onShare}
ariaLabel={t("shareAriaLabel")}
ariaLabel={shareAriaLabel}
className={outlineButtonClass}
>
{t("share")}
{shareLabel}
</Button>
)}
@@ -97,14 +104,14 @@ export function CreateFlowTopNavView({
palette={buttonPalette}
size="xsmall"
type="button"
ariaLabel={t("exportAriaLabel")}
ariaLabel={exportAriaLabel}
aria-haspopup="menu"
aria-expanded={exportMenuOpen}
aria-controls={exportMenuId}
onClick={() => setExportMenuOpen((o) => !o)}
className={`justify-center gap-[var(--spacing-scale-002,2px)] !pl-[var(--spacing-scale-012,12px)] !pr-[var(--spacing-scale-006,6px)] md:!pr-[var(--spacing-scale-006,6px)] ${outlineButtonClass}`}
>
<span>{t("export")}</span>
<span>{exportLabel}</span>
<svg
width="12"
height="12"
@@ -166,11 +173,11 @@ export function CreateFlowTopNavView({
size="xsmall"
onClick={onDuplicate}
ariaLabel={
duplicateAriaLabel ?? duplicateLabel ?? t("editAriaLabel")
duplicateAriaLabel ?? duplicateLabel ?? editAriaLabel
}
className={outlineButtonClass}
>
{duplicateLabel ?? t("edit")}
{duplicateLabel ?? editLabel}
</Button>
)}
@@ -180,10 +187,10 @@ export function CreateFlowTopNavView({
palette={buttonPalette}
size="xsmall"
onClick={onEdit}
ariaLabel={t("editAriaLabel")}
ariaLabel={editAriaLabel}
className={outlineButtonClass}
>
{t("edit")}
{editLabel}
</Button>
)}
@@ -194,10 +201,10 @@ export function CreateFlowTopNavView({
size="xsmall"
type="button"
onClick={onManageStakeholders}
ariaLabel={t("manageStakeholdersAriaLabel")}
ariaLabel={manageStakeholdersAriaLabel}
className={outlineButtonClass}
>
{t("manageStakeholders")}
{manageStakeholdersLabel}
</Button>
) : null}
@@ -219,12 +226,12 @@ export function CreateFlowTopNavView({
<header
className={`bg-black w-full ${className}`}
role="banner"
aria-label={t("bannerAriaLabel")}
aria-label={bannerAriaLabel}
>
<nav
className="flex items-center justify-between mx-auto max-w-[639px] md:max-w-[1920px] px-[var(--spacing-measures-spacing-500,20px)] md:px-[48px] py-[var(--spacing-measures-spacing-300,12px)] md:py-[var(--spacing-measures-spacing-016,16px)]"
role="navigation"
aria-label={t("navAriaLabel")}
aria-label={navAriaLabel}
>
<Logo size="createFlow" wordmark palette={buttonPalette} />
+3 -3
View File
@@ -192,13 +192,13 @@ const Footer = memo(() => {
lg:gap-10
lg:text-sm lg:leading-5"
>
<Link href="#" className={legalLinkClass}>
<Link href={t("legal.privacyPolicyHref")} className={legalLinkClass}>
{t("legal.privacyPolicy")}
</Link>
<Link href="#" className={legalLinkClass}>
<Link href={t("legal.termsOfServiceHref")} className={legalLinkClass}>
{t("legal.termsOfService")}
</Link>
<Link href="#" className={legalLinkClass}>
<Link href={t("legal.cookiesSettingsHref")} className={legalLinkClass}>
{t("legal.cookiesSettings")}
</Link>
</div>
@@ -1,5 +1,8 @@
"use client";
/**
* Figma: "Sections / AskOrganizer" (see registry).
*/
import { memo, useCallback, useState } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import { useAnalytics } from "../../../hooks";
@@ -43,7 +46,6 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
subtitle,
description,
buttonText,
buttonHref,
className = "",
variant: variantProp = "centered",
onContactClick,
@@ -51,7 +53,7 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
const variant = variantProp;
const t = useTranslation();
const defaultButtonText = buttonText ?? t("askOrganizer.buttonText");
const analyticsHref = buttonHref ?? "modal";
const ctaAriaLabel = t("askOrganizer.ariaLabel");
const { trackEvent, trackCustomEvent } = useAnalytics();
const [inquiryOpen, setInquiryOpen] = useState(false);
@@ -74,11 +76,9 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
const labelledBy = title ? "ask-organizer-headline" : undefined;
const handleContactClick = (
event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>,
) => {
if (buttonHref) {
// Legacy link CTA: do not intercept navigation.
const handleContactClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
trackEvent({
event: "contact_button_click",
category: "engagement",
@@ -86,46 +86,30 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
component: "AskOrganizer",
variant: resolvedVariant,
});
trackCustomEvent(
"contact_button_click",
{
component: "AskOrganizer",
variant: resolvedVariant,
buttonText: defaultButtonText,
buttonHref: analyticsHref,
buttonHref: "modal",
},
onContactClick as
| ((_data: Record<string, unknown>) => void)
| undefined,
);
return event;
}
event.preventDefault();
trackEvent({
event: "contact_button_click",
category: "engagement",
label: "ask_organizer",
component: "AskOrganizer",
variant: resolvedVariant,
});
trackCustomEvent(
"contact_button_click",
{
component: "AskOrganizer",
variant: resolvedVariant,
buttonText: defaultButtonText,
buttonHref: analyticsHref,
},
onContactClick as
| ((_data: Record<string, unknown>) => void)
| undefined,
);
setInquiryOpen(true);
return event;
};
setInquiryOpen(true);
},
[
defaultButtonText,
onContactClick,
resolvedVariant,
trackCustomEvent,
trackEvent,
],
);
const closeInquiry = useCallback(() => {
setInquiryOpen(false);
@@ -138,7 +122,7 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
subtitle={subtitle}
description={description}
buttonText={defaultButtonText}
buttonHref={buttonHref}
ctaAriaLabel={ctaAriaLabel}
className={className}
sectionPadding={sectionPadding}
contentGap={`${contentGap} ${styles.container}`}
@@ -12,10 +12,6 @@ export interface AskOrganizerProps {
subtitle?: string;
description?: string;
buttonText?: string;
/**
* @deprecated Modal-only flow (CR-107). Omit; kept optional for Storybook overrides.
*/
buttonHref?: string;
className?: string;
/**
* Ask organizer variant.
@@ -26,7 +22,6 @@ export interface AskOrganizerProps {
component: string;
variant: string;
buttonText: string;
buttonHref?: string;
timestamp: string;
}) => void;
}
@@ -36,7 +31,7 @@ export interface AskOrganizerViewProps {
subtitle?: string;
description?: string;
buttonText: string;
buttonHref?: string;
ctaAriaLabel: string;
className: string;
sectionPadding: string;
contentGap: string;
@@ -44,6 +39,6 @@ export interface AskOrganizerViewProps {
variant: AskOrganizerVariant;
labelledBy?: string;
onContactClick: (
_event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>,
_event: React.MouseEvent<HTMLButtonElement>,
) => void;
}
@@ -1,6 +1,5 @@
"use client";
import { useTranslation } from "../../../contexts/MessagesContext";
import ContentLockup from "../../type/ContentLockup";
import Button from "../../buttons/Button";
import type { AskOrganizerViewProps } from "./AskOrganizer.types";
@@ -10,7 +9,7 @@ function AskOrganizerView({
subtitle,
description,
buttonText,
buttonHref,
ctaAriaLabel,
className,
sectionPadding,
contentGap,
@@ -19,8 +18,6 @@ function AskOrganizerView({
labelledBy,
onContactClick,
}: AskOrganizerViewProps) {
const t = useTranslation();
const ariaLabel = t("askOrganizer.ariaLabel");
const isUseCaseDetail = variant === "use-case-detail";
const lockupVariant =
variant === "inverse" || isUseCaseDetail ? "ask-inverse" : "ask";
@@ -33,14 +30,13 @@ function AskOrganizerView({
<section
className={`${sectionPadding} ${className}`}
aria-labelledby={labelledBy}
aria-label={labelledBy ? undefined : ariaLabel}
aria-label={labelledBy ? undefined : ctaAriaLabel}
tabIndex={-1}
data-figma-node={isUseCaseDetail ? "22015-42624" : "18116-15960"}
>
<div
className={`mx-auto flex w-full min-w-0 max-w-[1280px] flex-col md:min-w-[358px] ${contentGap} ${isUseCaseDetail ? "items-center" : ""}`}
>
{/* Content Lockup */}
<ContentLockup
title={title}
subtitle={subtitle}
@@ -50,18 +46,16 @@ function AskOrganizerView({
titleId={labelledBy}
/>
{/* Button */}
<div
className={`${buttonContainerClass} flex-wrap gap-y-[var(--spacing-scale-016)]`}
>
<Button
{...(buttonHref ? { href: buttonHref } : {})}
size="small"
buttonType="filled"
palette={buttonPalette}
className="!px-[var(--spacing-scale-010)] md:!px-[var(--spacing-scale-016)] md:!py-[var(--spacing-scale-012)] md:!text-[16px] md:!leading-[20px]"
onClick={onContactClick}
ariaLabel={ariaLabel}
ariaLabel={ctaAriaLabel}
data-testid="ask-organizer-cta"
>
{buttonText}
@@ -1,6 +1,7 @@
"use client";
import { memo } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import {
ASSETS,
getAssetPath,
@@ -23,6 +24,14 @@ const ContentBannerContainer = memo<ContentBannerProps>(
contentTone,
}) => {
const variant = variantProp;
const tUseCase = useTranslation("pages.useCasesCompletedRule");
const ruleCardLinkAriaLabel =
variant === "useCase" && rulePreview?.href
? tUseCase("ruleCardLinkAriaLabel").replace(
"{title}",
rulePreview.title,
)
: undefined;
const resolveHorizontalImage = (blogPost: BlogPost): string => {
if (blogPost.frontmatter?.thumbnail?.horizontal) {
@@ -71,6 +80,7 @@ const ContentBannerContainer = memo<ContentBannerProps>(
backgroundImageSection={backgroundImageSection}
rulePreview={rulePreview}
contentTone={contentTone}
ruleCardLinkAriaLabel={ruleCardLinkAriaLabel}
/>
);
},
@@ -41,4 +41,6 @@ export interface ContentBannerViewProps {
backgroundImageSection?: string;
rulePreview?: ContentBannerRulePreview;
contentTone?: ContentContainerToneValue;
/** `useCase` only: aria-label for linked rule preview. */
ruleCardLinkAriaLabel?: string;
}
@@ -3,7 +3,6 @@
import Image from "next/image";
import Link from "next/link";
import { memo } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import ContentContainer from "../../content/ContentContainer";
import Rule from "../../cards/Rule";
import {
@@ -155,6 +154,7 @@ function ContentBannerUseCaseView({
contentTone = "inverse",
leadingImageSrc,
leadingImageAlt,
ruleCardLinkAriaLabel,
}: Pick<
ContentBannerViewProps,
| "post"
@@ -162,8 +162,8 @@ function ContentBannerUseCaseView({
| "contentTone"
| "leadingImageSrc"
| "leadingImageAlt"
| "ruleCardLinkAriaLabel"
>) {
const t = useTranslation("pages.useCasesCompletedRule");
if (!rulePreview) {
return null;
}
@@ -198,10 +198,7 @@ function ContentBannerUseCaseView({
<Link
href={rulePreview.href}
className="block w-full rounded-[24px] outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-default-brand-primary)] focus-visible:ring-offset-2"
aria-label={t("ruleCardLinkAriaLabel").replace(
"{title}",
rulePreview.title,
)}
aria-label={ruleCardLinkAriaLabel ?? rulePreview.title}
>
<Rule
title={rulePreview.title}
@@ -6,6 +6,7 @@
import { memo, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useTranslation } from "../../../contexts/MessagesContext";
import { logger } from "../../../../lib/logger";
import { prepareFreshCreateFlowEntry } from "../../../(app)/create/utils/prepareFreshCreateFlowEntry";
import {
@@ -34,6 +35,8 @@ declare global {
const RuleStackContainer = memo<RuleStackProps>(
({ className = "", initialGridEntries, translationNamespace, twoColumnsFromMd }) => {
const router = useRouter();
const namespace = translationNamespace ?? "pages.home.ruleStack";
const t = useTranslation(namespace);
const [gridEntries, setGridEntries] = useState<TemplateGridCardEntry[] | null>(
() => initialGridEntries ?? null,
);
@@ -107,7 +110,9 @@ const RuleStackContainer = memo<RuleStackProps>(
className={className}
onTemplateClick={handleTemplateClick}
gridEntries={gridEntries}
translationNamespace={translationNamespace ?? "pages.home.ruleStack"}
sectionTitle={t("title")}
sectionSubtitle={t("subtitle")}
seeAllTemplatesLabel={t("button.seeAllTemplates")}
twoColumnsFromMd={twoColumnsFromMd}
/>
);
@@ -23,6 +23,8 @@ export interface RuleStackViewProps {
onTemplateClick: (_slug: string) => void;
/** `null` while loading curated templates from the API. */
gridEntries: TemplateGridCardEntry[] | null;
translationNamespace: string;
sectionTitle: string;
sectionSubtitle: string;
seeAllTemplatesLabel: string;
twoColumnsFromMd?: boolean;
}
@@ -1,6 +1,5 @@
"use client";
import { useTranslation } from "../../../contexts/MessagesContext";
import SectionHeader from "../../type/SectionHeader";
import Button from "../../buttons/Button";
import { GovernanceTemplateGrid } from "../GovernanceTemplateGrid";
@@ -12,12 +11,11 @@ export function RuleStackView({
className,
onTemplateClick,
gridEntries,
translationNamespace,
sectionTitle,
sectionSubtitle,
seeAllTemplatesLabel,
twoColumnsFromMd = false,
}: RuleStackViewProps) {
const t = useTranslation(translationNamespace);
const buttonText = t("button.seeAllTemplates");
return (
<section
data-figma-node="22085-860413"
@@ -35,8 +33,8 @@ export function RuleStackView({
`}
>
<SectionHeader
title={t("title")}
subtitle={t("subtitle")}
title={sectionTitle}
subtitle={sectionSubtitle}
variant="multi-line"
ruleStackDesktopTypeScale
twoColumnsFromMd={twoColumnsFromMd}
@@ -69,7 +67,7 @@ export function RuleStackView({
size="large"
href="/templates"
>
{buttonText}
{seeAllTemplatesLabel}
</Button>
</div>
</section>