From 943c50c9b7b21e23aab7289907386b6417a2592e Mon Sep 17 00:00:00 2001 From: miquelmatoses Date: Tue, 23 Jun 2026 00:25:46 +0200 Subject: [PATCH] refactor(ui): extract MethodologyNote, DisplayHeading, gradient const; rename local RoleCard Pixel-identical DRY extractions (audit items #4, #5, #6, #9): - #4 MethodologyNote: the report disclaimer footnote (bg-gray-100 rounded px-5 py-4 text-xs leading-relaxed) was duplicated verbatim in four report pages. Extracted to components/report/MethodologyNote.jsx; call sites render class-identical output. - #5 DisplayHeading: the inline style={{ fontFamily: 'var(--mm-font-display)' }} recurred across blog/faq headings. Wrapped in components/ui/DisplayHeading (passthrough: same tag via `as`, same className). Covers all 7 occurrences. Note: --mm-font-heading (used only in admin) is defined nowhere in mm-design or this repo, so it was NOT merged with --mm-font-display. - #6 gradient: the brand-blue gradient with the tokenless #00297a dark stop is now a single constant in src/design/gradients.js, consumed by both sites. The #1a3a6b notice-text color is a different element family and was left. - #9 rename: the local RoleCard in RolesPage is now RoleSummaryCard to stop colliding with the shared components/report/RoleCard. Pure rename. Co-Authored-By: Claude Opus 4.8 --- src/components/BlogTestCTA.jsx | 8 +++---- src/components/report/MethodologyNote.jsx | 19 +++++++++++++++++ src/components/report/index.js | 1 + src/components/ui/DisplayHeading.jsx | 17 +++++++++++++++ src/components/ui/index.js | 1 + src/design/gradients.js | 13 ++++++++++++ src/pages/BlogIndexPage.jsx | 21 +++++++----------- src/pages/FaqPage.jsx | 9 +++----- src/pages/FirstQuarterResultsPage.jsx | 6 ++---- src/pages/FullMoonResultsPage.jsx | 6 ++---- src/pages/LastQuarterPage.jsx | 6 ++---- src/pages/NewMoonResultsPage.jsx | 6 ++---- src/pages/RolesPage.jsx | 4 ++-- src/pages/blog/BlogArticlePage.jsx | 26 +++++++++-------------- 14 files changed, 86 insertions(+), 57 deletions(-) create mode 100644 src/components/report/MethodologyNote.jsx create mode 100644 src/components/ui/DisplayHeading.jsx create mode 100644 src/design/gradients.js diff --git a/src/components/BlogTestCTA.jsx b/src/components/BlogTestCTA.jsx index c85b7b077..4819c53eb 100644 --- a/src/components/BlogTestCTA.jsx +++ b/src/components/BlogTestCTA.jsx @@ -10,7 +10,7 @@ * Copy uses Cercol product vocabulary only (never academic instrument names). */ import { Link } from 'react-router-dom' -import { Card, SectionLabel } from './ui' +import { Card, SectionLabel, DisplayHeading } from './ui' import { trackEvent } from '../lib/api' // Localized copy. es/fr/de/da are flagged for human review in the PR. @@ -59,12 +59,12 @@ export default function BlogTestCTA({ slug, lang = 'en', category, compact = fal return ( {!compact && Cèrcol} -

{heading} -

+ {!compact &&

{c.p}

} + {children} + + ) +} diff --git a/src/components/report/index.js b/src/components/report/index.js index c9d348b76..0116cdca9 100644 --- a/src/components/report/index.js +++ b/src/components/report/index.js @@ -5,3 +5,4 @@ export { default as RoleCard } from './RoleCard' export { default as RadarDataCard } from './RadarDataCard' export { RoleComparisonView } from './RoleComparisonView' export { SurprisesPanel } from './SurprisesPanel' +export { default as MethodologyNote } from './MethodologyNote' diff --git a/src/components/ui/DisplayHeading.jsx b/src/components/ui/DisplayHeading.jsx new file mode 100644 index 000000000..1c1f067fa --- /dev/null +++ b/src/components/ui/DisplayHeading.jsx @@ -0,0 +1,17 @@ +/** + * DisplayHeading — a heading rendered in the mm-design display family + * (Playfair Display via --mm-font-display). + * + * Centralizes the repeated `style={{ fontFamily: 'var(--mm-font-display)' }}` + * so call sites no longer carry the inline font style. Output is identical to + * the previous markup: the same tag, the same className, the same font family. + * + * Pass the element via `as` (defaults to h2) and styling via className. + */ +export default function DisplayHeading({ as: Tag = 'h2', className = '', children, ...rest }) { + return ( + + {children} + + ) +} diff --git a/src/components/ui/index.js b/src/components/ui/index.js index f139e539e..4745510a6 100644 --- a/src/components/ui/index.js +++ b/src/components/ui/index.js @@ -2,3 +2,4 @@ export { default as Button } from './Button' export { default as Card } from './Card' export { default as Badge } from './Badge' export { default as SectionLabel } from './SectionLabel' +export { default as DisplayHeading } from './DisplayHeading' diff --git a/src/design/gradients.js b/src/design/gradients.js new file mode 100644 index 000000000..c0acdd2dd --- /dev/null +++ b/src/design/gradients.js @@ -0,0 +1,13 @@ +/** + * Shared gradient constants. + * + * mm-design has no token for the dark-blue gradient stop (#00297a), so the + * brand-blue gradient lives here in ONE place rather than being copy-pasted + * across pages. The light stop uses the mm-design blue token; only the dark + * stop is a local literal. + * + * If mm-design later adds a matching dark-blue token, replace the literal + * below and this stays the single source of truth. + */ +export const BRAND_BLUE_GRADIENT = + 'linear-gradient(135deg, var(--mm-color-blue) 0%, #00297a 100%)' diff --git a/src/pages/BlogIndexPage.jsx b/src/pages/BlogIndexPage.jsx index 8c56482de..a80d597e3 100644 --- a/src/pages/BlogIndexPage.jsx +++ b/src/pages/BlogIndexPage.jsx @@ -9,7 +9,8 @@ import { Link, useLocation } from 'react-router-dom' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { SectionLabel } from '../components/ui' +import { SectionLabel, DisplayHeading } from '../components/ui' +import { BRAND_BLUE_GRADIENT } from '../design/gradients' import { getBlogPosts } from '../lib/api' import { normalizeUnsplashUrl } from '../utils/unsplash' @@ -177,12 +178,9 @@ export default function BlogIndexPage() { {t('blog.label')} -

+ {t('blog.heading')} -

+

{t('blog.subtitle')}

@@ -307,22 +305,19 @@ export default function BlogIndexPage() { ) : (
)}
{/* Card body */}
-

