Implement about page
This commit is contained in:
@@ -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;
|
||||
@@ -1,2 +1,5 @@
|
||||
export { default } from "./QuoteBlock.container";
|
||||
export type { QuoteBlockProps } from "./QuoteBlock.types";
|
||||
export type {
|
||||
QuoteBlockProps,
|
||||
QuoteBlockVariantValue,
|
||||
} from "./QuoteBlock.types";
|
||||
|
||||
Reference in New Issue
Block a user