Frontend Performance Optimization #21
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
Generated
+96
-16
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user