{localise(post.title)} -

+ {desc && (

{desc} diff --git a/src/pages/FaqPage.jsx b/src/pages/FaqPage.jsx index 2303543d0..80b6df68d 100644 --- a/src/pages/FaqPage.jsx +++ b/src/pages/FaqPage.jsx @@ -5,7 +5,7 @@ * Uses HTML details/summary for accessible accordion without JS state. */ import { useTranslation } from 'react-i18next' -import { SectionLabel } from '../components/ui' +import { SectionLabel, DisplayHeading } from '../components/ui' import { usePageMeta } from '../hooks/usePageMeta' const SECTIONS = [ @@ -71,12 +71,9 @@ export default function FaqPage() { {t('faq.label')} -

+ {t('faq.heading')} -

+ {SECTIONS.map(({ labelKey, keys }) => ( diff --git a/src/pages/FirstQuarterResultsPage.jsx b/src/pages/FirstQuarterResultsPage.jsx index 825c09a58..2ea450c5f 100644 --- a/src/pages/FirstQuarterResultsPage.jsx +++ b/src/pages/FirstQuarterResultsPage.jsx @@ -24,7 +24,7 @@ import { useAuth } from '../context/AuthContext' import { colors } from '../design/tokens' import RoleProbabilityBars from '../components/RoleProbabilityBars' import { Card, Button, Badge, SectionLabel } from '../components/ui' -import { DimensionRow, FacetAccordion, ReportPageHeader, RoleCard, RadarDataCard } from '../components/report' +import { DimensionRow, FacetAccordion, ReportPageHeader, RoleCard, RadarDataCard, MethodologyNote } from '../components/report' import InstrumentNudge from '../components/InstrumentNudge' @@ -173,9 +173,7 @@ export default function FirstQuarterResultsPage() {
{/* Disclaimer */} -
- {t('fqResults.disclaimer')} -
+ {t('fqResults.disclaimer')} diff --git a/src/pages/FullMoonResultsPage.jsx b/src/pages/FullMoonResultsPage.jsx index f204ea329..ebc77960a 100644 --- a/src/pages/FullMoonResultsPage.jsx +++ b/src/pages/FullMoonResultsPage.jsx @@ -31,7 +31,7 @@ import { useAuth } from '../context/AuthContext' import { colors } from '../design/tokens' import RoleProbabilityBars from '../components/RoleProbabilityBars' import { Card, Button, Badge, SectionLabel } from '../components/ui' -import { DimensionRow, FacetAccordion, ReportPageHeader, RoleCard, RadarDataCard, RoleComparisonView, SurprisesPanel } from '../components/report' +import { DimensionRow, FacetAccordion, ReportPageHeader, RoleCard, RadarDataCard, RoleComparisonView, SurprisesPanel, MethodologyNote } from '../components/report' const MIN_WITNESSES_FOR_REPORT = 2 @@ -384,9 +384,7 @@ export default function FullMoonResultsPage() { {/* Disclaimer */} -
- {t('fmResults.disclaimer')} -
+ {t('fmResults.disclaimer')} diff --git a/src/pages/LastQuarterPage.jsx b/src/pages/LastQuarterPage.jsx index 8f5f2649e..f4a55430d 100644 --- a/src/pages/LastQuarterPage.jsx +++ b/src/pages/LastQuarterPage.jsx @@ -26,7 +26,7 @@ import { zscoresToRaw, } from '../utils/team-narrative' import { RoleIcon, LastQuarterIcon, DimensionIcon, ChevronRightIcon } from '../components/MoonIcons' -import { DimensionRow, ReportPageHeader, RadarDataCard } from '../components/report' +import { DimensionRow, ReportPageHeader, RadarDataCard, MethodologyNote } from '../components/report' import RadarChart from '../components/RadarChart' import { Card, SectionLabel, Button } from '../components/ui' import { colors, ROLE_COLORS, DOMAIN_ICON_CLASSES, BALANCE_COLORS } from '../design/tokens' @@ -516,9 +516,7 @@ export default function LastQuarterPage() { {/* Disclaimer */} -
- {t('fmResults.disclaimer')} -
+ {t('fmResults.disclaimer')} diff --git a/src/pages/NewMoonResultsPage.jsx b/src/pages/NewMoonResultsPage.jsx index 992436795..79d8876d4 100644 --- a/src/pages/NewMoonResultsPage.jsx +++ b/src/pages/NewMoonResultsPage.jsx @@ -17,7 +17,7 @@ import { useAuth } from '../context/AuthContext' import { colors } from '../design/tokens' import { Card, Button, SectionLabel } from '../components/ui' import { NewMoonIcon } from '../components/MoonIcons' -import { DimensionRow, ReportPageHeader, RadarDataCard } from '../components/report' +import { DimensionRow, ReportPageHeader, RadarDataCard, MethodologyNote } from '../components/report' import InstrumentNudge from '../components/InstrumentNudge' @@ -128,9 +128,7 @@ export default function NewMoonResultsPage() { {/* Disclaimer */} -
- {t('newMoonResults.disclaimer')} -
+ {t('newMoonResults.disclaimer')} diff --git a/src/pages/RolesPage.jsx b/src/pages/RolesPage.jsx index a5eedbb28..eb13e7bb8 100644 --- a/src/pages/RolesPage.jsx +++ b/src/pages/RolesPage.jsx @@ -23,7 +23,7 @@ const ROLES = [ { key: 'R12', accent: 'text-gray-600', bg: 'bg-gray-50' }, // Badger B- V- ] -function RoleCard({ roleKey, accent, bg, t }) { +function RoleSummaryCard({ roleKey, accent, bg, t }) { const name = t(`roles.${roleKey}.name`) const ca = t(`roles.${roleKey}.ca`) const essence = t(`roles.${roleKey}.essence`) @@ -121,7 +121,7 @@ export default function RolesPage() {
{ROLES.map(({ key, accent, bg }) => ( - + ))}
diff --git a/src/pages/blog/BlogArticlePage.jsx b/src/pages/blog/BlogArticlePage.jsx index 01c64575f..0399a2854 100644 --- a/src/pages/blog/BlogArticlePage.jsx +++ b/src/pages/blog/BlogArticlePage.jsx @@ -11,6 +11,8 @@ import { marked } from 'marked' import { getBlogPost, getBlogPosts, trackBlogView } from '../../lib/api' import { normalizeUnsplashUrl } from '../../utils/unsplash' import BlogTestCTA from '../../components/BlogTestCTA' +import { DisplayHeading } from '../../components/ui' +import { BRAND_BLUE_GRADIENT } from '../../design/gradients' // Configure marked with custom renderers once at module load time marked.use({ @@ -533,21 +535,16 @@ export default function BlogArticlePage() { ) : (
)}
{/* Article header */}
-

+ {title} -

+ {description && (

{description}

)} @@ -688,12 +685,9 @@ export default function BlogArticlePage() { {/* Related articles */} {relatedPosts.length > 0 && (
-

+ {RELATED_LABEL[urlLang] || RELATED_LABEL.en} -

+
{relatedPosts.map(p => { const relatedHref = urlLang === 'en' ? `/blog/${p.slug}` : `/${urlLang}/blog/${p.slug}` @@ -703,12 +697,12 @@ export default function BlogArticlePage() { to={relatedHref} className="group block rounded-xl border p-4 hover:shadow-md transition-shadow bg-white" > -

{localise(p.title, urlLang)} -

+ {localise(p.description, urlLang) && (

{localise(p.description, urlLang)}