From f6a0673082bdb954a737e5051a2b3de81636bd5b Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Wed, 10 Dec 2025 22:14:17 -0700 Subject: [PATCH 1/5] Convert from JSX to TSX --- app/api/web-vitals/{route.js => route.ts} | 58 ++++++-- app/blog/[slug]/{page.js => page.tsx} | 21 ++- app/blog/{page.js => page.tsx} | 3 +- .../{AskOrganizer.js => AskOrganizer.tsx} | 47 +++++- app/components/{Avatar.js => Avatar.tsx} | 13 +- ...AvatarContainer.js => AvatarContainer.tsx} | 12 +- app/components/{Button.js => Button.tsx} | 24 ++- app/components/{Checkbox.js => Checkbox.tsx} | 28 +++- ...itionalHeader.js => ConditionalHeader.tsx} | 0 .../{ContentBanner.js => ContentBanner.tsx} | 11 +- ...ntentContainer.js => ContentContainer.tsx} | 13 +- .../{ContentLockup.js => ContentLockup.tsx} | 32 +++- ...mplate.js => ContentThumbnailTemplate.tsx} | 25 ++-- .../{ContextMenu.js => ContextMenu.tsx} | 9 +- app/components/ContextMenuDivider.js | 21 --- app/components/ContextMenuDivider.tsx | 27 ++++ ...ContextMenuItem.js => ContextMenuItem.tsx} | 28 +++- ...tMenuSection.js => ContextMenuSection.tsx} | 10 +- .../{ErrorBoundary.js => ErrorBoundary.tsx} | 19 ++- app/components/FeatureGrid.js | 93 ------------ app/components/FeatureGrid.tsx | 100 +++++++++++++ app/components/{Footer.js => Footer.tsx} | 45 ++---- app/components/{Header.js => Header.tsx} | 46 ++++-- .../{HeaderTab.js => HeaderTab.tsx} | 10 +- .../{HeroBanner.js => HeroBanner.tsx} | 12 +- .../{HeroDecor.js => HeroDecor.tsx} | 6 +- .../{HomeHeader.js => HomeHeader.tsx} | 54 +++++-- ...agePlaceholder.js => ImagePlaceholder.tsx} | 14 +- app/components/{Input.js => Input.tsx} | 52 +++++-- app/components/{Logo.js => Logo.tsx} | 29 +++- app/components/{LogoWall.js => LogoWall.tsx} | 15 +- app/components/{MenuBar.js => MenuBar.tsx} | 12 +- .../{MenuBarItem.js => MenuBarItem.tsx} | 42 ++++-- app/components/{MiniCard.js => MiniCard.tsx} | 17 ++- .../{NavigationItem.js => NavigationItem.tsx} | 20 ++- app/components/NumberedCard.js | 26 ---- app/components/NumberedCard.tsx | 35 +++++ .../{NumberedCards.js => NumberedCards.tsx} | 16 +- .../{QuoteBlock.js => QuoteBlock.tsx} | 45 +++++- .../{QuoteDecor.js => QuoteDecor.tsx} | 6 +- .../{RadioButton.js => RadioButton.tsx} | 22 ++- .../{RadioGroup.js => RadioGroup.tsx} | 26 +++- ...RelatedArticles.js => RelatedArticles.tsx} | 60 ++++---- app/components/{RuleCard.js => RuleCard.tsx} | 28 +++- .../{RuleStack.js => RuleStack.tsx} | 21 ++- .../{SectionHeader.js => SectionHeader.tsx} | 11 +- .../{SectionNumber.js => SectionNumber.tsx} | 8 +- app/components/{Select.js => Select.tsx} | 99 +++++++++---- .../{SelectDropdown.js => SelectDropdown.tsx} | 9 +- .../{SelectOption.js => SelectOption.tsx} | 27 +++- .../{Separator.js => Separator.tsx} | 0 app/components/{Switch.js => Switch.tsx} | 35 +++-- app/components/{TextArea.js => TextArea.tsx} | 53 +++++-- app/components/{Toggle.js => Toggle.tsx} | 51 +++++-- .../{ToggleGroup.js => ToggleGroup.tsx} | 41 ++++-- ...alsDashboard.js => WebVitalsDashboard.tsx} | 42 +++++- app/{layout.js => layout.tsx} | 6 +- app/learn/{page.js => page.tsx} | 0 app/monitor/{page.js => page.tsx} | 0 app/{not-found.js => not-found.tsx} | 0 app/{page.js => page.tsx} | 0 lib/{assetUtils.js => assetUtils.ts} | 8 +- lib/{cache.js => cache.ts} | 117 ++++++++------- lib/{content.js => content.ts} | 138 +++++++++++------- lib/{mdx.js => mdx.ts} | 119 ++++++++++----- lib/types.ts | 37 +++++ lib/{validation.js => validation.ts} | 100 ++++++++++--- tsconfig.json | 8 +- 68 files changed, 1527 insertions(+), 635 deletions(-) rename app/api/web-vitals/{route.js => route.ts} (68%) rename app/blog/[slug]/{page.js => page.tsx} (94%) rename app/blog/{page.js => page.tsx} (96%) rename app/components/{AskOrganizer.js => AskOrganizer.tsx} (76%) rename app/components/{Avatar.js => Avatar.tsx} (69%) rename app/components/{AvatarContainer.js => AvatarContainer.tsx} (65%) rename app/components/{Button.js => Button.tsx} (91%) rename app/components/{Checkbox.js => Checkbox.tsx} (90%) rename app/components/{ConditionalHeader.js => ConditionalHeader.tsx} (100%) rename app/components/{ContentBanner.js => ContentBanner.tsx} (90%) rename app/components/{ContentContainer.js => ContentContainer.tsx} (95%) rename app/components/{ContentLockup.js => ContentLockup.tsx} (93%) rename app/components/{ContentThumbnailTemplate.js => ContentThumbnailTemplate.tsx} (87%) rename app/components/{ContextMenu.js => ContextMenu.tsx} (77%) delete mode 100644 app/components/ContextMenuDivider.js create mode 100644 app/components/ContextMenuDivider.tsx rename app/components/{ContextMenuItem.js => ContextMenuItem.tsx} (82%) rename app/components/{ContextMenuSection.js => ContextMenuSection.tsx} (73%) rename app/components/{ErrorBoundary.js => ErrorBoundary.tsx} (76%) delete mode 100644 app/components/FeatureGrid.js create mode 100644 app/components/FeatureGrid.tsx rename app/components/{Footer.js => Footer.tsx} (68%) rename app/components/{Header.js => Header.tsx} (87%) rename app/components/{HeaderTab.js => HeaderTab.tsx} (90%) rename app/components/{HeroBanner.js => HeroBanner.tsx} (93%) rename app/components/{HeroDecor.js => HeroDecor.tsx} (97%) rename app/components/{HomeHeader.js => HomeHeader.tsx} (87%) rename app/components/{ImagePlaceholder.js => ImagePlaceholder.tsx} (74%) rename app/components/{Input.js => Input.tsx} (82%) rename app/components/{Logo.js => Logo.tsx} (90%) rename app/components/{LogoWall.js => LogoWall.tsx} (94%) rename app/components/{MenuBar.js => MenuBar.tsx} (77%) rename app/components/{MenuBarItem.js => MenuBarItem.tsx} (90%) rename app/components/{MiniCard.js => MiniCard.tsx} (92%) rename app/components/{NavigationItem.js => NavigationItem.tsx} (82%) delete mode 100644 app/components/NumberedCard.js create mode 100644 app/components/NumberedCard.tsx rename app/components/{NumberedCards.js => NumberedCards.tsx} (90%) rename app/components/{QuoteBlock.js => QuoteBlock.tsx} (92%) rename app/components/{QuoteDecor.js => QuoteDecor.tsx} (93%) rename app/components/{RadioButton.js => RadioButton.tsx} (90%) rename app/components/{RadioGroup.js => RadioGroup.tsx} (73%) rename app/components/{RelatedArticles.js => RelatedArticles.tsx} (79%) rename app/components/{RuleCard.js => RuleCard.tsx} (84%) rename app/components/{RuleStack.js => RuleStack.tsx} (89%) rename app/components/{SectionHeader.js => SectionHeader.tsx} (94%) rename app/components/{SectionNumber.js => SectionNumber.tsx} (85%) rename app/components/{Select.js => Select.tsx} (78%) rename app/components/{SelectDropdown.js => SelectDropdown.tsx} (78%) rename app/components/{SelectOption.js => SelectOption.tsx} (81%) rename app/components/{Separator.js => Separator.tsx} (100%) rename app/components/{Switch.js => Switch.tsx} (81%) rename app/components/{TextArea.js => TextArea.tsx} (82%) rename app/components/{Toggle.js => Toggle.tsx} (82%) rename app/components/{ToggleGroup.js => ToggleGroup.tsx} (75%) rename app/components/{WebVitalsDashboard.js => WebVitalsDashboard.tsx} (90%) rename app/{layout.js => layout.tsx} (93%) rename app/learn/{page.js => page.tsx} (100%) rename app/monitor/{page.js => page.tsx} (100%) rename app/{not-found.js => not-found.tsx} (100%) rename app/{page.js => page.tsx} (100%) rename lib/{assetUtils.js => assetUtils.ts} (89%) rename lib/{cache.js => cache.ts} (58%) rename lib/{content.js => content.ts} (64%) rename lib/{mdx.js => mdx.ts} (74%) create mode 100644 lib/types.ts rename lib/{validation.js => validation.ts} (62%) diff --git a/app/api/web-vitals/route.js b/app/api/web-vitals/route.ts similarity index 68% rename from app/api/web-vitals/route.js rename to app/api/web-vitals/route.ts index 6dd1db4..468ce50 100644 --- a/app/api/web-vitals/route.js +++ b/app/api/web-vitals/route.ts @@ -1,20 +1,52 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import fs from "fs"; import path from "path"; const WEB_VITALS_DIR = path.join(process.cwd(), ".next", "web-vitals"); +interface WebVitalData { + metric: string; + data: { + value: number; + rating: string; + }; + url: string; + userAgent: string; + timestamp: string; + receivedAt: string; +} + +interface WebVitalMetrics { + [metric: string]: { + count: number; + average: number; + min: number; + max: number; + goodCount: number; + needsImprovementCount: number; + poorCount: number; + lastUpdated: string; + }; +} + // Ensure web-vitals directory exists if (!fs.existsSync(WEB_VITALS_DIR)) { fs.mkdirSync(WEB_VITALS_DIR, { recursive: true }); } -export async function POST(request) { +export async function POST(request: NextRequest) { try { - const { metric, data, url, userAgent, timestamp } = await request.json(); + const body = await request.json(); + const { metric, data, url, userAgent, timestamp } = body as { + metric: string; + data: { value: number; rating: string }; + url: string; + userAgent: string; + timestamp: string; + }; // Store the metric data - const vitalsData = { + const vitalsData: WebVitalData = { metric, data, url, @@ -25,13 +57,15 @@ export async function POST(request) { // Save to file (in production, you would save to a database) const filePath = path.join(WEB_VITALS_DIR, `${metric}.json`); - let existingData = []; + let existingData: WebVitalData[] = []; if (fs.existsSync(filePath)) { try { - existingData = JSON.parse(fs.readFileSync(filePath, "utf8")); + const fileContent = fs.readFileSync(filePath, "utf8"); + existingData = JSON.parse(fileContent) as WebVitalData[]; } catch (error) { - console.warn("Could not parse existing vitals data:", error.message); + const err = error as Error; + console.warn("Could not parse existing vitals data:", err.message); } } @@ -61,7 +95,7 @@ export async function POST(request) { export async function GET() { try { - const metrics = {}; + const metrics: WebVitalMetrics = {}; if (fs.existsSync(WEB_VITALS_DIR)) { const files = fs.readdirSync(WEB_VITALS_DIR); @@ -69,9 +103,11 @@ export async function GET() { files.forEach((file) => { if (file.endsWith(".json")) { const metric = file.replace(".json", ""); - const data = JSON.parse( - fs.readFileSync(path.join(WEB_VITALS_DIR, file), "utf8"), + const fileContent = fs.readFileSync( + path.join(WEB_VITALS_DIR, file), + "utf8", ); + const data = JSON.parse(fileContent) as WebVitalData[]; if (data.length > 0) { const values = data @@ -96,7 +132,7 @@ export async function GET() { (r) => r === "needs-improvement", ).length, poorCount: ratings.filter((r) => r === "poor").length, - lastUpdated: data[data.length - 1]?.receivedAt, + lastUpdated: data[data.length - 1]?.receivedAt || "", }; } } diff --git a/app/blog/[slug]/page.js b/app/blog/[slug]/page.tsx similarity index 94% rename from app/blog/[slug]/page.js rename to app/blog/[slug]/page.tsx index 2533b18..ece8c43 100644 --- a/app/blog/[slug]/page.js +++ b/app/blog/[slug]/page.tsx @@ -1,8 +1,9 @@ import { notFound } from "next/navigation"; -import Link from "next/link"; +import type { Metadata } from "next"; import { getBlogPostBySlug, getAllBlogPosts as getAllPosts, + type BlogPost, } from "../../../lib/content"; import ContentBanner from "../../components/ContentBanner"; import RelatedArticles from "../../components/RelatedArticles"; @@ -17,6 +18,10 @@ const askOrganizerData = { buttonHref: "#contact", }; +interface PageProps { + params: Promise<{ slug: string }>; +} + /** * Generate static params for all blog posts * This enables static generation for all blog posts at build time @@ -36,7 +41,9 @@ export async function generateStaticParams() { /** * Generate metadata for each blog post */ -export async function generateMetadata({ params }) { +export async function generateMetadata({ + params, +}: PageProps): Promise { try { const { slug } = await params; const post = getBlogPostBySlug(slug); @@ -80,7 +87,7 @@ export async function generateMetadata({ params }) { /** * Dynamic blog post page */ -export default async function BlogPostPage({ params }) { +export default async function BlogPostPage({ params }: PageProps) { // Get the blog post data const { slug } = await params; const post = getBlogPostBySlug(slug); @@ -97,7 +104,11 @@ export default async function BlogPostPage({ params }) { const slugOrder = allPosts.map((post) => post.slug); // Simple related articles algorithm based on content similarity - const getRelatedArticles = (currentPost, allPosts, limit = 3) => { + const getRelatedArticles = ( + currentPost: BlogPost, + allPosts: BlogPost[], + limit = 3, + ): BlogPost[] => { const otherPosts = allPosts.filter((p) => p.slug !== currentPost.slug); // Score posts based on content similarity @@ -202,7 +213,7 @@ export default async function BlogPostPage({ params }) { }; // Get article-specific background color from frontmatter - const getBackgroundColor = (post) => { + const getBackgroundColor = (post: BlogPost): string => { if (post.frontmatter?.background?.color) { return post.frontmatter.background.color; } diff --git a/app/blog/page.js b/app/blog/page.tsx similarity index 96% rename from app/blog/page.js rename to app/blog/page.tsx index dead414..56b125d 100644 --- a/app/blog/page.js +++ b/app/blog/page.tsx @@ -1,8 +1,9 @@ import { getAllBlogPosts } from "../../lib/content"; import ContentThumbnailTemplate from "../components/ContentThumbnailTemplate"; import ContentContainer from "../components/ContentContainer"; +import type { Metadata } from "next"; -export const metadata = { +export const metadata: Metadata = { title: "Blog - CommunityRule", description: "Learn about community governance, decision-making, and building successful organizations.", diff --git a/app/components/AskOrganizer.js b/app/components/AskOrganizer.tsx similarity index 76% rename from app/components/AskOrganizer.js rename to app/components/AskOrganizer.tsx index aa99ce3..ec97761 100644 --- a/app/components/AskOrganizer.js +++ b/app/components/AskOrganizer.tsx @@ -4,7 +4,35 @@ import React, { memo } from "react"; import ContentLockup from "./ContentLockup"; import Button from "./Button"; -const AskOrganizer = memo( +interface AskOrganizerProps { + title?: string; + subtitle?: string; + description?: string; + buttonText?: string; + buttonHref?: string; + className?: string; + variant?: "centered" | "left-aligned" | "compact" | "inverse"; + onContactClick?: (data: { + event: string; + component: string; + variant: string; + buttonText: string; + buttonHref: string; + timestamp: string; + }) => void; +} + +declare global { + interface Window { + gtag?: ( + command: string, + eventName: string, + params?: Record + ) => void; + } +} + +const AskOrganizer = memo( ({ title, subtitle, @@ -12,11 +40,13 @@ const AskOrganizer = memo( buttonText = "Ask an organizer", buttonHref = "#", className = "", - variant = "centered", // centered, left-aligned, compact - onContactClick, // Analytics callback + variant = "centered", + onContactClick, }) => { // Analytics tracking for contact button clicks - const handleContactClick = (event) => { + const handleContactClick = ( + event: React.MouseEvent + ) => { // Track contact button interaction if (onContactClick) { onContactClick({ @@ -40,7 +70,10 @@ const AskOrganizer = memo( }; // Variant-specific styling - const variantStyles = { + const variantStyles: Record< + string, + { container: string; buttonContainer: string } + > = { centered: { container: "text-center", buttonContainer: "flex justify-center", @@ -98,7 +131,7 @@ const AskOrganizer = memo( variant={variant === "inverse" ? "primary" : "default"} className="xl:!px-[var(--spacing-scale-020)] xl:!py-[var(--spacing-scale-012)] xl:!text-[24px] xl:!leading-[28px]" onClick={handleContactClick} - aria-label={`${buttonText} - Contact an organizer for help`} + ariaLabel={`${buttonText} - Contact an organizer for help`} > {buttonText} @@ -106,7 +139,7 @@ const AskOrganizer = memo( ); - }, + } ); AskOrganizer.displayName = "AskOrganizer"; diff --git a/app/components/Avatar.js b/app/components/Avatar.tsx similarity index 69% rename from app/components/Avatar.js rename to app/components/Avatar.tsx index aa447e7..42a6401 100644 --- a/app/components/Avatar.js +++ b/app/components/Avatar.tsx @@ -1,8 +1,15 @@ import React, { memo } from "react"; -const Avatar = memo( +interface AvatarProps extends React.ImgHTMLAttributes { + src: string; + alt: string; + size?: "small" | "medium" | "large" | "xlarge"; + className?: string; +} + +const Avatar = memo( ({ src, alt, size = "small", className = "", ...props }) => { - const sizeStyles = { + const sizeStyles: Record = { small: "w-[var(--spacing-scale-016)] h-[var(--spacing-scale-016)]", medium: "w-[18px] h-[18px]", large: "w-[var(--spacing-scale-024)] h-[var(--spacing-scale-024)]", @@ -12,7 +19,7 @@ const Avatar = memo( const baseStyles = `rounded-[var(--radius-measures-radius-full)] object-cover ${sizeStyles[size]} ${className}`; return {alt}; - }, + } ); Avatar.displayName = "Avatar"; diff --git a/app/components/AvatarContainer.js b/app/components/AvatarContainer.tsx similarity index 65% rename from app/components/AvatarContainer.js rename to app/components/AvatarContainer.tsx index e77156c..aaedac9 100644 --- a/app/components/AvatarContainer.js +++ b/app/components/AvatarContainer.tsx @@ -1,8 +1,14 @@ import React, { memo } from "react"; -const AvatarContainer = memo( +interface AvatarContainerProps extends React.HTMLAttributes { + children?: React.ReactNode; + size?: "small" | "medium" | "large" | "xlarge"; + className?: string; +} + +const AvatarContainer = memo( ({ children, size = "small", className = "", ...props }) => { - const sizeStyles = { + const sizeStyles: Record = { small: "flex -space-x-[var(--spacing-scale-008)]", medium: "flex -space-x-[9px]", large: "flex -space-x-[var(--spacing-scale-010)]", @@ -16,7 +22,7 @@ const AvatarContainer = memo( {children} ); - }, + } ); AvatarContainer.displayName = "AvatarContainer"; diff --git a/app/components/Button.js b/app/components/Button.tsx similarity index 91% rename from app/components/Button.js rename to app/components/Button.tsx index 497ee44..1621b7a 100644 --- a/app/components/Button.js +++ b/app/components/Button.tsx @@ -1,6 +1,20 @@ import React, { memo } from "react"; -const Button = memo( +interface ButtonProps extends React.ButtonHTMLAttributes { + children: React.ReactNode; + variant?: "default" | "secondary" | "primary" | "outlined" | "dark" | "inverse"; + size?: "xsmall" | "small" | "medium" | "large" | "xlarge"; + className?: string; + disabled?: boolean; + type?: "button" | "submit" | "reset"; + onClick?: (e: React.MouseEvent) => void; + href?: string; + target?: string; + rel?: string; + ariaLabel?: string; +} + +const Button = memo( ({ children, variant = "default", @@ -15,7 +29,7 @@ const Button = memo( ariaLabel, ...props }) => { - const sizeStyles = { + const sizeStyles: Record = { xsmall: "px-[var(--spacing-scale-006)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-001)]", small: @@ -27,7 +41,7 @@ const Button = memo( "px-[var(--spacing-scale-020)] py-[var(--spacing-scale-012)] gap-[var(--spacing-scale-008)]", }; - const fontStyles = { + const fontStyles: Record = { xsmall: "font-inter text-[10px] leading-[12px] font-medium tracking-[0%]", small: "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]", medium: "font-inter text-[14px] leading-[16px] font-medium tracking-[0%]", @@ -35,7 +49,7 @@ const Button = memo( xlarge: "font-inter text-[24px] leading-[28px] font-normal tracking-[0%]", }; - const variantStyles = { + const variantStyles: Record = { default: "bg-[var(--color-surface-inverse-primary)] text-[var(--color-content-inverse-primary)] hover:bg-[var(--color-surface-inverse-primary)] hover:text-[var(--color-content-inverse-brand-primary)] hover:outline-[var(--border-color-default-brandprimary)] hover:outline-inset hover:scale-[1.02] hover:shadow-lg focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-content-default-brand-primary)] focus:ring-offset-1 focus:scale-[1.02] active:bg-[var(--color-surface-inverse-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:outline-[var(--border-color-default-brandprimary)] active:outline-offset-1 active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100 disabled:hover:shadow-none disabled:hover:outline-none", secondary: @@ -49,7 +63,7 @@ const Button = memo( "bg-transparent text-[var(--color-content-inverse-primary)] hover:text-[var(--color-content-inverse-brand-primary)] hover:scale-[1.02] hover:bg-transparent hover:outline-none focus:outline-1 focus:outline-inset focus:outline-[var(--border-color-default-tertiary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-tertiary)] focus:blur-[0px] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-primary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", }; - const hoverOutlineStyles = { + const hoverOutlineStyles: Record = { xsmall: "hover:outline-1", small: "hover:outline-1", medium: "hover:outline-1", diff --git a/app/components/Checkbox.js b/app/components/Checkbox.tsx similarity index 90% rename from app/components/Checkbox.js rename to app/components/Checkbox.tsx index a395c13..1487bcb 100644 --- a/app/components/Checkbox.js +++ b/app/components/Checkbox.tsx @@ -2,12 +2,30 @@ import React, { memo, useId } from "react"; +interface CheckboxProps { + checked?: boolean; + mode?: "standard" | "inverse"; + state?: "default" | "hover" | "focus"; + disabled?: boolean; + label?: string; + className?: string; + onChange?: (data: { + checked: boolean; + value?: string; + event: React.MouseEvent | React.KeyboardEvent; + }) => void; + id?: string; + name?: string; + value?: string; + ariaLabel?: string; +} + /** * Checkbox * A basic controlled checkbox with visual modes and interaction states. * This is a minimal first pass; visuals will be refined collaboratively. */ -const Checkbox = memo( +const Checkbox = memo( ({ checked = false, mode = "standard", // "standard" | "inverse" @@ -38,7 +56,7 @@ const Checkbox = memo( // Visual container depending on state const baseBox = `flex items-center justify-center shrink-0 w-[var(--measures-sizing-024)] h-[var(--measures-sizing-024)] rounded-[var(--measures-radius-medium)] transition-all duration-200 ease-in-out`; - const stateStyles = { + const stateStyles: Record = { default: "", hover: "", focus: "", @@ -73,7 +91,7 @@ const Checkbox = memo( const conditionalFocusClass = "focus:outline focus:outline-1 focus:outline-[var(--color-border-default-utility-info)] focus:shadow-[0_0_10px_1px_var(--color-surface-inverse-brand-primary)]"; - const handleToggle = (e) => { + const handleToggle = (e: React.MouseEvent | React.KeyboardEvent) => { if (disabled) return; onChange?.({ checked: !checked, @@ -87,7 +105,7 @@ const Checkbox = memo( const checkboxId = id || `checkbox-${generatedId}`; const accessibilityProps = { - role: "checkbox", + role: "checkbox" as const, "aria-checked": checked ? "true" : "false", ...(disabled && { "aria-disabled": "true", tabIndex: -1 }), ...(!disabled && { tabIndex: 0 }), @@ -160,7 +178,7 @@ const Checkbox = memo( /> ); - }, + } ); Checkbox.displayName = "Checkbox"; diff --git a/app/components/ConditionalHeader.js b/app/components/ConditionalHeader.tsx similarity index 100% rename from app/components/ConditionalHeader.js rename to app/components/ConditionalHeader.tsx diff --git a/app/components/ContentBanner.js b/app/components/ContentBanner.tsx similarity index 90% rename from app/components/ContentBanner.js rename to app/components/ContentBanner.tsx index f2a3511..54609f4 100644 --- a/app/components/ContentBanner.js +++ b/app/components/ContentBanner.tsx @@ -3,10 +3,15 @@ import React, { memo } from "react"; import { getAssetPath } from "../../lib/assetUtils"; import ContentContainer from "./ContentContainer"; +import type { BlogPost } from "../../lib/content"; -const ContentBanner = memo(({ post }) => { +interface ContentBannerProps { + post: BlogPost; +} + +const ContentBanner = memo(({ post }) => { // Get article-specific horizontal thumbnail (small) and banner (md+) - const getBackgroundImage = (post) => { + const getBackgroundImage = (post: BlogPost): string => { if (post.frontmatter?.thumbnail?.horizontal) { return `/content/blog/${post.frontmatter.thumbnail.horizontal}`; } @@ -14,7 +19,7 @@ const ContentBanner = memo(({ post }) => { return getAssetPath("assets/Content_Banner.svg"); }; - const getBannerImageMd = (post) => { + const getBannerImageMd = (post: BlogPost): string => { // Use banner.horizontal when provided; fallback to horizontal thumbnail if (post.frontmatter?.banner?.horizontal) { return `/content/blog/${post.frontmatter.banner.horizontal}`; diff --git a/app/components/ContentContainer.js b/app/components/ContentContainer.tsx similarity index 95% rename from app/components/ContentContainer.js rename to app/components/ContentContainer.tsx index 6d1835e..4e11b40 100644 --- a/app/components/ContentContainer.js +++ b/app/components/ContentContainer.tsx @@ -2,11 +2,18 @@ import React, { memo } from "react"; import { getAssetPath, ASSETS } from "../../lib/assetUtils"; +import type { BlogPost } from "../../lib/content"; -const ContentContainer = memo( +interface ContentContainerProps { + post: BlogPost; + width?: string; + size?: "xs" | "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 getIconImage = (slug: string): string => { const icons = [ getAssetPath(ASSETS.ICON_1), getAssetPath(ASSETS.ICON_2), @@ -123,7 +130,7 @@ const ContentContainer = memo( ); - }, + } ); ContentContainer.displayName = "ContentContainer"; diff --git a/app/components/ContentLockup.js b/app/components/ContentLockup.tsx similarity index 93% rename from app/components/ContentLockup.js rename to app/components/ContentLockup.tsx index 63c42d2..b98ae66 100644 --- a/app/components/ContentLockup.js +++ b/app/components/ContentLockup.tsx @@ -4,7 +4,31 @@ import React, { memo } from "react"; import Button from "./Button"; import { getAssetPath } from "../../lib/assetUtils"; -const ContentLockup = memo( +interface ContentLockupProps { + title?: string; + subtitle?: string; + description?: string; + ctaText?: string; + ctaHref?: string; + buttonClassName?: string; + variant?: "hero" | "feature" | "learn" | "ask" | "ask-inverse"; + linkText?: string; + linkHref?: string; + alignment?: "center" | "left"; +} + +interface VariantStyle { + container: string; + textContainer: string; + titleGroup: string; + titleContainer: string; + title: string; + subtitle: string; + description?: string; + shape: string; +} + +const ContentLockup = memo( ({ title, subtitle, @@ -15,10 +39,10 @@ const ContentLockup = memo( variant = "hero", linkText, linkHref, - alignment = "center", // center, left + alignment = "center", }) => { // Variant-specific styling - const variantStyles = { + const variantStyles: Record = { hero: { container: "flex flex-col gap-[var(--spacing-scale-006)] sm:gap-[var(--spacing-scale-012)] md:gap-[var(--spacing-scale-020)] lg:gap-[var(--spacing-scale-020)] relative z-10", @@ -179,7 +203,7 @@ const ContentLockup = memo( )} ); - }, + } ); ContentLockup.displayName = "ContentLockup"; diff --git a/app/components/ContentThumbnailTemplate.js b/app/components/ContentThumbnailTemplate.tsx similarity index 87% rename from app/components/ContentThumbnailTemplate.js rename to app/components/ContentThumbnailTemplate.tsx index b39474a..ef13965 100644 --- a/app/components/ContentThumbnailTemplate.js +++ b/app/components/ContentThumbnailTemplate.tsx @@ -4,19 +4,26 @@ import React, { memo } from "react"; import Link from "next/link"; import ContentContainer from "./ContentContainer"; import { getAssetPath, ASSETS } from "../../lib/assetUtils"; +import type { BlogPost } from "../../lib/content"; /** * ContentThumbnailTemplate component for displaying blog post previews * Simplified version to debug infinite loop */ -const ContentThumbnailTemplate = memo( - ({ - post, - className = "", - variant = "vertical", // Internal prop for testing/development - }) => { +interface ContentThumbnailTemplateProps { + post: BlogPost; + className?: string; + variant?: "vertical" | "horizontal"; + slugOrder?: string[]; +} + +const ContentThumbnailTemplate = memo( + ({ post, className = "", variant = "vertical", slugOrder }) => { // Get article-specific background image from frontmatter - const getBackgroundImage = (post, variant) => { + const getBackgroundImage = ( + post: BlogPost, + variant: "vertical" | "horizontal" + ): string => { // Check if post has thumbnail images defined in frontmatter if (post.frontmatter?.thumbnail) { const imageName = @@ -31,7 +38,7 @@ const ContentThumbnailTemplate = memo( } // Fallback to default images if no thumbnail specified - const fallbackImages = { + const fallbackImages: Record = { vertical: getAssetPath(ASSETS.VERTICAL_1), horizontal: getAssetPath(ASSETS.HORIZONTAL_1), }; @@ -91,7 +98,7 @@ const ContentThumbnailTemplate = memo( ); - }, + } ); ContentThumbnailTemplate.displayName = "ContentThumbnailTemplate"; diff --git a/app/components/ContextMenu.js b/app/components/ContextMenu.tsx similarity index 77% rename from app/components/ContextMenu.js rename to app/components/ContextMenu.tsx index 1498910..03af1c7 100644 --- a/app/components/ContextMenu.js +++ b/app/components/ContextMenu.tsx @@ -2,7 +2,12 @@ import React, { forwardRef, memo } from "react"; -const ContextMenu = forwardRef( +interface ContextMenuProps extends React.HTMLAttributes { + className?: string; + children?: React.ReactNode; +} + +const ContextMenu = forwardRef( ({ className = "", children, ...props }, ref) => { const menuClasses = ` bg-black @@ -28,7 +33,7 @@ const ContextMenu = forwardRef( {children} ); - }, + } ); ContextMenu.displayName = "ContextMenu"; diff --git a/app/components/ContextMenuDivider.js b/app/components/ContextMenuDivider.js deleted file mode 100644 index 9eb2d32..0000000 --- a/app/components/ContextMenuDivider.js +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import React, { forwardRef, memo } from "react"; - -const ContextMenuDivider = forwardRef(({ className = "", ...props }, ref) => { - const dividerClasses = ` - border-t border-[var(--color-border-default-tertiary)] - my-1 - ${className} - ` - .trim() - .replace(/\s+/g, " "); - - return ( -
- ); -}); - -ContextMenuDivider.displayName = "ContextMenuDivider"; - -export default memo(ContextMenuDivider); diff --git a/app/components/ContextMenuDivider.tsx b/app/components/ContextMenuDivider.tsx new file mode 100644 index 0000000..a8d2c1c --- /dev/null +++ b/app/components/ContextMenuDivider.tsx @@ -0,0 +1,27 @@ +"use client"; + +import React, { forwardRef, memo } from "react"; + +interface ContextMenuDividerProps extends React.HTMLAttributes { + className?: string; +} + +const ContextMenuDivider = forwardRef( + ({ className = "", ...props }, ref) => { + const dividerClasses = ` + border-t border-[var(--color-border-default-tertiary)] + my-1 + ${className} + ` + .trim() + .replace(/\s+/g, " "); + + return ( +
+ ); + } +); + +ContextMenuDivider.displayName = "ContextMenuDivider"; + +export default memo(ContextMenuDivider); diff --git a/app/components/ContextMenuItem.js b/app/components/ContextMenuItem.tsx similarity index 82% rename from app/components/ContextMenuItem.js rename to app/components/ContextMenuItem.tsx index b84d462..5c5dec7 100644 --- a/app/components/ContextMenuItem.js +++ b/app/components/ContextMenuItem.tsx @@ -2,7 +2,19 @@ import React, { forwardRef, memo, useCallback } from "react"; -const ContextMenuItem = forwardRef( +interface ContextMenuItemProps extends React.HTMLAttributes { + children?: React.ReactNode; + selected?: boolean; + hasSubmenu?: boolean; + disabled?: boolean; + className?: string; + onClick?: ( + e: React.MouseEvent | React.KeyboardEvent + ) => void; + size?: "small" | "medium" | "large"; +} + +const ContextMenuItem = forwardRef( ( { children, @@ -14,9 +26,9 @@ const ContextMenuItem = forwardRef( size = "medium", ...props }, - ref, + ref ) => { - const getTextSize = () => { + const getTextSize = (): string => { switch (size) { case "small": return "text-[10px] leading-[14px]"; @@ -52,16 +64,16 @@ const ContextMenuItem = forwardRef( .replace(/\s+/g, " "); const handleClick = useCallback( - (e) => { + (e: React.MouseEvent) => { if (!disabled && onClick) { onClick(e); } }, - [disabled, onClick], + [disabled, onClick] ); const handleKeyDown = useCallback( - (e) => { + (e: React.KeyboardEvent) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); if (!disabled && onClick) { @@ -69,7 +81,7 @@ const ContextMenuItem = forwardRef( } } }, - [disabled, onClick], + [disabled, onClick] ); return ( @@ -119,7 +131,7 @@ const ContextMenuItem = forwardRef( )}
); - }, + } ); ContextMenuItem.displayName = "ContextMenuItem"; diff --git a/app/components/ContextMenuSection.js b/app/components/ContextMenuSection.tsx similarity index 73% rename from app/components/ContextMenuSection.js rename to app/components/ContextMenuSection.tsx index 2592ae3..789f687 100644 --- a/app/components/ContextMenuSection.js +++ b/app/components/ContextMenuSection.tsx @@ -2,7 +2,13 @@ import React, { forwardRef, memo } from "react"; -const ContextMenuSection = forwardRef( +interface ContextMenuSectionProps extends React.HTMLAttributes { + title?: string; + children?: React.ReactNode; + className?: string; +} + +const ContextMenuSection = forwardRef( ({ title, children, className = "", ...props }, ref) => { const sectionClasses = ` ${className} @@ -22,7 +28,7 @@ const ContextMenuSection = forwardRef( {children}
); - }, + } ); ContextMenuSection.displayName = "ContextMenuSection"; diff --git a/app/components/ErrorBoundary.js b/app/components/ErrorBoundary.tsx similarity index 76% rename from app/components/ErrorBoundary.js rename to app/components/ErrorBoundary.tsx index 19e7a8b..915a6a1 100644 --- a/app/components/ErrorBoundary.js +++ b/app/components/ErrorBoundary.tsx @@ -1,19 +1,28 @@ "use client"; -import React, { Component } from "react"; +import React, { Component, type ReactNode } from "react"; -class ErrorBoundary extends Component { - constructor(props) { +interface ErrorBoundaryProps { + children: ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false, error: null }; } - static getDerivedStateFromError(error) { + static getDerivedStateFromError(error: Error): ErrorBoundaryState { // Update state so the next render will show the fallback UI return { hasError: true, error }; } - componentDidCatch(error, errorInfo) { + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { // Log the error to an error reporting service console.error("ErrorBoundary caught an error:", error, errorInfo); } diff --git a/app/components/FeatureGrid.js b/app/components/FeatureGrid.js deleted file mode 100644 index 9e879e9..0000000 --- a/app/components/FeatureGrid.js +++ /dev/null @@ -1,93 +0,0 @@ -"use client"; - -import React, { memo, useMemo } from "react"; -import ContentLockup from "./ContentLockup"; -import MiniCard from "./MiniCard"; -import Image from "next/image"; - -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 ( -
-
-
- {/* Feature Content Lockup */} -
- -
- - {/* MiniCard Grid */} -
- {features.map((feature, index) => ( - - ))} -
-
-
-
- ); -}); - -FeatureGrid.displayName = "FeatureGrid"; - -export default FeatureGrid; diff --git a/app/components/FeatureGrid.tsx b/app/components/FeatureGrid.tsx new file mode 100644 index 0000000..4b39450 --- /dev/null +++ b/app/components/FeatureGrid.tsx @@ -0,0 +1,100 @@ +"use client"; + +import React, { memo, useMemo } from "react"; +import ContentLockup from "./ContentLockup"; +import MiniCard from "./MiniCard"; + +interface FeatureGridProps { + title?: string; + subtitle?: string; + className?: string; +} + +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 ( +
+
+
+ {/* Feature Content Lockup */} +
+ +
+ + {/* MiniCard Grid */} +
+ {features.map((feature, index) => ( + + ))} +
+
+
+
+ ); + } +); + +FeatureGrid.displayName = "FeatureGrid"; + +export default FeatureGrid; diff --git a/app/components/Footer.js b/app/components/Footer.tsx similarity index 68% rename from app/components/Footer.js rename to app/components/Footer.tsx index b0facad..aeaf386 100644 --- a/app/components/Footer.js +++ b/app/components/Footer.tsx @@ -101,56 +101,29 @@ const Footer = memo(() => { - {/* Navigation Section */} -
+ {/* Links Section */} +
- {/* Bottom section */} -
-
- © All right reserved -
- + {/* Copyright */} +
+ © {new Date().getFullYear()} Media Economies Design Lab. All rights + reserved.
diff --git a/app/components/Header.js b/app/components/Header.tsx similarity index 87% rename from app/components/Header.js rename to app/components/Header.tsx index 7e18ebd..8bb6b15 100644 --- a/app/components/Header.js +++ b/app/components/Header.tsx @@ -24,19 +24,23 @@ export const avatarImages = [ ]; export const logoConfig = [ - { breakpoint: "block sm:hidden", size: "header", showText: false }, - { breakpoint: "hidden sm:block md:hidden", size: "header", showText: true }, + { breakpoint: "block sm:hidden", size: "header" as const, showText: false }, + { + breakpoint: "hidden sm:block md:hidden", + size: "header" as const, + showText: true, + }, { breakpoint: "hidden md:block lg:hidden", - size: "headerMd", + size: "headerMd" as const, showText: true, }, { breakpoint: "hidden lg:block xl:hidden", - size: "headerLg", + size: "headerLg" as const, showText: true, }, - { breakpoint: "hidden xl:block", size: "headerXl", showText: true }, + { breakpoint: "hidden xl:block", size: "headerXl" as const, showText: true }, ]; const Header = memo(() => { @@ -55,7 +59,7 @@ const Header = memo(() => { }, }; - const renderNavigationItems = (size) => { + const renderNavigationItems = (size: string) => { return navigationItems.map((item, index) => ( { )); }; - const renderAvatarGroup = (containerSize, avatarSize) => { + const renderAvatarGroup = ( + containerSize: "small" | "medium" | "large" | "xlarge", + avatarSize: "small" | "medium" | "large" | "xlarge" + ) => { return ( {avatarImages.map((avatar, index) => ( @@ -84,7 +91,7 @@ const Header = memo(() => { ); }; - const renderLoginButton = (size) => { + const renderLoginButton = (size: string) => { return ( Log in @@ -92,7 +99,11 @@ const Header = memo(() => { ); }; - const renderCreateRuleButton = (buttonSize, containerSize, avatarSize) => { + const renderCreateRuleButton = ( + buttonSize: string, + containerSize: "small" | "medium" | "large" | "xlarge", + avatarSize: "small" | "medium" | "large" | "xlarge" + ) => { return ( ); - }), + }) ); ToggleGroup.displayName = "ToggleGroup"; diff --git a/app/components/WebVitalsDashboard.js b/app/components/WebVitalsDashboard.tsx similarity index 90% rename from app/components/WebVitalsDashboard.js rename to app/components/WebVitalsDashboard.tsx index fe20a7e..b1de767 100644 --- a/app/components/WebVitalsDashboard.js +++ b/app/components/WebVitalsDashboard.tsx @@ -2,8 +2,36 @@ import React, { useState, useEffect, memo } from "react"; +interface VitalData { + value: number; + rating: "good" | "needs-improvement" | "poor" | "unknown"; +} + +interface Vitals { + lcp: VitalData; + fid: VitalData; + cls: VitalData; + fcp: VitalData; + ttfb: VitalData; +} + +interface MetricData { + count: number; + average: number; + min: number; + max: number; + goodCount: number; + needsImprovementCount: number; + poorCount: number; + lastUpdated?: string; +} + +interface Metrics { + [key: string]: MetricData; +} + const WebVitalsDashboard = memo(() => { - const [vitals, setVitals] = useState({ + const [vitals, setVitals] = useState({ lcp: { value: 0, rating: "unknown" }, fid: { value: 0, rating: "unknown" }, cls: { value: 0, rating: "unknown" }, @@ -11,7 +39,7 @@ const WebVitalsDashboard = memo(() => { ttfb: { value: 0, rating: "unknown" }, }); - const [metrics, setMetrics] = useState({}); + const [metrics, setMetrics] = useState({}); const [loading, setLoading] = useState(true); useEffect(() => { @@ -19,7 +47,7 @@ const WebVitalsDashboard = memo(() => { const fetchVitals = async () => { try { const response = await fetch("/api/web-vitals"); - const data = await response.json(); + const data = (await response.json()) as { metrics?: Metrics }; setMetrics(data.metrics || {}); } catch (error) { console.error("Error fetching web vitals:", error); @@ -88,12 +116,12 @@ const WebVitalsDashboard = memo(() => { }, })); }); - }, + } ); } }, []); - const getRatingColor = (rating) => { + const getRatingColor = (rating: string): string => { switch (rating) { case "good": return "text-green-600 bg-green-50"; @@ -106,7 +134,7 @@ const WebVitalsDashboard = memo(() => { } }; - const getRatingIcon = (rating) => { + const getRatingIcon = (rating: string): string => { switch (rating) { case "good": return "✅"; @@ -119,7 +147,7 @@ const WebVitalsDashboard = memo(() => { } }; - const formatValue = (metric, value) => { + const formatValue = (metric: string, value: number): string => { if (metric === "cls") { return value.toFixed(3); } diff --git a/app/layout.js b/app/layout.tsx similarity index 93% rename from app/layout.js rename to app/layout.tsx index 89d3c51..e1e0ce8 100644 --- a/app/layout.js +++ b/app/layout.tsx @@ -1,4 +1,6 @@ import { Inter, Bricolage_Grotesque, Space_Grotesk } from "next/font/google"; +import type { Metadata } from "next"; +import type { ReactNode } from "react"; import "./globals.css"; import Header from "./components/Header"; import HomeHeader from "./components/HomeHeader"; @@ -32,7 +34,7 @@ const spaceGrotesk = Space_Grotesk({ fallback: ["system-ui", "arial"], }); -export const metadata = { +export const metadata: Metadata = { title: "CommunityRule - Build operating manuals for successful communities", description: "Help your community make important decisions in a way that reflects its unique values.", @@ -77,7 +79,7 @@ export const metadata = { }, }; -export default function RootLayout({ children }) { +export default function RootLayout({ children }: { children: ReactNode }) { return ( diff --git a/app/learn/page.js b/app/learn/page.tsx similarity index 100% rename from app/learn/page.js rename to app/learn/page.tsx diff --git a/app/monitor/page.js b/app/monitor/page.tsx similarity index 100% rename from app/monitor/page.js rename to app/monitor/page.tsx diff --git a/app/not-found.js b/app/not-found.tsx similarity index 100% rename from app/not-found.js rename to app/not-found.tsx diff --git a/app/page.js b/app/page.tsx similarity index 100% rename from app/page.js rename to app/page.tsx diff --git a/lib/assetUtils.js b/lib/assetUtils.ts similarity index 89% rename from lib/assetUtils.js rename to lib/assetUtils.ts index 0ef4211..074c9dd 100644 --- a/lib/assetUtils.js +++ b/lib/assetUtils.ts @@ -6,10 +6,10 @@ /** * Get the correct asset path based on environment - * @param {string} assetPath - The asset path (e.g., "assets/Logo.svg") - * @returns {string} - The correct path for the current environment + * @param assetPath - The asset path (e.g., "assets/Logo.svg") + * @returns The correct path for the current environment */ -export function getAssetPath(assetPath) { +export function getAssetPath(assetPath: string): string { // Check if we're in Storybook environment const isStorybook = typeof window !== "undefined" && @@ -55,4 +55,4 @@ export const ASSETS = { // Content page decorative shapes CONTENT_SHAPE_1: "assets/Content_Shape_1.svg", CONTENT_SHAPE_2: "assets/Content_Shape_2.svg", -}; +} as const; diff --git a/lib/cache.js b/lib/cache.ts similarity index 58% rename from lib/cache.js rename to lib/cache.ts index 0446d5a..3727639 100644 --- a/lib/cache.js +++ b/lib/cache.ts @@ -3,10 +3,10 @@ */ // In-memory cache for blog posts -const blogPostCache = new Map(); -const blogListCache = new Map(); -const tagCache = new Map(); -const authorCache = new Map(); +const blogPostCache = new Map>(); +const blogListCache = new Map>(); +const tagCache = new Map>(); +const authorCache = new Map>(); // Cache configuration const isDevelopment = @@ -17,13 +17,16 @@ const MAX_CACHE_SIZE = 100; // Maximum number of cached items /** * Cache entry with timestamp */ -class CacheEntry { - constructor(data) { +class CacheEntry { + data: T; + timestamp: number; + + constructor(data: T) { this.data = data; this.timestamp = Date.now(); } - isExpired() { + isExpired(): boolean { // In development, always consider cache expired (no caching) if (isDevelopment) return true; return Date.now() - this.timestamp > CACHE_TTL; @@ -32,11 +35,11 @@ class CacheEntry { /** * Get cached blog post data - * @param {string} key - Cache key - * @returns {Object|null} Cached data or null if not found/expired + * @param key - Cache key + * @returns Cached data or null if not found/expired */ -function getCached(key) { - const entry = blogPostCache.get(key); +function getCached(key: string): T | null { + const entry = blogPostCache.get(key) as CacheEntry | undefined; if (!entry || entry.isExpired()) { blogPostCache.delete(key); return null; @@ -46,10 +49,10 @@ function getCached(key) { /** * Set cached blog post data - * @param {string} key - Cache key - * @param {Object} data - Data to cache + * @param key - Cache key + * @param data - Data to cache */ -function setCached(key, data) { +function setCached(key: string, data: T): void { // Implement LRU eviction if cache is full if (blogPostCache.size >= MAX_CACHE_SIZE) { const oldestKey = blogPostCache.keys().next().value; @@ -62,7 +65,7 @@ function setCached(key, data) { /** * Clear expired cache entries */ -function clearExpiredCache() { +function clearExpiredCache(): void { for (const [key, entry] of blogPostCache.entries()) { if (entry.isExpired()) { blogPostCache.delete(key); @@ -73,7 +76,7 @@ function clearExpiredCache() { /** * Clear all caches */ -export function clearAllCaches() { +export function clearAllCaches(): void { blogPostCache.clear(); blogListCache.clear(); tagCache.clear(); @@ -82,50 +85,50 @@ export function clearAllCaches() { /** * Get cached blog post by slug - * @param {string} slug - Blog post slug - * @returns {Object|null} Cached blog post or null + * @param slug - Blog post slug + * @returns Cached blog post or null */ -export function getCachedBlogPost(slug) { - return getCached(`post:${slug}`); +export function getCachedBlogPost(slug: string): T | null { + return getCached(`post:${slug}`); } /** * Cache blog post data - * @param {string} slug - Blog post slug - * @param {Object} postData - Blog post data + * @param slug - Blog post slug + * @param postData - Blog post data */ -export function cacheBlogPost(slug, postData) { +export function cacheBlogPost(slug: string, postData: T): void { setCached(`post:${slug}`, postData); } /** * Get cached blog post list - * @param {string} key - Cache key for list (e.g., 'all', 'recent', 'tag:governance') - * @returns {Array|null} Cached list or null + * @param key - Cache key for list (e.g., 'all', 'recent', 'tag:governance') + * @returns Cached list or null */ -export function getCachedBlogList(key) { +export function getCachedBlogList(key: string): T[] | null { const entry = blogListCache.get(key); if (!entry || entry.isExpired()) { blogListCache.delete(key); return null; } - return entry.data; + return entry.data as T[]; } /** * Cache blog post list - * @param {string} key - Cache key - * @param {Array} listData - List data to cache + * @param key - Cache key + * @param listData - List data to cache */ -export function cacheBlogList(key, listData) { +export function cacheBlogList(key: string, listData: T[]): void { blogListCache.set(key, new CacheEntry(listData)); } /** * Get cached tags - * @returns {Array|null} Cached tags or null + * @returns Cached tags or null */ -export function getCachedTags() { +export function getCachedTags(): string[] | null { const entry = tagCache.get("all"); if (!entry || entry.isExpired()) { tagCache.delete("all"); @@ -136,17 +139,17 @@ export function getCachedTags() { /** * Cache tags - * @param {Array} tags - Tags to cache + * @param tags - Tags to cache */ -export function cacheTags(tags) { +export function cacheTags(tags: string[]): void { tagCache.set("all", new CacheEntry(tags)); } /** * Get cached authors - * @returns {Array|null} Cached authors or null + * @returns Cached authors or null */ -export function getCachedAuthors() { +export function getCachedAuthors(): string[] | null { const entry = authorCache.get("all"); if (!entry || entry.isExpired()) { authorCache.delete("all"); @@ -157,17 +160,17 @@ export function getCachedAuthors() { /** * Cache authors - * @param {Array} authors - Authors to cache + * @param authors - Authors to cache */ -export function cacheAuthors(authors) { +export function cacheAuthors(authors: string[]): void { authorCache.set("all", new CacheEntry(authors)); } /** * Invalidate cache for a specific blog post - * @param {string} slug - Blog post slug + * @param slug - Blog post slug */ -export function invalidateBlogPostCache(slug) { +export function invalidateBlogPostCache(slug: string): void { blogPostCache.delete(`post:${slug}`); // Also invalidate list caches since they might contain this post blogListCache.clear(); @@ -176,15 +179,25 @@ export function invalidateBlogPostCache(slug) { /** * Invalidate all caches */ -export function invalidateAllCaches() { +export function invalidateAllCaches(): void { clearAllCaches(); } +export interface CacheStats { + blogPostCacheSize: number; + blogListCacheSize: number; + tagCacheSize: number; + authorCacheSize: number; + totalCacheSize: number; + maxCacheSize: number; + cacheTTL: number; +} + /** * Get cache statistics - * @returns {Object} Cache statistics + * @returns Cache statistics */ -export function getCacheStats() { +export function getCacheStats(): CacheStats { clearExpiredCache(); return { @@ -193,7 +206,7 @@ export function getCacheStats() { tagCacheSize: tagCache.size, authorCacheSize: authorCache.size, totalCacheSize: - blogPostCache.size + blogListCache.size + tagCache.size + authorCacheSize, + blogPostCache.size + blogListCache.size + tagCache.size + authorCache.size, maxCacheSize: MAX_CACHE_SIZE, cacheTTL: CACHE_TTL, }; @@ -201,10 +214,13 @@ export function getCacheStats() { /** * Warm up cache with frequently accessed data - * @param {Function} getAllPosts - Function to get all blog posts - * @param {Function} getAllTags - Function to get all tags + * @param getAllPosts - Function to get all blog posts + * @param getAllTags - Function to get all tags */ -export async function warmCache(getAllPosts, getAllTags) { +export async function warmCache( + getAllPosts: () => T[], + getAllTags: () => string[], +): Promise { try { // Cache all blog posts const allPosts = getAllPosts(); @@ -220,7 +236,8 @@ export async function warmCache(getAllPosts, getAllTags) { // Cache individual posts (first 10) allPosts.slice(0, 10).forEach((post) => { - cacheBlogPost(post.slug, post); + const postWithSlug = post as { slug: string }; + cacheBlogPost(postWithSlug.slug, post); }); console.log("Cache warmed up successfully"); @@ -231,9 +248,9 @@ export async function warmCache(getAllPosts, getAllTags) { /** * Check if cache is healthy - * @returns {boolean} True if cache is healthy + * @returns True if cache is healthy */ -export function isCacheHealthy() { +export function isCacheHealthy(): boolean { try { clearExpiredCache(); return blogPostCache.size < MAX_CACHE_SIZE; diff --git a/lib/content.js b/lib/content.ts similarity index 64% rename from lib/content.js rename to lib/content.ts index 480968c..44b8ed4 100644 --- a/lib/content.js +++ b/lib/content.ts @@ -1,18 +1,31 @@ import fs from "fs"; import path from "path"; import matter from "gray-matter"; -import { validateBlogPost, sanitizeBlogPost } from "./validation.js"; +import { + validateBlogPost, + sanitizeBlogPost, + type BlogPostFrontmatter, +} from "./validation"; /** * Content processing utilities for blog posts */ +export interface BlogPost { + slug: string; + frontmatter: BlogPostFrontmatter; + content: string; + htmlContent: string; + filePath: string; + lastModified: Date; +} + /** * Generate a URL-friendly slug from a string - * @param {string} text - Text to convert to slug - * @returns {string} URL-friendly slug + * @param text - Text to convert to slug + * @returns URL-friendly slug */ -function generateSlug(text) { +function generateSlug(text: string): string { return text .toLowerCase() .replace(/[^\w\s-]/g, "") // Remove special characters @@ -23,9 +36,9 @@ function generateSlug(text) { /** * Get all blog post files from the content directory - * @returns {Array} Array of file paths + * @returns Array of file paths */ -export function markdownToHtml(markdown) { +export function markdownToHtml(markdown: string): string { if (!markdown) return ""; return ( @@ -54,13 +67,13 @@ export function markdownToHtml(markdown) { ); } -export function getBlogPostFiles() { +export function getBlogPostFiles(): string[] { const contentDirectory = path.join(process.cwd(), "content/blog"); try { const files = fs.readdirSync(contentDirectory); return files.filter( - (file) => file.endsWith(".md") || file.endsWith(".mdx"), + (file) => file.endsWith(".md") || file.endsWith(".mdx") ); } catch (error) { console.error("Error reading blog content directory:", error); @@ -70,10 +83,10 @@ export function getBlogPostFiles() { /** * Parse a single blog post file - * @param {string} filePath - Path to the markdown file - * @returns {Object|null} Parsed blog post data or null if invalid + * @param filePath - Path to the markdown file + * @returns Parsed blog post data or null if invalid */ -export function parseBlogPost(filePath) { +export function parseBlogPost(filePath: string): BlogPost | null { const fullPath = path.join(process.cwd(), "content/blog", filePath); try { @@ -84,7 +97,7 @@ export function parseBlogPost(filePath) { if (!validationResult.isValid) { console.error( `Validation errors for ${filePath}:`, - validationResult.errors, + validationResult.errors ); return null; } @@ -108,51 +121,53 @@ export function parseBlogPost(filePath) { /** * Get all blog posts, sorted by date - * @returns {Array} Array of parsed blog post objects + * @returns Array of parsed blog post objects */ -export function getAllBlogPosts() { +export function getAllBlogPosts(): BlogPost[] { const fileNames = getBlogPostFiles(); const allPosts = fileNames .map((fileName) => parseBlogPost(fileName)) - .filter(Boolean) // Filter out nulls (invalid posts) + .filter((post): post is BlogPost => post !== null) // Filter out nulls (invalid posts) .sort( - (a, b) => new Date(b.frontmatter.date) - new Date(a.frontmatter.date), + (a, b) => + new Date(b.frontmatter.date).getTime() - + new Date(a.frontmatter.date).getTime() ); // Sort by date descending return allPosts; } /** * Get a single blog post by its slug - * @param {string} slug - The slug of the blog post - * @returns {Object|null} The parsed blog post data or null if not found + * @param slug - The slug of the blog post + * @returns The parsed blog post data or null if not found */ -export function getBlogPostBySlug(slug) { +export function getBlogPostBySlug(slug: string): BlogPost | null { const allPosts = getAllBlogPosts(); return allPosts.find((post) => post.slug === slug) || null; } /** * Get related blog posts based on provided slugs or fallback to recent posts. - * @param {string} currentPostSlug - The slug of the current post to exclude. - * @param {string[]} relatedSlugs - Array of slugs for explicitly related posts. - * @param {number} limit - Maximum number of related posts to return. - * @returns {Array} Array of related blog post objects. + * @param currentPostSlug - The slug of the current post to exclude. + * @param relatedSlugs - Array of slugs for explicitly related posts. + * @param limit - Maximum number of related posts to return. + * @returns Array of related blog post objects. */ export function getRelatedBlogPosts( - currentPostSlug, - relatedSlugs = [], - limit = 3, -) { + currentPostSlug: string, + relatedSlugs: string[] = [], + limit: number = 3 +): BlogPost[] { const allPosts = getAllBlogPosts(); const filteredPosts = allPosts.filter( - (post) => post.slug !== currentPostSlug, + (post) => post.slug !== currentPostSlug ); - let related = []; + let related: BlogPost[] = []; if (relatedSlugs && relatedSlugs.length > 0) { related = relatedSlugs .map((slug) => filteredPosts.find((post) => post.slug === slug)) - .filter(Boolean); // Filter out any related slugs that don't exist + .filter((post): post is BlogPost => post !== undefined); // Filter out any related slugs that don't exist } // If not enough related posts, or no related slugs provided, fill with recent posts @@ -170,11 +185,11 @@ export function getRelatedBlogPosts( /** * Get all unique tags from all blog posts. - * @returns {string[]} Array of unique tags. + * @returns Array of unique tags. */ -export function getAllTags() { +export function getAllTags(): string[] { const allPosts = getAllBlogPosts(); - const tags = new Set(); + const tags = new Set(); allPosts.forEach((post) => { if (post.frontmatter.tags) { post.frontmatter.tags.forEach((tag) => tags.add(tag)); @@ -185,23 +200,23 @@ export function getAllTags() { /** * Get blog posts filtered by a specific tag. - * @param {string} tag - The tag to filter by. - * @returns {Object[]} Array of blog post objects matching the tag. + * @param tag - The tag to filter by. + * @returns Array of blog post objects matching the tag. */ -export function getBlogPostsByTag(tag) { +export function getBlogPostsByTag(tag: string): BlogPost[] { const allPosts = getAllBlogPosts(); return allPosts.filter( - (post) => post.frontmatter.tags && post.frontmatter.tags.includes(tag), + (post) => post.frontmatter.tags && post.frontmatter.tags.includes(tag) ); } /** * Search blog posts by text content - * @param {string} query - Search query - * @param {number} limit - Maximum number of results - * @returns {Object[]} Array of matching blog post objects + * @param query - Search query + * @param limit - Maximum number of results + * @returns Array of matching blog post objects */ -export function searchBlogPosts(query, limit = 10) { +export function searchBlogPosts(query: string, limit: number = 10): BlogPost[] { if (!query || query.trim() === "") return []; const searchTerm = query.toLowerCase().trim(); @@ -216,7 +231,7 @@ export function searchBlogPosts(query, limit = 10) { .includes(searchTerm); const contentMatch = post.content.toLowerCase().includes(searchTerm); const tagMatch = post.frontmatter.tags?.some((tag) => - tag.toLowerCase().includes(searchTerm), + tag.toLowerCase().includes(searchTerm) ); return titleMatch || descriptionMatch || contentMatch || tagMatch; @@ -227,31 +242,42 @@ export function searchBlogPosts(query, limit = 10) { /** * Get blog posts by author - * @param {string} author - Author name to filter by - * @returns {Object[]} Array of blog post objects by the author + * @param author - Author name to filter by + * @returns Array of blog post objects by the author */ -export function getBlogPostsByAuthor(author) { +export function getBlogPostsByAuthor(author: string): BlogPost[] { const allPosts = getAllBlogPosts(); return allPosts.filter( - (post) => post.frontmatter.author.toLowerCase() === author.toLowerCase(), + (post) => post.frontmatter.author.toLowerCase() === author.toLowerCase() ); } /** * Get recent blog posts - * @param {number} limit - Maximum number of posts to return - * @returns {Object[]} Array of recent blog post objects + * @param limit - Maximum number of posts to return + * @returns Array of recent blog post objects */ -export function getRecentBlogPosts(limit = 5) { +export function getRecentBlogPosts(limit: number = 5): BlogPost[] { const allPosts = getAllBlogPosts(); return allPosts.slice(0, limit); } +export interface BlogStats { + totalPosts: number; + totalTags: number; + totalAuthors: number; + dateRange: { + earliest: string | null; + latest: string | null; + }; + averagePostsPerMonth: number; +} + /** * Get blog post statistics - * @returns {Object} Statistics about blog posts + * @returns Statistics about blog posts */ -export function getBlogStats() { +export function getBlogStats(): BlogStats { const allPosts = getAllBlogPosts(); const tags = getAllTags(); @@ -272,11 +298,13 @@ export function getBlogStats() { (allPosts.length / Math.max( 1, - (new Date(allPosts[0].frontmatter.date) - - new Date(allPosts[allPosts.length - 1].frontmatter.date)) / - (1000 * 60 * 60 * 24 * 30), + (new Date(allPosts[0].frontmatter.date).getTime() - + new Date( + allPosts[allPosts.length - 1].frontmatter.date + ).getTime()) / + (1000 * 60 * 60 * 24 * 30) )) * - 10, + 10 ) / 10 : 0, }; diff --git a/lib/mdx.js b/lib/mdx.ts similarity index 74% rename from lib/mdx.js rename to lib/mdx.ts index de67c15..b3848e4 100644 --- a/lib/mdx.js +++ b/lib/mdx.ts @@ -2,11 +2,47 @@ * MDX processing utilities for enhanced markdown content */ +export interface Heading { + level: number; + text: string; + id: string; + line: number; +} + +export interface Link { + text: string; + url: string; + index: number; +} + +export interface Image { + alt: string; + src: string; + index: number; +} + +export interface ProcessedMarkdown { + content: string; + htmlContent: string; + headings: Heading[]; + links: Link[]; + images: Image[]; +} + +export interface ProcessedFrontmatter { + publishedDate: Date; + year: number; + month: number; + day: number; + isRecent: boolean; + [key: string]: unknown; +} + /** * Format date consistently across the markdown pipeline * Uses "Month Year" format (e.g., "April 2025") */ -export function formatDate(dateString) { +export function formatDate(dateString: string): string { const date = new Date(dateString); return date.toLocaleDateString("en-US", { year: "numeric", @@ -16,10 +52,10 @@ export function formatDate(dateString) { /** * Process markdown content and extract metadata - * @param {string} markdown - Raw markdown content - * @returns {object} Processed content with metadata + * @param markdown - Raw markdown content + * @returns Processed content with metadata */ -export function processMarkdown(markdown) { +export function processMarkdown(markdown: string): ProcessedMarkdown { if (!markdown) { return { content: "", @@ -53,13 +89,13 @@ export function processMarkdown(markdown) { /** * Extract all headings from markdown content - * @param {string} markdown - Raw markdown content - * @returns {Array} Array of heading objects with level, text, and id + * @param markdown - Raw markdown content + * @returns Array of heading objects with level, text, and id */ -function extractHeadings(markdown) { +function extractHeadings(markdown: string): Heading[] { const headingRegex = /^(#{1,6})\s+(.+)$/gm; - const headings = []; - let match; + const headings: Heading[] = []; + let match: RegExpExecArray | null; while ((match = headingRegex.exec(markdown)) !== null) { const level = match[1].length; @@ -79,13 +115,13 @@ function extractHeadings(markdown) { /** * Extract all links from markdown content - * @param {string} markdown - Raw markdown content - * @returns {Array} Array of link objects + * @param markdown - Raw markdown content + * @returns Array of link objects */ -function extractLinks(markdown) { +function extractLinks(markdown: string): Link[] { const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; - const links = []; - let match; + const links: Link[] = []; + let match: RegExpExecArray | null; while ((match = linkRegex.exec(markdown)) !== null) { links.push({ @@ -100,13 +136,13 @@ function extractLinks(markdown) { /** * Extract all images from markdown content - * @param {string} markdown - Raw markdown content - * @returns {Array} Array of image objects + * @param markdown - Raw markdown content + * @returns Array of image objects */ -function extractImages(markdown) { +function extractImages(markdown: string): Image[] { const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; - const images = []; - let match; + const images: Image[] = []; + let match: RegExpExecArray | null; while ((match = imageRegex.exec(markdown)) !== null) { images.push({ @@ -121,10 +157,10 @@ function extractImages(markdown) { /** * Generate a unique ID for a heading - * @param {string} text - Heading text - * @returns {string} Unique ID + * @param text - Heading text + * @returns Unique ID */ -function generateHeadingId(text) { +function generateHeadingId(text: string): string { return text .toLowerCase() .replace(/[^\w\s-]/g, "") @@ -137,10 +173,10 @@ function generateHeadingId(text) { * Convert markdown to HTML with enhanced formatting * - Preserves extra blank lines between paragraphs as visible gaps * (each extra blank line becomes

 

) - * @param {string} markdown - Raw markdown content - * @returns {string} HTML content + * @param markdown - Raw markdown content + * @returns HTML content */ -function markdownToHtml(markdown) { +function markdownToHtml(markdown: string): string { if (!markdown) return ""; // Normalize line endings @@ -266,10 +302,10 @@ function markdownToHtml(markdown) { /** * Generate a table of contents from headings - * @param {Array} headings - Array of heading objects - * @returns {string} HTML table of contents + * @param headings - Array of heading objects + * @returns HTML table of contents */ -export function generateTableOfContents(headings) { +export function generateTableOfContents(headings: Heading[]): string { if (!headings || headings.length === 0) return ""; let toc = ' ); - } + }, ); MenuBar.displayName = "MenuBar"; diff --git a/app/components/MenuBarItem.tsx b/app/components/MenuBarItem.tsx index b1b64ef..1513fec 100644 --- a/app/components/MenuBarItem.tsx +++ b/app/components/MenuBarItem.tsx @@ -1,7 +1,6 @@ import React, { memo } from "react"; -interface MenuBarItemProps - extends React.AnchorHTMLAttributes { +interface MenuBarItemProps extends React.AnchorHTMLAttributes { href?: string; children?: React.ReactNode; variant?: "default" | "home"; @@ -180,7 +179,7 @@ const MenuBarItem = memo( {children} ); - } + }, ); MenuBarItem.displayName = "MenuBarItem"; diff --git a/app/components/MiniCard.tsx b/app/components/MiniCard.tsx index 1e1f506..b93d89e 100644 --- a/app/components/MiniCard.tsx +++ b/app/components/MiniCard.tsx @@ -129,7 +129,7 @@ const MiniCard = memo( {cardContent} ); - } + }, ); MiniCard.displayName = "MiniCard"; diff --git a/app/components/NavigationItem.tsx b/app/components/NavigationItem.tsx index b48f1b7..418f0cb 100644 --- a/app/components/NavigationItem.tsx +++ b/app/components/NavigationItem.tsx @@ -1,7 +1,6 @@ import React, { memo } from "react"; -interface NavigationItemProps - extends React.AnchorHTMLAttributes { +interface NavigationItemProps extends React.AnchorHTMLAttributes { href?: string; children?: React.ReactNode; variant?: "default"; @@ -64,7 +63,7 @@ const NavigationItem = memo( {children} ); - } + }, ); NavigationItem.displayName = "NavigationItem"; diff --git a/app/components/NumberedCard.tsx b/app/components/NumberedCard.tsx index b17bbc9..b140bb9 100644 --- a/app/components/NumberedCard.tsx +++ b/app/components/NumberedCard.tsx @@ -27,7 +27,7 @@ const NumberedCard = memo( ); - } + }, ); NumberedCard.displayName = "NumberedCard"; diff --git a/app/components/NumberedCards.tsx b/app/components/NumberedCards.tsx index 634eeae..195725b 100644 --- a/app/components/NumberedCards.tsx +++ b/app/components/NumberedCards.tsx @@ -32,7 +32,7 @@ const NumberedCards = memo(({ title, subtitle, cards }) => { text: card.text, })), }), - [title, subtitle, cards] + [title, subtitle, cards], ); return ( diff --git a/app/components/QuoteBlock.tsx b/app/components/QuoteBlock.tsx index 77c590f..709129d 100644 --- a/app/components/QuoteBlock.tsx +++ b/app/components/QuoteBlock.tsx @@ -111,7 +111,7 @@ const QuoteBlock = memo( const handleImageError = (error: unknown) => { console.warn( `QuoteBlock: Failed to load avatar image for ${author}:`, - error + error, ); setImageError(true); setImageLoading(false); @@ -275,7 +275,7 @@ const QuoteBlock = memo( ); - } + }, ); QuoteBlock.displayName = "QuoteBlock"; diff --git a/app/components/RadioButton.tsx b/app/components/RadioButton.tsx index 3de9841..f59a5bd 100644 --- a/app/components/RadioButton.tsx +++ b/app/components/RadioButton.tsx @@ -94,7 +94,7 @@ const RadioButton = ({ onChange({ checked: true, value }); } }, - [disabled, onChange, checked, value] + [disabled, onChange, checked, value], ); return ( diff --git a/app/components/RadioGroup.tsx b/app/components/RadioGroup.tsx index cfa7841..7e4cab3 100644 --- a/app/components/RadioGroup.tsx +++ b/app/components/RadioGroup.tsx @@ -42,7 +42,7 @@ const RadioGroup = ({ onChange({ value: optionValue }); } }, - [disabled, onChange] + [disabled, onChange], ); return ( diff --git a/app/components/RelatedArticles.tsx b/app/components/RelatedArticles.tsx index 005c364..0dc6657 100644 --- a/app/components/RelatedArticles.tsx +++ b/app/components/RelatedArticles.tsx @@ -15,7 +15,7 @@ const RelatedArticles = memo( // Memoize filtered posts to prevent unnecessary re-computations const filteredPosts = useMemo( () => relatedPosts.filter((post) => post.slug !== currentPostSlug), - [relatedPosts, currentPostSlug] + [relatedPosts, currentPostSlug], ); const [currentIndex, setCurrentIndex] = useState(0); @@ -43,7 +43,7 @@ const RelatedArticles = memo( document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }, - [] + [], ); // Memoize transform style to prevent unnecessary recalculations @@ -54,7 +54,7 @@ const RelatedArticles = memo( : "none", scrollBehavior: !isMobile ? "smooth" : "auto", }), - [isMobile, currentIndex] + [isMobile, currentIndex], ); // Memoize progress bar style calculation @@ -64,10 +64,10 @@ const RelatedArticles = memo( index === currentIndex ? `${progress}%` : index < currentIndex - ? "100%" - : "0%", + ? "100%" + : "0%", }), - [currentIndex, progress] + [currentIndex, progress], ); // Check if we're on mobile (below lg breakpoint) @@ -165,7 +165,7 @@ const RelatedArticles = memo( ); - } + }, ); RelatedArticles.displayName = "RelatedArticles"; diff --git a/app/components/RuleCard.tsx b/app/components/RuleCard.tsx index 04dd305..01e6a91 100644 --- a/app/components/RuleCard.tsx +++ b/app/components/RuleCard.tsx @@ -16,7 +16,7 @@ declare global { gtag?: ( command: string, eventName: string, - params?: Record + params?: Record, ) => void; analytics?: { track: (eventName: string, params?: Record) => void; @@ -93,7 +93,7 @@ const RuleCard = memo( )} ); - } + }, ); RuleCard.displayName = "RuleCard"; diff --git a/app/components/RuleStack.tsx b/app/components/RuleStack.tsx index 670734c..0856a43 100644 --- a/app/components/RuleStack.tsx +++ b/app/components/RuleStack.tsx @@ -15,7 +15,7 @@ declare global { gtag?: ( command: string, eventName: string, - params?: Record + params?: Record, ) => void; analytics?: { track: (eventName: string, params?: Record) => void; diff --git a/app/components/SectionHeader.tsx b/app/components/SectionHeader.tsx index 9666ff4..d763b67 100644 --- a/app/components/SectionHeader.tsx +++ b/app/components/SectionHeader.tsx @@ -59,7 +59,7 @@ const SectionHeader = memo( ); - } + }, ); SectionHeader.displayName = "SectionHeader"; diff --git a/app/components/Select.tsx b/app/components/Select.tsx index 9411cf3..7711956 100644 --- a/app/components/Select.tsx +++ b/app/components/Select.tsx @@ -51,7 +51,7 @@ const Select = forwardRef( options, ...props }, - ref + ref, ) => { const generatedId = useId(); const selectId = id || `select-${generatedId}`; @@ -94,7 +94,7 @@ const Select = forwardRef( selectRef.current.focus(); } }, - [onChange] + [onChange], ); // Handle select button click @@ -116,7 +116,7 @@ const Select = forwardRef( setIsOpen(false); } }, - [disabled, isOpen] + [disabled, isOpen], ); const getSizeStyles = (): string => { @@ -253,7 +253,7 @@ const Select = forwardRef( // Handle options prop if (options && Array.isArray(options)) { const selectedOption = options.find( - (option) => option.value === selectedValue + (option) => option.value === selectedValue, ); return selectedOption ? selectedOption.label : placeholder; } @@ -261,7 +261,7 @@ const Select = forwardRef( // Handle children (option elements) const selectedOption = React.Children.toArray(children).find( (child) => - React.isValidElement(child) && child.props.value === selectedValue + React.isValidElement(child) && child.props.value === selectedValue, ) as | React.ReactElement<{ value: string; children: React.ReactNode }> | undefined; @@ -353,7 +353,7 @@ const Select = forwardRef( onClick={() => handleOptionSelect( optionProps.value, - String(optionProps.children) + String(optionProps.children), ) } > @@ -369,7 +369,7 @@ const Select = forwardRef( ); - } + }, ); Select.displayName = "Select"; diff --git a/app/components/SelectDropdown.tsx b/app/components/SelectDropdown.tsx index 874c26c..7c97a08 100644 --- a/app/components/SelectDropdown.tsx +++ b/app/components/SelectDropdown.tsx @@ -34,7 +34,7 @@ const SelectDropdown = forwardRef( {children} ); - } + }, ); SelectDropdown.displayName = "SelectDropdown"; diff --git a/app/components/SelectOption.tsx b/app/components/SelectOption.tsx index 8bd8450..d1638d3 100644 --- a/app/components/SelectOption.tsx +++ b/app/components/SelectOption.tsx @@ -8,7 +8,7 @@ interface SelectOptionProps { disabled?: boolean; className?: string; onClick?: ( - e: React.MouseEvent | React.KeyboardEvent + e: React.MouseEvent | React.KeyboardEvent, ) => void; size?: "small" | "medium" | "large"; } @@ -24,7 +24,7 @@ const SelectOption = forwardRef( size = "medium", ...props }, - ref + ref, ) => { const getTextSize = (): string => { switch (size) { @@ -67,7 +67,7 @@ const SelectOption = forwardRef( onClick(e); } }, - [disabled, onClick] + [disabled, onClick], ); const handleKeyDown = useCallback( @@ -79,7 +79,7 @@ const SelectOption = forwardRef( } } }, - [disabled, onClick] + [disabled, onClick], ); return ( @@ -114,7 +114,7 @@ const SelectOption = forwardRef( ); - } + }, ); SelectOption.displayName = "SelectOption"; diff --git a/app/components/Switch.tsx b/app/components/Switch.tsx index 7dd4c93..4b30214 100644 --- a/app/components/Switch.tsx +++ b/app/components/Switch.tsx @@ -1,12 +1,14 @@ import React, { memo, useCallback, useId, forwardRef } from "react"; -interface SwitchProps - extends Omit, "onChange"> { +interface SwitchProps extends Omit< + React.ButtonHTMLAttributes, + "onChange" +> { checked?: boolean; onChange?: ( e: | React.MouseEvent - | React.KeyboardEvent + | React.KeyboardEvent, ) => void; onFocus?: (e: React.FocusEvent) => void; onBlur?: (e: React.FocusEvent) => void; @@ -36,7 +38,7 @@ const Switch = memo( onChange(e); } }, - [onChange] + [onChange], ); const handleKeyDown = useCallback( @@ -48,7 +50,7 @@ const Switch = memo( } } }, - [onChange] + [onChange], ); const handleFocus = useCallback( @@ -57,7 +59,7 @@ const Switch = memo( onFocus(e); } }, - [onFocus] + [onFocus], ); const handleBlur = useCallback( @@ -66,7 +68,7 @@ const Switch = memo( onBlur(e); } }, - [onBlur] + [onBlur], ); // Switch track styles based on checked state @@ -170,7 +172,7 @@ const Switch = memo( {label && {label}} ); - }) + }), ); Switch.displayName = "Switch"; diff --git a/app/components/TextArea.tsx b/app/components/TextArea.tsx index 4327c0d..4247ea2 100644 --- a/app/components/TextArea.tsx +++ b/app/components/TextArea.tsx @@ -2,11 +2,10 @@ import React, { memo, useCallback, forwardRef, useId } from "react"; -interface TextAreaProps - extends Omit< - React.TextareaHTMLAttributes, - "size" | "onChange" | "onFocus" | "onBlur" - > { +interface TextAreaProps extends Omit< + React.TextareaHTMLAttributes, + "size" | "onChange" | "onFocus" | "onBlur" +> { size?: "small" | "medium" | "large"; labelVariant?: "default" | "horizontal"; state?: "default" | "active" | "hover" | "focus"; @@ -42,7 +41,7 @@ const TextArea = forwardRef( rows, ...props }, - ref + ref, ) => { // Generate unique ID for accessibility if not provided const generatedId = useId(); @@ -161,7 +160,7 @@ const TextArea = forwardRef( onChange(e); } }, - [disabled, onChange] + [disabled, onChange], ); const handleFocus = useCallback( @@ -170,7 +169,7 @@ const TextArea = forwardRef( onFocus(e); } }, - [disabled, onFocus] + [disabled, onFocus], ); const handleBlur = useCallback( @@ -179,7 +178,7 @@ const TextArea = forwardRef( onBlur(e); } }, - [disabled, onBlur] + [disabled, onBlur], ); return ( @@ -213,7 +212,7 @@ const TextArea = forwardRef( ); - } + }, ); TextArea.displayName = "TextArea"; diff --git a/app/components/Toggle.tsx b/app/components/Toggle.tsx index 543fd08..4c2d35f 100644 --- a/app/components/Toggle.tsx +++ b/app/components/Toggle.tsx @@ -1,13 +1,15 @@ import React, { memo, useCallback, useId, forwardRef } from "react"; -interface ToggleProps - extends Omit, "onChange"> { +interface ToggleProps extends Omit< + React.ButtonHTMLAttributes, + "onChange" +> { label?: string; checked?: boolean; onChange?: ( e: | React.MouseEvent - | React.KeyboardEvent + | React.KeyboardEvent, ) => void; onFocus?: (e: React.FocusEvent) => void; onBlur?: (e: React.FocusEvent) => void; @@ -37,7 +39,7 @@ const Toggle = forwardRef( className = "", ...props }, - ref + ref, ) => { const toggleId = useId(); const labelId = useId(); @@ -141,13 +143,13 @@ const Toggle = forwardRef( ( e: | React.MouseEvent - | React.KeyboardEvent + | React.KeyboardEvent, ) => { if (!disabled && onChange) { onChange(e); } }, - [disabled, onChange] + [disabled, onChange], ); const handleFocus = useCallback( @@ -156,7 +158,7 @@ const Toggle = forwardRef( onFocus(e); } }, - [disabled, onFocus] + [disabled, onFocus], ); const handleBlur = useCallback( @@ -165,7 +167,7 @@ const Toggle = forwardRef( onBlur(e); } }, - [disabled, onBlur] + [disabled, onBlur], ); const handleKeyDown = useCallback( @@ -177,7 +179,7 @@ const Toggle = forwardRef( } } }, - [disabled, onChange] + [disabled, onChange], ); return ( @@ -213,7 +215,7 @@ const Toggle = forwardRef( ); - } + }, ); Toggle.displayName = "Toggle"; diff --git a/app/components/ToggleGroup.tsx b/app/components/ToggleGroup.tsx index 6c4fe4d..ef0e067 100644 --- a/app/components/ToggleGroup.tsx +++ b/app/components/ToggleGroup.tsx @@ -1,7 +1,9 @@ import React, { memo, useCallback, useId, forwardRef } from "react"; -interface ToggleGroupProps - extends Omit, "onChange"> { +interface ToggleGroupProps extends Omit< + React.ButtonHTMLAttributes, + "onChange" +> { children?: React.ReactNode; className?: string; position?: "left" | "middle" | "right"; @@ -11,7 +13,7 @@ interface ToggleGroupProps onChange?: ( e: | React.MouseEvent - | React.KeyboardEvent + | React.KeyboardEvent, ) => void; onFocus?: (e: React.FocusEvent) => void; onBlur?: (e: React.FocusEvent) => void; @@ -72,7 +74,7 @@ const ToggleGroup = memo( onChange(e); } }, - [onChange] + [onChange], ); const handleFocus = useCallback( @@ -81,7 +83,7 @@ const ToggleGroup = memo( onFocus(e); } }, - [onFocus] + [onFocus], ); const handleBlur = useCallback( @@ -90,7 +92,7 @@ const ToggleGroup = memo( onBlur(e); } }, - [onBlur] + [onBlur], ); const handleKeyDown = useCallback( @@ -102,7 +104,7 @@ const ToggleGroup = memo( } } }, - [onChange] + [onChange], ); const toggleClasses = ` @@ -146,7 +148,7 @@ const ToggleGroup = memo( {showText ? children : children || "☰"} ); - }) + }), ); ToggleGroup.displayName = "ToggleGroup"; diff --git a/app/components/WebVitalsDashboard.tsx b/app/components/WebVitalsDashboard.tsx index b1de767..272e766 100644 --- a/app/components/WebVitalsDashboard.tsx +++ b/app/components/WebVitalsDashboard.tsx @@ -116,7 +116,7 @@ const WebVitalsDashboard = memo(() => { }, })); }); - } + }, ); } }, []); diff --git a/docs/assets/DocsRenderer-PQXLIZUC-CZs-lgH4.js b/docs/assets/DocsRenderer-PQXLIZUC-CZs-lgH4.js index f0d1eab..f533007 100644 --- a/docs/assets/DocsRenderer-PQXLIZUC-CZs-lgH4.js +++ b/docs/assets/DocsRenderer-PQXLIZUC-CZs-lgH4.js @@ -8691,7 +8691,6 @@ function $b(e, t) { --t && Ze() && !(De < 48 || De > 102 || (De > 57 && De < 65) || (De > 70 && De < 97)); - ); return io(e, Lo() + (t < 6 && Dt() == 32 && Ze() == 32)); } @@ -8747,7 +8746,6 @@ function No(e, t, r, n, o, a, i, l, s) { A = n, S = w; x; - ) switch (((g = C), (C = Ze()))) { case 40: @@ -8934,7 +8932,6 @@ var wD = P(function (e, t, r) { for ( var n = 0, o = 0; (n = o), (o = Dt()), n === 38 && o === 12 && (t[r] = 1), !Un(o); - ) Ze(); return io(e, Je); @@ -8972,7 +8969,6 @@ var wD = P(function (e, t, r) { r = e.parent, n = e.column === r.column && e.line === r.line; r.type !== "rule"; - ) if (((r = r.parent), !r)) return; if ( @@ -12469,7 +12465,6 @@ function H1(e, t, r, n, o, a, i, l, s) { f++; }, "_loop"); f < d.length; - ) v(); if (c !== d.length - 1) { @@ -13261,7 +13256,6 @@ var q1, for ( var i = o || "", l = a || "div", s = {}, u = 0, d, p, c; u < i.length; - ) ((r.lastIndex = u), (c = r.exec(i)), @@ -13304,7 +13298,6 @@ var q1, for ( var l = [], s = String(i || n), u = s.indexOf(t), d = 0, p = !1, c; !p; - ) (u === -1 && ((u = s.length), (p = !0)), (c = s.slice(d, u).trim()), @@ -13760,7 +13753,6 @@ var q1, We--, be++; ++We < be; - ) if ( (bt === f && (Ce = ke[pr] || 1), (bt = H.charCodeAt(We)), bt === x) @@ -13794,7 +13786,6 @@ var q1, Ap = I[qt], Ie--; ++Ie < be && ((Ae = H.charCodeAt(Ie)), !!Ap(Ae)); - ) ((Me += u(Ae)), qt === D && s.call(r, Me) && ((Ve = Me), (Pr = r[Me]))); @@ -14769,7 +14760,6 @@ var q1, for ( L = _[I], L = typeof L == "string" ? [L] : L, j = L.length, B = -1; ++B < j; - ) k[L[B]] = k[I]; } diff --git a/docs/assets/axe-kJbNpjRz.js b/docs/assets/axe-kJbNpjRz.js index dae0b3c..fa12f23 100644 --- a/docs/assets/axe-kJbNpjRz.js +++ b/docs/assets/axe-kJbNpjRz.js @@ -1332,7 +1332,6 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho I = R.next(), b = 0; !I.done; - ) ((S = h ? d.call(h, v, I.value, b) : I.value), g ? ((f.value = S), p(w, b, f)) : (w[b] = S), @@ -1772,7 +1771,6 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho for ( s = c[1][d], c[0].splice(d, 1), c[1].splice(d, 1); !c[0].length && m.length; - ) ((d = m.pop()), (c = m.pop()), @@ -1853,7 +1851,6 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho for ( l = d[1][f], d[0].splice(f, 1), d[1].splice(f, 1); !d[0].length && p.length; - ) ((f = p.pop()), (d = p.pop()), @@ -2705,7 +2702,6 @@ Consider to rely on 'then' or 'done' mode instead.`); ` || c === "\r" || c === "\f"; - ) ((b = !0), n++, (c = a.charAt(n))); return b; @@ -2750,7 +2746,6 @@ Consider to rely on 'then' or 'done' mode instead.`); p(), (c = a.charAt(n)), !(n >= l || c === "," || c === ")")); - ) if (u[c]) { var _ = c; @@ -3728,7 +3723,6 @@ Consider to rely on 'then' or 'done' mode instead.`); xe = be.join(""), Me = []; xe.length; - ) (Me.push(parseInt(xe.substring(0, 8), 2)), (xe = xe.substring(8))); @@ -5058,7 +5052,6 @@ Consider to rely on 'then' or 'done' mode instead.`); _ = [], F; D > w; - ) ((F = g[w++]), (!r || (b ? F in v : l(v, F))) && @@ -5275,7 +5268,6 @@ Consider to rely on 'then' or 'done' mode instead.`); for ( var c = i(l), d = o(l), f = d.length, p = 0, m; f > p; - ) a.f(s, (m = d[p++]), c[m]); return s; @@ -6666,7 +6658,6 @@ Consider to rely on 'then' or 'done' mode instead.`); i = "", o = t.charCodeAt(0); ++a < r; - ) { if (((n = t.charCodeAt(a)), n == 0)) { i += "�"; @@ -6956,12 +6947,10 @@ Consider to rely on 'then' or 'done' mode instead.`); o.children.length && (a.push(r), (r = o.children.slice())); !r.length && a.length; - ) r = a.pop(); }; r.length; - ) n(); return t; @@ -7546,7 +7535,6 @@ Consider to rely on 'then' or 'done' mode instead.`); for ( var n = Array.isArray(t), i = n ? t[r] : t, o = ul(e, i); !o && a && e.parent; - ) ((e = e.parent), (o = ul(e, i))); if (r > 0) { @@ -7677,7 +7665,6 @@ Consider to rely on 'then' or 'done' mode instead.`); n < 16 && (t[a + n++] = pl[i]); }); n < 16; - ) t[a + n++] = 0; return t; @@ -9119,7 +9106,6 @@ Consider to rely on 'then' or 'done' mode instead.`); ), u = r ? o.nextNode() : o.currentNode; u; - ) { var s = le(u); (s && s.parent @@ -9534,7 +9520,6 @@ Consider to rely on 'then' or 'done' mode instead.`); a.shadowId === r.shadowId && !i && (n.push(a), a.props.nodeName !== "legend"); - ) { if (a._inDisabledFieldset !== void 0) { i = a._inDisabledFieldset; @@ -9648,7 +9633,6 @@ Consider to rely on 'then' or 'done' mode instead.`); !o.find(function (v) { return v.root === i.getRootNode(); }); - ) i = i.getRootNode().host; if ( @@ -15010,7 +14994,6 @@ Consider to rely on 'then' or 'done' mode instead.`); h = p, v = bt(d, c); v - h > m; - ) { var g = Ta(d); g = ir(g, { space: i, method: "clip" }); @@ -16253,7 +16236,6 @@ Consider to rely on 'then' or 'done' mode instead.`); return Math.max(I, N); }, 0); b > o; - ) { b = 0; for (var D = 1; D < v.length && v.length < d; D++) { @@ -19034,7 +19016,6 @@ See: https://github.com/dequelabs/axe-core/blob/master/doc/context.md`, for ( L.head.appendChild(a); (o = L.elementFromPoint(n, i)) && l.indexOf(o) === -1; - ) (l.push(o), c.push({ @@ -19048,7 +19029,6 @@ See: https://github.com/dequelabs/axe-core/blob/master/doc/context.md`, l.push(L.documentElement)), u = c.length; (s = c[--u]); - ) l[u].style.setProperty( t, @@ -19088,7 +19068,6 @@ See: https://github.com/dequelabs/axe-core/blob/master/doc/context.md`, o = q1(i, t, null, e[0].shadowId, a.pop()), u = []; o.vNodesIndex < o.vNodes.length; - ) { for ( var s, @@ -19151,7 +19130,6 @@ See: https://github.com/dequelabs/axe-core/blob/master/doc/context.md`, c.children.length && (n.push(o), (o = q1(c.children, f, d, c.shadowId, a.pop()))); o.vNodesIndex === o.vNodes.length && n.length; - ) (a.push(o), (o = n.pop())); } @@ -32224,7 +32202,6 @@ See: https://github.com/dequelabs/axe-core/blob/master/doc/context.md`, r.parent && !r.parent._hasRegionDescendant && r.parent.actualNode !== L.body; - ) r = r.parent; return r; @@ -32327,7 +32304,6 @@ See: https://github.com/dequelabs/axe-core/blob/master/doc/context.md`, for ( var t = e, r = e.textContent.trim(), a = r; a === r && t !== void 0; - ) { var n = -1; if (((e = t), e.children.length === 0)) return e; @@ -34252,7 +34228,6 @@ See: https://github.com/dequelabs/axe-core/blob/master/doc/context.md`, return !er(u); })); r; - ) (r.nodeName.toUpperCase() === "LABEL" && n.indexOf(r) === -1 && @@ -35367,7 +35342,6 @@ See: https://github.com/dequelabs/axe-core/blob/master/doc/context.md`, } }; (r = n.shift()); - ) i(); return a; diff --git a/docs/assets/iframe-D_aMTKb2.js b/docs/assets/iframe-D_aMTKb2.js index 967f77f..cf18815 100644 --- a/docs/assets/iframe-D_aMTKb2.js +++ b/docs/assets/iframe-D_aMTKb2.js @@ -7995,7 +7995,6 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho c = "", d = o.charCodeAt(0); ++u < i; - ) { if (((l = o.charCodeAt(u)), l == 0)) { c += "�"; @@ -15710,7 +15709,6 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho for ( var d = l.length, p = "", f = 0, h = 0, m = c, y = r; c > -1 && c < d; - ) { var b = u(l[c + 1], 4), g = u(l[c + 2], 0), @@ -18394,7 +18392,6 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho for ( var P = _; P < x.length - 1 && x[P].charCodeAt(1) + 1 === x[P + 1].charCodeAt(1); - ) P += 1; var D = 1 + P - _; @@ -20086,7 +20083,6 @@ var rg = Mt("warn"), for ( r.push(t[0].replace(n, "%c").replace(a, "%c")); (o = n.exec(t[0])); - ) (r.push(o[2]), r.push("")); for (let i = 1; i < t.length; i++) r.push(t[i]); @@ -31017,7 +31013,6 @@ function Fr(e, t) { for ( var a = GT(n.length), o = em(r) ? Object(new r(a)) : new Array(a), i = 0, u; i < a; - ) ((u = n[i]), t ? (o[i] = t(u, i)) : (o[i] = u), (i += 1)); return ((o.length = a), o); @@ -33355,7 +33350,6 @@ function gD() { k = 0, I = !1; _ < D; - ) { switch (((L = Q[Q.length - 1]), L.tag)) { case "JS": @@ -36166,7 +36160,6 @@ function Ir(e, t) { for ( var a = pF(n.length), o = xm(r) ? Object(new r(a)) : new Array(a), i = 0, u; i < a; - ) ((u = n[i]), t ? (o[i] = t(u, i)) : (o[i] = u), (i += 1)); return ((o.length = a), o); @@ -41431,7 +41424,6 @@ function w0(e, t, r = !0) { t === void 0 ? n >= (r ? Math.max(e.childNodes.length - 1, 0) : 0) : n <= e.childNodes.length; - ) { if (t && n === e.childNodes.length) throw new Error("The given offset is out of bounds."); @@ -49723,7 +49715,6 @@ var Ld = Se({ for ( ; (b = y.exec(u)) && (!c || c.index + c[0].length !== u.length); - ) ((!c || b.index + b[0].length !== c.index + c[0].length) && (c = b), @@ -50727,9 +50718,8 @@ async function Uq( ) { let { renderElement: i, unmountElement: u } = await jt( async () => { - const { renderElement: f, unmountElement: h } = await import( - "./react-18-TXN0K6I-.js" - ); + const { renderElement: f, unmountElement: h } = + await import("./react-18-TXN0K6I-.js"); return { renderElement: f, unmountElement: h }; }, __vite__mapDeps([34, 3]), @@ -55349,7 +55339,6 @@ var KM = $((...e) => { E.push(new f(w, null, null, null)), _.push(new f(null, null, null, null)); E.length; - ) { if (((P = E.pop()), P === Q)) { if ( @@ -55414,7 +55403,6 @@ var KM = $((...e) => { E.push(L), _.push(L); E.length; - ) { if (((L = E.pop()), L === W)) { if ( @@ -55512,7 +55500,6 @@ var KM = $((...e) => { var T; L < _.length && ((T = _[L]), !(T.extendedRange[1] > q.range[0])); - ) T.extendedRange[1] === q.range[0] ? (q.leadingComments || (q.leadingComments = []), @@ -55530,7 +55517,6 @@ var KM = $((...e) => { var T; L < _.length && ((T = _[L]), !(q.range[1] < T.extendedRange[0])); - ) q.range[1] === T.extendedRange[0] ? (q.trailingComments || (q.trailingComments = []), @@ -56656,7 +56642,6 @@ var KM = $((...e) => { for ( var g = y.originalColumn; y && y.originalLine === p && y.originalColumn == g; - ) (h.push({ line: t.getArg(y, "generatedLine", null), @@ -56784,7 +56769,6 @@ var KM = $((...e) => { F, L; x < v; - ) if (d.charAt(x) === ";") (f++, x++, (h = 0)); else if (d.charAt(x) === ",") x++; @@ -57569,7 +57553,6 @@ var KM = $((...e) => { (O = +(O.slice(0, j) + O.slice(j + 1)) + "")), de = 0; O.charCodeAt(O.length + de - 1) === 48; - ) --de; return ( @@ -57770,7 +57753,6 @@ var KM = $((...e) => { for ( de = j[O], le = 0; le < de.length && i.code.isWhiteSpace(de.charCodeAt(le)); - ) ++le; me > le && (me = le); @@ -60583,7 +60565,6 @@ var Qs, this.expect(U.braceL), e && this.enterScope(0); this.type !== U.braceR; - ) { var n = this.parseStatement(null); t.body.push(n); @@ -61502,7 +61483,6 @@ var Qs, this.potentialArrowAt === e.start, o = !1; ; - ) { var i = this.parseSubscript(e, t, r, n, a, o); if ( @@ -62881,7 +62861,6 @@ var Qs, for ( e.lastStringValue += Ys(e.lastIntValue); this.regexp_eatRegExpIdentifierPart(e); - ) e.lastStringValue += Ys(e.lastIntValue); return !0; @@ -63305,7 +63284,6 @@ var Qs, r = this.options.onComment && this.curPosition(), n = this.input.charCodeAt((this.pos += e)); this.pos < this.input.length && !io(n); - ) n = this.input.charCodeAt(++this.pos); this.options.onComment && @@ -63863,7 +63841,6 @@ var Qs, for ( var e = "", t = !0, r = this.pos, n = this.options.ecmaVersion >= 6; this.pos < this.input.length; - ) { var a = this.fullCharCodeAtPos(); if (ha(a, n)) this.pos += a <= 65535 ? 1 : 2; @@ -64407,7 +64384,6 @@ var Qs, !c.allowNamespacedObjects && this.unexpected(); this.eat(h.dot); - ) { let P = this.startNodeAt(C, E); ((P.object = _), @@ -64479,7 +64455,6 @@ var Qs, for ( P && (_.name = P); this.type !== h.slash && this.type !== m.jsxTagEnd; - ) _.attributes.push(this.jsx_parseAttribute()); return ( @@ -70025,7 +70000,6 @@ function VB() { return K; }; E < g.length; - ) { var L = P("CHAR"), q = P("NAME"), @@ -75268,9 +75242,8 @@ var R1 = !1, ) { if (!R1) { let { toHaveNoViolations: p } = await jt(async () => { - const { toHaveNoViolations: f } = await import( - "./matchers-7Z3WT2CE-CcSi9QFY.js" - ); + const { toHaveNoViolations: f } = + await import("./matchers-7Z3WT2CE-CcSi9QFY.js"); return { toHaveNoViolations: f }; }, []); (S1.extend({ toHaveNoViolations: p }), (R1 = !0)); diff --git a/docs/assets/react-18-TXN0K6I-.js b/docs/assets/react-18-TXN0K6I-.js index 61c0ff8..77b4717 100644 --- a/docs/assets/react-18-TXN0K6I-.js +++ b/docs/assets/react-18-TXN0K6I-.js @@ -123,7 +123,6 @@ function zd() { for ( Ua(g), Tl = K(Jl); Tl !== null && !(Tl.expirationTime > g && ye()); - ) { var p = Tl.callback; if (typeof p == "function") { @@ -1059,7 +1058,6 @@ function Ad() { for ( e = t = 0; t < i.length && !i[t].includes("DetermineComponentFrameRoot"); - ) t++; for (; e < d.length && !d[e].includes("DetermineComponentFrameRoot"); ) @@ -1068,7 +1066,6 @@ function Ad() { for ( t = i.length - 1, e = d.length - 1; 1 <= t && 0 <= e && i[t] !== d[e]; - ) e--; for (; 1 <= t && 0 <= e; t--, e--) @@ -5220,7 +5217,6 @@ Error generating stack: ` + for ( l = a.child, u = Ca(l, l.pendingProps), a.child = u, u.return = a; l.sibling !== null; - ) ((l = l.sibling), (u = u.sibling = Ca(l, l.pendingProps)), @@ -5368,7 +5364,6 @@ Error generating stack: ` + u = k0(a, null, t, u), a.child = u; u; - ) ((u.flags = (u.flags & -3) | 4096), (u = u.sibling)); } @@ -5696,7 +5691,6 @@ Error generating stack: ` + for ( l.sibling.return = l.return, l = l.sibling; l.tag !== 5 && l.tag !== 6 && l.tag !== 18; - ) { if ( (l.tag === 27 && wa(l.type)) || @@ -5799,7 +5793,6 @@ Error generating stack: ` + T !== n || (t !== 0 && T.nodeType !== 3) || (i = f + t), T.nodeType === 3 && (f += T.nodeValue.length), (m = T.firstChild) !== null; - ) ((s = T), (T = m)); for (;;) { @@ -5822,7 +5815,6 @@ Error generating stack: ` + for ( qc = { focusedElem: l, selectionRange: u }, on = !1, yl = a; yl !== null; - ) if ( ((a = yl), (l = a.child), (a.subtreeFlags & 1024) !== 0 && l !== null) @@ -7545,7 +7537,6 @@ Error generating stack: ` + l = u, u = a.child; u !== null; - ) (X1(u, l), (u = u.sibling)); return (Z(nl, (nl.current & 1) | 2), a.child); @@ -8650,7 +8641,6 @@ Error generating stack: ` + e = l.expirationTimes, n = l.pendingLanes & -62914561; 0 < n; - ) { var f = 31 - Nl(n), c = 1 << f, diff --git a/docs/sb-addons/chromatic-com-storybook-1/manager-bundle.js b/docs/sb-addons/chromatic-com-storybook-1/manager-bundle.js index e337a7a..774a590 100644 --- a/docs/sb-addons/chromatic-com-storybook-1/manager-bundle.js +++ b/docs/sb-addons/chromatic-com-storybook-1/manager-bundle.js @@ -16781,7 +16781,6 @@ try { for ( var t = bn(10) ? document.body : null, n = e.offsetParent || null; n === t && e.nextElementSibling; - ) n = (e = e.nextElementSibling).offsetParent; var r = n && n.nodeName; @@ -18136,7 +18135,6 @@ try { r = new DataView(t.buffer), i = e.byteLength; i--; - ) if (n.getUint8(i) !== r.getUint8(i)) return !1; return !0; diff --git a/docs/sb-addons/onboarding-3/manager-bundle.js b/docs/sb-addons/onboarding-3/manager-bundle.js index ffd3eb4..c73576e 100644 --- a/docs/sb-addons/onboarding-3/manager-bundle.js +++ b/docs/sb-addons/onboarding-3/manager-bundle.js @@ -1457,7 +1457,6 @@ try { for ( var t = Xe(10) ? document.body : null, n = e.offsetParent || null; n === t && e.nextElementSibling; - ) n = (e = e.nextElementSibling).offsetParent; var r = n && n.nodeName; @@ -2457,7 +2456,6 @@ try { r = new DataView(t.buffer), o = e.byteLength; o--; - ) if (n.getUint8(o) !== r.getUint8(o)) return !1; return !0; diff --git a/docs/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js b/docs/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js index 31683fe..d1f4e39 100644 --- a/docs/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js +++ b/docs/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js @@ -1702,7 +1702,6 @@ This is deprecated and won't work in Storybook 8 anymore. for ( var d = l.length, m = "", p = 0, f = 0, g = u, y = r; u > -1 && u < d; - ) { var E = s(l[u + 1], 4), b = s(l[u + 2], 0), @@ -4407,7 +4406,6 @@ This is deprecated and won't work in Storybook 8 anymore. var P = B; P < S.length - 1 && S[P].charCodeAt(1) + 1 === S[P + 1].charCodeAt(1); - ) P += 1; var L = 1 + P - B; @@ -21503,7 +21501,6 @@ ${S.description}`); var P = B; P < S.length - 1 && S[P].charCodeAt(1) + 1 === S[P + 1].charCodeAt(1); - ) P += 1; var L = 1 + P - B; diff --git a/docs/sb-manager/globals-runtime.js b/docs/sb-manager/globals-runtime.js index 20a43c1..e674910 100644 --- a/docs/sb-manager/globals-runtime.js +++ b/docs/sb-manager/globals-runtime.js @@ -569,7 +569,6 @@ var T9 = R((Ge) => { for ( rC(t), kn = Eo(aa); kn !== null && (!(kn.expirationTime > t) || (e && !P9())); - ) { var n = kn.callback; if (typeof n == "function") { @@ -1076,7 +1075,6 @@ al vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mod s = o.length - 1, l = i.length - 1; 1 <= s && 0 <= l && o[s] !== i[l]; - ) l--; for (; 1 <= s && 0 <= l; s--, l--) @@ -1496,7 +1494,6 @@ al vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mod p0.innerHTML = "" + t.valueOf().toString() + "", t = p0.firstChild; e.firstChild; - ) e.removeChild(e.firstChild); for (; t.firstChild; ) e.appendChild(t.firstChild); @@ -2022,7 +2019,6 @@ al vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mod o = e.expirationTimes, i = e.pendingLanes; 0 < i; - ) { var s = 31 - _o(i), l = 1 << s, @@ -3989,7 +3985,6 @@ _reactEvents$" + Zu, for ( e = e.return; e !== null && e.tag !== 5 && e.tag !== 3 && e.tag !== 13; - ) e = e.return; gn = e; @@ -6251,7 +6246,6 @@ on" && for ( e = t.child, r = Li(e, e.pendingProps), t.child = r, r.return = t; e.sibling !== null; - ) ((e = e.sibling), (r = r.sibling = Li(e, e.pendingProps)), @@ -6803,7 +6797,6 @@ oll", n = r, r = t.child; r !== null; - ) ((i = r), (e = n), @@ -7006,7 +6999,6 @@ oll", f !== i || (n !== 0 && f.nodeType !== 3) || (u = s + n), f.nodeType === 3 && (s += f.nodeValue.length), (m = f.firstChild) !== null; - ) ((p = f), (f = m)); for (;;) { @@ -7029,7 +7021,6 @@ oll", for ( JC = { focusedElem: e, selectionRange: r }, V0 = !1, ae = t; ae !== null; - ) if ( ((t = ae), (e = t.child), (t.subtreeFlags & 1028) !== 0 && e !== null) @@ -7167,7 +7158,6 @@ oll", for ( e.sibling.return = e.return, e = e.sibling; e.tag !== 5 && e.tag !== 6 && e.tag !== 18; - ) { if (e.flags & 2 || e.child === null || e.tag === 4) continue e; ((e.child.return = e), (e = e.child)); @@ -8083,7 +8073,6 @@ oll", e.pingedLanes &= ~t, e = e.expirationTimes; 0 < t; - ) { var r = 31 - _o(t), n = 1 << r; @@ -8782,7 +8771,6 @@ oll", r = Dk(t, null, n, r), t.child = r; r; - ) ((r.flags = (r.flags & -3) | 4096), (r = r.sibling)); else { @@ -9783,7 +9771,6 @@ var zge, for ( r.push(t[0].replace(n, "%c").replace(o, "%c")); (i = n.exec(t[0])); - ) (r.push(i[2]), r.push("")); for (let s = 1; s < t.length; s++) r.push(t[s]); @@ -10076,7 +10063,6 @@ function xD(e, t) { --t && Fr() && !(Mt < 48 || Mt > 102 || (Mt > 57 && Mt < 65) || (Mt > 70 && Mt < 97)); - ); return vc(e, dp() + (t < 6 && zn() == 32 && Fr() == 32)); } @@ -10160,7 +10146,6 @@ function Dg(e, t, r, n, o, i, s, l, u) { C = n, _ = E; b; - ) switch (((v = w), (w = Fr()))) { // ( @@ -10621,7 +10606,6 @@ var obe, for ( var o = 0, i = 0; (o = i), (i = zn()), o === 38 && i === 12 && (r[n] = 1), !hc(i); - ) Fr(); return vc(t, Dr); @@ -10668,7 +10652,6 @@ var obe, n = t.parent, o = t.column === n.column && t.line === n.line; n.type !== "rule"; - ) if (((n = n.parent), !n)) return; if ( @@ -14781,7 +14764,6 @@ function rwe(e, t, r, n, o, i, s, l, u) { m++; }, "_loop"); m < d.length; - ) g(); if (p !== d.length - 1) { @@ -15748,7 +15730,6 @@ var hB = R((Wmt, mB) => { for ( var r = e || "", n = t || "div", o = {}, i = 0, s, l, u; i < r.length; - ) ((pB.lastIndex = i), (u = pB.exec(r)), @@ -15799,7 +15780,6 @@ var bB = R((N3) => { for ( var t = [], r = String(e || Pp), n = r.indexOf(M3), o = 0, i = !1, s; !i; - ) (n === -1 && ((n = r.length), (i = !0)), (s = r.slice(o, n).trim()), @@ -16294,7 +16274,6 @@ hexadecimal", m--, p++; ++m < p; - ) if ((_ === WB && (y = f[v] || 1), (_ = e.charCodeAt(m)), _ === GB)) { if ( @@ -16326,7 +16305,6 @@ hexadecimal", V = _1[D], se--; ++se < p && ((O = e.charCodeAt(se)), !!V(O)); - ) ((C += Oc(O)), D === Ic && REe.call(zB, C) && ((E = C), (Q = zB[C]))); ((S = e.charCodeAt(se) === PEe), @@ -17782,7 +17760,6 @@ var m$ = R((_ht, p$) => { for ( i = n[o], i = typeof i == "string" ? [i] : i, s = i.length, l = -1; ++l < s; - ) r[i[l]] = r[o]; } @@ -24041,7 +24018,6 @@ var NW = R((dCt, MW) => { for ( var r = e.length, n = "", o = 0, i = 0, s = t, l = OW; t > -1 && t < r; - ) { var u = IW(e[t + 1], 4), c = IW(e[t + 2], 0), @@ -35219,7 +35195,6 @@ var W6 = R((V6, Fre) => { for ( var n = String(r), o = n.length, i = -1, s, l = "", u = n.charCodeAt(0); ++i < o; - ) { if (((s = n.charCodeAt(i)), s == 0)) { l += "\uFFFD"; @@ -71648,7 +71623,6 @@ function An(e, t) { s = 0, l; s < o; - ) ((l = n[s]), t ? (i[s] = t(l, s)) : (i[s] = l), (s += 1)); return ((i.length = o), i); @@ -74091,7 +74065,6 @@ function zBe() { $ = 0, D = !1; A < q; - ) { switch (((U = G[G.length - 1]), U.tag)) { case "JS": @@ -77069,7 +77042,6 @@ function On(e, t) { s = 0, l; s < o; - ) ((l = n[s]), t ? (i[s] = t(l, s)) : (i[s] = l), (s += 1)); return ((i.length = o), i); @@ -83614,7 +83586,6 @@ function zce(e, t, r = !0) { t === void 0 ? n >= (r ? Math.max(e.childNodes.length - 1, 0) : 0) : n <= e.childNodes.length; - ) { if (t && n === e.childNodes.length) throw new Error("The given offset is out of bounds."); diff --git a/docs/sb-manager/runtime.js b/docs/sb-manager/runtime.js index a3e276f..c8ff5c2 100644 --- a/docs/sb-manager/runtime.js +++ b/docs/sb-manager/runtime.js @@ -695,7 +695,6 @@ var su = we((_A, iu) => { for ( var o = e.length, i = "", r = 0, n = 0, l = t, u = ru; t > -1 && t < o; - ) { var c = nu(e[t + 1], 4), d = nu(e[t + 2], 0), diff --git a/docs/vite-inject-mocker-entry.js b/docs/vite-inject-mocker-entry.js index 476414d..6eb6ac5 100644 --- a/docs/vite-inject-mocker-entry.js +++ b/docs/vite-inject-mocker-entry.js @@ -861,7 +861,6 @@ function ze() { J = 0, S = !1; p < L; - ) { switch ((($ = E[E.length - 1]), $.tag)) { case "JS": diff --git a/lib/cache.ts b/lib/cache.ts index 3727639..1e7e714 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -206,7 +206,10 @@ export function getCacheStats(): CacheStats { tagCacheSize: tagCache.size, authorCacheSize: authorCache.size, totalCacheSize: - blogPostCache.size + blogListCache.size + tagCache.size + authorCache.size, + blogPostCache.size + + blogListCache.size + + tagCache.size + + authorCache.size, maxCacheSize: MAX_CACHE_SIZE, cacheTTL: CACHE_TTL, }; diff --git a/lib/content.ts b/lib/content.ts index 44b8ed4..1780bcf 100644 --- a/lib/content.ts +++ b/lib/content.ts @@ -1,11 +1,8 @@ import fs from "fs"; import path from "path"; import matter from "gray-matter"; -import { - validateBlogPost, - sanitizeBlogPost, - type BlogPostFrontmatter, -} from "./validation"; +import { validateBlogPost, sanitizeBlogPost } from "./validation"; +import type { BlogPostFrontmatter } from "./validation"; /** * Content processing utilities for blog posts diff --git a/lib/types.ts b/lib/types.ts index b85479a..37b0611 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -3,15 +3,9 @@ */ // Re-export types from other modules for convenience -export type { - BlogPost, - BlogStats, -} from "./content"; +export type { BlogPost, BlogStats } from "./content"; -export type { - BlogPostFrontmatter, - ValidationResult, -} from "./validation"; +export type { BlogPostFrontmatter, ValidationResult } from "./validation"; export type { Heading, @@ -21,9 +15,7 @@ export type { ProcessedFrontmatter, } from "./mdx"; -export type { - CacheStats, -} from "./cache"; +export type { CacheStats } from "./cache"; // Additional shared types export interface ComponentProps { diff --git a/stories/Button.visual.stories.js b/stories/Button.visual.stories.js index 4860075..c64b5c8 100644 --- a/stories/Button.visual.stories.js +++ b/stories/Button.visual.stories.js @@ -1,4 +1,4 @@ -import Button from "../app/components/Button.js"; +import Button from "../app/components/Button"; import { within, userEvent } from "@storybook/test"; export default { diff --git a/stories/Footer.responsive.stories.js b/stories/Footer.responsive.stories.js index 3f8d936..0c1817f 100644 --- a/stories/Footer.responsive.stories.js +++ b/stories/Footer.responsive.stories.js @@ -1,4 +1,4 @@ -import Footer from "../app/components/Footer.js"; +import Footer from "../app/components/Footer"; import { within, userEvent } from "@storybook/test"; export default { diff --git a/stories/Header.responsive.stories.js b/stories/Header.responsive.stories.js index 4c958a8..e1ff4c2 100644 --- a/stories/Header.responsive.stories.js +++ b/stories/Header.responsive.stories.js @@ -1,4 +1,4 @@ -import Header from "../app/components/Header.js"; +import Header from "../app/components/Header"; export default { title: "Components/Header/Responsive", diff --git a/tests/integration/layout.integration.test.jsx b/tests/integration/layout.integration.test.jsx index 58864b2..aec49d8 100644 --- a/tests/integration/layout.integration.test.jsx +++ b/tests/integration/layout.integration.test.jsx @@ -14,7 +14,7 @@ describe("Layout Integration", () => {
-
, + ); // Check that CommunityRule branding appears in both header and footer @@ -30,31 +30,33 @@ describe("Layout Integration", () => {
-
, + ); // Header navigation items expect( screen.getAllByRole("menuitem", { name: "Navigate to Use cases page" }) - .length, + .length ).toBeGreaterThan(0); expect( - screen.getAllByRole("menuitem", { name: "Navigate to Learn page" }) - .length, + screen.getAllByRole("menuitem", { name: "Navigate to Learn page" }).length ).toBeGreaterThan(0); expect( - screen.getAllByRole("menuitem", { name: "Navigate to About page" }) - .length, + screen.getAllByRole("menuitem", { name: "Navigate to About page" }).length ).toBeGreaterThan(0); // Footer navigation items (should be present in footer as well) - const useCasesLinks = screen.getAllByRole("link", { name: "Use cases" }); - const learnLinks = screen.getAllByRole("link", { name: "Learn" }); - const aboutLinks = screen.getAllByRole("link", { name: "About" }); + // Footer has navigation links that match header + const footerUseCasesLinks = screen.getAllByRole("link", { + name: "Use cases", + }); + const footerLearnLinks = screen.getAllByRole("link", { name: "Learn" }); + const footerAboutLinks = screen.getAllByRole("link", { name: "About" }); - expect(useCasesLinks.length).toBeGreaterThan(0); - expect(learnLinks.length).toBeGreaterThan(0); - expect(aboutLinks.length).toBeGreaterThan(0); + // Check that footer has these links (they may be in header too, so getAllByRole will find both) + expect(footerUseCasesLinks.length).toBeGreaterThan(0); + expect(footerLearnLinks.length).toBeGreaterThan(0); + expect(footerAboutLinks.length).toBeGreaterThan(0); }); test("header navigation is interactive", async () => { @@ -94,23 +96,23 @@ describe("Layout Integration", () => { // Contact information expect(screen.getByText("medlab@colorado.edu")).toBeInTheDocument(); expect( - screen.getByRole("link", { name: "medlab@colorado.edu" }), + screen.getByRole("link", { name: "medlab@colorado.edu" }) ).toHaveAttribute("href", "mailto:medlab@colorado.edu"); // Social media links expect( - screen.getByRole("link", { name: "Follow us on Bluesky" }), + screen.getByRole("link", { name: "Follow us on Bluesky" }) ).toBeInTheDocument(); expect( - screen.getByRole("link", { name: "Follow us on GitLab" }), + screen.getByRole("link", { name: "Follow us on GitLab" }) ).toBeInTheDocument(); // Legal links expect( - screen.getByRole("link", { name: "Privacy Policy" }), + screen.getByRole("link", { name: "Privacy Policy" }) ).toBeInTheDocument(); expect( - screen.getByRole("link", { name: "Terms of Service" }), + screen.getByRole("link", { name: "Terms of Service" }) ).toBeInTheDocument(); }); @@ -136,7 +138,7 @@ describe("Layout Integration", () => {
-
, + ); // Header should have banner role @@ -157,7 +159,7 @@ describe("Layout Integration", () => {
-
, + ); // Header should have responsive navigation elements @@ -174,7 +176,7 @@ describe("Layout Integration", () => {
-
, + ); // Get all interactive elements @@ -197,7 +199,7 @@ describe("Layout Integration", () => {
-
, + ); // Header provides main navigation @@ -217,7 +219,7 @@ describe("Layout Integration", () => {
-
, + ); // Main navigation in header @@ -230,13 +232,13 @@ describe("Layout Integration", () => { (link) => link.textContent?.includes("Use cases") || link.textContent?.includes("Learn") || - link.textContent?.includes("About"), + link.textContent?.includes("About") ); expect(navigationLinks.length).toBeGreaterThan(0); // Contact information in footer expect( - screen.getByRole("link", { name: "medlab@colorado.edu" }), + screen.getByRole("link", { name: "medlab@colorado.edu" }) ).toBeInTheDocument(); }); }); diff --git a/tests/unit/BlogPage.test.jsx b/tests/unit/BlogPage.test.jsx index 3742a4d..3f57d8a 100644 --- a/tests/unit/BlogPage.test.jsx +++ b/tests/unit/BlogPage.test.jsx @@ -111,9 +111,8 @@ describe("BlogPostPage", () => { vi.clearAllMocks(); // Mock the content functions - const { getBlogPostBySlug, getAllBlogPosts } = await import( - "../../lib/content" - ); + const { getBlogPostBySlug, getAllBlogPosts } = + await import("../../lib/content"); vi.mocked(getBlogPostBySlug).mockReturnValue(mockPost); vi.mocked(getAllBlogPosts).mockReturnValue([mockPost, ...mockRelatedPosts]); }); diff --git a/tests/unit/Footer.test.jsx b/tests/unit/Footer.test.jsx index fefe69b..8298f00 100644 --- a/tests/unit/Footer.test.jsx +++ b/tests/unit/Footer.test.jsx @@ -27,7 +27,7 @@ describe("Footer", () => { expect(schemaData.email).toBe("medlab@colorado.edu"); expect(schemaData.url).toBe("https://communityrule.com"); expect(schemaData.sameAs).toContain( - "https://bsky.app/profile/medlabboulder", + "https://bsky.app/profile/medlabboulder" ); expect(schemaData.sameAs).toContain("https://gitlab.com/medlabboulder"); }); @@ -36,7 +36,7 @@ describe("Footer", () => { render(