From 2ed878af815f329cb8d11f4cfcb96e559a807e83 Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:50:33 -0600 Subject: [PATCH] Add memo optimization --- app/components/AskOrganizer.js | 194 ++++----- app/components/Avatar.js | 34 +- app/components/AvatarContainer.js | 41 +- app/components/Button.js | 188 ++++----- app/components/ContentBanner.js | 9 +- app/components/ContentContainer.js | 206 +++++----- app/components/ContentLockup.js | 325 +++++++-------- app/components/ContentThumbnailTemplate.js | 120 +++--- app/components/ErrorBoundary.js | 49 +++ app/components/FeatureGrid.js | 89 +++-- app/components/Footer.js | 9 +- app/components/Header.js | 9 +- app/components/HeaderTab.js | 74 ++-- app/components/HeroBanner.js | 71 ++-- app/components/HeroDecor.js | 8 +- app/components/HomeHeader.js | 23 +- app/components/ImagePlaceholder.js | 56 +-- app/components/Logo.js | 49 ++- app/components/LogoWall.js | 20 +- app/components/MenuBar.js | 59 +-- app/components/MenuBarItem.js | 292 +++++++------- app/components/MiniCard.js | 204 +++++----- app/components/NavigationItem.js | 10 +- app/components/NumberedCard.js | 7 +- app/components/NumberedCards.js | 36 +- app/components/QuoteBlock.js | 443 +++++++++++---------- app/components/QuoteDecor.js | 8 +- app/components/RelatedArticles.js | 271 +++++++------ app/components/RuleCard.js | 132 +++--- app/components/RuleStack.js | 8 +- app/components/SectionHeader.js | 84 ++-- app/components/SectionNumber.js | 16 +- app/components/Separator.js | 10 +- app/layout.js | 6 + next.config.mjs | 59 ++- package-lock.json | 112 +++++- package.json | 1 + 37 files changed, 1852 insertions(+), 1480 deletions(-) create mode 100644 app/components/ErrorBoundary.js diff --git a/app/components/AskOrganizer.js b/app/components/AskOrganizer.js index 0063608..e24f611 100644 --- a/app/components/AskOrganizer.js +++ b/app/components/AskOrganizer.js @@ -1,110 +1,114 @@ "use client"; -import React from "react"; +import React, { memo } from "react"; import ContentLockup from "./ContentLockup"; import Button from "./Button"; -const AskOrganizer = ({ - title, - subtitle, - description, - buttonText = "Ask an organizer", - buttonHref = "#", - className = "", - variant = "centered", // centered, left-aligned, compact - onContactClick, // Analytics callback -}) => { - // Analytics tracking for contact button clicks - const handleContactClick = (event) => { - // Track contact button interaction - if (onContactClick) { - onContactClick({ - event: "contact_button_click", - component: "AskOrganizer", - variant, - buttonText, - buttonHref, - timestamp: new Date().toISOString(), - }); - } +const AskOrganizer = memo( + ({ + title, + subtitle, + description, + buttonText = "Ask an organizer", + buttonHref = "#", + className = "", + variant = "centered", // centered, left-aligned, compact + onContactClick, // Analytics callback + }) => { + // Analytics tracking for contact button clicks + const handleContactClick = (event) => { + // Track contact button interaction + if (onContactClick) { + onContactClick({ + event: "contact_button_click", + component: "AskOrganizer", + variant, + buttonText, + buttonHref, + timestamp: new Date().toISOString(), + }); + } - // Additional analytics tracking (can be expanded) - if (typeof window !== "undefined" && window.gtag) { - window.gtag("event", "contact_button_click", { - event_category: "engagement", - event_label: "ask_organizer", - value: 1, - }); - } - }; + // Additional analytics tracking (can be expanded) + if (typeof window !== "undefined" && window.gtag) { + window.gtag("event", "contact_button_click", { + event_category: "engagement", + event_label: "ask_organizer", + value: 1, + }); + } + }; - // Variant-specific styling - const variantStyles = { - centered: { - container: "text-center", - buttonContainer: "flex justify-center", - }, - "left-aligned": { - container: "text-left", - buttonContainer: "flex justify-start", - }, - compact: { - container: "text-center", - buttonContainer: "flex justify-center", - }, - inverse: { - container: "text-center", - buttonContainer: "flex justify-center", - }, - }; + // Variant-specific styling + const variantStyles = { + centered: { + container: "text-center", + buttonContainer: "flex justify-center", + }, + "left-aligned": { + container: "text-left", + buttonContainer: "flex justify-start", + }, + compact: { + container: "text-center", + buttonContainer: "flex justify-center", + }, + inverse: { + container: "text-center", + buttonContainer: "flex justify-center", + }, + }; - const styles = variantStyles[variant] || variantStyles.centered; + const styles = variantStyles[variant] || variantStyles.centered; - // Section padding based on variant - const sectionPadding = - variant === "compact" - ? "py-[var(--spacing-scale-016)] px-[var(--spacing-scale-016)] md:py-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-032)]" - : "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-032)] md:py-[var(--spacing-scale-096)] md:px-[var(--spacing-scale-064)]"; + // Section padding based on variant + const sectionPadding = + variant === "compact" + ? "py-[var(--spacing-scale-016)] px-[var(--spacing-scale-016)] md:py-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-032)]" + : "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-032)] md:py-[var(--spacing-scale-096)] md:px-[var(--spacing-scale-064)]"; - // Gap between content and button based on variant - const contentGap = - variant === "compact" - ? "gap-[var(--spacing-scale-020)]" - : "gap-[var(--spacing-scale-040)]"; + // Gap between content and button based on variant + const contentGap = + variant === "compact" + ? "gap-[var(--spacing-scale-020)]" + : "gap-[var(--spacing-scale-040)]"; - return ( -
-
- {/* Content Lockup */} - + return ( +
+
+ {/* Content Lockup */} + - {/* Button */} -
- + {/* Button */} +
+ +
-
-
- ); -}; +
+ ); + } +); + +AskOrganizer.displayName = "AskOrganizer"; export default AskOrganizer; diff --git a/app/components/Avatar.js b/app/components/Avatar.js index fbb366a..357065f 100644 --- a/app/components/Avatar.js +++ b/app/components/Avatar.js @@ -1,18 +1,20 @@ -export default function Avatar({ - src, - alt, - size = "small", - className = "", - ...props -}) { - const sizeStyles = { - small: "w-[var(--spacing-scale-016)] h-[var(--spacing-scale-016)]", - medium: "w-[18px] h-[18px]", - large: "w-[var(--spacing-scale-024)] h-[var(--spacing-scale-024)]", - xlarge: "w-[var(--spacing-scale-032)] h-[var(--spacing-scale-032)]", - }; +import React, { memo } from "react"; - const baseStyles = `rounded-[var(--radius-measures-radius-full)] object-cover ${sizeStyles[size]} ${className}`; +const Avatar = memo( + ({ src, alt, size = "small", className = "", ...props }) => { + const sizeStyles = { + small: "w-[var(--spacing-scale-016)] h-[var(--spacing-scale-016)]", + medium: "w-[18px] h-[18px]", + large: "w-[var(--spacing-scale-024)] h-[var(--spacing-scale-024)]", + xlarge: "w-[var(--spacing-scale-032)] h-[var(--spacing-scale-032)]", + }; - return {alt}; -} + const baseStyles = `rounded-[var(--radius-measures-radius-full)] object-cover ${sizeStyles[size]} ${className}`; + + return {alt}; + } +); + +Avatar.displayName = "Avatar"; + +export default Avatar; diff --git a/app/components/AvatarContainer.js b/app/components/AvatarContainer.js index 128775d..1fa638b 100644 --- a/app/components/AvatarContainer.js +++ b/app/components/AvatarContainer.js @@ -1,21 +1,24 @@ -export default function AvatarContainer({ - children, - size = "small", - className = "", - ...props -}) { - const sizeStyles = { - small: "flex -space-x-[var(--spacing-scale-008)]", - medium: "flex -space-x-[9px]", - large: "flex -space-x-[var(--spacing-scale-010)]", - xlarge: "flex -space-x-[13px]", - }; +import React, { memo } from "react"; - const baseStyles = `items-center ${sizeStyles[size]} ${className}`; +const AvatarContainer = memo( + ({ children, size = "small", className = "", ...props }) => { + const sizeStyles = { + small: "flex -space-x-[var(--spacing-scale-008)]", + medium: "flex -space-x-[9px]", + large: "flex -space-x-[var(--spacing-scale-010)]", + xlarge: "flex -space-x-[13px]", + }; - return ( -
- {children} -
- ); -} + const baseStyles = `items-center ${sizeStyles[size]} ${className}`; + + return ( +
+ {children} +
+ ); + } +); + +AvatarContainer.displayName = "AvatarContainer"; + +export default AvatarContainer; diff --git a/app/components/Button.js b/app/components/Button.js index 2324ee2..c029042 100644 --- a/app/components/Button.js +++ b/app/components/Button.js @@ -1,108 +1,116 @@ -export default function Button({ - children, - variant = "default", - size = "xsmall", - className = "", - disabled = false, - type = "button", - onClick, - href, - target, - rel, - ariaLabel, - ...props -}) { - const sizeStyles = { - xsmall: - "px-[var(--spacing-scale-006)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-001)]", - small: - "px-[var(--spacing-measures-spacing-008)] py-[var(--spacing-measures-spacing-008)] gap-[var(--spacing-scale-004)]", - medium: "p-[var(--spacing-scale-010)] gap-[var(--spacing-scale-004)]", - large: - "px-[var(--spacing-scale-012)] py-[var(--spacing-scale-010)] gap-[var(--spacing-scale-004)]", - xlarge: - "px-[var(--spacing-scale-020)] py-[var(--spacing-scale-012)] gap-[var(--spacing-scale-008)]", - }; +import React, { memo } from "react"; - const fontStyles = { - xsmall: "font-inter text-[10px] leading-[12px] font-medium tracking-[0%]", - small: "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]", - medium: "font-inter text-[14px] leading-[16px] font-medium tracking-[0%]", - large: "font-inter text-[16px] leading-[20px] font-medium tracking-[0%]", - xlarge: "font-inter text-[24px] leading-[28px] font-normal tracking-[0%]", - }; +const Button = memo( + ({ + children, + variant = "default", + size = "xsmall", + className = "", + disabled = false, + type = "button", + onClick, + href, + target, + rel, + ariaLabel, + ...props + }) => { + const sizeStyles = { + xsmall: + "px-[var(--spacing-scale-006)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-001)]", + small: + "px-[var(--spacing-measures-spacing-008)] py-[var(--spacing-measures-spacing-008)] gap-[var(--spacing-scale-004)]", + medium: "p-[var(--spacing-scale-010)] gap-[var(--spacing-scale-004)]", + large: + "px-[var(--spacing-scale-012)] py-[var(--spacing-scale-010)] gap-[var(--spacing-scale-004)]", + xlarge: + "px-[var(--spacing-scale-020)] py-[var(--spacing-scale-012)] gap-[var(--spacing-scale-008)]", + }; - const variantStyles = { - default: - "bg-[var(--color-surface-inverse-primary)] text-[var(--color-content-inverse-primary)] hover:bg-[var(--color-surface-inverse-primary)] hover:text-[var(--color-content-inverse-brand-primary)] hover:outline-[var(--border-color-default-brandprimary)] hover:outline-inset hover:scale-[1.02] hover:shadow-lg focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-content-default-brand-primary)] focus:ring-offset-1 focus:scale-[1.02] active:bg-[var(--color-surface-inverse-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:outline-[var(--border-color-default-brandprimary)] active:outline-offset-1 active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100 disabled:hover:shadow-none disabled:hover:outline-none", - secondary: - "bg-transparent text-[var(--color-content-default-brand-primary)] hover:text-[var(--color-content-default-primary)] hover:scale-[1.02] hover:bg-transparent hover:outline-none focus:outline-1 focus:outline-inset focus:outline-[var(--border-color-default-tertiary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:blur-[0px] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", - primary: - "bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] hover:bg-[var(--color-surface-default-primary)] hover:text-[var(--color-content-default-brand-primary)] hover:scale-[1.02] focus:bg-[var(--color-surface-default-primary)] focus:text-[var(--color-content-default-brand-primary)] focus:outline-none focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:blur-[0px] focus:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-primary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", - outlined: - "bg-transparent text-[var(--color-content-default-primary)] border-[1.5px] border-[var(--color-content-default-primary)] hover:bg-transparent hover:text-[var(--color-content-default-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-content-default-brand-primary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-default-primary)] focus:outline-none focus:border-[1.5px] focus:border-[var(--color-content-default-primary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:blur-[0px] focus:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:border-transparent active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:border-[1.5px] disabled:border-[var(--color-surface-default-secondary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", - dark: "bg-transparent text-[var(--color-content-inverse-primary)] border border-[var(--border-color-default-primary)] hover:bg-transparent hover:text-[var(--color-content-inverse-brand-primary)] hover:border hover:border-[var(--border-color-inverse-brandprimary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-inverse-primary)] focus:outline-none focus:border focus:border-[var(--border-color-default-primary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:blur-[0px] focus:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:border-transparent active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-primary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", - inverse: - "bg-transparent text-[var(--color-content-inverse-primary)] hover:text-[var(--color-content-inverse-brand-primary)] hover:scale-[1.02] hover:bg-transparent hover:outline-none focus:outline-1 focus:outline-inset focus:outline-[var(--border-color-default-tertiary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-tertiary)] focus:blur-[0px] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-primary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", - }; + const fontStyles = { + xsmall: "font-inter text-[10px] leading-[12px] font-medium tracking-[0%]", + small: "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]", + medium: "font-inter text-[14px] leading-[16px] font-medium tracking-[0%]", + large: "font-inter text-[16px] leading-[20px] font-medium tracking-[0%]", + xlarge: "font-inter text-[24px] leading-[28px] font-normal tracking-[0%]", + }; - const hoverOutlineStyles = { - xsmall: "hover:outline-1", - small: "hover:outline-1", - medium: "hover:outline-1", - large: "hover:outline-2", - xlarge: "hover:outline-[2.5px]", - }; + const variantStyles = { + default: + "bg-[var(--color-surface-inverse-primary)] text-[var(--color-content-inverse-primary)] hover:bg-[var(--color-surface-inverse-primary)] hover:text-[var(--color-content-inverse-brand-primary)] hover:outline-[var(--border-color-default-brandprimary)] hover:outline-inset hover:scale-[1.02] hover:shadow-lg focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-content-default-brand-primary)] focus:ring-offset-1 focus:scale-[1.02] active:bg-[var(--color-surface-inverse-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:outline-[var(--border-color-default-brandprimary)] active:outline-offset-1 active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100 disabled:hover:shadow-none disabled:hover:outline-none", + secondary: + "bg-transparent text-[var(--color-content-default-brand-primary)] hover:text-[var(--color-content-default-primary)] hover:scale-[1.02] hover:bg-transparent hover:outline-none focus:outline-1 focus:outline-inset focus:outline-[var(--border-color-default-tertiary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:blur-[0px] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", + primary: + "bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] hover:bg-[var(--color-surface-default-primary)] hover:text-[var(--color-content-default-brand-primary)] hover:scale-[1.02] focus:bg-[var(--color-surface-default-primary)] focus:text-[var(--color-content-default-brand-primary)] focus:outline-none focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:blur-[0px] focus:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-primary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", + outlined: + "bg-transparent text-[var(--color-content-default-primary)] border-[1.5px] border-[var(--color-content-default-primary)] hover:bg-transparent hover:text-[var(--color-content-default-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-content-default-brand-primary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-default-primary)] focus:outline-none focus:border-[1.5px] focus:border-[var(--color-content-default-primary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:blur-[0px] focus:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:border-transparent active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:border-[1.5px] disabled:border-[var(--color-surface-default-secondary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", + dark: "bg-transparent text-[var(--color-content-inverse-primary)] border border-[var(--border-color-default-primary)] hover:bg-transparent hover:text-[var(--color-content-inverse-brand-primary)] hover:border hover:border-[var(--border-color-inverse-brandprimary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-inverse-primary)] focus:outline-none focus:border focus:border-[var(--border-color-default-primary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:blur-[0px] focus:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:border-transparent active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-primary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", + inverse: + "bg-transparent text-[var(--color-content-inverse-primary)] hover:text-[var(--color-content-inverse-brand-primary)] hover:scale-[1.02] hover:bg-transparent hover:outline-none focus:outline-1 focus:outline-inset focus:outline-[var(--border-color-default-tertiary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-tertiary)] focus:blur-[0px] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-primary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", + }; - // Only apply outline styles to default and secondary variants, not primary, outlined, dark, or inverse - const outlineStyles = - variant === "primary" || - variant === "outlined" || - variant === "dark" || - variant === "inverse" - ? "" - : hoverOutlineStyles[size]; + const hoverOutlineStyles = { + xsmall: "hover:outline-1", + small: "hover:outline-1", + medium: "hover:outline-1", + large: "hover:outline-2", + xlarge: "hover:outline-[2.5px]", + }; - const baseStyles = `inline-flex items-center justify-start box-border ${sizeStyles[size]} rounded-[var(--radius-measures-radius-full)] ${fontStyles[size]} transition-all duration-500 ease-in-out cursor-pointer ${variantStyles[variant]} ${outlineStyles}`; + // Only apply outline styles to default and secondary variants, not primary, outlined, dark, or inverse + const outlineStyles = + variant === "primary" || + variant === "outlined" || + variant === "dark" || + variant === "inverse" + ? "" + : hoverOutlineStyles[size]; - let finalVariant = variant; - if (disabled) { - finalVariant = "default"; - } + const baseStyles = `inline-flex items-center justify-start box-border ${sizeStyles[size]} rounded-[var(--radius-measures-radius-full)] ${fontStyles[size]} transition-all duration-500 ease-in-out cursor-pointer ${variantStyles[variant]} ${outlineStyles}`; - const combinedStyles = `${baseStyles} ${className}`; + let finalVariant = variant; + if (disabled) { + finalVariant = "default"; + } - const accessibilityProps = { - ...(ariaLabel && { "aria-label": ariaLabel }), - ...(disabled && { "aria-disabled": "true" }), - ...(target && { target }), - ...(rel && { rel }), - tabIndex: disabled ? -1 : 0, - ...props, - }; + const combinedStyles = `${baseStyles} ${className}`; + + const accessibilityProps = { + ...(ariaLabel && { "aria-label": ariaLabel }), + ...(disabled && { "aria-disabled": "true" }), + ...(target && { target }), + ...(rel && { rel }), + tabIndex: disabled ? -1 : 0, + ...props, + }; + + if (href && !disabled) { + return ( + + {children} + + ); + } - if (href && !disabled) { return ( - {children} - + ); } +); - return ( - - ); -} +Button.displayName = "Button"; + +export default Button; diff --git a/app/components/ContentBanner.js b/app/components/ContentBanner.js index 0615058..f2a3511 100644 --- a/app/components/ContentBanner.js +++ b/app/components/ContentBanner.js @@ -1,9 +1,10 @@ "use client"; +import React, { memo } from "react"; import { getAssetPath } from "../../lib/assetUtils"; import ContentContainer from "./ContentContainer"; -export default function ContentBanner({ post }) { +const ContentBanner = memo(({ post }) => { // Get article-specific horizontal thumbnail (small) and banner (md+) const getBackgroundImage = (post) => { if (post.frontmatter?.thumbnail?.horizontal) { @@ -71,4 +72,8 @@ export default function ContentBanner({ post }) { ); -} +}); + +ContentBanner.displayName = "ContentBanner"; + +export default ContentBanner; diff --git a/app/components/ContentContainer.js b/app/components/ContentContainer.js index 8092062..cd725a6 100644 --- a/app/components/ContentContainer.js +++ b/app/components/ContentContainer.js @@ -1,127 +1,131 @@ "use client"; -import React from "react"; +import React, { memo } from "react"; import { getAssetPath, ASSETS } from "../../lib/assetUtils"; -const ContentContainer = ({ post, width = "200px", size = "responsive" }) => { - // Get the corresponding icon based on the same logic as background images - const getIconImage = (slug) => { - const icons = [ - getAssetPath(ASSETS.ICON_1), - getAssetPath(ASSETS.ICON_2), - getAssetPath(ASSETS.ICON_3), - ]; +const ContentContainer = memo( + ({ post, width = "200px", size = "responsive" }) => { + // Get the corresponding icon based on the same logic as background images + const getIconImage = (slug) => { + const icons = [ + getAssetPath(ASSETS.ICON_1), + getAssetPath(ASSETS.ICON_2), + getAssetPath(ASSETS.ICON_3), + ]; - if (!slug) return icons[0]; + if (!slug) return icons[0]; - // Use the same cycling logic as background images to ensure matching - const slugOrder = [ - "building-community-trust", - "operational-security-mutual-aid", - "making-decisions-without-hierarchy", - "resolving-active-conflicts", - ]; - const index = slugOrder.indexOf(slug); - const finalIndex = index >= 0 ? index % icons.length : 0; - return icons[finalIndex]; - }; + // Use the same cycling logic as background images to ensure matching + const slugOrder = [ + "building-community-trust", + "operational-security-mutual-aid", + "making-decisions-without-hierarchy", + "resolving-active-conflicts", + ]; + const index = slugOrder.indexOf(slug); + const finalIndex = index >= 0 ? index % icons.length : 0; + return icons[finalIndex]; + }; - const iconImage = getIconImage(post.slug); + const iconImage = getIconImage(post.slug); - // Choose styling based on size prop - const containerClasses = - size === "xs" - ? "relative z-20 h-full flex flex-col gap-[var(--measures-spacing-012)]" - : "relative z-20 h-full flex flex-col gap-[var(--measures-spacing-012)] sm:gap-[var(--measures-spacing-016)] md:gap-[18px] lg:gap-[var(--measures-spacing-024)]"; + // Choose styling based on size prop + const containerClasses = + size === "xs" + ? "relative z-20 h-full flex flex-col gap-[var(--measures-spacing-012)]" + : "relative z-20 h-full flex flex-col gap-[var(--measures-spacing-012)] sm:gap-[var(--measures-spacing-016)] md:gap-[18px] lg:gap-[var(--measures-spacing-024)]"; - return ( -
- {/* Content Container - gap between icon and text */} + return (
- {/* Icon */} -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {`Icon -
- - {/* Text Container */} + {/* Content Container - gap between icon and text */}
- {/* Title */} -

- {post.frontmatter.title} -

+ {/* Icon */} +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {`Icon +
- {/* Description */} -

- {post.frontmatter.description} -

+ {/* Title */} +

+ {post.frontmatter.title} +

+ + {/* Description */} +

+ {post.frontmatter.description} +

+
+
+ + {/* Metadata Container - horizontal with 8px gap */} +
+ {/* Author Name */} + + {post.frontmatter.author} + + + {/* Date */} + + {new Date(post.frontmatter.date).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + })} +
+ ); + } +); - {/* Metadata Container - horizontal with 8px gap */} -
- {/* Author Name */} - - {post.frontmatter.author} - - - {/* Date */} - - {new Date(post.frontmatter.date).toLocaleDateString("en-US", { - year: "numeric", - month: "long", - })} - -
- - ); -}; +ContentContainer.displayName = "ContentContainer"; export default ContentContainer; diff --git a/app/components/ContentLockup.js b/app/components/ContentLockup.js index c425059..66fc490 100644 --- a/app/components/ContentLockup.js +++ b/app/components/ContentLockup.js @@ -1,178 +1,187 @@ "use client"; +import React, { memo } from "react"; import Button from "./Button"; import { getAssetPath } from "../../lib/assetUtils"; -const ContentLockup = ({ - title, - subtitle, - description, - ctaText, - ctaHref, - buttonClassName = "", - variant = "hero", - linkText, - linkHref, - alignment = "center", // center, left -}) => { - // Variant-specific styling - const variantStyles = { - hero: { - container: - "flex flex-col gap-[var(--spacing-scale-006)] sm:gap-[var(--spacing-scale-012)] md:gap-[var(--spacing-scale-020)] lg:gap-[var(--spacing-scale-020)] relative z-10", - textContainer: - "flex flex-col md:gap-[var(--spacing-scale-004)] lg:gap-[var(--spacing-scale-008)] xl:gap-[var(--spacing-scale-020)]", - titleGroup: "flex flex-col xl:gap-0", - titleContainer: - "flex gap-[var(--spacing-scale-008)] xl:gap-[var(--spacing-scale-010)] items-center", - title: - "font-bricolage-grotesque font-medium text-[32px] leading-[32px] sm:text-[52px] sm:leading-[52px] md:text-[44px] md:leading-[44px] lg:text-[64px] lg:leading-[64px] xl:text-[96px] xl:leading-[110%] text-[var(--color-content-inverse-primary)]", - subtitle: - "font-bricolage-grotesque font-medium text-[32px] leading-[32px] sm:text-[52px] sm:leading-[52px] md:text-[44px] md:leading-[44px] lg:text-[64px] lg:leading-[64px] xl:text-[96px] xl:leading-[110%] text-[var(--color-content-inverse-primary)]", - description: - "font-inter font-normal text-[18px] leading-[130%] lg:text-[24px] lg:leading-[32px] xl:text-[32px] xl:leading-[40px] text-[var(--color-content-inverse-primary)]", - shape: - "w-[27.2px] h-[27.2px] md:w-[34px] md:h-[34px] lg:w-[50px] lg:h-[50px]", - }, - feature: { - container: "flex flex-col gap-[var(--spacing-scale-012)] relative z-10", - textContainer: "flex flex-col gap-[var(--spacing-scale-012)]", - titleGroup: "flex flex-col gap-[var(--spacing-scale-012)]", - titleContainer: "flex gap-[var(--spacing-scale-008)] items-center", - title: - "font-bricolage-grotesque font-medium text-[32px] leading-[130%] tracking-[0] text-[var(--color-content-default-primary)]", - subtitle: - "font-space-grotesk font-normal text-[20px] leading-[130%] tracking-[0] text-[var(--color-content-default-primary)]", - description: - "font-inter font-normal text-[16px] leading-[140%] lg:text-[18px] lg:leading-[150%] xl:text-[20px] xl:leading-[160%] text-[var(--color-content-secondary)]", - shape: - "w-[20px] h-[20px] md:w-[24px] md:h-[24px] lg:w-[28px] lg:h-[28px]", - }, - learn: { - container: - "flex flex-col gap-[var(--spacing-scale-012)] relative z-10 pt-[var(--spacing-scale-016)] pb-[var(--spacing-scale-016)] px-[var(--spacing-scale-020)] sm:pt-[var(--spacing-scale-040)] sm:pb-0 md:pt-[var(--spacing-scale-056)] md:px-[var(--spacing-scale-032)] lg:pt-[var(--spacing-scale-056)] lg:px-[var(--spacing-scale-064)]", - textContainer: - "flex flex-col gap-[var(--spacing-scale-012)] md:gap-[var(--spacing-scale-016)]", - titleGroup: - "flex flex-col gap-[var(--spacing-scale-012)] md:gap-[var(--spacing-scale-016)] lg:gap-[var(--spacing-scale-008)]", - titleContainer: "flex gap-[var(--spacing-scale-008)] items-center", - title: - "font-bricolage-grotesque font-medium text-[28px] leading-[36px] tracking-[0] md:text-[44px] md:leading-[110%] lg:text-[52px] text-[var(--color-content-default-primary)]", - subtitle: - "font-space-grotesk font-normal text-[16px] leading-[24px] tracking-[0] lg:text-[24px] lg:leading-[28px] text-[var(--color-content-default-primary)]", - description: - "font-inter font-normal text-[16px] leading-[140%] lg:text-[18px] lg:leading-[150%] xl:text-[20px] xl:leading-[160%] text-[var(--color-content-secondary)]", - shape: - "w-[20px] h-[20px] md:w-[24px] md:h-[24px] lg:w-[28px] lg:h-[28px]", - }, - ask: { - container: "flex flex-col gap-[var(--spacing-scale-008)] relative z-10", - textContainer: "flex flex-col gap-[var(--spacing-scale-008)]", - titleGroup: "flex flex-col gap-[var(--spacing-scale-008)]", - titleContainer: "flex gap-[var(--spacing-scale-008)] items-center", - title: - "font-bricolage-grotesque font-medium text-[36px] leading-[110%] tracking-[0] md:text-[44px] md:leading-[110%] xl:text-[52px] xl:leading-[110%] text-[var(--color-content-default-brand-primary)]", - subtitle: - "font-inter font-normal text-[18px] leading-[130%] tracking-[0] md:text-[24px] md:leading-[32px] text-[var(--color-content-default-primary)]", - shape: - "w-[16px] h-[16px] md:w-[20px] md:h-[20px] lg:w-[24px] lg:h-[24px]", - }, - "ask-inverse": { - container: "flex flex-col gap-[var(--spacing-scale-008)] relative z-10", - textContainer: "flex flex-col gap-[var(--spacing-scale-008)]", - titleGroup: "flex flex-col gap-[var(--spacing-scale-008)]", - titleContainer: "flex gap-[var(--spacing-scale-008)] items-center", - title: - "font-bricolage-grotesque font-medium text-[36px] leading-[110%] tracking-[0] md:text-[44px] md:leading-[110%] xl:text-[52px] xl:leading-[110%] text-[var(--color-content-inverse-primary)]", - subtitle: - "font-inter font-normal text-[18px] leading-[130%] tracking-[0] md:text-[24px] md:leading-[32px] text-[var(--color-content-inverse-primary)]", - shape: - "w-[16px] h-[16px] md:w-[20px] md:h-[20px] lg:w-[24px] lg:h-[24px]", - }, - }; +const ContentLockup = memo( + ({ + title, + subtitle, + description, + ctaText, + ctaHref, + buttonClassName = "", + variant = "hero", + linkText, + linkHref, + alignment = "center", // center, left + }) => { + // Variant-specific styling + const variantStyles = { + hero: { + container: + "flex flex-col gap-[var(--spacing-scale-006)] sm:gap-[var(--spacing-scale-012)] md:gap-[var(--spacing-scale-020)] lg:gap-[var(--spacing-scale-020)] relative z-10", + textContainer: + "flex flex-col md:gap-[var(--spacing-scale-004)] lg:gap-[var(--spacing-scale-008)] xl:gap-[var(--spacing-scale-020)]", + titleGroup: "flex flex-col xl:gap-0", + titleContainer: + "flex gap-[var(--spacing-scale-008)] xl:gap-[var(--spacing-scale-010)] items-center", + title: + "font-bricolage-grotesque font-medium text-[32px] leading-[32px] sm:text-[52px] sm:leading-[52px] md:text-[44px] md:leading-[44px] lg:text-[64px] lg:leading-[64px] xl:text-[96px] xl:leading-[110%] text-[var(--color-content-inverse-primary)]", + subtitle: + "font-bricolage-grotesque font-medium text-[32px] leading-[32px] sm:text-[52px] sm:leading-[52px] md:text-[44px] md:leading-[44px] lg:text-[64px] lg:leading-[64px] xl:text-[96px] xl:leading-[110%] text-[var(--color-content-inverse-primary)]", + description: + "font-inter font-normal text-[18px] leading-[130%] lg:text-[24px] lg:leading-[32px] xl:text-[32px] xl:leading-[40px] text-[var(--color-content-inverse-primary)]", + shape: + "w-[27.2px] h-[27.2px] md:w-[34px] md:h-[34px] lg:w-[50px] lg:h-[50px]", + }, + feature: { + container: "flex flex-col gap-[var(--spacing-scale-012)] relative z-10", + textContainer: "flex flex-col gap-[var(--spacing-scale-012)]", + titleGroup: "flex flex-col gap-[var(--spacing-scale-012)]", + titleContainer: "flex gap-[var(--spacing-scale-008)] items-center", + title: + "font-bricolage-grotesque font-medium text-[32px] leading-[130%] tracking-[0] text-[var(--color-content-default-primary)]", + subtitle: + "font-space-grotesk font-normal text-[20px] leading-[130%] tracking-[0] text-[var(--color-content-default-primary)]", + description: + "font-inter font-normal text-[16px] leading-[140%] lg:text-[18px] lg:leading-[150%] xl:text-[20px] xl:leading-[160%] text-[var(--color-content-secondary)]", + shape: + "w-[20px] h-[20px] md:w-[24px] md:h-[24px] lg:w-[28px] lg:h-[28px]", + }, + learn: { + container: + "flex flex-col gap-[var(--spacing-scale-012)] relative z-10 pt-[var(--spacing-scale-016)] pb-[var(--spacing-scale-016)] px-[var(--spacing-scale-020)] sm:pt-[var(--spacing-scale-040)] sm:pb-0 md:pt-[var(--spacing-scale-056)] md:px-[var(--spacing-scale-032)] lg:pt-[var(--spacing-scale-056)] lg:px-[var(--spacing-scale-064)]", + textContainer: + "flex flex-col gap-[var(--spacing-scale-012)] md:gap-[var(--spacing-scale-016)]", + titleGroup: + "flex flex-col gap-[var(--spacing-scale-012)] md:gap-[var(--spacing-scale-016)] lg:gap-[var(--spacing-scale-008)]", + titleContainer: "flex gap-[var(--spacing-scale-008)] items-center", + title: + "font-bricolage-grotesque font-medium text-[28px] leading-[36px] tracking-[0] md:text-[44px] md:leading-[110%] lg:text-[52px] text-[var(--color-content-default-primary)]", + subtitle: + "font-space-grotesk font-normal text-[16px] leading-[24px] tracking-[0] lg:text-[24px] lg:leading-[28px] text-[var(--color-content-default-primary)]", + description: + "font-inter font-normal text-[16px] leading-[140%] lg:text-[18px] lg:leading-[150%] xl:text-[20px] xl:leading-[160%] text-[var(--color-content-secondary)]", + shape: + "w-[20px] h-[20px] md:w-[24px] md:h-[24px] lg:w-[28px] lg:h-[28px]", + }, + ask: { + container: "flex flex-col gap-[var(--spacing-scale-008)] relative z-10", + textContainer: "flex flex-col gap-[var(--spacing-scale-008)]", + titleGroup: "flex flex-col gap-[var(--spacing-scale-008)]", + titleContainer: "flex gap-[var(--spacing-scale-008)] items-center", + title: + "font-bricolage-grotesque font-medium text-[36px] leading-[110%] tracking-[0] md:text-[44px] md:leading-[110%] xl:text-[52px] xl:leading-[110%] text-[var(--color-content-default-brand-primary)]", + subtitle: + "font-inter font-normal text-[18px] leading-[130%] tracking-[0] md:text-[24px] md:leading-[32px] text-[var(--color-content-default-primary)]", + shape: + "w-[16px] h-[16px] md:w-[20px] md:h-[20px] lg:w-[24px] lg:h-[24px]", + }, + "ask-inverse": { + container: "flex flex-col gap-[var(--spacing-scale-008)] relative z-10", + textContainer: "flex flex-col gap-[var(--spacing-scale-008)]", + titleGroup: "flex flex-col gap-[var(--spacing-scale-008)]", + titleContainer: "flex gap-[var(--spacing-scale-008)] items-center", + title: + "font-bricolage-grotesque font-medium text-[36px] leading-[110%] tracking-[0] md:text-[44px] md:leading-[110%] xl:text-[52px] xl:leading-[110%] text-[var(--color-content-inverse-primary)]", + subtitle: + "font-inter font-normal text-[18px] leading-[130%] tracking-[0] md:text-[24px] md:leading-[32px] text-[var(--color-content-inverse-primary)]", + shape: + "w-[16px] h-[16px] md:w-[20px] md:h-[20px] lg:w-[24px] lg:h-[24px]", + }, + }; - const styles = variantStyles[variant] || variantStyles.hero; + const styles = variantStyles[variant] || variantStyles.hero; - return ( -
- {variant === "ask" || variant === "ask-inverse" ? ( - /* Simplified structure for ask variant */ -
+ return ( +
+ {variant === "ask" || variant === "ask-inverse" ? ( + /* Simplified structure for ask variant */
-

{title}

-
-

{subtitle}

-
- ) : ( - /* Full structure for other variants */ -
- {/* Title and subtitle group */} -
- {/* Title container */} -
+

{title}

- {variant === "hero" && ( - - )}
- - {/* Subtitle */}

{subtitle}

+ ) : ( + /* Full structure for other variants */ +
+ {/* Title and subtitle group */} +
+ {/* Title container */} +
+

{title}

+ {variant === "hero" && ( + + )} +
- {/* Description */} - {description &&

{description}

} -
- )} + {/* Subtitle */} +

{subtitle}

+
- {/* Link for feature variant */} - {variant === "feature" && linkText && ( - - {linkText} - - )} + {/* Description */} + {description &&

{description}

} +
+ )} - {/* CTA Button */} - {ctaText && ( -
- {/* Small button for xsm and sm breakpoints */} -
- + {/* Link for feature variant */} + {variant === "feature" && linkText && ( + + {linkText} + + )} + + {/* CTA Button */} + {ctaText && ( +
+ {/* Small button for xsm and sm breakpoints */} +
+ +
+ {/* Large button for md and lg breakpoints */} +
+ +
+ {/* XLarge button for xl breakpoint */} +
+ +
- {/* Large button for md and lg breakpoints */} -
- -
- {/* XLarge button for xl breakpoint */} -
- -
-
- )} -
- ); -}; + )} +
+ ); + } +); + +ContentLockup.displayName = "ContentLockup"; export default ContentLockup; diff --git a/app/components/ContentThumbnailTemplate.js b/app/components/ContentThumbnailTemplate.js index 3e7acfb..a3b7e96 100644 --- a/app/components/ContentThumbnailTemplate.js +++ b/app/components/ContentThumbnailTemplate.js @@ -1,6 +1,6 @@ "use client"; -import React from "react"; +import React, { memo } from "react"; import Link from "next/link"; import ContentContainer from "./ContentContainer"; import { getAssetPath, ASSETS } from "../../lib/assetUtils"; @@ -9,87 +9,91 @@ import { getAssetPath, ASSETS } from "../../lib/assetUtils"; * ContentThumbnailTemplate component for displaying blog post previews * Simplified version to debug infinite loop */ -const ContentThumbnailTemplate = ({ - post, - className = "", - variant = "vertical", // Internal prop for testing/development -}) => { - // Get article-specific background image from frontmatter - const getBackgroundImage = (post, variant) => { - // Check if post has thumbnail images defined in frontmatter - if (post.frontmatter?.thumbnail) { - const imageName = - variant === "vertical" - ? post.frontmatter.thumbnail.vertical - : post.frontmatter.thumbnail.horizontal; +const ContentThumbnailTemplate = memo( + ({ + post, + className = "", + variant = "vertical", // Internal prop for testing/development + }) => { + // Get article-specific background image from frontmatter + const getBackgroundImage = (post, variant) => { + // Check if post has thumbnail images defined in frontmatter + if (post.frontmatter?.thumbnail) { + const imageName = + variant === "vertical" + ? post.frontmatter.thumbnail.vertical + : post.frontmatter.thumbnail.horizontal; - if (imageName) { - // Return path to image in public/content/blog directory - return `/content/blog/${imageName}`; + if (imageName) { + // Return path to image in public/content/blog directory + return `/content/blog/${imageName}`; + } } - } - // Fallback to default images if no thumbnail specified - const fallbackImages = { - vertical: getAssetPath(ASSETS.VERTICAL_1), - horizontal: getAssetPath(ASSETS.HORIZONTAL_1), + // Fallback to default images if no thumbnail specified + const fallbackImages = { + vertical: getAssetPath(ASSETS.VERTICAL_1), + horizontal: getAssetPath(ASSETS.HORIZONTAL_1), + }; + + return fallbackImages[variant] || fallbackImages.vertical; }; - return fallbackImages[variant] || fallbackImages.vertical; - }; + const backgroundImage = getBackgroundImage(post, variant); - const backgroundImage = getBackgroundImage(post, variant); + if (variant === "vertical") { + return ( + +
+ {/* Background SVG - fills container with maintained aspect */} +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {`Background + {/* Gradient overlay for better text readability */} +
+
- if (variant === "vertical") { + {/* Content Section - positioned within the padding constraints */} + +
+ + ); + } + + // Horizontal variant return ( -
- {/* Background SVG - fills container with maintained aspect */} +
+ {/* Background SVG - sized to fit the 320x225.5 container exactly */}
{/* eslint-disable-next-line @next/next/no-img-element */} {`Background - {/* Gradient overlay for better text readability */} -
+ {/* Gradient overlay */} +
- {/* Content Section - positioned within the padding constraints */} - + {/* Content - positioned within the padding constraints */} +
); } +); - // Horizontal variant - return ( - -
- {/* Background SVG - sized to fit the 320x225.5 container exactly */} -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {`Background - {/* Gradient overlay */} -
-
- - {/* Content - positioned within the padding constraints */} - -
- - ); -}; +ContentThumbnailTemplate.displayName = "ContentThumbnailTemplate"; export default ContentThumbnailTemplate; diff --git a/app/components/ErrorBoundary.js b/app/components/ErrorBoundary.js new file mode 100644 index 0000000..d8ee65f --- /dev/null +++ b/app/components/ErrorBoundary.js @@ -0,0 +1,49 @@ +"use client"; + +import React, { Component } from "react"; + +class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error) { + // Update state so the next render will show the fallback UI + return { hasError: true, error }; + } + + componentDidCatch(error, errorInfo) { + // Log the error to an error reporting service + console.error("ErrorBoundary caught an error:", error, errorInfo); + } + + render() { + if (this.state.hasError) { + // Fallback UI using design tokens + return ( +
+
+

+ Something went wrong +

+

+ We're sorry, but something unexpected happened. +

+ +
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; + diff --git a/app/components/FeatureGrid.js b/app/components/FeatureGrid.js index ecda70e..46b4802 100644 --- a/app/components/FeatureGrid.js +++ b/app/components/FeatureGrid.js @@ -1,11 +1,49 @@ "use client"; -import React from "react"; +import React, { memo, useMemo } from "react"; import ContentLockup from "./ContentLockup"; import MiniCard from "./MiniCard"; import Image from "next/image"; -const FeatureGrid = ({ title, subtitle, className = "" }) => { +const FeatureGrid = memo(({ title, subtitle, className = "" }) => { + // Memoize the feature data to prevent unnecessary re-renders + const features = useMemo( + () => [ + { + backgroundColor: "bg-[var(--color-surface-default-brand-royal)]", + labelLine1: "Decision-making", + labelLine2: "support", + panelContent: "/assets/Feature_Support.png", + ariaLabel: "Decision-making support tools", + href: "#decision-making", + }, + { + backgroundColor: "bg-[#D1FFE2]", + labelLine1: "Values alignment", + labelLine2: "exercises", + panelContent: "/assets/Feature_Exercises.png", + ariaLabel: "Values alignment exercises", + href: "#values-alignment", + }, + { + backgroundColor: "bg-[#F4CAFF]", + labelLine1: "Membership", + labelLine2: "guidance", + panelContent: "/assets/Feature_Guidance.png", + ariaLabel: "Membership guidance resources", + href: "#membership-guidance", + }, + { + backgroundColor: "bg-[#CBDDFF]", + labelLine1: "Conflict resolution", + labelLine2: "tools", + panelContent: "/assets/Feature_Tools.png", + ariaLabel: "Conflict resolution tools", + href: "#conflict-resolution", + }, + ], + [] + ); return (
{ role="grid" aria-label="Feature tools and services" > - - - - + {features.map((feature, index) => ( + + ))}
); -}; +}); + +FeatureGrid.displayName = "FeatureGrid"; export default FeatureGrid; diff --git a/app/components/Footer.js b/app/components/Footer.js index 80f489f..b0facad 100644 --- a/app/components/Footer.js +++ b/app/components/Footer.js @@ -1,8 +1,9 @@ +import React, { memo } from "react"; import Logo from "./Logo"; import Separator from "./Separator"; import { getAssetPath, ASSETS } from "../../lib/assetUtils"; -export default function Footer() { +const Footer = memo(() => { // Schema markup for organization information const schemaData = { "@context": "https://schema.org", @@ -155,4 +156,8 @@ export default function Footer() { ); -} +}); + +Footer.displayName = "Footer"; + +export default Footer; diff --git a/app/components/Header.js b/app/components/Header.js index dc56642..7e18ebd 100644 --- a/app/components/Header.js +++ b/app/components/Header.js @@ -1,5 +1,6 @@ "use client"; +import React, { memo } from "react"; import { usePathname } from "next/navigation"; import Logo from "./Logo"; import MenuBar from "./MenuBar"; @@ -38,7 +39,7 @@ export const logoConfig = [ { breakpoint: "hidden xl:block", size: "headerXl", showText: true }, ]; -export default function Header() { +const Header = memo(() => { const pathname = usePathname(); // Schema markup for site navigation @@ -214,4 +215,8 @@ export default function Header() { ); -} +}); + +Header.displayName = "Header"; + +export default Header; diff --git a/app/components/HeaderTab.js b/app/components/HeaderTab.js index 7557e61..98b74d8 100644 --- a/app/components/HeaderTab.js +++ b/app/components/HeaderTab.js @@ -1,39 +1,41 @@ +import React, { memo } from "react"; import { getAssetPath } from "../../lib/assetUtils"; -export default function HeaderTab({ - children, - className = "", - stretch = false, - ...props -}) { - const stretchClasses = stretch - ? "flex-1 sm:mr-[var(--spacing-scale-008)] md:mr-[185px] lg:mr-[var(--spacing-scale-024)] xl:mr-[var(--spacing-scale-032)]" - : ""; +const HeaderTab = memo( + ({ children, className = "", stretch = false, ...props }) => { + const stretchClasses = stretch + ? "flex-1 sm:mr-[var(--spacing-scale-008)] md:mr-[185px] lg:mr-[var(--spacing-scale-024)] xl:mr-[var(--spacing-scale-032)]" + : ""; - return ( -
- {children} - - - -
- ); -} + return ( +
+ {children} + + + +
+ ); + } +); + +HeaderTab.displayName = "HeaderTab"; + +export default HeaderTab; diff --git a/app/components/HeroBanner.js b/app/components/HeroBanner.js index 75a333e..6c55335 100644 --- a/app/components/HeroBanner.js +++ b/app/components/HeroBanner.js @@ -1,47 +1,54 @@ "use client"; +import React, { memo } from "react"; import ContentLockup from "./ContentLockup"; import HeroDecor from "./HeroDecor"; import { getAssetPath } from "../../lib/assetUtils"; -const HeroBanner = ({ title, subtitle, description, ctaText, ctaHref }) => { - return ( -
-
- {/* Frame container for content */} -
- {/* DECORATIONS (behind content) */} - +
+ {/* Frame container for content */} +
+ {/* DECORATIONS (behind content) */} + - - {/* Content lockup - Large variant */} -
- -
- {/* Hero Image Container */} -
- Hero illustration + {/* Content lockup - Large variant */} +
+ +
+ + {/* Hero Image Container */} +
+ Hero illustration +
-
-
- ); -}; + + ); + } +); + +HeroBanner.displayName = "HeroBanner"; export default HeroBanner; diff --git a/app/components/HeroDecor.js b/app/components/HeroDecor.js index 8ce8adc..bb4074a 100644 --- a/app/components/HeroDecor.js +++ b/app/components/HeroDecor.js @@ -1,6 +1,8 @@ "use client"; -const HeroDecor = ({ className = "" }) => { +import React, { memo } from "react"; + +const HeroDecor = memo(({ className = "" }) => { return ( { ); -}; +}); + +HeroDecor.displayName = "HeroDecor"; export default HeroDecor; diff --git a/app/components/HomeHeader.js b/app/components/HomeHeader.js index be749dd..56173af 100644 --- a/app/components/HomeHeader.js +++ b/app/components/HomeHeader.js @@ -1,5 +1,6 @@ "use client"; +import React, { memo } from "react"; import { usePathname } from "next/navigation"; import Logo from "./Logo"; import MenuBar from "./MenuBar"; @@ -9,7 +10,7 @@ import AvatarContainer from "./AvatarContainer"; import Avatar from "./Avatar"; import HeaderTab from "./HeaderTab"; -export default function HomeHeader() { +const HomeHeader = memo(() => { const pathname = usePathname(); // Schema markup for site navigation (home page specific) @@ -33,9 +34,9 @@ export default function HomeHeader() { ]; const avatarImages = [ - { src: "assets/Avatar_1.png", alt: "Avatar 1" }, - { src: "assets/Avatar_2.png", alt: "Avatar 2" }, - { src: "assets/Avatar_3.png", alt: "Avatar 3" }, + { src: "/assets/Avatar_1.png", alt: "Avatar 1" }, + { src: "/assets/Avatar_2.png", alt: "Avatar 2" }, + { src: "/assets/Avatar_3.png", alt: "Avatar 3" }, ]; const logoConfig = [ @@ -78,10 +79,10 @@ export default function HomeHeader() { ? size === "home" || size === "homeMd" ? "homeMd" : size === "large" - ? "large" - : size === "homeXlarge" - ? "homeXlarge" - : "xsmallUseCases" + ? "large" + : size === "homeXlarge" + ? "homeXlarge" + : "xsmallUseCases" : size } variant={ @@ -241,4 +242,8 @@ export default function HomeHeader() { ); -} +}); + +HomeHeader.displayName = "HomeHeader"; + +export default HomeHeader; diff --git a/app/components/ImagePlaceholder.js b/app/components/ImagePlaceholder.js index c762aaa..3a60673 100644 --- a/app/components/ImagePlaceholder.js +++ b/app/components/ImagePlaceholder.js @@ -1,37 +1,41 @@ "use client"; -import React from "react"; +import React, { memo } from "react"; /** * Simple image placeholder component for testing * Generates colored backgrounds with text overlays */ -const ImagePlaceholder = ({ - width = 260, - height = 390, - text = "Blog Image", - color = "blue", - className = "", -}) => { - const colors = { - blue: "bg-blue-500", - green: "bg-green-500", - purple: "bg-purple-500", - red: "bg-red-500", - orange: "bg-orange-500", - teal: "bg-teal-500", - }; +const ImagePlaceholder = memo( + ({ + width = 260, + height = 390, + text = "Blog Image", + color = "blue", + className = "", + }) => { + const colors = { + blue: "bg-blue-500", + green: "bg-green-500", + purple: "bg-purple-500", + red: "bg-red-500", + orange: "bg-orange-500", + teal: "bg-teal-500", + }; - const bgColor = colors[color] || colors.blue; + const bgColor = colors[color] || colors.blue; - return ( -
- {text} -
- ); -}; + return ( +
+ {text} +
+ ); + } +); + +ImagePlaceholder.displayName = "ImagePlaceholder"; export default ImagePlaceholder; diff --git a/app/components/Logo.js b/app/components/Logo.js index 53d9a39..9365b19 100644 --- a/app/components/Logo.js +++ b/app/components/Logo.js @@ -1,7 +1,8 @@ +import React, { memo } from "react"; import Link from "next/link"; import { getAssetPath, ASSETS } from "../../lib/assetUtils"; -export default function Logo({ size = "default", showText = true }) { +const Logo = memo(({ size = "default", showText = true }) => { // Size configurations const sizes = { default: { @@ -94,26 +95,26 @@ export default function Logo({ size = "default", showText = true }) { size === "homeHeaderXsmall" ? sizes.homeHeaderXsmall : size === "homeHeaderSm" - ? sizes.homeHeaderSm - : size === "homeHeaderMd" - ? sizes.homeHeaderMd - : size === "homeHeaderLg" - ? sizes.homeHeaderLg - : size === "homeHeaderXl" - ? sizes.homeHeaderXl - : size === "header" - ? sizes.header - : size === "headerMd" - ? sizes.headerMd - : size === "headerLg" - ? sizes.headerLg - : size === "headerXl" - ? sizes.headerXl - : size === "footer" - ? sizes.footer - : size === "footerLg" - ? sizes.footerLg - : sizes.default; + ? sizes.homeHeaderSm + : size === "homeHeaderMd" + ? sizes.homeHeaderMd + : size === "homeHeaderLg" + ? sizes.homeHeaderLg + : size === "homeHeaderXl" + ? sizes.homeHeaderXl + : size === "header" + ? sizes.header + : size === "headerMd" + ? sizes.headerMd + : size === "headerLg" + ? sizes.headerLg + : size === "headerXl" + ? sizes.headerXl + : size === "footer" + ? sizes.footer + : size === "footerLg" + ? sizes.footerLg + : sizes.default; return ( @@ -165,4 +166,8 @@ export default function Logo({ size = "default", showText = true }) {
); -} +}); + +Logo.displayName = "Logo"; + +export default Logo; diff --git a/app/components/LogoWall.js b/app/components/LogoWall.js index d3a8c5d..b76374a 100644 --- a/app/components/LogoWall.js +++ b/app/components/LogoWall.js @@ -1,45 +1,45 @@ "use client"; -import { useState, useEffect } from "react"; +import React, { useState, useEffect, memo } from "react"; import Image from "next/image"; -const LogoWall = ({ logos = [] }) => { +const LogoWall = memo(({ logos = [] }) => { const [isVisible, setIsVisible] = useState(false); // Default logos if none provided - ordered for mobile (3 rows × 2 columns) const defaultLogos = [ { - src: "assets/Section/Logo_FoodNotBombs.png", + src: "/assets/Section/Logo_FoodNotBombs.png", alt: "Food Not Bombs", size: "h-11 lg:h-14 xl:h-[70px]", order: "order-1 sm:order-4", // Mobile: row 1 col 1, SM: row 2 col 1 (bottom left) }, { - src: "assets/Section/Logo_StartCOOP.png", + src: "/assets/Section/Logo_StartCOOP.png", alt: "Start COOP", size: "h-[42px] lg:h-[53px] xl:h-[66px]", order: "order-2 sm:order-2", // Mobile: row 1 col 2, SM: row 1 col 2 (top middle) }, { - src: "assets/Section/Logo_Metagov.png", + src: "/assets/Section/Logo_Metagov.png", alt: "Metagov", size: "h-6 lg:h-8 xl:h-[41px]", order: "order-3 sm:order-1", // Mobile: row 2 col 1, SM: row 1 col 1 (top left) }, { - src: "assets/Section/Logo_OpenCivics.png", + src: "/assets/Section/Logo_OpenCivics.png", alt: "Open Civics", size: "h-8 lg:h-10 xl:h-[50px]", order: "order-4 sm:order-5 md:order-6", // Mobile: row 2 col 2, SM: row 2 col 2, MD: swapped with Mutual Aid CO }, { - src: "assets/Section/Logo_MutualAidCO.png", + src: "/assets/Section/Logo_MutualAidCO.png", alt: "Mutual Aid CO", size: "h-11 lg:h-14 xl:h-[70px]", order: "order-5 sm:order-6 md:order-5", // Mobile: row 3 col 1, SM: row 2 col 3, MD: swapped with OpenCivics }, { - src: "assets/Section/Logo_CUBoulder.png", + src: "/assets/Section/Logo_CUBoulder.png", alt: "CU Boulder", size: "h-10 lg:h-12 xl:h-[60px]", order: "order-6 sm:order-3", // Mobile: row 3 col 2, SM: row 1 col 3 (top right) @@ -98,6 +98,8 @@ const LogoWall = ({ logos = [] }) => {
); -}; +}); + +LogoWall.displayName = "LogoWall"; export default LogoWall; diff --git a/app/components/MenuBar.js b/app/components/MenuBar.js index 275d6d3..03dc5e2 100644 --- a/app/components/MenuBar.js +++ b/app/components/MenuBar.js @@ -1,30 +1,33 @@ -export default function MenuBar({ - children, - className = "", - size = "default", - ...props -}) { - const sizeStyles = { - xsmall: - "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-001)] rounded-[4px]", - default: - "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-001)]", - medium: - "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-004)]", - large: - "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-012)]", - }; +import React, { memo } from "react"; - const baseStyles = `flex items-center ${sizeStyles[size]} ${className}`; +const MenuBar = memo( + ({ children, className = "", size = "default", ...props }) => { + const sizeStyles = { + xsmall: + "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-001)] rounded-[4px]", + default: + "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-001)]", + medium: + "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-004)]", + large: + "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-012)]", + }; - return ( - - ); -} + const baseStyles = `flex items-center ${sizeStyles[size]} ${className}`; + + return ( + + ); + } +); + +MenuBar.displayName = "MenuBar"; + +export default MenuBar; diff --git a/app/components/MenuBarItem.js b/app/components/MenuBarItem.js index 0bb73d4..496698f 100644 --- a/app/components/MenuBarItem.js +++ b/app/components/MenuBarItem.js @@ -1,158 +1,166 @@ -export default function MenuBarItem({ - href = "#", - children, - variant = "default", - size = "default", - className = "", - disabled = false, - isActive = false, - ariaLabel, - ...props -}) { - const variantStyles = { - default: - "bg-transparent text-[var(--color-content-default-brand-primary)] hover:bg-[var(--color-surface-default-tertiary)] hover:text-[var(--color-content-default-brand-primary)] hover:scale-[1.02] active:bg-transparent active:text-[var(--color-content-default-brand-primary)] active:scale-[0.98] disabled:bg-[var(--color-surface-default-tertiary)] disabled:text-[var(--color-content-default-tertiary)] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 disabled:active:scale-100", - home: "bg-transparent text-[var(--color-content-inverse-primary)] hover:bg-[var(--color-content-default-brand-accent)] hover:text-[var(--color-content-inverse-primary)] hover:scale-[1.02] active:bg-transparent active:text-[var(--color-content-inverse-primary)] active:scale-[0.98] disabled:bg-[var(--color-surface-default-tertiary)] disabled:text-[var(--color-content-default-tertiary)] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 disabled:active:scale-100", - }; +import React, { memo } from "react"; - const activeOutlineStyles = { - xsmall: - "active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]", - xsmallUseCases: - "active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]", - default: - "active:outline-1 active:outline-[var(--color-content-default-brand-primary)] focus:outline-1 focus:outline-[var(--color-content-default-brand-primary)]", - homeMd: - "active:outline-[1.5px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-brand-primary)]", - homeUseCases: - "active:outline-[1.5px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-brand-primary)]", - large: - "active:outline-[1.75px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-brand-primary)]", - largeUseCases: - "active:outline-[1.75px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-brand-primary)]", - homeXlarge: - "active:outline-[2px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[2px] focus:outline-[var(--color-content-default-brand-primary)]", - xlarge: - "active:outline-2 active:outline-[var(--color-content-default-brand-primary)] focus:outline-2 focus:outline-[var(--color-content-default-brand-primary)]", - }; +const MenuBarItem = memo( + ({ + href = "#", + children, + variant = "default", + size = "default", + className = "", + disabled = false, + isActive = false, + ariaLabel, + ...props + }) => { + const variantStyles = { + default: + "bg-transparent text-[var(--color-content-default-brand-primary)] hover:bg-[var(--color-surface-default-tertiary)] hover:text-[var(--color-content-default-brand-primary)] hover:scale-[1.02] active:bg-transparent active:text-[var(--color-content-default-brand-primary)] active:scale-[0.98] disabled:bg-[var(--color-surface-default-tertiary)] disabled:text-[var(--color-content-default-tertiary)] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 disabled:active:scale-100", + home: "bg-transparent text-[var(--color-content-inverse-primary)] hover:bg-[var(--color-content-default-brand-accent)] hover:text-[var(--color-content-inverse-primary)] hover:scale-[1.02] active:bg-transparent active:text-[var(--color-content-inverse-primary)] active:scale-[0.98] disabled:bg-[var(--color-surface-default-tertiary)] disabled:text-[var(--color-content-default-tertiary)] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 disabled:active:scale-100", + }; - const homeOutlineStyles = { - xsmall: - "active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]", - xsmallUseCases: - "active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]", - default: - "active:outline-[1.5px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-primary)]", - homeMd: - "active:outline-[1.5px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-primary)]", - homeUseCases: - "active:outline-[1.5px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-primary)]", - largeUseCases: - "active:outline-[1.75px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-primary)]", - large: - "active:outline-[1.75px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-primary)]", - homeXlarge: - "active:outline-[2px] active:outline-[var(--color-content-default-primary)] focus:outline-[2px] focus:outline-[var(--color-content-default-primary)]", - xlarge: - "active:outline-2 active:outline-[var(--color-content-default-primary)] focus:outline-2 focus:outline-[var(--color-content-default-primary)]", - }; + const activeOutlineStyles = { + xsmall: + "active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]", + xsmallUseCases: + "active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]", + default: + "active:outline-1 active:outline-[var(--color-content-default-brand-primary)] focus:outline-1 focus:outline-[var(--color-content-default-brand-primary)]", + homeMd: + "active:outline-[1.5px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-brand-primary)]", + homeUseCases: + "active:outline-[1.5px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-brand-primary)]", + large: + "active:outline-[1.75px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-brand-primary)]", + largeUseCases: + "active:outline-[1.75px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-brand-primary)]", + homeXlarge: + "active:outline-[2px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[2px] focus:outline-[var(--color-content-default-brand-primary)]", + xlarge: + "active:outline-2 active:outline-[var(--color-content-default-brand-primary)] focus:outline-2 focus:outline-[var(--color-content-default-brand-primary)]", + }; - const activeStateStyles = { - xsmall: - "!outline-1 !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-1 focus:!outline-[var(--color-content-default-brand-primary)]", - xsmallUseCases: - "!outline-1 !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-1 focus:!outline-[var(--color-content-default-brand-primary)]", - default: - "!outline-[1.5px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.5px] focus:!outline-[var(--color-content-default-brand-primary)]", - homeMd: - "!outline-[1.5px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.5px] focus:!outline-[var(--color-content-default-brand-primary)]", - homeUseCases: - "!outline-[1.5px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.5px] focus:!outline-[var(--color-content-default-brand-primary)]", - large: - "!outline-[1.75px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.75px] focus:!outline-[var(--color-content-default-brand-primary)]", - largeUseCases: - "!outline-[1.75px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.75px] focus:!outline-[var(--color-content-default-brand-primary)]", - homeXlarge: - "!outline-[2px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[2px] focus:!outline-[var(--color-content-default-brand-primary)]", - xlarge: - "!outline-2 !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-2 focus:!outline-[var(--color-content-default-brand-primary)]", - }; + const homeOutlineStyles = { + xsmall: + "active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]", + xsmallUseCases: + "active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]", + default: + "active:outline-[1.5px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-primary)]", + homeMd: + "active:outline-[1.5px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-primary)]", + homeUseCases: + "active:outline-[1.5px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-primary)]", + largeUseCases: + "active:outline-[1.75px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-primary)]", + large: + "active:outline-[1.75px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-primary)]", + homeXlarge: + "active:outline-[2px] active:outline-[var(--color-content-default-primary)] focus:outline-[2px] focus:outline-[var(--color-content-default-primary)]", + xlarge: + "active:outline-2 active:outline-[var(--color-content-default-primary)] focus:outline-2 focus:outline-[var(--color-content-default-primary)]", + }; - const sizeStyles = { - default: - "px-[var(--spacing-measures-spacing-016)] py-[var(--spacing-measures-spacing-016)] gap-[var(--spacing-scale-004)]", - xsmall: - "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)] gap-[var(--spacing-scale-004)]", - xsmallUseCases: - "px-[var(--spacing-scale-002)] py-[var(--spacing-scale-002)] gap-[var(--spacing-scale-004)]", - homeMd: - "px-[var(--spacing-scale-008)] py-[var(--spacing-scale-008)] gap-[var(--spacing-scale-004)]", - homeUseCases: - "px-[var(--spacing-scale-002)] py-[var(--spacing-scale-008)] gap-[var(--spacing-scale-004)]", - large: - "px-[var(--spacing-scale-012)] py-[var(--spacing-scale-012)] gap-[var(--spacing-scale-004)] h-[44px]", - largeUseCases: - "px-[var(--spacing-scale-012)] py-[var(--spacing-scale-012)] gap-[var(--spacing-scale-004)] h-[44px]", - homeXlarge: - "px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] gap-[var(--spacing-scale-004)] h-[44px]", - xlarge: - "px-[var(--spacing-scale-016)] py-[var(--spacing-scale-008)] gap-[var(--spacing-scale-004)] h-[44px]", - }; + const activeStateStyles = { + xsmall: + "!outline-1 !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-1 focus:!outline-[var(--color-content-default-brand-primary)]", + xsmallUseCases: + "!outline-1 !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-1 focus:!outline-[var(--color-content-default-brand-primary)]", + default: + "!outline-[1.5px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.5px] focus:!outline-[var(--color-content-default-brand-primary)]", + homeMd: + "!outline-[1.5px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.5px] focus:!outline-[var(--color-content-default-brand-primary)]", + homeUseCases: + "!outline-[1.5px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.5px] focus:!outline-[var(--color-content-default-brand-primary)]", + large: + "!outline-[1.75px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.75px] focus:!outline-[var(--color-content-default-brand-primary)]", + largeUseCases: + "!outline-[1.75px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.75px] focus:!outline-[var(--color-content-default-brand-primary)]", + homeXlarge: + "!outline-[2px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[2px] focus:!outline-[var(--color-content-default-brand-primary)]", + xlarge: + "!outline-2 !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-2 focus:!outline-[var(--color-content-default-brand-primary)]", + }; - const smallTextStyle = - "font-inter text-[10px] leading-[12px] font-medium tracking-[0%]"; - const mediumTextStyle = - "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]"; - const largeTextStyle = - "font-inter text-[16px] leading-[20px] font-medium tracking-[0%]"; - const xlargeTextStyle = - "font-inter text-[24px] leading-[28px] font-normal tracking-[0%]"; + const sizeStyles = { + default: + "px-[var(--spacing-measures-spacing-016)] py-[var(--spacing-measures-spacing-016)] gap-[var(--spacing-scale-004)]", + xsmall: + "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)] gap-[var(--spacing-scale-004)]", + xsmallUseCases: + "px-[var(--spacing-scale-002)] py-[var(--spacing-scale-002)] gap-[var(--spacing-scale-004)]", + homeMd: + "px-[var(--spacing-scale-008)] py-[var(--spacing-scale-008)] gap-[var(--spacing-scale-004)]", + homeUseCases: + "px-[var(--spacing-scale-002)] py-[var(--spacing-scale-008)] gap-[var(--spacing-scale-004)]", + large: + "px-[var(--spacing-scale-012)] py-[var(--spacing-scale-012)] gap-[var(--spacing-scale-004)] h-[44px]", + largeUseCases: + "px-[var(--spacing-scale-012)] py-[var(--spacing-scale-012)] gap-[var(--spacing-scale-004)] h-[44px]", + homeXlarge: + "px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] gap-[var(--spacing-scale-004)] h-[44px]", + xlarge: + "px-[var(--spacing-scale-016)] py-[var(--spacing-scale-008)] gap-[var(--spacing-scale-004)] h-[44px]", + }; - const textStyles = { - default: smallTextStyle, - xsmall: smallTextStyle, - xsmallUseCases: smallTextStyle, - home: smallTextStyle, - homeMd: mediumTextStyle, - homeUseCases: mediumTextStyle, - large: largeTextStyle, - largeUseCases: largeTextStyle, - homeXlarge: xlargeTextStyle, - xlarge: xlargeTextStyle, - }; + const smallTextStyle = + "font-inter text-[10px] leading-[12px] font-medium tracking-[0%]"; + const mediumTextStyle = + "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]"; + const largeTextStyle = + "font-inter text-[16px] leading-[20px] font-medium tracking-[0%]"; + const xlargeTextStyle = + "font-inter text-[24px] leading-[28px] font-normal tracking-[0%]"; - const baseStyles = `inline-flex items-center ${sizeStyles[size]} rounded-[var(--radius-measures-radius-full)] ${textStyles[size]} transition-all duration-200 ease-in-out cursor-pointer focus:scale-[1.02]`; + const textStyles = { + default: smallTextStyle, + xsmall: smallTextStyle, + xsmallUseCases: smallTextStyle, + home: smallTextStyle, + homeMd: mediumTextStyle, + homeUseCases: mediumTextStyle, + large: largeTextStyle, + largeUseCases: largeTextStyle, + homeXlarge: xlargeTextStyle, + xlarge: xlargeTextStyle, + }; - let finalVariant = variant; - if (disabled) { - finalVariant = "default"; - } + const baseStyles = `inline-flex items-center ${sizeStyles[size]} rounded-[var(--radius-measures-radius-full)] ${textStyles[size]} transition-all duration-200 ease-in-out cursor-pointer focus:scale-[1.02]`; - const combinedStyles = `${baseStyles} ${variantStyles[finalVariant]} ${ - finalVariant === "home" - ? homeOutlineStyles[size] - : activeOutlineStyles[size] - } ${isActive ? activeStateStyles[size] : ""} ${className}`; + let finalVariant = variant; + if (disabled) { + finalVariant = "default"; + } - const accessibilityProps = { - ...(ariaLabel && { "aria-label": ariaLabel }), - ...(disabled && { "aria-disabled": "true" }), - role: "menuitem", - tabIndex: disabled ? -1 : 0, - ...props, - }; + const combinedStyles = `${baseStyles} ${variantStyles[finalVariant]} ${ + finalVariant === "home" + ? homeOutlineStyles[size] + : activeOutlineStyles[size] + } ${isActive ? activeStateStyles[size] : ""} ${className}`; + + const accessibilityProps = { + ...(ariaLabel && { "aria-label": ariaLabel }), + ...(disabled && { "aria-disabled": "true" }), + role: "menuitem", + tabIndex: disabled ? -1 : 0, + ...props, + }; + + if (disabled) { + return ( + + {children} + + ); + } - if (disabled) { return ( - + {children} - + ); } +); - return ( - - {children} - - ); -} +MenuBarItem.displayName = "MenuBarItem"; + +export default MenuBarItem; diff --git a/app/components/MiniCard.js b/app/components/MiniCard.js index a1e2b3a..aaccc6a 100644 --- a/app/components/MiniCard.js +++ b/app/components/MiniCard.js @@ -1,112 +1,124 @@ "use client"; -import React from "react"; +import React, { memo } from "react"; import Image from "next/image"; -const MiniCard = ({ - children, - className = "", - backgroundColor = "bg-[var(--color-surface-default-brand-royal)]", - panelContent, - label, - labelLine1, - labelLine2, - onClick, - href, - ariaLabel, -}) => { - const cardContent = ( -
- {/* Top part - Inner panel */} -
- {/* Content for the inner panel */} - {panelContent && ( -
- { -
- )} - {children} -
+const MiniCard = memo( + ({ + children, + className = "", + backgroundColor = "bg-[var(--color-surface-default-brand-royal)]", + panelContent, + label, + labelLine1, + labelLine2, + onClick, + href, + ariaLabel, + }) => { + const cardContent = ( +
+ {/* Top part - Inner panel */} +
+ {/* Content for the inner panel */} + {panelContent && ( +
+ { +
+ )} + {children} +
- {/* Bottom part - Text container */} -
- {labelLine1 && labelLine2 ? ( - <> -
{labelLine1}
-
{labelLine2}
-
 
- - ) : ( - label - )} + {/* Bottom part - Text container */} +
+ {labelLine1 && labelLine2 ? ( + <> +
{labelLine1}
+
{labelLine2}
+
 
+ + ) : ( + label + )} +
-
- ); - - // If href is provided, render as a link - if (href) { - return ( - - {cardContent} - ); - } - // If onClick is provided, render as a button - if (onClick) { - return ( - + ); + } + + // Default render as a div + return ( +
{cardContent} - +
); } +); - // Default render as a div - return ( -
- {cardContent} -
- ); -}; +MiniCard.displayName = "MiniCard"; export default MiniCard; diff --git a/app/components/NavigationItem.js b/app/components/NavigationItem.js index 68075a5..fefe774 100644 --- a/app/components/NavigationItem.js +++ b/app/components/NavigationItem.js @@ -1,4 +1,6 @@ -export default function NavigationItem({ +import React, { memo } from "react"; + +const NavigationItem = memo(({ href = "#", children, variant = "default", @@ -50,4 +52,8 @@ export default function NavigationItem({ {children} ); -} +}); + +NavigationItem.displayName = "NavigationItem"; + +export default NavigationItem; diff --git a/app/components/NumberedCard.js b/app/components/NumberedCard.js index c0c91bd..8a53bc7 100644 --- a/app/components/NumberedCard.js +++ b/app/components/NumberedCard.js @@ -1,8 +1,9 @@ "use client"; +import React, { memo } from "react"; import SectionNumber from "./SectionNumber"; -const NumberedCard = ({ number, text, iconShape, iconColor }) => { +const NumberedCard = memo(({ number, text, iconShape, iconColor }) => { return (
{/* Section Number - Top right (lg breakpoint) */} @@ -18,6 +19,8 @@ const NumberedCard = ({ number, text, iconShape, iconColor }) => {
); -}; +}); + +NumberedCard.displayName = "NumberedCard"; export default NumberedCard; diff --git a/app/components/NumberedCards.js b/app/components/NumberedCards.js index ac196d7..23f9e7a 100644 --- a/app/components/NumberedCards.js +++ b/app/components/NumberedCards.js @@ -1,23 +1,27 @@ "use client"; +import React, { memo, useMemo } from "react"; import NumberedCard from "./NumberedCard"; import SectionHeader from "./SectionHeader"; import Button from "./Button"; -const NumberedCards = ({ title, subtitle, cards }) => { - // Schema markup for SEO - const schemaData = { - "@context": "https://schema.org", - "@type": "HowTo", - name: title, - description: subtitle, - step: cards.map((card, index) => ({ - "@type": "HowToStep", - position: index + 1, - name: card.text, - text: card.text, - })), - }; +const NumberedCards = memo(({ title, subtitle, cards }) => { + // Memoize schema data to prevent unnecessary re-computations + const schemaData = useMemo( + () => ({ + "@context": "https://schema.org", + "@type": "HowTo", + name: title, + description: subtitle, + step: cards.map((card, index) => ({ + "@type": "HowToStep", + position: index + 1, + name: card.text, + text: card.text, + })), + }), + [title, subtitle, cards] + ); return ( <> @@ -70,6 +74,8 @@ const NumberedCards = ({ title, subtitle, cards }) => { ); -}; +}); + +NumberedCards.displayName = "NumberedCards"; export default NumberedCards; diff --git a/app/components/QuoteBlock.js b/app/components/QuoteBlock.js index 2609426..f67f232 100644 --- a/app/components/QuoteBlock.js +++ b/app/components/QuoteBlock.js @@ -1,247 +1,252 @@ "use client"; -import React, { useState } from "react"; +import React, { useState, memo } from "react"; import Image from "next/image"; import QuoteDecor from "./QuoteDecor"; -const QuoteBlock = ({ - variant = "standard", - className = "", - quote = "The rules of decision-making must be open and available to everyone, and this can happen only if they are formalized.", - author = "Jo Freeman", - source = "The Tyranny of Structurelessness", - avatarSrc = "assets/Quote_Avatar.svg", - id, - fallbackAvatarSrc = "assets/Quote_Avatar.svg", // Fallback avatar - onError, // Error callback -}) => { - const [imageError, setImageError] = useState(false); - const [imageLoading, setImageLoading] = useState(true); +const QuoteBlock = memo( + ({ + variant = "standard", + className = "", + quote = "The rules of decision-making must be open and available to everyone, and this can happen only if they are formalized.", + author = "Jo Freeman", + source = "The Tyranny of Structurelessness", + avatarSrc = "/assets/Quote_Avatar.svg", + id, + fallbackAvatarSrc = "/assets/Quote_Avatar.svg", // Fallback avatar + onError, // Error callback + }) => { + const [imageError, setImageError] = useState(false); + const [imageLoading, setImageLoading] = useState(true); - // Variant configurations - const variants = { - compact: { - container: "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-016)]", - card: "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-016)] md:py-[var(--spacing-scale-040)] md:px-[var(--spacing-scale-024)] rounded-[var(--radius-measures-radius-small)]", - gap: "gap-[var(--spacing-scale-016)] md:gap-[var(--spacing-scale-024)]", - avatarGap: "gap-[var(--spacing-scale-012)]", - avatar: "w-[48px] h-[48px] md:w-[64px] md:h-[64px]", - quote: "text-[16px] leading-[120%] md:text-[20px] md:leading-[110%]", - author: "text-[10px] leading-[120%] md:text-[12px]", - source: "text-[10px] leading-[120%] md:text-[12px]", - showDecor: false, - }, - standard: { - container: - "md:py-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-016)] lg:p-[var(--spacing-scale-064)]", - card: "py-[var(--spacing-scale-064)] px-[var(--spacing-scale-020)] md:py-[var(--spacing-scale-064)] md:px-[var(--spacing-scale-048)] md:rounded-[var(--radius-measures-radius-medium)] lg:py-[var(--spacing-scale-064)] lg:pl-[120px] lg:pr-[320px]", - gap: "gap-[var(--spacing-scale-024)] md:gap-[var(--spacing-scale-048)] lg:gap-[var(--spacing-scale-064)] xl:gap-[105px]", - avatarGap: - "gap-[var(--spacing-scale-020)] lg:gap-[var(--spacing-scale-018)] xl:gap-[var(--spacing-scale-032)]", - avatar: - "md:w-[120px] md:h-[120px] lg:w-[150px] lg:h-[150px] xl:w-[200px] xl:h-[200px]", - quote: - "text-[18px] leading-[120%] md:text-[36px] md:leading-[110%] md:tracking-[0px] lg:text-[52px] xl:text-[64px]", - author: - "text-[12px] leading-[120%] md:text-[18px] md:leading-[120%] md:tracking-[0.24px] lg:text-[24px] xl:text-[32px]", - source: - "text-[12px] leading-[120%] md:text-[18px] md:leading-[120%] md:tracking-[0.24px] lg:text-[24px] xl:text-[32px]", - showDecor: true, - }, - extended: { - container: - "py-[var(--spacing-scale-048)] px-[var(--spacing-scale-024)] md:py-[var(--spacing-scale-064)] md:px-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-080)] lg:px-[var(--spacing-scale-048)]", - card: "py-[var(--spacing-scale-080)] px-[var(--spacing-scale-032)] md:py-[var(--spacing-scale-096)] md:px-[var(--spacing-scale-064)] md:rounded-[var(--radius-measures-radius-large)] lg:py-[var(--spacing-scale-112)] lg:pl-[160px] lg:pr-[400px]", - gap: "gap-[var(--spacing-scale-032)] md:gap-[var(--spacing-scale-064)] lg:gap-[var(--spacing-scale-080)] xl:gap-[140px]", - avatarGap: - "gap-[var(--spacing-scale-032)] lg:gap-[var(--spacing-scale-040)] xl:gap-[var(--spacing-scale-048)]", - avatar: - "w-[80px] h-[80px] md:w-[140px] md:h-[140px] lg:w-[180px] lg:h-[180px] xl:w-[240px] xl:h-[240px]", - quote: - "text-[20px] leading-[120%] md:text-[40px] md:leading-[110%] md:tracking-[0px] lg:text-[60px] xl:text-[72px]", - author: - "text-[14px] leading-[120%] md:text-[20px] md:leading-[120%] md:tracking-[0.24px] lg:text-[28px] xl:text-[36px]", - source: - "text-[14px] leading-[120%] md:text-[20px] md:leading-[120%] md:tracking-[0.24px] lg:text-[28px] xl:text-[36px]", - showDecor: true, - }, - }; + // Variant configurations + const variants = { + compact: { + container: + "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-016)]", + card: "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-016)] md:py-[var(--spacing-scale-040)] md:px-[var(--spacing-scale-024)] rounded-[var(--radius-measures-radius-small)]", + gap: "gap-[var(--spacing-scale-016)] md:gap-[var(--spacing-scale-024)]", + avatarGap: "gap-[var(--spacing-scale-012)]", + avatar: "w-[48px] h-[48px] md:w-[64px] md:h-[64px]", + quote: "text-[16px] leading-[120%] md:text-[20px] md:leading-[110%]", + author: "text-[10px] leading-[120%] md:text-[12px]", + source: "text-[10px] leading-[120%] md:text-[12px]", + showDecor: false, + }, + standard: { + container: + "md:py-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-016)] lg:p-[var(--spacing-scale-064)]", + card: "py-[var(--spacing-scale-064)] px-[var(--spacing-scale-020)] md:py-[var(--spacing-scale-064)] md:px-[var(--spacing-scale-048)] md:rounded-[var(--radius-measures-radius-medium)] lg:py-[var(--spacing-scale-064)] lg:pl-[120px] lg:pr-[320px]", + gap: "gap-[var(--spacing-scale-024)] md:gap-[var(--spacing-scale-048)] lg:gap-[var(--spacing-scale-064)] xl:gap-[105px]", + avatarGap: + "gap-[var(--spacing-scale-020)] lg:gap-[var(--spacing-scale-018)] xl:gap-[var(--spacing-scale-032)]", + avatar: + "md:w-[120px] md:h-[120px] lg:w-[150px] lg:h-[150px] xl:w-[200px] xl:h-[200px]", + quote: + "text-[18px] leading-[120%] md:text-[36px] md:leading-[110%] md:tracking-[0px] lg:text-[52px] xl:text-[64px]", + author: + "text-[12px] leading-[120%] md:text-[18px] md:leading-[120%] md:tracking-[0.24px] lg:text-[24px] xl:text-[32px]", + source: + "text-[12px] leading-[120%] md:text-[18px] md:leading-[120%] md:tracking-[0.24px] lg:text-[24px] xl:text-[32px]", + showDecor: true, + }, + extended: { + container: + "py-[var(--spacing-scale-048)] px-[var(--spacing-scale-024)] md:py-[var(--spacing-scale-064)] md:px-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-080)] lg:px-[var(--spacing-scale-048)]", + card: "py-[var(--spacing-scale-080)] px-[var(--spacing-scale-032)] md:py-[var(--spacing-scale-096)] md:px-[var(--spacing-scale-064)] md:rounded-[var(--radius-measures-radius-large)] lg:py-[var(--spacing-scale-112)] lg:pl-[160px] lg:pr-[400px]", + gap: "gap-[var(--spacing-scale-032)] md:gap-[var(--spacing-scale-064)] lg:gap-[var(--spacing-scale-080)] xl:gap-[140px]", + avatarGap: + "gap-[var(--spacing-scale-032)] lg:gap-[var(--spacing-scale-040)] xl:gap-[var(--spacing-scale-048)]", + avatar: + "w-[80px] h-[80px] md:w-[140px] md:h-[140px] lg:w-[180px] lg:h-[180px] xl:w-[240px] xl:h-[240px]", + quote: + "text-[20px] leading-[120%] md:text-[40px] md:leading-[110%] md:tracking-[0px] lg:text-[60px] xl:text-[72px]", + author: + "text-[14px] leading-[120%] md:text-[20px] md:leading-[120%] md:tracking-[0.24px] lg:text-[28px] xl:text-[36px]", + source: + "text-[14px] leading-[120%] md:text-[20px] md:leading-[120%] md:tracking-[0.24px] lg:text-[28px] xl:text-[36px]", + showDecor: true, + }, + }; - const config = variants[variant] || variants.standard; + 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 quoteId = `${baseId}-content`; - const authorId = `${baseId}-author`; + // Use provided ID or generate a stable one based on content + const baseId = id || `quote-${author.toLowerCase().replace(/\s+/g, "-")}`; + const quoteId = `${baseId}-content`; + const authorId = `${baseId}-author`; - // Error handling functions - const handleImageError = (error) => { - console.warn( - `QuoteBlock: Failed to load avatar image for ${author}:`, - error, - ); - setImageError(true); - setImageLoading(false); + // Error handling functions + const handleImageError = (error) => { + console.warn( + `QuoteBlock: Failed to load avatar image for ${author}:`, + error + ); + setImageError(true); + setImageLoading(false); - // Call error callback if provided - if (onError) { - onError({ - type: "image_load_error", - message: `Failed to load avatar for ${author}`, - author, - avatarSrc, - error, - }); + // Call error callback if provided + if (onError) { + onError({ + type: "image_load_error", + message: `Failed to load avatar for ${author}`, + author, + avatarSrc, + error, + }); + } + }; + + const handleImageLoad = () => { + setImageLoading(false); + setImageError(false); + }; + + // Validate required props + if (!quote || !author) { + console.error("QuoteBlock: Missing required props (quote or author)"); + if (onError) { + onError({ + type: "missing_props", + message: "QuoteBlock requires quote and author props", + quote: !!quote, + author: !!author, + }); + } + return null; // Don't render if missing required props } - }; - const handleImageLoad = () => { - setImageLoading(false); - setImageError(false); - }; + // Determine which avatar to use + const currentAvatarSrc = imageError ? fallbackAvatarSrc : avatarSrc; - // Validate required props - if (!quote || !author) { - console.error("QuoteBlock: Missing required props (quote or author)"); - if (onError) { - onError({ - type: "missing_props", - message: "QuoteBlock requires quote and author props", - quote: !!quote, - author: !!author, - }); - } - return null; // Don't render if missing required props - } - - // Determine which avatar to use - const currentAvatarSrc = imageError ? fallbackAvatarSrc : avatarSrc; - - return ( -
-
- {/* Background with noise texture */}
#grain\')', - }} - /> + className={`${config.card} bg-[var(--color-surface-default-brand-darker-accent)] relative overflow-hidden`} + > + {/* Background with noise texture */} +
#grain\')', + }} + /> - {/* DECORATIONS (behind content) */} - {config.showDecor && ( -
- ); -}; + + ); + } +); + +QuoteBlock.displayName = "QuoteBlock"; export default QuoteBlock; diff --git a/app/components/QuoteDecor.js b/app/components/QuoteDecor.js index cd2a5f4..2efe876 100644 --- a/app/components/QuoteDecor.js +++ b/app/components/QuoteDecor.js @@ -1,6 +1,8 @@ "use client"; -const QuoteDecor = ({ className = "" }) => { +import React, { memo } from "react"; + +const QuoteDecor = memo(({ className = "" }) => { return ( { ); -}; +}); + +QuoteDecor.displayName = "QuoteDecor"; export default QuoteDecor; diff --git a/app/components/RelatedArticles.js b/app/components/RelatedArticles.js index 4cdf891..a5cd6ed 100644 --- a/app/components/RelatedArticles.js +++ b/app/components/RelatedArticles.js @@ -1,152 +1,163 @@ "use client"; -import { useState, useEffect } from "react"; +import React, { useState, useEffect, memo, useMemo, useCallback } from "react"; import ContentThumbnailTemplate from "./ContentThumbnailTemplate"; -export default function RelatedArticles({ - relatedPosts, - currentPostSlug, - slugOrder = [], -}) { - // Filter out the current post from related posts - const filteredPosts = relatedPosts.filter( - (post) => post.slug !== currentPostSlug, - ); +const RelatedArticles = memo( + ({ relatedPosts, currentPostSlug, slugOrder = [] }) => { + // Memoize filtered posts to prevent unnecessary re-computations + const filteredPosts = useMemo( + () => relatedPosts.filter((post) => post.slug !== currentPostSlug), + [relatedPosts, currentPostSlug] + ); - const [currentIndex, setCurrentIndex] = useState(0); - const [progress, setProgress] = useState(0); - const [isMobile, setIsMobile] = useState(true); + const [currentIndex, setCurrentIndex] = useState(0); + const [progress, setProgress] = useState(0); + const [isMobile, setIsMobile] = useState(true); - // Check if we're on mobile (below lg breakpoint) - useEffect(() => { - const checkScreenSize = () => { - setIsMobile(window.innerWidth < 1024); // lg breakpoint is 1024px - }; + // Memoize the mouse down handler to prevent unnecessary re-renders + const handleMouseDown = useCallback((e) => { + const slider = e.currentTarget; + const startX = e.pageX - slider.offsetLeft; + const scrollLeft = slider.scrollLeft; - checkScreenSize(); - window.addEventListener("resize", checkScreenSize); - return () => window.removeEventListener("resize", checkScreenSize); - }, []); + const handleMouseMove = (e) => { + const x = e.pageX - slider.offsetLeft; + const walk = (x - startX) * 2; + slider.scrollLeft = scrollLeft - walk; + }; - // Auto-advance every 3 seconds (only on mobile) - useEffect(() => { - if (filteredPosts.length <= 1 || !isMobile) return; + const handleMouseUp = () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; - const interval = setInterval(() => { - setProgress(0); - setCurrentIndex((prev) => (prev + 1) % filteredPosts.length); - }, 3000); + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }, []); - return () => clearInterval(interval); - }, [filteredPosts.length, isMobile]); + // Memoize transform style to prevent unnecessary recalculations + const transformStyle = useMemo( + () => ({ + transform: isMobile + ? `translateX(calc(50% - 130px - ${currentIndex * 260}px))` + : "none", + scrollBehavior: !isMobile ? "smooth" : "auto", + }), + [isMobile, currentIndex] + ); - // Progress animation (only on mobile) - useEffect(() => { - if (filteredPosts.length <= 1 || !isMobile) return; + // Memoize progress bar style calculation + const getProgressStyle = useCallback( + (index) => ({ + width: + index === currentIndex + ? `${progress}%` + : index < currentIndex + ? "100%" + : "0%", + }), + [currentIndex, progress] + ); - const progressInterval = setInterval(() => { - setProgress((prev) => { - if (prev >= 100) { - return 0; - } - return prev + 1; - }); - }, 30); // 30ms intervals for smooth animation + // Check if we're on mobile (below lg breakpoint) + useEffect(() => { + const checkScreenSize = () => { + setIsMobile(window.innerWidth < 1024); // lg breakpoint is 1024px + }; - return () => clearInterval(progressInterval); - }, [currentIndex, filteredPosts.length, isMobile]); + checkScreenSize(); + window.addEventListener("resize", checkScreenSize); + return () => window.removeEventListener("resize", checkScreenSize); + }, []); - if (filteredPosts.length === 0) { - return null; - } + // Auto-advance every 3 seconds (only on mobile) + useEffect(() => { + if (filteredPosts.length <= 1 || !isMobile) return; - return ( -
-
-

- Related Articles -

+ const interval = setInterval(() => { + setProgress(0); + setCurrentIndex((prev) => (prev + 1) % filteredPosts.length); + }, 3000); - {/* Horizontal Articles Row - Carousel on mobile, Scrollable slider on desktop */} -
-
{ - const slider = e.currentTarget; - const startX = e.pageX - slider.offsetLeft; - const scrollLeft = slider.scrollLeft; + return () => clearInterval(interval); + }, [filteredPosts.length, isMobile]); - const handleMouseMove = (e) => { - const x = e.pageX - slider.offsetLeft; - const walk = (x - startX) * 2; - slider.scrollLeft = scrollLeft - walk; - }; + // Progress animation (only on mobile) + useEffect(() => { + if (filteredPosts.length <= 1 || !isMobile) return; - const handleMouseUp = () => { - document.removeEventListener( - "mousemove", - handleMouseMove, - ); - document.removeEventListener("mouseup", handleMouseUp); - }; + const progressInterval = setInterval(() => { + setProgress((prev) => { + if (prev >= 100) { + return 0; + } + return prev + 1; + }); + }, 30); // 30ms intervals for smooth animation - document.addEventListener("mousemove", handleMouseMove); - document.addEventListener("mouseup", handleMouseUp); - } - : undefined - } - > - {filteredPosts.map((relatedPost, index) => ( -
- -
- ))} -
-
+ return () => clearInterval(progressInterval); + }, [currentIndex, filteredPosts.length, isMobile]); - {/* Progress bars - only show on mobile */} - {isMobile && ( -
- {filteredPosts.map((relatedPost, index) => ( -
+ if (filteredPosts.length === 0) { + return null; + } + + return ( +
+
+

+ Related Articles +

+ + {/* Horizontal Articles Row - Carousel on mobile, Scrollable slider on desktop */} +
+
+ {filteredPosts.map((relatedPost, index) => (
-
- ))} + key={relatedPost.slug} + className="flex flex-col items-center flex-shrink-0" + > + +
+ ))} +
- )} -
-
- ); -} + + {/* Progress bars - only show on mobile */} + {isMobile && ( +
+ {filteredPosts.map((relatedPost, index) => ( +
+
+
+ ))} +
+ )} +
+ + ); + } +); + +RelatedArticles.displayName = "RelatedArticles"; + +export default RelatedArticles; diff --git a/app/components/RuleCard.js b/app/components/RuleCard.js index 291b90f..1588246 100644 --- a/app/components/RuleCard.js +++ b/app/components/RuleCard.js @@ -1,73 +1,79 @@ "use client"; -const RuleCard = ({ - title, - description, - icon, - backgroundColor = "bg-[var(--color-community-teal-100)]", - className = "", - onClick, -}) => { - const handleClick = () => { - // Basic analytics event tracking - if (typeof window !== "undefined" && window.gtag) { - window.gtag("event", "template_selected", { - template_name: title, - template_type: "governance_pattern", - }); - } +import React, { memo } from "react"; - // Custom analytics event for other tracking systems - if (typeof window !== "undefined" && window.analytics) { - window.analytics.track("Template Selected", { - templateName: title, - templateType: "governance_pattern", - }); - } +const RuleCard = memo( + ({ + title, + description, + icon, + backgroundColor = "bg-[var(--color-community-teal-100)]", + className = "", + onClick, + }) => { + const handleClick = () => { + // Basic analytics event tracking + if (typeof window !== "undefined" && window.gtag) { + window.gtag("event", "template_selected", { + template_name: title, + template_type: "governance_pattern", + }); + } - if (onClick) onClick(); - }; + // Custom analytics event for other tracking systems + if (typeof window !== "undefined" && window.analytics) { + window.analytics.track("Template Selected", { + templateName: title, + templateType: "governance_pattern", + }); + } - const handleKeyDown = (event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - handleClick(); - } - }; + if (onClick) onClick(); + }; - return ( -
- {/* Header Container */} -
- {/* Icon Container */} - {icon && ( -
- {icon} -
- )} - {/* Title Container */} - {title && ( -
-

- {title} -

-
+ const handleKeyDown = (event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + handleClick(); + } + }; + + return ( +
+ {/* Header Container */} +
+ {/* Icon Container */} + {icon && ( +
+ {icon} +
+ )} + {/* Title Container */} + {title && ( +
+

+ {title} +

+
+ )} +
+ {description && ( +

+ {description} +

)}
- {description && ( -

- {description} -

- )} -
- ); -}; + ); + } +); + +RuleCard.displayName = "RuleCard"; export default RuleCard; diff --git a/app/components/RuleStack.js b/app/components/RuleStack.js index 1d67509..3c00d7f 100644 --- a/app/components/RuleStack.js +++ b/app/components/RuleStack.js @@ -1,12 +1,12 @@ "use client"; -import React from "react"; +import React, { memo } from "react"; import Image from "next/image"; import RuleCard from "./RuleCard"; import Button from "./Button"; import { getAssetPath } from "../../lib/assetUtils"; -const RuleStack = ({ className = "" }) => { +const RuleStack = memo(({ className = "" }) => { const handleTemplateClick = (templateName) => { // Basic analytics tracking if (typeof window !== "undefined") { @@ -99,6 +99,8 @@ const RuleStack = ({ className = "" }) => {
); -}; +}); + +RuleStack.displayName = "RuleStack"; export default RuleStack; diff --git a/app/components/SectionHeader.js b/app/components/SectionHeader.js index 6be3c1b..244f0f8 100644 --- a/app/components/SectionHeader.js +++ b/app/components/SectionHeader.js @@ -1,54 +1,60 @@ "use client"; -const SectionHeader = ({ title, subtitle, titleLg, variant = "default" }) => { - return ( -
- {/* Title Container - Left side (lg breakpoint) */} -
-

- {title} - {titleLg || title} -

-
+import React, { memo } from "react"; - {/* Subtitle Container */} +const SectionHeader = memo( + ({ title, subtitle, titleLg, variant = "default" }) => { + return (
-

- {subtitle} -

+

+ {title} + {titleLg || title} +

+
+ + {/* Subtitle Container */} +
+

+ {subtitle} +

+
-
- ); -}; + ); + } +); + +SectionHeader.displayName = "SectionHeader"; export default SectionHeader; diff --git a/app/components/SectionNumber.js b/app/components/SectionNumber.js index 14d5f4d..b2c847e 100644 --- a/app/components/SectionNumber.js +++ b/app/components/SectionNumber.js @@ -1,16 +1,18 @@ "use client"; -const SectionNumber = ({ number }) => { +import React, { memo } from "react"; + +const SectionNumber = memo(({ number }) => { const getImageSrc = (num) => { switch (num) { case 1: - return "assets/SectionNumber_1.png"; + return "/assets/SectionNumber_1.png"; case 2: - return "assets/SectionNumber_2.png"; + return "/assets/SectionNumber_2.png"; case 3: - return "assets/SectionNumber_3.png"; + return "/assets/SectionNumber_3.png"; default: - return "assets/SectionNumber_1.png"; + return "/assets/SectionNumber_1.png"; } }; @@ -28,6 +30,8 @@ const SectionNumber = ({ number }) => {
); -}; +}); + +SectionNumber.displayName = "SectionNumber"; export default SectionNumber; diff --git a/app/components/Separator.js b/app/components/Separator.js index f7eff38..d487c59 100644 --- a/app/components/Separator.js +++ b/app/components/Separator.js @@ -1,7 +1,13 @@ -export default function Separator() { +import React, { memo } from "react"; + +const Separator = memo(() => { return (
); -} +}); + +Separator.displayName = "Separator"; + +export default Separator; diff --git a/app/layout.js b/app/layout.js index 871f049..89d3c51 100644 --- a/app/layout.js +++ b/app/layout.js @@ -10,6 +10,8 @@ const inter = Inter({ weight: ["400", "500", "600", "700"], variable: "--font-inter", display: "swap", + preload: true, + fallback: ["system-ui", "arial"], }); const bricolageGrotesque = Bricolage_Grotesque({ @@ -17,6 +19,8 @@ const bricolageGrotesque = Bricolage_Grotesque({ weight: ["400", "500", "700", "800"], variable: "--font-bricolage-grotesque", display: "swap", + preload: true, + fallback: ["system-ui", "arial"], }); const spaceGrotesk = Space_Grotesk({ @@ -24,6 +28,8 @@ const spaceGrotesk = Space_Grotesk({ weight: ["400", "500", "700"], variable: "--font-space-grotesk", display: "swap", + preload: true, + fallback: ["system-ui", "arial"], }); export const metadata = { diff --git a/next.config.mjs b/next.config.mjs index 9f27b3d..27b957b 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -5,12 +5,69 @@ const nextConfig = { eslint: { ignoreDuringBuilds: true, }, - webpack(config) { + // Performance optimizations + experimental: { + optimizeCss: true, + optimizePackageImports: ["react", "react-dom"], + }, + // Compression + compress: true, + // Image optimization + images: { + formats: ["image/webp", "image/avif"], + minimumCacheTTL: 60, + dangerouslyAllowSVG: true, + contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", + }, + // Headers for caching + async headers() { + return [ + { + source: "/(.*)", + headers: [ + { + key: "X-Content-Type-Options", + value: "nosniff", + }, + { + key: "X-Frame-Options", + value: "DENY", + }, + { + key: "X-XSS-Protection", + value: "1; mode=block", + }, + ], + }, + { + source: "/static/(.*)", + headers: [ + { + key: "Cache-Control", + value: "public, max-age=31536000, immutable", + }, + ], + }, + ]; + }, + webpack(config, { dev, isServer }) { + // SVG handling config.module.rules.push({ test: /\.svg$/, issuer: /\.[jt]sx?$/, use: ["@svgr/webpack"], }); + + // Production optimizations + if (!dev && !isServer) { + // Tree shaking optimization + config.optimization = { + ...config.optimization, + usedExports: true, + sideEffects: false, + }; + } + return config; }, }; diff --git a/package-lock.json b/package-lock.json index f7839de..a641a1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@mdx-js/loader": "^3.1.1", "@mdx-js/react": "^3.1.1", "@next/mdx": "^15.5.2", + "critters": "^0.0.23", "gray-matter": "^4.0.3", "next": "15.2.4", "react": "^19.0.0", @@ -8097,7 +8098,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -8890,7 +8890,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, "license": "ISC" }, "node_modules/brace-expansion": { @@ -9185,7 +9184,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -9491,7 +9489,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -9504,7 +9501,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "devOptional": true, "license": "MIT" }, "node_modules/color-string": { @@ -9778,6 +9774,95 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/critters": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.23.tgz", + "integrity": "sha512-/MCsQbuzTPA/ZTOjjyr2Na5o3lRpr8vd0MZE8tMP0OBNg/VrLxWHteVKalQ8KR+fBmUadbJLdoyEz9sT+q84qg==", + "license": "Apache-2.0", + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^5.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.2", + "htmlparser2": "^8.0.2", + "postcss": "^8.4.23", + "postcss-media-query-parser": "^0.2.3" + } + }, + "node_modules/critters/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/critters/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/critters/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/critters/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/critters/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -9814,7 +9899,6 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -9831,7 +9915,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", @@ -9846,7 +9929,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, "funding": [ { "type": "github", @@ -9859,7 +9941,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" @@ -9875,7 +9956,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -9904,7 +9984,6 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -10576,7 +10655,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -12782,7 +12860,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -18108,7 +18185,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" @@ -19163,7 +19239,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -19188,6 +19263,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -21534,7 +21615,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" diff --git a/package.json b/package.json index afe21ef..3f57505 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@mdx-js/loader": "^3.1.1", "@mdx-js/react": "^3.1.1", "@next/mdx": "^15.5.2", + "critters": "^0.0.23", "gray-matter": "^4.0.3", "next": "15.2.4", "react": "^19.0.0",