Implement about page

This commit is contained in:
adilallo
2026-05-13 23:08:36 -06:00
parent d2dfa099a2
commit b6b9b63608
69 changed files with 1834 additions and 28 deletions
@@ -5,11 +5,13 @@ import { logger } from "../../../../lib/logger";
import QuoteBlockView from "./QuoteBlock.view";
import type { QuoteBlockProps, VariantConfig } from "./QuoteBlock.types";
/** Figma: portrait variants standard | compact | extended; statement = Section/Quote (22137:890679, copy scale 22135:889716 from md). */
const QuoteBlockContainer = memo<QuoteBlockProps>(
({
variant: variantProp = "standard",
className = "",
quote = "The rules of decision-making must be open and available to everyone, and this can happen only if they are formalized.",
quoteSecondary,
author = "Jo Freeman",
source = "The Tyranny of Structurelessness",
avatarSrc = "/assets/Quote_Avatar.svg",
@@ -69,12 +71,29 @@ const QuoteBlockContainer = memo<QuoteBlockProps>(
"text-[14px] leading-[120%] md:text-[20px] md:leading-[120%] md:tracking-[0.24px] lg:text-[28px] xl:text-[36px]",
showDecor: true,
},
statement: {
container:
"flex w-full flex-col items-center px-[var(--spacing-scale-032)] py-[var(--spacing-scale-048)] md:px-[var(--spacing-scale-096)] md:py-[var(--space-1200)]",
card: "",
gap: "",
avatarGap: "",
avatar: "",
quote: "",
author: "",
source: "",
showDecor: false,
statementLayout: true,
},
};
const config = variants[variant] || variants.standard;
// Use provided ID or generate a stable one based on content
const baseId = id || `quote-${author.toLowerCase().replace(/\s+/g, "-")}`;
const baseId =
id ||
(variant === "statement"
? "statement-quote"
: `quote-${author.toLowerCase().replace(/\s+/g, "-")}`);
const quoteId = `${baseId}-content`;
const authorId = `${baseId}-author`;
@@ -105,7 +124,22 @@ const QuoteBlockContainer = memo<QuoteBlockProps>(
};
// Validate required props
if (!quote || !author) {
if (variant === "statement") {
if (!quote?.trim() || !quoteSecondary?.trim()) {
logger.error(
"QuoteBlock: statement variant requires non-empty quote and quoteSecondary",
);
if (onError) {
onError({
type: "missing_props",
message:
"QuoteBlock statement variant requires quote and quoteSecondary",
quote: !!quote?.trim() && !!quoteSecondary?.trim(),
});
}
return null;
}
} else if (!quote || !author) {
logger.error("QuoteBlock: Missing required props (quote or author)");
if (onError) {
onError({
@@ -125,6 +159,7 @@ const QuoteBlockContainer = memo<QuoteBlockProps>(
<QuoteBlockView
className={className}
quote={quote}
quoteSecondary={quoteSecondary}
author={author}
source={source}
quoteId={quoteId}
@@ -1,12 +1,18 @@
export type QuoteBlockVariantValue = "compact" | "standard" | "extended";
export type QuoteBlockVariantValue =
| "compact"
| "standard"
| "extended"
| "statement";
export interface QuoteBlockProps {
/**
* Quote block variant.
*/
/** Default `standard` (home portrait quote). `statement` is About-only dual-paragraph layout; isolated branch in QuoteBlock.view. */
variant?: QuoteBlockVariantValue;
className?: string;
quote?: string;
/**
* Second paragraph for **`statement`** variant (Figma Section/Quote 22137:890679).
*/
quoteSecondary?: string;
author?: string;
source?: string;
avatarSrc?: string;
@@ -32,11 +38,16 @@ export interface VariantConfig {
author: string;
source: string;
showDecor: boolean;
/**
* When true, render Figma **Section/Quote** layout (yellow surface, dual paragraphs, no attribution).
*/
statementLayout?: boolean;
}
export interface QuoteBlockViewProps {
className: string;
quote: string;
quoteSecondary?: string;
author: string;
source?: string;
quoteId: string;
@@ -4,11 +4,13 @@ import { memo } from "react";
import Image from "next/image";
import { useTranslation } from "../../../contexts/MessagesContext";
import QuoteDecor from "./QuoteDecor";
import QuoteStatementDecor from "./QuoteStatementDecor";
import type { QuoteBlockViewProps } from "./QuoteBlock.types";
function QuoteBlockView({
className,
quote,
quoteSecondary,
author,
source,
quoteId,
@@ -23,6 +25,37 @@ function QuoteBlockView({
const t = useTranslation("quoteBlock");
const avatarAlt = t("avatarAlt").replace("{author}", author);
if (config.statementLayout) {
if (!quoteSecondary?.trim()) {
return null;
}
const statementTextClass =
"font-bricolage-grotesque text-[28px] font-bold leading-9 tracking-[var(--text-xx-large-heading--letter-spacing)] text-[var(--color-surface-default-tertiary)] md:text-[length:var(--text-xx-large-heading)] md:leading-[length:var(--text-xx-large-heading--line-height)]";
return (
<section
className={`${config.container} ${className}`.trim()}
aria-labelledby={quoteId}
role="region"
>
<div
className="relative box-border flex w-full max-w-[1440px] shrink-0 flex-col items-center justify-center gap-[var(--space-800)] overflow-hidden rounded-[var(--spacing-scale-020)] bg-[var(--color-surface-invert-brand-primary,#fefcc9)] px-[var(--spacing-scale-032)] py-[var(--spacing-scale-048)] md:px-[var(--space-1800)] md:py-[var(--space-2400)]"
>
<QuoteStatementDecor />
<div className="relative z-10 flex w-full min-w-0 shrink-0 flex-col items-center gap-9 text-center md:gap-[length:var(--text-xx-large-heading--line-height)]">
<p id={quoteId} className={`${statementTextClass} mb-0 w-full min-w-0`}>
{quote}
</p>
<p className={`${statementTextClass} mb-0 w-full min-w-0`}>
{quoteSecondary}
</p>
</div>
</div>
</section>
);
}
return (
<section
className={`${config.container} ${className}`}
@@ -0,0 +1,41 @@
"use client";
import { memo } from "react";
import { getAssetPath, quoteStatementShapePath } from "../../../../lib/assetUtils";
/** Figma: Section / Quote — Shapes (22137:890679). Radial asset + horizontal gradient mask (side lobes only); grain matches QuoteBlock/HeroDecor. Background `cover` so wide banners still fill lateral mask stripes (square sized by panel height misses them when centered). */
const EDGE_MASK =
"linear-gradient(to right, #fff 0%, #fff 14%, rgba(255,255,255,0) 30%, rgba(255,255,255,0) 70%, #fff 86%, #fff 100%)";
const GRAIN_MULTIPLY_FILTER =
'url(\'data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg"><defs><filter id="grain" filterUnits="objectBoundingBox" x="0" y="0" width="1" height="1" colorInterpolationFilters="sRGB"><feTurbulence type="fractalNoise" baseFrequency="0.4" numOctaves="3" seed="7" stitchTiles="stitch" result="noise"/><feColorMatrix in="noise" result="softNoise" type="matrix" values="0.8 0 0 0 0.3 0 0.6 0 0 0.2 0 0 1.0 0 0.4 0 0 0 0.25 0"/><feComposite in="softNoise" in2="SourceAlpha" operator="in" result="maskedNoise"/><feBlend in="SourceGraphic" in2="maskedNoise" mode="multiply"/></filter></defs></svg>#grain\')';
const QuoteStatementDecor = memo<{ className?: string }>(({ className = "" }) => {
const src = getAssetPath(quoteStatementShapePath());
const bg = `url("${src}")`;
return (
<div
className={`pointer-events-none absolute inset-0 z-0 overflow-hidden opacity-[0.55] select-none ${className}`.trim()}
aria-hidden
style={{
backgroundImage: bg,
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
backgroundPosition: "center center",
WebkitMaskImage: EDGE_MASK,
maskImage: EDGE_MASK,
WebkitMaskSize: "100% 100%",
maskSize: "100% 100%",
WebkitMaskRepeat: "no-repeat",
maskRepeat: "no-repeat",
filter: GRAIN_MULTIPLY_FILTER,
WebkitFilter: GRAIN_MULTIPLY_FILTER,
}}
/>
);
});
QuoteStatementDecor.displayName = "QuoteStatementDecor";
export default QuoteStatementDecor;
+4 -1
View File
@@ -1,2 +1,5 @@
export { default } from "./QuoteBlock.container";
export type { QuoteBlockProps } from "./QuoteBlock.types";
export type {
QuoteBlockProps,
QuoteBlockVariantValue,
} from "./QuoteBlock.types";