Frontend Performance Optimization #21

Merged
an.di merged 7 commits from adilallo/enhancement/FrontendPerformanceOptimization into main 2025-10-08 17:15:25 +00:00
37 changed files with 1852 additions and 1480 deletions
Showing only changes of commit 2ed878af81 - Show all commits
+8 -4
View File
@@ -1,10 +1,11 @@
"use client";
import React from "react";
import React, { memo } from "react";
import ContentLockup from "./ContentLockup";
import Button from "./Button";
const AskOrganizer = ({
const AskOrganizer = memo(
({
title,
subtitle,
description,
@@ -13,7 +14,7 @@ const AskOrganizer = ({
className = "",
variant = "centered", // centered, left-aligned, compact
onContactClick, // Analytics callback
}) => {
}) => {
// Analytics tracking for contact button clicks
const handleContactClick = (event) => {
// Track contact button interaction
@@ -105,6 +106,9 @@ const AskOrganizer = ({
</div>
</section>
);
};
}
);
AskOrganizer.displayName = "AskOrganizer";
export default AskOrganizer;
+10 -8
View File
@@ -1,10 +1,7 @@
export default function Avatar({
src,
alt,
size = "small",
className = "",
...props
}) {
import React, { memo } from "react";
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]",
@@ -15,4 +12,9 @@ export default function Avatar({
const baseStyles = `rounded-[var(--radius-measures-radius-full)] object-cover ${sizeStyles[size]} ${className}`;
return <img src={src} alt={alt} className={baseStyles} {...props} />;
}
}
);
Avatar.displayName = "Avatar";
export default Avatar;
+10 -7
View File
@@ -1,9 +1,7 @@
export default function AvatarContainer({
children,
size = "small",
className = "",
...props
}) {
import React, { memo } from "react";
const AvatarContainer = memo(
({ children, size = "small", className = "", ...props }) => {
const sizeStyles = {
small: "flex -space-x-[var(--spacing-scale-008)]",
medium: "flex -space-x-[9px]",
@@ -18,4 +16,9 @@ export default function AvatarContainer({
{children}
</div>
);
}
}
);
AvatarContainer.displayName = "AvatarContainer";
export default AvatarContainer;
+11 -3
View File
@@ -1,4 +1,7 @@
export default function Button({
import React, { memo } from "react";
const Button = memo(
({
children,
variant = "default",
size = "xsmall",
@@ -11,7 +14,7 @@ export default function Button({
rel,
ariaLabel,
...props
}) {
}) => {
const sizeStyles = {
xsmall:
"px-[var(--spacing-scale-006)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-001)]",
@@ -105,4 +108,9 @@ export default function Button({
{children}
</button>
);
}
}
);
Button.displayName = "Button";
export default Button;
+7 -2
View File
@@ -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 }) {
</div>
</div>
);
}
});
ContentBanner.displayName = "ContentBanner";
export default ContentBanner;
+7 -3
View File
@@ -1,9 +1,10 @@
"use client";
import React from "react";
import React, { memo } from "react";
import { getAssetPath, ASSETS } from "../../lib/assetUtils";
const ContentContainer = ({ post, width = "200px", size = "responsive" }) => {
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 = [
@@ -122,6 +123,9 @@ const ContentContainer = ({ post, width = "200px", size = "responsive" }) => {
</div>
</div>
);
};
}
);
ContentContainer.displayName = "ContentContainer";
export default ContentContainer;
+13 -4
View File
@@ -1,9 +1,11 @@
"use client";
import React, { memo } from "react";
import Button from "./Button";
import { getAssetPath } from "../../lib/assetUtils";
const ContentLockup = ({
const ContentLockup = memo(
({
title,
subtitle,
description,
@@ -14,7 +16,7 @@ const ContentLockup = ({
linkText,
linkHref,
alignment = "center", // center, left
}) => {
}) => {
// Variant-specific styling
const variantStyles = {
hero: {
@@ -159,7 +161,11 @@ const ContentLockup = ({
</div>
{/* Large button for md and lg breakpoints */}
<div className="hidden md:block xl:hidden">
<Button variant="primary" size="large" className={buttonClassName}>
<Button
variant="primary"
size="large"
className={buttonClassName}
>
{ctaText}
</Button>
</div>
@@ -173,6 +179,9 @@ const ContentLockup = ({
)}
</div>
);
};
}
);
ContentLockup.displayName = "ContentLockup";
export default ContentLockup;
+8 -4
View File
@@ -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,11 +9,12 @@ import { getAssetPath, ASSETS } from "../../lib/assetUtils";
* ContentThumbnailTemplate component for displaying blog post previews
* Simplified version to debug infinite loop
*/
const ContentThumbnailTemplate = ({
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
@@ -90,6 +91,9 @@ const ContentThumbnailTemplate = ({
</div>
</Link>
);
};
}
);
ContentThumbnailTemplate.displayName = "ContentThumbnailTemplate";
export default ContentThumbnailTemplate;
+49
View File
@@ -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 (
<div className="min-h-[200px] flex items-center justify-center p-[var(--spacing-scale-016)]">
<div className="text-center">
<h2 className="text-xl font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-008)]">
Something went wrong
</h2>
<p className="text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-016)]">
We're sorry, but something unexpected happened.
</p>
<button
onClick={() => this.setState({ hasError: false, error: null })}
className="px-[var(--spacing-scale-016)] py-[var(--spacing-scale-008)] bg-[var(--color-surface-default-brand-royal)] text-[var(--color-content-inverse-primary)] rounded-[var(--radius-measures-radius-small)] hover:bg-[var(--color-surface-hover-brand-royal)] transition-colors"
>
Try again
</button>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
+52 -33
View File
@@ -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 (
<section
className={`p-0 lg:p-[var(--spacing-scale-064)] ${className}`}
@@ -32,43 +70,24 @@ const FeatureGrid = ({ title, subtitle, className = "" }) => {
role="grid"
aria-label="Feature tools and services"
>
{features.map((feature, index) => (
<MiniCard
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"
/>
<MiniCard
backgroundColor="bg-[#D1FFE2]"
labelLine1="Values alignment"
labelLine2="exercises"
panelContent="assets/Feature_Exercises.png"
ariaLabel="Values alignment exercises"
href="#values-alignment"
/>
<MiniCard
backgroundColor="bg-[#F4CAFF]"
labelLine1="Membership"
labelLine2="guidance"
panelContent="assets/Feature_Guidance.png"
ariaLabel="Membership guidance resources"
href="#membership-guidance"
/>
<MiniCard
backgroundColor="bg-[#CBDDFF]"
labelLine1="Conflict resolution"
labelLine2="tools"
panelContent="assets/Feature_Tools.png"
ariaLabel="Conflict resolution tools"
href="#conflict-resolution"
key={index}
backgroundColor={feature.backgroundColor}
labelLine1={feature.labelLine1}
labelLine2={feature.labelLine2}
panelContent={feature.panelContent}
ariaLabel={feature.ariaLabel}
href={feature.href}
/>
))}
</div>
</div>
</div>
</section>
);
};
});
FeatureGrid.displayName = "FeatureGrid";
export default FeatureGrid;
+7 -2
View File
@@ -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>
</>
);
}
});
Footer.displayName = "Footer";
export default Footer;
+7 -2
View File
@@ -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>
</>
);
}
});
Header.displayName = "Header";
export default Header;
+9 -7
View File
@@ -1,11 +1,8 @@
import React, { memo } from "react";
import { getAssetPath } from "../../lib/assetUtils";
export default function HeaderTab({
children,
className = "",
stretch = false,
...props
}) {
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)]"
: "";
@@ -36,4 +33,9 @@ export default function HeaderTab({
/>
</div>
);
}
}
);
HeaderTab.displayName = "HeaderTab";
export default HeaderTab;
+9 -2
View File
@@ -1,10 +1,12 @@
"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 }) => {
const HeroBanner = memo(
({ title, subtitle, description, ctaText, ctaHref }) => {
return (
<section className="bg-transparent px-[var(--spacing-scale-008)] sm:px-[var(--spacing-scale-010)] md:px-[var(--spacing-scale-016)] lg:px-[var(--spacing-scale-024)] xl:px-[var(--spacing-scale-048)]">
<div className="flex flex-col gap-[var(--spacing-scale-010)]">
@@ -36,12 +38,17 @@ const HeroBanner = ({ title, subtitle, description, ctaText, ctaHref }) => {
src={getAssetPath("assets/HeroImage.png")}
alt="Hero illustration"
className="w-full h-auto"
loading="eager"
fetchPriority="high"
/>
</div>
</div>
</div>
</section>
);
};
}
);
HeroBanner.displayName = "HeroBanner";
export default HeroBanner;
+6 -2
View File
@@ -1,6 +1,8 @@
"use client";
const HeroDecor = ({ className = "" }) => {
import React, { memo } from "react";
const HeroDecor = memo(({ className = "" }) => {
return (
<svg
className={`text-[var(--color-surface-default-brand-lighter-accent)] opacity-50 ${className}`}
@@ -65,6 +67,8 @@ const HeroDecor = ({ className = "" }) => {
</g>
</svg>
);
};
});
HeroDecor.displayName = "HeroDecor";
export default HeroDecor;
+10 -5
View File
@@ -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 = [
@@ -241,4 +242,8 @@ export default function HomeHeader() {
</header>
</>
);
}
});
HomeHeader.displayName = "HomeHeader";
export default HomeHeader;
+8 -4
View File
@@ -1,18 +1,19 @@
"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 = ({
const ImagePlaceholder = memo(
({
width = 260,
height = 390,
text = "Blog Image",
color = "blue",
className = "",
}) => {
}) => {
const colors = {
blue: "bg-blue-500",
green: "bg-green-500",
@@ -32,6 +33,9 @@ const ImagePlaceholder = ({
{text}
</div>
);
};
}
);
ImagePlaceholder.displayName = "ImagePlaceholder";
export default ImagePlaceholder;
+7 -2
View File
@@ -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: {
@@ -165,4 +166,8 @@ export default function Logo({ size = "default", showText = true }) {
</div>
</Link>
);
}
});
Logo.displayName = "Logo";
export default Logo;
+11 -9
View File
@@ -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 = [] }) => {
</div>
</section>
);
};
});
LogoWall.displayName = "LogoWall";
export default LogoWall;
+10 -7
View File
@@ -1,9 +1,7 @@
export default function MenuBar({
children,
className = "",
size = "default",
...props
}) {
import React, { memo } from "react";
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]",
@@ -27,4 +25,9 @@ export default function MenuBar({
{children}
</nav>
);
}
}
);
MenuBar.displayName = "MenuBar";
export default MenuBar;
+11 -3
View File
@@ -1,4 +1,7 @@
export default function MenuBarItem({
import React, { memo } from "react";
const MenuBarItem = memo(
({
href = "#",
children,
variant = "default",
@@ -8,7 +11,7 @@ export default function MenuBarItem({
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",
@@ -155,4 +158,9 @@ export default function MenuBarItem({
{children}
</a>
);
}
}
);
MenuBarItem.displayName = "MenuBarItem";
export default MenuBarItem;
+22 -10
View File
@@ -1,9 +1,10 @@
"use client";
import React from "react";
import React, { memo } from "react";
import Image from "next/image";
const MiniCard = ({
const MiniCard = memo(
({
children,
className = "",
backgroundColor = "bg-[var(--color-surface-default-brand-royal)]",
@@ -14,7 +15,7 @@ const MiniCard = ({
onClick,
href,
ariaLabel,
}) => {
}) => {
const cardContent = (
<div className={`h-[186px] flex flex-col gap-[7px] ${className}`}>
{/* Top part - Inner panel */}
@@ -33,10 +34,12 @@ const MiniCard = ({
"Feature icon"
}
className="max-w-[58px] max-h-[58px] w-auto h-auto object-contain"
unoptimized
width={0}
height={0}
sizes="100vw"
width={58}
height={58}
sizes="(max-width: 768px) 50vw, 25vw"
loading="lazy"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="
/>
</div>
)}
@@ -65,7 +68,10 @@ const MiniCard = ({
href={href}
className="block focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 rounded-[var(--radius-measures-radius-xlarge)] transition-all duration-200 hover:scale-[1.02]"
aria-label={
ariaLabel || `${labelLine1} ${labelLine2}` || label || "Feature card"
ariaLabel ||
`${labelLine1} ${labelLine2}` ||
label ||
"Feature card"
}
tabIndex={0}
>
@@ -81,7 +87,10 @@ const MiniCard = ({
onClick={onClick}
className="block w-full text-left focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 rounded-[var(--radius-measures-radius-xlarge)] transition-all duration-200 hover:scale-[1.02]"
aria-label={
ariaLabel || `${labelLine1} ${labelLine2}` || label || "Feature card"
ariaLabel ||
`${labelLine1} ${labelLine2}` ||
label ||
"Feature card"
}
tabIndex={0}
onKeyDown={(e) => {
@@ -107,6 +116,9 @@ const MiniCard = ({
{cardContent}
</div>
);
};
}
);
MiniCard.displayName = "MiniCard";
export default MiniCard;
+8 -2
View File
@@ -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}
</a>
);
}
});
NavigationItem.displayName = "NavigationItem";
export default NavigationItem;
+5 -2
View File
@@ -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 (
<div className="bg-[var(--color-surface-inverse-primary)] rounded-[12px] p-5 shadow-lg flex flex-col gap-4 sm:p-8 sm:gap-8 sm:flex-row sm:items-center lg:p-8 lg:gap-0 lg:flex-row lg:items-stretch lg:relative lg:h-[238px]">
{/* Section Number - Top right (lg breakpoint) */}
@@ -18,6 +19,8 @@ const NumberedCard = ({ number, text, iconShape, iconColor }) => {
</div>
</div>
);
};
});
NumberedCard.displayName = "NumberedCard";
export default NumberedCard;
+11 -5
View File
@@ -1,12 +1,14 @@
"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 = {
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,
@@ -17,7 +19,9 @@ const NumberedCards = ({ title, subtitle, cards }) => {
name: card.text,
text: card.text,
})),
};
}),
[title, subtitle, cards]
);
return (
<>
@@ -70,6 +74,8 @@ const NumberedCards = ({ title, subtitle, cards }) => {
</section>
</>
);
};
});
NumberedCards.displayName = "NumberedCards";
export default NumberedCards;
+13 -8
View File
@@ -1,27 +1,29 @@
"use client";
import React, { useState } from "react";
import React, { useState, memo } from "react";
import Image from "next/image";
import QuoteDecor from "./QuoteDecor";
const QuoteBlock = ({
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",
avatarSrc = "/assets/Quote_Avatar.svg",
id,
fallbackAvatarSrc = "assets/Quote_Avatar.svg", // Fallback avatar
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)]",
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)]",
@@ -78,7 +80,7 @@ const QuoteBlock = ({
const handleImageError = (error) => {
console.warn(
`QuoteBlock: Failed to load avatar image for ${author}:`,
error,
error
);
setImageError(true);
setImageLoading(false);
@@ -242,6 +244,9 @@ const QuoteBlock = ({
</div>
</section>
);
};
}
);
QuoteBlock.displayName = "QuoteBlock";
export default QuoteBlock;
+6 -2
View File
@@ -1,6 +1,8 @@
"use client";
const QuoteDecor = ({ className = "" }) => {
import React, { memo } from "react";
const QuoteDecor = memo(({ className = "" }) => {
return (
<svg
className={`text-[var(--color-surface-inverse-brand-primary)] opacity-100 w-full h-full md:max-w-[640px] lg:max-w-[850px] xl:max-w-[1100px] ${className}`}
@@ -68,6 +70,8 @@ const QuoteDecor = ({ className = "" }) => {
</g>
</svg>
);
};
});
QuoteDecor.displayName = "QuoteDecor";
export default QuoteDecor;
+61 -50
View File
@@ -1,22 +1,65 @@
"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);
// 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;
const handleMouseMove = (e) => {
const x = e.pageX - slider.offsetLeft;
const walk = (x - startX) * 2;
slider.scrollLeft = scrollLeft - walk;
};
const handleMouseUp = () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}, []);
// 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]
);
// Memoize progress bar style calculation
const getProgressStyle = useCallback(
(index) => ({
width:
index === currentIndex
? `${progress}%`
: index < currentIndex
? "100%"
: "0%",
}),
[currentIndex, progress]
);
// Check if we're on mobile (below lg breakpoint)
useEffect(() => {
const checkScreenSize = () => {
@@ -75,38 +118,8 @@ export default function RelatedArticles({
? "overflow-x-auto scrollbar-hide cursor-grab active:cursor-grabbing"
: ""
}`}
style={{
transform: isMobile
? `translateX(calc(50% - 130px - ${currentIndex * 260}px))`
: "none",
scrollBehavior: !isMobile ? "smooth" : "auto",
}}
onMouseDown={
!isMobile
? (e) => {
const slider = e.currentTarget;
const startX = e.pageX - slider.offsetLeft;
const scrollLeft = slider.scrollLeft;
const handleMouseMove = (e) => {
const x = e.pageX - slider.offsetLeft;
const walk = (x - startX) * 2;
slider.scrollLeft = scrollLeft - walk;
};
const handleMouseUp = () => {
document.removeEventListener(
"mousemove",
handleMouseMove,
);
document.removeEventListener("mouseup", handleMouseUp);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}
: undefined
}
style={transformStyle}
onMouseDown={!isMobile ? handleMouseDown : undefined}
>
{filteredPosts.map((relatedPost, index) => (
<div
@@ -133,14 +146,7 @@ export default function RelatedArticles({
>
<div
className="h-full bg-gray-600 rounded-full transition-all duration-75 ease-linear"
style={{
width:
index === currentIndex
? `${progress}%`
: index < currentIndex
? "100%"
: "0%",
}}
style={getProgressStyle(index)}
/>
</div>
))}
@@ -149,4 +155,9 @@ export default function RelatedArticles({
</div>
</section>
);
}
}
);
RelatedArticles.displayName = "RelatedArticles";
export default RelatedArticles;
+9 -3
View File
@@ -1,13 +1,16 @@
"use client";
const RuleCard = ({
import React, { memo } from "react";
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) {
@@ -68,6 +71,9 @@ const RuleCard = ({
)}
</div>
);
};
}
);
RuleCard.displayName = "RuleCard";
export default RuleCard;
+5 -3
View File
@@ -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 = "" }) => {
</div>
</section>
);
};
});
RuleStack.displayName = "RuleStack";
export default RuleStack;
+8 -2
View File
@@ -1,6 +1,9 @@
"use client";
const SectionHeader = ({ title, subtitle, titleLg, variant = "default" }) => {
import React, { memo } from "react";
const SectionHeader = memo(
({ title, subtitle, titleLg, variant = "default" }) => {
return (
<div
className={
@@ -49,6 +52,9 @@ const SectionHeader = ({ title, subtitle, titleLg, variant = "default" }) => {
</div>
</div>
);
};
}
);
SectionHeader.displayName = "SectionHeader";
export default SectionHeader;
+10 -6
View File
@@ -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 }) => {
</div>
</div>
);
};
});
SectionNumber.displayName = "SectionNumber";
export default SectionNumber;
+8 -2
View File
@@ -1,7 +1,13 @@
export default function Separator() {
import React, { memo } from "react";
const Separator = memo(() => {
return (
<div className="flex flex-col items-center self-stretch">
<div className="flex items-start self-stretch h-px w-full bg-[var(--border-color-default-secondary)]" />
</div>
);
}
});
Separator.displayName = "Separator";
export default Separator;
+6
View File
@@ -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 = {
+58 -1
View File
@@ -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;
},
};
+96 -16
View File
@@ -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"
+1
View File
@@ -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",