Merge pull request 'ESLint Fixes' (#27) from adilallo/maintenance/ESLintFixes into main

Reviewed-on: #27
This commit was merged in pull request #27.
This commit is contained in:
2026-01-28 19:44:05 +00:00
96 changed files with 465 additions and 327 deletions
+13 -14
View File
@@ -491,20 +491,19 @@ jobs:
# - run: npm run test:sb # - run: npm run test:sb
# env: { CI: true } # env: { CI: true }
# Temporarily disabled - 523 pre-existing ESLint issues will be addressed in separate ticket lint:
# lint: runs-on: [self-hosted, macos-latest]
# runs-on: [self-hosted, macos-latest] steps:
# steps: - uses: actions/checkout@v4
# - uses: actions/checkout@v4 - uses: actions/setup-node@v4
# - uses: actions/setup-node@v4 if: ${{ github.server_url == 'https://github.com' }}
# if: ${{ github.server_url == 'https://github.com' }} with: { node-version: 20, cache: npm }
# with: { node-version: 20, cache: npm } - uses: actions/setup-node@v4
# - uses: actions/setup-node@v4 if: ${{ github.server_url != 'https://github.com' || !github.server_url }}
# if: ${{ github.server_url != 'https://github.com' || !github.server_url }} with: { node-version: 20 }
# with: { node-version: 20 } - run: npm ci
# - run: npm ci - run: npm run lint
# - run: npm run lint - run: npm exec prettier -- --check "**/*.{js,jsx,ts,tsx,json,css,md}"
# - run: npm exec prettier -- --check "**/*.{js,jsx,ts,tsx,json,css,md}"
build: build:
runs-on: [self-hosted, macos-latest] runs-on: [self-hosted, macos-latest]
+5 -4
View File
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { logger } from "../../../lib/logger";
const WEB_VITALS_DIR = path.join(process.cwd(), ".next", "web-vitals"); const WEB_VITALS_DIR = path.join(process.cwd(), ".next", "web-vitals");
@@ -65,7 +66,7 @@ export async function POST(request: NextRequest) {
existingData = JSON.parse(fileContent) as WebVitalData[]; existingData = JSON.parse(fileContent) as WebVitalData[];
} catch (error) { } catch (error) {
const err = error as Error; const err = error as Error;
console.warn("Could not parse existing vitals data:", err.message); logger.warn("Could not parse existing vitals data:", err.message);
} }
} }
@@ -79,13 +80,13 @@ export async function POST(request: NextRequest) {
fs.writeFileSync(filePath, JSON.stringify(existingData, null, 2)); fs.writeFileSync(filePath, JSON.stringify(existingData, null, 2));
// Log for monitoring // Log for monitoring
console.log( logger.info(
`Web Vital received: ${metric} = ${data.value}ms (${data.rating})`, `Web Vital received: ${metric} = ${data.value}ms (${data.rating})`,
); );
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} catch (error) { } catch (error) {
console.error("Error processing web vital:", error); logger.error("Error processing web vital:", error);
return NextResponse.json( return NextResponse.json(
{ error: "Internal server error" }, { error: "Internal server error" },
{ status: 500 }, { status: 500 },
@@ -141,7 +142,7 @@ export async function GET() {
return NextResponse.json({ metrics }); return NextResponse.json({ metrics });
} catch (error) { } catch (error) {
console.error("Error fetching web vitals:", error); logger.error("Error fetching web vitals:", error);
return NextResponse.json( return NextResponse.json(
{ error: "Internal server error" }, { error: "Internal server error" },
{ status: 500 }, { status: 500 },
+8 -3
View File
@@ -6,6 +6,7 @@ import {
getAllBlogPosts as getAllPosts, getAllBlogPosts as getAllPosts,
type BlogPost, type BlogPost,
} from "../../../lib/content"; } from "../../../lib/content";
import { logger } from "../../../lib/logger";
import ContentBanner from "../../components/ContentBanner"; import ContentBanner from "../../components/ContentBanner";
import AskOrganizer from "../../components/AskOrganizer"; import AskOrganizer from "../../components/AskOrganizer";
import { getAssetPath, ASSETS } from "../../../lib/assetUtils"; import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
@@ -44,7 +45,7 @@ export async function generateStaticParams() {
slug: post.slug, slug: post.slug,
})); }));
} catch (error) { } catch (error) {
console.error("Error generating static params:", error); logger.error("Error generating static params:", error);
return []; return [];
} }
} }
@@ -87,7 +88,7 @@ export async function generateMetadata({
}, },
}; };
} catch (error) { } catch (error) {
console.error("Error generating metadata:", error); logger.error("Error generating metadata:", error);
return { return {
title: "Blog Post", title: "Blog Post",
description: "A blog post from our community.", description: "A blog post from our community.",
@@ -162,7 +163,11 @@ export default async function BlogPostPage({ params }: PageProps) {
return scoredPosts return scoredPosts
.sort((a, b) => b.score - a.score) .sort((a, b) => b.score - a.score)
.slice(0, limit) .slice(0, limit)
.map(({ score, ...post }) => post); // Remove score from final result .map(({ score, ...post }) => {
// Score used for sorting, removed from final result
void score;
return post;
});
}; };
const relatedArticles = getRelatedArticles(post, allPosts); const relatedArticles = getRelatedArticles(post, allPosts);
+1 -1
View File
@@ -13,7 +13,7 @@ interface AskOrganizerProps {
buttonHref?: string; buttonHref?: string;
className?: string; className?: string;
variant?: "centered" | "left-aligned" | "compact" | "inverse"; variant?: "centered" | "left-aligned" | "compact" | "inverse";
onContactClick?: (data: { onContactClick?: (_data: {
event: string; event: string;
component: string; component: string;
variant: string; variant: string;
+1 -1
View File
@@ -14,7 +14,7 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
disabled?: boolean; disabled?: boolean;
type?: "button" | "submit" | "reset"; type?: "button" | "submit" | "reset";
onClick?: ( onClick?: (
e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>, _e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>,
) => void; ) => void;
href?: string; href?: string;
target?: string; target?: string;
+1 -1
View File
@@ -10,7 +10,7 @@ interface CheckboxProps {
disabled?: boolean; disabled?: boolean;
label?: string; label?: string;
className?: string; className?: string;
onChange?: (data: { onChange?: (_data: {
checked: boolean; checked: boolean;
value?: string; value?: string;
event: React.MouseEvent | React.KeyboardEvent; event: React.MouseEvent | React.KeyboardEvent;
+1 -1
View File
@@ -9,7 +9,7 @@ interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
disabled?: boolean; disabled?: boolean;
className?: string; className?: string;
onClick?: ( onClick?: (
e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>, _e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
) => void; ) => void;
size?: "small" | "medium" | "large"; size?: "small" | "medium" | "large";
} }
+2 -1
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import React, { Component, type ReactNode } from "react"; import React, { Component, type ReactNode } from "react";
import { logger } from "../../lib/logger";
interface ErrorBoundaryProps { interface ErrorBoundaryProps {
children: ReactNode; children: ReactNode;
@@ -24,7 +25,7 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log the error to an error reporting service // Log the error to an error reporting service
console.error("ErrorBoundary caught an error:", error, errorInfo); logger.error("ErrorBoundary caught an error:", error, errorInfo);
} }
render() { render() {
+9 -10
View File
@@ -15,9 +15,9 @@ interface InputProps extends Omit<
label?: string; label?: string;
placeholder?: string; placeholder?: string;
value?: string; value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void; onChange?: (_e: React.ChangeEvent<HTMLInputElement>) => void;
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void; onFocus?: (_e: React.FocusEvent<HTMLInputElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void; onBlur?: (_e: React.FocusEvent<HTMLInputElement>) => void;
className?: string; className?: string;
} }
@@ -151,13 +151,12 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
`.trim(); `.trim();
// Form field handlers with disabled state handling // Form field handlers with disabled state handling
const { handleChange, handleFocus, handleBlur } = useFormField< const { handleChange, handleFocus, handleBlur } =
HTMLInputElement useFormField<HTMLInputElement>(disabled, {
>(disabled, { onChange,
onChange, onFocus,
onFocus, onBlur,
onBlur, });
});
return ( return (
<div className={containerClasses}> <div className={containerClasses}>
+16 -18
View File
@@ -10,25 +10,23 @@ interface NumberedCardProps {
iconColor?: string; iconColor?: string;
} }
const NumberedCard = memo<NumberedCardProps>( const NumberedCard = memo<NumberedCardProps>(({ number, text }) => {
({ number, text, iconShape: _iconShape, iconColor: _iconColor }) => { return (
return ( <div className="bg-[var(--color-surface-inverse-primary)] rounded-[12px] p-5 shadow-lg flex flex-col gap-4 sm:p-8 sm:gap-8 sm:flex-row sm:items-center lg:p-8 lg:gap-0 lg:flex-row lg:items-stretch lg:relative lg:h-[238px]">
<div className="bg-[var(--color-surface-inverse-primary)] rounded-[12px] p-5 shadow-lg flex flex-col gap-4 sm:p-8 sm:gap-8 sm:flex-row sm:items-center lg:p-8 lg:gap-0 lg:flex-row lg:items-stretch lg:relative lg:h-[238px]"> {/* Section Number - Top right (lg breakpoint) */}
{/* Section Number - Top right (lg breakpoint) */} <div className="flex justify-end sm:justify-start sm:flex-shrink-0 lg:absolute lg:top-8 lg:right-8">
<div className="flex justify-end sm:justify-start sm:flex-shrink-0 lg:absolute lg:top-8 lg:right-8"> <SectionNumber number={number} />
<SectionNumber number={number} />
</div>
{/* Card Content - Bottom left (lg breakpoint) */}
<div className="sm:flex-1 lg:absolute lg:bottom-8 lg:left-8 lg:right-16">
<p className="font-bricolage-grotesque font-medium text-[24px] leading-[32px] sm:font-normal sm:leading-[24px] sm:text-[24px] lg:text-[24px] lg:leading-[24px] xl:text-[32px] xl:leading-[32px] text-[#141414]">
{text}
</p>
</div>
</div> </div>
);
}, {/* Card Content - Bottom left (lg breakpoint) */}
); <div className="sm:flex-1 lg:absolute lg:bottom-8 lg:left-8 lg:right-16">
<p className="font-bricolage-grotesque font-medium text-[24px] leading-[32px] sm:font-normal sm:leading-[24px] sm:text-[24px] lg:text-[24px] lg:leading-[24px] xl:text-[32px] xl:leading-[32px] text-[#141414]">
{text}
</p>
</div>
</div>
);
});
NumberedCard.displayName = "NumberedCard"; NumberedCard.displayName = "NumberedCard";
+4 -3
View File
@@ -3,6 +3,7 @@
import { useState, memo } from "react"; import { useState, memo } from "react";
import Image from "next/image"; import Image from "next/image";
import QuoteDecor from "./QuoteDecor"; import QuoteDecor from "./QuoteDecor";
import { logger } from "../../lib/logger";
interface QuoteBlockProps { interface QuoteBlockProps {
variant?: "compact" | "standard" | "extended"; variant?: "compact" | "standard" | "extended";
@@ -13,7 +14,7 @@ interface QuoteBlockProps {
avatarSrc?: string; avatarSrc?: string;
id?: string; id?: string;
fallbackAvatarSrc?: string; fallbackAvatarSrc?: string;
onError?: (error: { onError?: (_error: {
type: string; type: string;
message: string; message: string;
author?: string; author?: string;
@@ -109,7 +110,7 @@ const QuoteBlock = memo<QuoteBlockProps>(
// Error handling functions // Error handling functions
const handleImageError = (error: unknown) => { const handleImageError = (error: unknown) => {
console.warn( logger.warn(
`QuoteBlock: Failed to load avatar image for ${author}:`, `QuoteBlock: Failed to load avatar image for ${author}:`,
error, error,
); );
@@ -135,7 +136,7 @@ const QuoteBlock = memo<QuoteBlockProps>(
// Validate required props // Validate required props
if (!quote || !author) { if (!quote || !author) {
console.error("QuoteBlock: Missing required props (quote or author)"); logger.error("QuoteBlock: Missing required props (quote or author)");
if (onError) { if (onError) {
onError({ onError({
type: "missing_props", type: "missing_props",
+1 -1
View File
@@ -8,7 +8,7 @@ interface RadioButtonProps {
state?: "default" | "hover" | "focus"; state?: "default" | "hover" | "focus";
disabled?: boolean; disabled?: boolean;
label?: string; label?: string;
onChange?: (data: { checked: boolean; value?: string }) => void; onChange?: (_data: { checked: boolean; value?: string }) => void;
id?: string; id?: string;
name?: string; name?: string;
value?: string; value?: string;
+1 -1
View File
@@ -12,7 +12,7 @@ interface RadioOption {
interface RadioGroupProps { interface RadioGroupProps {
name?: string; name?: string;
value?: string; value?: string;
onChange?: (data: { value: string }) => void; onChange?: (_data: { value: string }) => void;
mode?: "standard" | "inverse"; mode?: "standard" | "inverse";
state?: "default" | "hover" | "focus"; state?: "default" | "hover" | "focus";
disabled?: boolean; disabled?: boolean;
+1 -1
View File
@@ -108,7 +108,7 @@ const RelatedArticles = memo<RelatedArticlesProps>(
} }
return ( return (
<section <section
className="py-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-064)]" className="py-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-064)]"
data-testid="related-articles" data-testid="related-articles"
> >
+4 -4
View File
@@ -14,12 +14,12 @@ interface RuleCardProps {
declare global { declare global {
interface Window { interface Window {
gtag?: ( gtag?: (
command: string, _command: string,
eventName: string, _eventName: string,
params?: Record<string, unknown>, _params?: Record<string, unknown>,
) => void; ) => void;
analytics?: { analytics?: {
track: (eventName: string, params?: Record<string, unknown>) => void; track: (_eventName: string, _params?: Record<string, unknown>) => void;
}; };
} }
} }
+6 -5
View File
@@ -5,6 +5,7 @@ import Image from "next/image";
import RuleCard from "./RuleCard"; import RuleCard from "./RuleCard";
import Button from "./Button"; import Button from "./Button";
import { getAssetPath } from "../../lib/assetUtils"; import { getAssetPath } from "../../lib/assetUtils";
import { logger } from "../../lib/logger";
interface RuleStackProps { interface RuleStackProps {
className?: string; className?: string;
@@ -13,12 +14,12 @@ interface RuleStackProps {
declare global { declare global {
interface Window { interface Window {
gtag?: ( gtag?: (
command: string, _command: string,
eventName: string, _eventName: string,
params?: Record<string, unknown>, _params?: Record<string, unknown>,
) => void; ) => void;
analytics?: { analytics?: {
track: (eventName: string, params?: Record<string, unknown>) => void; track: (_eventName: string, _params?: Record<string, unknown>) => void;
}; };
} }
} }
@@ -38,7 +39,7 @@ const RuleStack = memo<RuleStackProps>(({ className = "" }) => {
}); });
} }
} }
console.log(`${templateName} template clicked`); logger.debug(`${templateName} template clicked`);
}; };
return ( return (
+3 -2
View File
@@ -33,7 +33,7 @@ interface SelectProps {
className?: string; className?: string;
children?: React.ReactNode; children?: React.ReactNode;
value?: string; value?: string;
onChange?: (data: { target: { value: string; text: string } }) => void; onChange?: (_data: { target: { value: string; text: string } }) => void;
options?: SelectOptionData[]; options?: SelectOptionData[];
} }
@@ -115,10 +115,11 @@ const Select = forwardRef<HTMLButtonElement, SelectProps>(
const baseStyles = "w-full"; const baseStyles = "w-full";
switch (size) { switch (size) {
case "small": case "small": {
const smallHeight = const smallHeight =
labelVariant === "horizontal" ? "h-[30px]" : "h-[32px]"; labelVariant === "horizontal" ? "h-[30px]" : "h-[32px]";
return `${baseStyles} ${smallHeight} pl-[12px] pr-[36px] py-[8px] text-[10px] leading-[14px]`; return `${baseStyles} ${smallHeight} pl-[12px] pr-[36px] py-[8px] text-[10px] leading-[14px]`;
}
case "medium": case "medium":
return `${baseStyles} h-[36px] pl-[12px] pr-[36px] py-[8px] text-[14px] leading-[20px]`; return `${baseStyles} h-[36px] pl-[12px] pr-[36px] py-[8px] text-[14px] leading-[20px]`;
case "large": case "large":
+1 -1
View File
@@ -8,7 +8,7 @@ interface SelectOptionProps {
disabled?: boolean; disabled?: boolean;
className?: string; className?: string;
onClick?: ( onClick?: (
e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>, _e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
) => void; ) => void;
size?: "small" | "medium" | "large"; size?: "small" | "medium" | "large";
} }
+3 -3
View File
@@ -6,12 +6,12 @@ interface SwitchProps extends Omit<
> { > {
checked?: boolean; checked?: boolean;
onChange?: ( onChange?: (
e: _e:
| React.MouseEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>
| React.KeyboardEvent<HTMLButtonElement>, | React.KeyboardEvent<HTMLButtonElement>,
) => void; ) => void;
onFocus?: (e: React.FocusEvent<HTMLButtonElement>) => void; onFocus?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLButtonElement>) => void; onBlur?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
state?: "default" | "hover" | "focus"; state?: "default" | "hover" | "focus";
label?: string; label?: string;
className?: string; className?: string;
+9 -10
View File
@@ -15,9 +15,9 @@ interface TextAreaProps extends Omit<
label?: string; label?: string;
placeholder?: string; placeholder?: string;
value?: string; value?: string;
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void; onChange?: (_e: React.ChangeEvent<HTMLTextAreaElement>) => void;
onFocus?: (e: React.FocusEvent<HTMLTextAreaElement>) => void; onFocus?: (_e: React.FocusEvent<HTMLTextAreaElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void; onBlur?: (_e: React.FocusEvent<HTMLTextAreaElement>) => void;
className?: string; className?: string;
rows?: number; rows?: number;
} }
@@ -155,13 +155,12 @@ const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
`.trim(); `.trim();
// Form field handlers with disabled state handling // Form field handlers with disabled state handling
const { handleChange, handleFocus, handleBlur } = useFormField< const { handleChange, handleFocus, handleBlur } =
HTMLTextAreaElement useFormField<HTMLTextAreaElement>(disabled, {
>(disabled, { onChange,
onChange, onFocus,
onFocus, onBlur,
onBlur, });
});
return ( return (
<div className={containerClasses}> <div className={containerClasses}>
+3 -3
View File
@@ -7,12 +7,12 @@ interface ToggleProps extends Omit<
label?: string; label?: string;
checked?: boolean; checked?: boolean;
onChange?: ( onChange?: (
e: _e:
| React.MouseEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>
| React.KeyboardEvent<HTMLButtonElement>, | React.KeyboardEvent<HTMLButtonElement>,
) => void; ) => void;
onFocus?: (e: React.FocusEvent<HTMLButtonElement>) => void; onFocus?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLButtonElement>) => void; onBlur?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
disabled?: boolean; disabled?: boolean;
state?: "default" | "hover" | "focus"; state?: "default" | "hover" | "focus";
showIcon?: boolean; showIcon?: boolean;
+3 -3
View File
@@ -11,12 +11,12 @@ interface ToggleGroupProps extends Omit<
showText?: boolean; showText?: boolean;
ariaLabel?: string; ariaLabel?: string;
onChange?: ( onChange?: (
e: _e:
| React.MouseEvent<HTMLButtonElement> | React.MouseEvent<HTMLButtonElement>
| React.KeyboardEvent<HTMLButtonElement>, | React.KeyboardEvent<HTMLButtonElement>,
) => void; ) => void;
onFocus?: (e: React.FocusEvent<HTMLButtonElement>) => void; onFocus?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLButtonElement>) => void; onBlur?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
} }
const ToggleGroup = memo( const ToggleGroup = memo(
+2 -1
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useState, useEffect, memo } from "react"; import { useState, useEffect, memo } from "react";
import { logger } from "../../lib/logger";
interface VitalData { interface VitalData {
value: number; value: number;
@@ -50,7 +51,7 @@ const WebVitalsDashboard = memo(() => {
const data = (await response.json()) as { metrics?: Metrics }; const data = (await response.json()) as { metrics?: Metrics };
setMetrics(data.metrics || {}); setMetrics(data.metrics || {});
} catch (error) { } catch (error) {
console.error("Error fetching web vitals:", error); logger.error("Error fetching web vitals:", error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
+7 -2
View File
@@ -1,6 +1,6 @@
/** /**
* Custom hooks for reusable component logic * Custom hooks for reusable component logic
* *
* This module exports all custom hooks used throughout the application. * This module exports all custom hooks used throughout the application.
* Hooks encapsulate complex logic and state management that can be reused * Hooks encapsulate complex logic and state management that can be reused
* across multiple components. * across multiple components.
@@ -12,7 +12,12 @@ export { useComponentId } from "./useComponentId";
export { useFormField } from "./useFormField"; export { useFormField } from "./useFormField";
export { useComponentStyles } from "./useComponentStyles"; export { useComponentStyles } from "./useComponentStyles";
export { useSchemaData } from "./useSchemaData"; export { useSchemaData } from "./useSchemaData";
export { useMediaQuery, useIsMobile, useIsDesktop, BREAKPOINTS } from "./useMediaQuery"; export {
useMediaQuery,
useIsMobile,
useIsDesktop,
BREAKPOINTS,
} from "./useMediaQuery";
export { useFormValidation, validationRules } from "./useFormValidation"; export { useFormValidation, validationRules } from "./useFormValidation";
export type { export type {
SizeStyleConfig, SizeStyleConfig,
+8 -8
View File
@@ -28,20 +28,20 @@ interface AnalyticsEvent {
} }
interface UseAnalyticsReturn { interface UseAnalyticsReturn {
trackEvent: (event: AnalyticsEvent) => void; trackEvent: (_event: AnalyticsEvent) => void;
trackCustomEvent: ( trackCustomEvent: (
event: string, _event: string,
data: Record<string, unknown>, _data: Record<string, unknown>,
callback?: (data: Record<string, unknown>) => void, _callback?: (_data: Record<string, unknown>) => void,
) => void; ) => void;
} }
declare global { declare global {
interface Window { interface Window {
gtag?: ( gtag?: (
command: string, _command: string,
eventName: string, _eventName: string,
params?: Record<string, unknown>, _params?: Record<string, unknown>,
) => void; ) => void;
} }
} }
@@ -64,7 +64,7 @@ export function useAnalytics(): UseAnalyticsReturn {
const trackCustomEvent = ( const trackCustomEvent = (
event: string, event: string,
data: Record<string, unknown>, data: Record<string, unknown>,
callback?: (data: Record<string, unknown>) => void, callback?: (_data: Record<string, unknown>) => void,
) => { ) => {
// Execute custom callback if provided // Execute custom callback if provided
if (callback) { if (callback) {
+1 -1
View File
@@ -19,7 +19,7 @@ import { useEffect, RefObject } from "react";
*/ */
export function useClickOutside( export function useClickOutside(
refs: Array<RefObject<HTMLElement>>, refs: Array<RefObject<HTMLElement>>,
handler: (event: MouseEvent | TouchEvent) => void, handler: (_event: MouseEvent | TouchEvent) => void,
enabled: boolean = true, enabled: boolean = true,
): void { ): void {
useEffect(() => { useEffect(() => {
+2 -4
View File
@@ -24,7 +24,7 @@ export interface UseComponentStylesOptions {
error?: boolean; error?: boolean;
sizeStyles: SizeStyleConfig; sizeStyles: SizeStyleConfig;
stateStyles: StateStyleConfig; stateStyles: StateStyleConfig;
getStateStyles?: (params: { getStateStyles?: (_params: {
state?: string; state?: string;
disabled: boolean; disabled: boolean;
error: boolean; error: boolean;
@@ -61,9 +61,7 @@ export interface UseComponentStylesOptions {
* }); * });
* ``` * ```
*/ */
export function useComponentStyles( export function useComponentStyles(options: UseComponentStylesOptions): {
options: UseComponentStylesOptions,
): {
sizeClasses: Record<string, string>; sizeClasses: Record<string, string>;
stateClasses: Record<string, string>; stateClasses: Record<string, string>;
} { } {
+6 -6
View File
@@ -19,15 +19,15 @@ import { useCallback } from "react";
* ``` * ```
*/ */
interface FormFieldHandlers<T = HTMLElement> { interface FormFieldHandlers<T = HTMLElement> {
onChange?: (e: React.ChangeEvent<T>) => void; onChange?: (_e: React.ChangeEvent<T>) => void;
onFocus?: (e: React.FocusEvent<T>) => void; onFocus?: (_e: React.FocusEvent<T>) => void;
onBlur?: (e: React.FocusEvent<T>) => void; onBlur?: (_e: React.FocusEvent<T>) => void;
} }
interface UseFormFieldReturn<T = HTMLElement> { interface UseFormFieldReturn<T = HTMLElement> {
handleChange: (e: React.ChangeEvent<T>) => void; handleChange: (_e: React.ChangeEvent<T>) => void;
handleFocus: (e: React.FocusEvent<T>) => void; handleFocus: (_e: React.FocusEvent<T>) => void;
handleBlur: (e: React.FocusEvent<T>) => void; handleBlur: (_e: React.FocusEvent<T>) => void;
} }
export function useFormField<T extends HTMLElement = HTMLElement>( export function useFormField<T extends HTMLElement = HTMLElement>(
+33 -24
View File
@@ -3,7 +3,7 @@ import { useState, useCallback, useMemo } from "react";
/** /**
* Validation rule function type * Validation rule function type
*/ */
export type ValidationRule<T = string> = (value: T) => string | null; export type ValidationRule<T = string> = (_value: T) => string | null;
/** /**
* Validation rules for common patterns * Validation rules for common patterns
@@ -22,24 +22,30 @@ export const validationRules = {
return emailRegex.test(value) ? null : "Please enter a valid email address"; return emailRegex.test(value) ? null : "Please enter a valid email address";
}, },
minLength: (min: number) => (value: string): string | null => { minLength:
if (!value) return null; (min: number) =>
return value.length >= min (value: string): string | null => {
? null if (!value) return null;
: `Must be at least ${min} characters long`; return value.length >= min
}, ? null
: `Must be at least ${min} characters long`;
},
maxLength: (max: number) => (value: string): string | null => { maxLength:
if (!value) return null; (max: number) =>
return value.length <= max (value: string): string | null => {
? null if (!value) return null;
: `Must be no more than ${max} characters long`; return value.length <= max
}, ? null
: `Must be no more than ${max} characters long`;
},
pattern: (regex: RegExp, message: string) => (value: string): string | null => { pattern:
if (!value) return null; (regex: RegExp, message: string) =>
return regex.test(value) ? null : message; (value: string): string | null => {
}, if (!value) return null;
return regex.test(value) ? null : message;
},
}; };
/** /**
@@ -182,13 +188,16 @@ export function useFormValidation(options: UseFormValidationOptions) {
}, [initialValues]); }, [initialValues]);
// Set field value programmatically // Set field value programmatically
const setValue = useCallback((name: string, value: string) => { const setValue = useCallback(
setValues((prev) => ({ ...prev, [name]: value })); (name: string, value: string) => {
if (validateOnChange) { setValues((prev) => ({ ...prev, [name]: value }));
const error = validateField(name, value); if (validateOnChange) {
setErrors((prev) => ({ ...prev, [name]: error })); const error = validateField(name, value);
} setErrors((prev) => ({ ...prev, [name]: error }));
}, [validateOnChange, validateField]); }
},
[validateOnChange, validateField],
);
return { return {
values, values,
+2
View File
@@ -51,6 +51,8 @@ export function useMediaQuery(
} }
const media = window.matchMedia(mediaQuery); const media = window.matchMedia(mediaQuery);
// Initialize matches synchronously - this is safe for media queries
// eslint-disable-next-line react-hooks/rules-of-hooks
setMatches(media.matches); setMatches(media.matches);
// Create listener for changes // Create listener for changes
+9 -2
View File
@@ -144,7 +144,12 @@ export function useSchemaData(
type: "BreadcrumbList"; type: "BreadcrumbList";
items: Array<{ name: string; url: string }>; items: Array<{ name: string; url: string }>;
}, },
): SchemaOrganization | SchemaWebSite | SchemaHowTo | SchemaArticle | SchemaBreadcrumbList { ):
| SchemaOrganization
| SchemaWebSite
| SchemaHowTo
| SchemaArticle
| SchemaBreadcrumbList {
return useMemo(() => { return useMemo(() => {
switch (config.type) { switch (config.type) {
case "Organization": case "Organization":
@@ -216,7 +221,9 @@ export function useSchemaData(
"@id": config.mainEntityOfPage, "@id": config.mainEntityOfPage,
}, },
}), }),
...(config.articleSection && { articleSection: config.articleSection }), ...(config.articleSection && {
articleSection: config.articleSection,
}),
...(config.keywords && { keywords: config.keywords }), ...(config.keywords && { keywords: config.keywords }),
} as SchemaArticle; } as SchemaArticle;
+26 -12
View File
@@ -15,6 +15,7 @@ Detects clicks outside of specified elements. Useful for closing dropdowns, moda
**Location:** `app/hooks/useClickOutside.ts` **Location:** `app/hooks/useClickOutside.ts`
**Usage:** **Usage:**
```tsx ```tsx
import { useClickOutside } from "../hooks"; import { useClickOutside } from "../hooks";
@@ -26,6 +27,7 @@ useClickOutside([menuRef, buttonRef], () => setIsOpen(false), isOpen);
``` ```
**Parameters:** **Parameters:**
- `refs`: Array of refs to elements that should not trigger the callback - `refs`: Array of refs to elements that should not trigger the callback
- `handler`: Callback function to execute when clicking outside - `handler`: Callback function to execute when clicking outside
- `enabled`: Whether the hook is enabled (default: true) - `enabled`: Whether the hook is enabled (default: true)
@@ -41,6 +43,7 @@ Centralized analytics tracking for component interactions. Supports both Google
**Location:** `app/hooks/useAnalytics.ts` **Location:** `app/hooks/useAnalytics.ts`
**Usage:** **Usage:**
```tsx ```tsx
import { useAnalytics } from "../hooks"; import { useAnalytics } from "../hooks";
@@ -66,6 +69,7 @@ trackCustomEvent(
``` ```
**Returns:** **Returns:**
- `trackEvent`: Function to track standard analytics events - `trackEvent`: Function to track standard analytics events
- `trackCustomEvent`: Function to track custom events with optional callback - `trackCustomEvent`: Function to track custom events with optional callback
@@ -80,6 +84,7 @@ Generates unique component IDs for accessibility. Provides consistent ID generat
**Location:** `app/hooks/useComponentId.ts` **Location:** `app/hooks/useComponentId.ts`
**Usage:** **Usage:**
```tsx ```tsx
import { useComponentId } from "../hooks"; import { useComponentId } from "../hooks";
@@ -89,10 +94,12 @@ const { id, labelId } = useComponentId("input", props.id);
``` ```
**Parameters:** **Parameters:**
- `prefix`: Prefix for the generated ID (e.g., "input", "select") - `prefix`: Prefix for the generated ID (e.g., "input", "select")
- `providedId`: Optional ID provided via props (takes precedence) - `providedId`: Optional ID provided via props (takes precedence)
**Returns:** **Returns:**
- `id`: Component ID - `id`: Component ID
- `labelId`: Associated label ID for accessibility - `labelId`: Associated label ID for accessibility
@@ -107,6 +114,7 @@ Manages form field event handlers with disabled state handling. Ensures handlers
**Location:** `app/hooks/useFormField.ts` **Location:** `app/hooks/useFormField.ts`
**Usage:** **Usage:**
```tsx ```tsx
import { useFormField } from "../hooks"; import { useFormField } from "../hooks";
@@ -117,18 +125,16 @@ const { handleChange, handleFocus, handleBlur } = useFormField(disabled, {
}); });
// Use in component // Use in component
<input <input onChange={handleChange} onFocus={handleFocus} onBlur={handleBlur} />;
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
/>
``` ```
**Parameters:** **Parameters:**
- `disabled`: Whether the field is disabled - `disabled`: Whether the field is disabled
- `handlers`: Object containing onChange, onFocus, onBlur handlers - `handlers`: Object containing onChange, onFocus, onBlur handlers
**Returns:** **Returns:**
- `handleChange`: Wrapped onChange handler - `handleChange`: Wrapped onChange handler
- `handleFocus`: Wrapped onFocus handler - `handleFocus`: Wrapped onFocus handler
- `handleBlur`: Wrapped onBlur handler - `handleBlur`: Wrapped onBlur handler
@@ -144,6 +150,7 @@ Manages component size and state styles. Provides a consistent pattern for styli
**Location:** `app/hooks/useComponentStyles.ts` **Location:** `app/hooks/useComponentStyles.ts`
**Usage:** **Usage:**
```tsx ```tsx
import { useComponentStyles } from "../hooks"; import { useComponentStyles } from "../hooks";
@@ -178,6 +185,7 @@ Generates Schema.org structured data (JSON-LD) for SEO and search engines.
**Location:** `app/hooks/useSchemaData.ts` **Location:** `app/hooks/useSchemaData.ts`
**Usage:** **Usage:**
```tsx ```tsx
import { useSchemaData } from "../hooks"; import { useSchemaData } from "../hooks";
@@ -205,10 +213,11 @@ const orgSchema = useSchemaData({
<script <script
type="application/ld+json" type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }} dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }}
/> />;
``` ```
**Supported Types:** **Supported Types:**
- `Organization` - Organization information - `Organization` - Organization information
- `WebSite` - Website navigation and search - `WebSite` - Website navigation and search
- `HowTo` - Step-by-step instructions - `HowTo` - Step-by-step instructions
@@ -226,6 +235,7 @@ Responsive breakpoint detection using window.matchMedia.
**Location:** `app/hooks/useMediaQuery.ts` **Location:** `app/hooks/useMediaQuery.ts`
**Usage:** **Usage:**
```tsx ```tsx
import { useMediaQuery, useIsMobile, useIsDesktop } from "../hooks"; import { useMediaQuery, useIsMobile, useIsDesktop } from "../hooks";
@@ -242,6 +252,7 @@ const isDesktop = useIsDesktop(); // lg breakpoint and above
``` ```
**Available Breakpoints:** **Available Breakpoints:**
- `sm`: 640px - `sm`: 640px
- `md`: 768px - `md`: 768px
- `lg`: 1024px - `lg`: 1024px
@@ -259,6 +270,7 @@ Form validation with field-level error handling.
**Location:** `app/hooks/useFormValidation.ts` **Location:** `app/hooks/useFormValidation.ts`
**Usage:** **Usage:**
```tsx ```tsx
import { useFormValidation, validationRules } from "../hooks"; import { useFormValidation, validationRules } from "../hooks";
@@ -275,10 +287,7 @@ const {
initialValues: { email: "", password: "" }, initialValues: { email: "", password: "" },
validationRules: { validationRules: {
email: [validationRules.required, validationRules.email], email: [validationRules.required, validationRules.email],
password: [ password: [validationRules.required, validationRules.minLength(8)],
validationRules.required,
validationRules.minLength(8),
],
}, },
validateOnChange: true, validateOnChange: true,
validateOnBlur: true, validateOnBlur: true,
@@ -290,11 +299,14 @@ const {
value={values.email} value={values.email}
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
/> />;
{errors.email && touched.email && <span>{errors.email}</span>} {
errors.email && touched.email && <span>{errors.email}</span>;
}
``` ```
**Available Validation Rules:** **Available Validation Rules:**
- `validationRules.required` - Field is required - `validationRules.required` - Field is required
- `validationRules.email` - Valid email format - `validationRules.email` - Valid email format
- `validationRules.minLength(n)` - Minimum length - `validationRules.minLength(n)` - Minimum length
@@ -302,6 +314,7 @@ const {
- `validationRules.pattern(regex, message)` - Custom regex pattern - `validationRules.pattern(regex, message)` - Custom regex pattern
**Returns:** **Returns:**
- `values` - Current form values - `values` - Current form values
- `errors` - Field error messages - `errors` - Field error messages
- `touched` - Fields that have been interacted with - `touched` - Fields that have been interacted with
@@ -317,6 +330,7 @@ const {
## Best Practices ## Best Practices
1. **Import from index:** Always import hooks from `app/hooks` index file: 1. **Import from index:** Always import hooks from `app/hooks` index file:
```tsx ```tsx
import { useAnalytics, useComponentId } from "../hooks"; import { useAnalytics, useComponentId } from "../hooks";
``` ```
+6
View File
@@ -9,29 +9,35 @@ This directory contains all project documentation organized by topic.
Comprehensive guides for different aspects of the project: Comprehensive guides for different aspects of the project:
#### Testing #### Testing
- **[testing.md](./guides/testing.md)** - Complete testing strategy and philosophy - **[testing.md](./guides/testing.md)** - Complete testing strategy and philosophy
- **[testing-framework.md](./guides/testing-framework.md)** - Detailed testing framework documentation - **[testing-framework.md](./guides/testing-framework.md)** - Detailed testing framework documentation
- **[testing-quick-reference.md](./guides/testing-quick-reference.md)** - Quick reference for daily development - **[testing-quick-reference.md](./guides/testing-quick-reference.md)** - Quick reference for daily development
- **[visual-regression.md](./guides/visual-regression.md)** - Visual regression testing guide - **[visual-regression.md](./guides/visual-regression.md)** - Visual regression testing guide
#### Performance #### Performance
- **[performance.md](./guides/performance.md)** - Performance optimization and monitoring guide - **[performance.md](./guides/performance.md)** - Performance optimization and monitoring guide
#### Content #### Content
- **[content-creation.md](./guides/content-creation.md)** - Content creation guidelines - **[content-creation.md](./guides/content-creation.md)** - Content creation guidelines
## 🎯 Quick Navigation ## 🎯 Quick Navigation
### For New Team Members ### For New Team Members
1. Start with **[testing.md](./guides/testing.md)** to understand the testing strategy 1. Start with **[testing.md](./guides/testing.md)** to understand the testing strategy
2. Use **[testing-quick-reference.md](./guides/testing-quick-reference.md)** for daily development 2. Use **[testing-quick-reference.md](./guides/testing-quick-reference.md)** for daily development
3. Reference **[performance.md](./guides/performance.md)** for performance optimization 3. Reference **[performance.md](./guides/performance.md)** for performance optimization
### For Daily Development ### For Daily Development
- **[testing-quick-reference.md](./guides/testing-quick-reference.md)** - Essential commands and troubleshooting - **[testing-quick-reference.md](./guides/testing-quick-reference.md)** - Essential commands and troubleshooting
- **[testing-framework.md](./guides/testing-framework.md)** - Detailed testing explanations - **[testing-framework.md](./guides/testing-framework.md)** - Detailed testing explanations
### For Specific Topics ### For Specific Topics
- **Visual Testing**: [visual-regression.md](./guides/visual-regression.md) - **Visual Testing**: [visual-regression.md](./guides/visual-regression.md)
- **Performance**: [performance.md](./guides/performance.md) - **Performance**: [performance.md](./guides/performance.md)
- **Content**: [content-creation.md](./guides/content-creation.md) - **Content**: [content-creation.md](./guides/content-creation.md)
+85 -1
View File
@@ -7,6 +7,8 @@ import tseslint from "@typescript-eslint/eslint-plugin";
import tsparser from "@typescript-eslint/parser"; import tsparser from "@typescript-eslint/parser";
import nextPlugin from "@next/eslint-plugin-next"; import nextPlugin from "@next/eslint-plugin-next";
import globals from "globals"; import globals from "globals";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
const eslintConfig = [ const eslintConfig = [
// Base JavaScript recommended rules // Base JavaScript recommended rules
@@ -38,6 +40,7 @@ const eslintConfig = [
...globals.node, ...globals.node,
...globals.browser, ...globals.browser,
...globals.es2021, ...globals.es2021,
React: "readonly",
}, },
parserOptions: { parserOptions: {
ecmaFeatures: { ecmaFeatures: {
@@ -45,6 +48,21 @@ const eslintConfig = [
}, },
}, },
}, },
plugins: {
react,
"react-hooks": reactHooks,
},
settings: {
react: {
version: "detect",
},
},
rules: {
...react.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
"react/react-in-jsx-scope": "off", // React 19 doesn't require React import
"react/prop-types": "off", // Using TypeScript for prop validation
},
}, },
// TypeScript files configuration // TypeScript files configuration
{ {
@@ -55,6 +73,8 @@ const eslintConfig = [
...globals.node, ...globals.node,
...globals.browser, ...globals.browser,
...globals.es2021, ...globals.es2021,
React: "readonly",
process: "readonly",
}, },
parserOptions: { parserOptions: {
ecmaVersion: "latest", ecmaVersion: "latest",
@@ -68,13 +88,33 @@ const eslintConfig = [
plugins: { plugins: {
"@typescript-eslint": tseslint, "@typescript-eslint": tseslint,
"@next/next": nextPlugin, "@next/next": nextPlugin,
react,
"react-hooks": reactHooks,
},
settings: {
react: {
version: "detect",
},
}, },
rules: { rules: {
...react.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
"react/react-in-jsx-scope": "off", // React 19 doesn't require React import
"react/prop-types": "off", // Using TypeScript for prop validation
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": [
"error", "error",
{ {
argsIgnorePattern: "^_", argsIgnorePattern: "^_",
varsIgnorePattern: "^_", varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
"no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
}, },
], ],
"@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-explicit-any": "warn",
@@ -89,7 +129,51 @@ const eslintConfig = [
rules: { rules: {
// Basic rules // Basic rules
"react/no-unescaped-entities": "off", "react/no-unescaped-entities": "off",
"no-console": "warn", // Default: discourage console usage, but allow warn/error as "standard practice"
"no-console": ["warn", { allow: ["warn", "error"] }],
},
},
// App/lib code: no console.* (enforced)
{
files: ["app/**/*.{ts,tsx,js,jsx}", "lib/**/*.{ts,tsx,js,jsx}"],
rules: {
"no-console": "error",
},
},
// Tests/Storybook/scripts: console is acceptable
{
files: [
"tests/**/*.{ts,tsx,js,jsx}",
"stories/**/*.{ts,tsx,js,jsx}",
"scripts/**/*.{ts,js}",
],
rules: {
"no-console": "off",
},
},
// Config files - allow Node.js globals
{
files: ["*.config.{js,mjs,ts}", "scripts/**/*.{js,ts}"],
languageOptions: {
globals: {
...globals.node,
process: "readonly",
require: "readonly",
console: "readonly",
__dirname: "readonly",
__filename: "readonly",
module: "readonly",
exports: "readonly",
},
},
},
// Storybook files - disable React hooks rules (render functions are called by Storybook)
// This must come AFTER the general rules to override them
{
files: ["**/*.stories.{js,jsx,ts,tsx}"],
rules: {
"react-hooks/rules-of-hooks": "off",
"react-hooks/exhaustive-deps": "off",
}, },
}, },
]; ];
+5 -3
View File
@@ -2,6 +2,8 @@
* Content caching utilities for improved performance * Content caching utilities for improved performance
*/ */
import { logger } from "./logger";
// In-memory cache for blog posts // In-memory cache for blog posts
const blogPostCache = new Map<string, CacheEntry<unknown>>(); const blogPostCache = new Map<string, CacheEntry<unknown>>();
const blogListCache = new Map<string, CacheEntry<unknown[]>>(); const blogListCache = new Map<string, CacheEntry<unknown[]>>();
@@ -243,9 +245,9 @@ export async function warmCache<T>(
cacheBlogPost(postWithSlug.slug, post); cacheBlogPost(postWithSlug.slug, post);
}); });
console.log("Cache warmed up successfully"); logger.info("Cache warmed up successfully");
} catch (error) { } catch (error) {
console.error("Error warming up cache:", error); logger.error("Error warming up cache:", error);
} }
} }
@@ -258,7 +260,7 @@ export function isCacheHealthy(): boolean {
clearExpiredCache(); clearExpiredCache();
return blogPostCache.size < MAX_CACHE_SIZE; return blogPostCache.size < MAX_CACHE_SIZE;
} catch (error) { } catch (error) {
console.error("Cache health check failed:", error); logger.error("Cache health check failed:", error);
return false; return false;
} }
} }
+4 -3
View File
@@ -3,6 +3,7 @@ import path from "path";
import matter from "gray-matter"; import matter from "gray-matter";
import { validateBlogPost, sanitizeBlogPost } from "./validation"; import { validateBlogPost, sanitizeBlogPost } from "./validation";
import type { BlogPostFrontmatter } from "./validation"; import type { BlogPostFrontmatter } from "./validation";
import { logger } from "./logger";
/** /**
* Content processing utilities for blog posts * Content processing utilities for blog posts
@@ -73,7 +74,7 @@ export function getBlogPostFiles(): string[] {
(file) => file.endsWith(".md") || file.endsWith(".mdx"), (file) => file.endsWith(".md") || file.endsWith(".mdx"),
); );
} catch (error) { } catch (error) {
console.error("Error reading blog content directory:", error); logger.error("Error reading blog content directory:", error);
return []; return [];
} }
} }
@@ -92,7 +93,7 @@ export function parseBlogPost(filePath: string): BlogPost | null {
const validationResult = validateBlogPost(data); const validationResult = validateBlogPost(data);
if (!validationResult.isValid) { if (!validationResult.isValid) {
console.error( logger.error(
`Validation errors for ${filePath}:`, `Validation errors for ${filePath}:`,
validationResult.errors, validationResult.errors,
); );
@@ -111,7 +112,7 @@ export function parseBlogPost(filePath: string): BlogPost | null {
lastModified: fs.statSync(fullPath).mtime, lastModified: fs.statSync(fullPath).mtime,
}; };
} catch (error) { } catch (error) {
console.error(`Error parsing blog post file ${filePath}:`, error); logger.error(`Error parsing blog post file ${filePath}:`, error);
return null; return null;
} }
} }
+26
View File
@@ -0,0 +1,26 @@
/* eslint-disable no-console */
/**
* Minimal logger wrapper.
*
* - Centralizes logging so we can swap implementations later (e.g. pino/sentry).
* - Avoids sprinkling `console.*` throughout app code.
*/
type LoggerArgs = unknown[];
const isProd = process.env.NODE_ENV === "production";
export const logger = {
debug: (...args: LoggerArgs) => {
if (!isProd) console.debug(...args);
},
info: (...args: LoggerArgs) => {
console.info(...args);
},
warn: (...args: LoggerArgs) => {
console.warn(...args);
},
error: (...args: LoggerArgs) => {
console.error(...args);
},
};
+1
View File
@@ -1,5 +1,6 @@
import createMDX from "@next/mdx"; import createMDX from "@next/mdx";
/* eslint-env node */
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
// Performance optimizations // Performance optimizations
+1 -1
View File
@@ -6,7 +6,7 @@
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx", "lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 9999",
"postinstall": "npm rebuild lightningcss", "postinstall": "npm rebuild lightningcss",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"storybook:local": "storybook dev -p 6006", "storybook:local": "storybook dev -p 6006",
+2 -3
View File
@@ -5,7 +5,6 @@
* Monitors Core Web Vitals and performance metrics * Monitors Core Web Vitals and performance metrics
*/ */
const { execSync } = require("child_process");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
@@ -67,7 +66,7 @@ class PerformanceMonitor {
execSync("curl -s http://localhost:3000 > /dev/null", { execSync("curl -s http://localhost:3000 > /dev/null", {
stdio: "pipe", stdio: "pipe",
}); });
} catch (error) { } catch {
console.warn( console.warn(
"⚠️ Development server not running, skipping Lighthouse CI...", "⚠️ Development server not running, skipping Lighthouse CI...",
); );
@@ -82,7 +81,7 @@ class PerformanceMonitor {
// Parse Lighthouse results // Parse Lighthouse results
await this.parseLighthouseResults(); await this.parseLighthouseResults();
} catch (error) { } catch {
console.warn("⚠️ Lighthouse CI failed, continuing with other metrics..."); console.warn("⚠️ Lighthouse CI failed, continuing with other metrics...");
} }
} }
+1 -1
View File
@@ -202,7 +202,7 @@ class PerformanceTester {
execSync("curl -s http://localhost:3000 > /dev/null", { execSync("curl -s http://localhost:3000 > /dev/null", {
stdio: "pipe", stdio: "pipe",
}); });
} catch (error) { } catch {
console.warn( console.warn(
"⚠️ Development server not running, skipping Lighthouse CI...", "⚠️ Development server not running, skipping Lighthouse CI...",
); );
+16 -16
View File
@@ -54,25 +54,25 @@ export const Variants = {
children: "Button", children: "Button",
size: "large", size: "large",
}, },
render: (args) => ( render: (_args) => (
<div className="space-y-4"> <div className="space-y-4">
<div className="space-x-4"> <div className="space-x-4">
<Button {...args} variant="default"> <Button {..._args} variant="default">
Default Default
</Button> </Button>
<Button {...args} variant="secondary"> <Button {..._args} variant="secondary">
Secondary Secondary
</Button> </Button>
<Button {...args} variant="primary"> <Button {..._args} variant="primary">
Primary Primary
</Button> </Button>
<Button {...args} variant="outlined"> <Button {..._args} variant="outlined">
Outlined Outlined
</Button> </Button>
<Button {...args} variant="dark"> <Button {..._args} variant="dark">
Dark Dark
</Button> </Button>
<Button {...args} variant="inverse"> <Button {..._args} variant="inverse">
Inverse Inverse
</Button> </Button>
</div> </div>
@@ -92,22 +92,22 @@ export const Sizes = {
children: "Button", children: "Button",
variant: "default", variant: "default",
}, },
render: (args) => ( render: (_args) => (
<div className="space-y-4"> <div className="space-y-4">
<div className="space-x-4"> <div className="space-x-4">
<Button {...args} size="xsmall"> <Button {..._args} size="xsmall">
XSmall XSmall
</Button> </Button>
<Button {...args} size="small"> <Button {..._args} size="small">
Small Small
</Button> </Button>
<Button {...args} size="medium"> <Button {..._args} size="medium">
Medium Medium
</Button> </Button>
<Button {...args} size="large"> <Button {..._args} size="large">
Large Large
</Button> </Button>
<Button {...args} size="xlarge"> <Button {..._args} size="xlarge">
XLarge XLarge
</Button> </Button>
</div> </div>
@@ -128,11 +128,11 @@ export const States = {
size: "large", size: "large",
variant: "default", variant: "default",
}, },
render: (args) => ( render: (_args) => (
<div className="space-y-4"> <div className="space-y-4">
<div className="space-x-4"> <div className="space-x-4">
<Button {...args}>Normal</Button> <Button {..._args}>Normal</Button>
<Button {...args} disabled> <Button {..._args} disabled>
Disabled Disabled
</Button> </Button>
</div> </div>
-3
View File
@@ -5,9 +5,6 @@ import {
CheckedInteraction, CheckedInteraction,
StandardInteraction, StandardInteraction,
InverseInteraction, InverseInteraction,
KeyboardInteraction,
AccessibilityInteraction,
FormIntegration,
} from "../tests/storybook/Checkbox.interactions.test"; } from "../tests/storybook/Checkbox.interactions.test";
export default { export default {
+19
View File
@@ -19,3 +19,22 @@ export default {
}, },
}, },
}; };
export const Default = {
args: {
children: <div>Normal content</div>,
},
};
export const WithError = {
render: () => {
const ThrowError = () => {
throw new Error("Test error for ErrorBoundary");
};
return (
<ErrorBoundary>
<ThrowError />
</ErrorBoundary>
);
},
};
-3
View File
@@ -5,9 +5,6 @@ import {
CheckedInteraction, CheckedInteraction,
StandardInteraction, StandardInteraction,
InverseInteraction, InverseInteraction,
KeyboardInteraction,
AccessibilityInteraction,
FormIntegration,
} from "../tests/storybook/RadioButton.interactions.test"; } from "../tests/storybook/RadioButton.interactions.test";
const meta = { const meta = {
-4
View File
@@ -5,10 +5,6 @@ import {
StandardInteraction, StandardInteraction,
InverseInteraction, InverseInteraction,
InteractiveInteraction, InteractiveInteraction,
KeyboardInteraction,
AccessibilityInteraction,
SingleSelectionInteraction,
FormIntegration,
} from "../tests/storybook/RadioGroup.interactions.test"; } from "../tests/storybook/RadioGroup.interactions.test";
const meta = { const meta = {
+2 -1
View File
@@ -57,7 +57,8 @@ export const Default = {
}; };
export const AllVariants = { export const AllVariants = {
render: (args) => ( // eslint-disable-next-line no-unused-vars
render: (_args) => (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl">
<RuleCard <RuleCard
title="Consensus clusters" title="Consensus clusters"
@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { render, screen, waitFor } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { expect, test, describe, it, vi } from "vitest"; import { expect, describe, it, vi } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe"; import { axe, toHaveNoViolations } from "jest-axe";
import ContextMenu from "../../app/components/ContextMenu"; import ContextMenu from "../../app/components/ContextMenu";
import ContextMenuItem from "../../app/components/ContextMenuItem"; import ContextMenuItem from "../../app/components/ContextMenuItem";
@@ -39,7 +39,6 @@ describe("ContextMenu Components Accessibility", () => {
}); });
it("has proper focus management", async () => { it("has proper focus management", async () => {
const user = userEvent.setup();
render( render(
<ContextMenu> <ContextMenu>
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem> <ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
@@ -249,7 +248,6 @@ describe("ContextMenu Components Accessibility", () => {
}); });
it("maintains proper focus order", async () => { it("maintains proper focus order", async () => {
const user = userEvent.setup();
render(<TestMenu />); render(<TestMenu />);
const items = screen.getAllByRole("menuitem"); const items = screen.getAllByRole("menuitem");
@@ -340,7 +338,6 @@ describe("ContextMenu Components Accessibility", () => {
}); });
it("announces selection state changes", async () => { it("announces selection state changes", async () => {
const user = userEvent.setup();
const { rerender } = render( const { rerender } = render(
<ContextMenuItem onClick={vi.fn()} selected={false}> <ContextMenuItem onClick={vi.fn()} selected={false}>
Test Item Test Item
+1 -2
View File
@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { render, screen, waitFor } from "@testing-library/react"; import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { expect, test, describe, it, vi } from "vitest"; import { expect, describe, it, vi } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe"; import { axe, toHaveNoViolations } from "jest-axe";
import Select from "../../app/components/Select"; import Select from "../../app/components/Select";
@@ -136,7 +136,6 @@ describe("Select Component Accessibility", () => {
describe("Screen Reader Support", () => { describe("Screen Reader Support", () => {
it("announces selected option", async () => { it("announces selected option", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} value="option2" />); render(<Select {...defaultProps} value="option2" />);
const selectButton = screen.getByRole("button"); const selectButton = screen.getByRole("button");
+1 -1
View File
@@ -1,4 +1,4 @@
import { expect, test, describe, it, vi } from "vitest"; import { expect, test, describe, vi } from "vitest";
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { axe, toHaveNoViolations } from "jest-axe"; import { axe, toHaveNoViolations } from "jest-axe";
+1 -1
View File
@@ -1,4 +1,4 @@
import { expect, test, describe, it, vi } from "vitest"; import { expect, test, describe, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react"; import { render, screen, fireEvent } from "@testing-library/react";
import { axe, toHaveNoViolations } from "jest-axe"; import { axe, toHaveNoViolations } from "jest-axe";
import Toggle from "../../app/components/Toggle"; import Toggle from "../../app/components/Toggle";
@@ -102,9 +102,9 @@ test.describe("Accessibility Testing", () => {
// Test Enter key activation // Test Enter key activation
await page.keyboard.press("Enter"); await page.keyboard.press("Enter");
await page.waitForTimeout(100); // Brief pause to see if action occurs await page.waitForTimeout(100); // Brief pause to see if action occurs
} catch (error) { } catch (_error) {
// If focus fails, skip this button // If focus fails, skip this button
console.log(`Could not focus button ${i}: ${error.message}`); console.log(`Could not focus button ${i}: ${_error.message}`);
continue; continue;
} }
} }
@@ -332,7 +332,7 @@ test.describe("Accessibility Testing", () => {
// Page should handle errors gracefully // Page should handle errors gracefully
await expect(page.locator("body")).toBeVisible(); await expect(page.locator("body")).toBeVisible();
} catch (error) { } catch (_error) {
// If reload fails, that's also acceptable - page should handle errors gracefully // If reload fails, that's also acceptable - page should handle errors gracefully
await expect(page.locator("body")).toBeVisible(); await expect(page.locator("body")).toBeVisible();
} }
@@ -168,7 +168,6 @@ describe("RadioButton Accessibility", () => {
}); });
it("maintains focus management", async () => { it("maintains focus management", async () => {
const user = userEvent.setup();
const handleChange = vi.fn(); const handleChange = vi.fn();
const { rerender } = render( const { rerender } = render(
@@ -227,7 +227,6 @@ describe("RadioGroup Accessibility", () => {
}); });
it("maintains focus management", async () => { it("maintains focus management", async () => {
const user = userEvent.setup();
const handleChange = vi.fn(); const handleChange = vi.fn();
const { rerender } = render( const { rerender } = render(
+5 -5
View File
@@ -39,9 +39,9 @@ describe("Accessibility - Component Level", () => {
// Check for proper heading structure (optional for header components) // Check for proper heading structure (optional for header components)
try { try {
const headings = screen.getAllByRole("heading"); screen.getAllByRole("heading");
// Headings are not required in header components, so this is optional // Headings are not required in header components, so this is optional
} catch (error) { } catch {
// No headings found, which is fine for a header component // No headings found, which is fine for a header component
} }
}); });
@@ -119,10 +119,10 @@ describe("Accessibility - Component Level", () => {
try { try {
element.focus(); element.focus();
expect(element).toHaveFocus(); expect(element).toHaveFocus();
} catch (error) { } catch {
// Some elements might not be focusable in test environment // Some elements might not be focusable in test environment
// This is acceptable for accessibility testing // This is acceptable for accessibility testing
console.log(`Could not focus element: ${error.message}`); // Intentionally ignore focus failures in JSDOM
} }
}); });
}); });
@@ -144,7 +144,7 @@ describe("Accessibility - Component Level", () => {
let headings; let headings;
try { try {
headings = screen.getAllByRole("heading"); headings = screen.getAllByRole("heading");
} catch (error) { } catch {
// No headings found, which is fine for a header component // No headings found, which is fine for a header component
return; return;
} }
@@ -33,27 +33,6 @@ const mockBlogPost = {
"<p>This is the main content of the test article.</p><p>It has multiple paragraphs.</p>", "<p>This is the main content of the test article.</p><p>It has multiple paragraphs.</p>",
}; };
const mockRelatedPosts = [
{
slug: "related-article-1",
frontmatter: {
title: "Related Article 1",
description: "First related article",
author: "Test Author",
date: "2025-04-14",
},
},
{
slug: "related-article-2",
frontmatter: {
title: "Related Article 2",
description: "Second related article",
author: "Test Author",
date: "2025-04-13",
},
},
];
describe("Content Page Rendering E2E", () => { describe("Content Page Rendering E2E", () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import Logo from "../../app/components/Logo"; import Logo from "../../app/components/Logo";
// Mock Next.js Link component // Mock Next.js Link component
+1 -1
View File
@@ -239,7 +239,7 @@ test.describe("Edge Cases and Error Scenarios", () => {
// Trigger a harmless error // Trigger a harmless error
try { try {
throw new Error("Test error"); throw new Error("Test error");
} catch (e) { } catch (_e) {
// Error handled // Error handled
} }
@@ -1,5 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import { expect, test, describe, vi } from "vitest"; import { expect, test, describe, vi } from "vitest";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import Checkbox from "../../app/components/Checkbox"; import Checkbox from "../../app/components/Checkbox";
@@ -1,6 +1,5 @@
import { render, screen, cleanup } from "@testing-library/react"; import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import { describe, test, expect, afterEach } from "vitest";
import { vi, describe, test, expect, afterEach } from "vitest";
import ContentLockup from "../../app/components/ContentLockup"; import ContentLockup from "../../app/components/ContentLockup";
afterEach(() => { afterEach(() => {
@@ -1,7 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { render, screen, waitFor } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { expect, test, describe, it, vi } from "vitest"; import { expect, describe, it, vi } from "vitest";
import ContextMenu from "../../app/components/ContextMenu"; import ContextMenu from "../../app/components/ContextMenu";
import ContextMenuItem from "../../app/components/ContextMenuItem"; import ContextMenuItem from "../../app/components/ContextMenuItem";
import ContextMenuSection from "../../app/components/ContextMenuSection"; import ContextMenuSection from "../../app/components/ContextMenuSection";
@@ -77,7 +77,6 @@ describe("ContextMenu Components Integration", () => {
it("shows submenu indicators correctly", () => { it("shows submenu indicators correctly", () => {
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />); render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
const setting1 = screen.getByText("Setting 1");
const arrow = screen const arrow = screen
.getByRole("menuitem", { name: "Setting 1" }) .getByRole("menuitem", { name: "Setting 1" })
.querySelector("svg"); .querySelector("svg");
@@ -87,7 +86,6 @@ describe("ContextMenu Components Integration", () => {
describe("Keyboard Navigation", () => { describe("Keyboard Navigation", () => {
it("navigates through menu items with arrow keys", async () => { it("navigates through menu items with arrow keys", async () => {
const user = userEvent.setup();
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />); render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
const items = screen.getAllByRole("menuitem"); const items = screen.getAllByRole("menuitem");
@@ -137,7 +135,6 @@ describe("ContextMenu Components Integration", () => {
}); });
it("skips disabled items during navigation", async () => { it("skips disabled items during navigation", async () => {
const user = userEvent.setup();
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />); render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
const items = screen.getAllByRole("menuitem"); const items = screen.getAllByRole("menuitem");
@@ -153,7 +150,7 @@ describe("ContextMenu Components Integration", () => {
describe("Dynamic Menu Updates", () => { describe("Dynamic Menu Updates", () => {
const DynamicMenu = ({ items, selectedValue, onItemClick }) => ( const DynamicMenu = ({ items, selectedValue, onItemClick }) => (
<ContextMenu> <ContextMenu>
{items.map((item, index) => ( {items.map((item) => (
<ContextMenuItem <ContextMenuItem
key={item.id} key={item.id}
onClick={() => onItemClick(item.id)} onClick={() => onItemClick(item.id)}
@@ -301,7 +298,6 @@ describe("ContextMenu Components Integration", () => {
describe("Performance", () => { describe("Performance", () => {
it("handles large menu lists efficiently", async () => { it("handles large menu lists efficiently", async () => {
const user = userEvent.setup();
const largeItems = Array.from({ length: 100 }, (_, i) => ({ const largeItems = Array.from({ length: 100 }, (_, i) => ({
id: `item${i}`, id: `item${i}`,
label: `Item ${i}`, label: `Item ${i}`,
@@ -329,7 +325,6 @@ describe("ContextMenu Components Integration", () => {
}); });
it("handles rapid state changes", async () => { it("handles rapid state changes", async () => {
const user = userEvent.setup();
const { rerender } = render( const { rerender } = render(
<ContextMenu> <ContextMenu>
<ContextMenuItem onClick={vi.fn()} selected={false}> <ContextMenuItem onClick={vi.fn()} selected={false}>
+1 -1
View File
@@ -1,5 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test, describe, vi } from "vitest"; import { expect, test, describe, vi } from "vitest";
import Input from "../../app/components/Input"; import Input from "../../app/components/Input";
@@ -35,7 +35,6 @@ describe("RadioButton Integration", () => {
render(<TestForm />); render(<TestForm />);
const option1 = screen.getByText("Option 1").closest("label");
const option2 = screen.getByText("Option 2").closest("label"); const option2 = screen.getByText("Option 2").closest("label");
const submitButton = screen.getByRole("button"); const submitButton = screen.getByRole("button");
@@ -55,7 +54,6 @@ describe("RadioButton Integration", () => {
it("handles keyboard navigation", async () => { it("handles keyboard navigation", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const handleChange = vi.fn();
function KeyboardForm() { function KeyboardForm() {
const [value, setValue] = useState("option1"); const [value, setValue] = useState("option1");
@@ -52,7 +52,6 @@ describe("RadioGroup Integration", () => {
it("handles keyboard navigation", async () => { it("handles keyboard navigation", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const handleChange = vi.fn();
function KeyboardForm() { function KeyboardForm() {
const [value, setValue] = useState("option1"); const [value, setValue] = useState("option1");
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import RelatedArticles from "../../app/components/RelatedArticles"; import RelatedArticles from "../../app/components/RelatedArticles";
// Mock ContentThumbnailTemplate // Mock ContentThumbnailTemplate
@@ -1,7 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { render, screen, waitFor } from "@testing-library/react"; import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { expect, test, describe, it, vi } from "vitest"; import { expect, describe, it } from "vitest";
import Select from "../../app/components/Select"; import Select from "../../app/components/Select";
describe("Select Component Integration", () => { describe("Select Component Integration", () => {
@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { render, screen, waitFor } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import Switch from "../../app/components/Switch"; import Switch from "../../app/components/Switch";
@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { expect, test, describe, it, vi } from "vitest"; import { expect, test, describe, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import TextArea from "../../app/components/TextArea"; import TextArea from "../../app/components/TextArea";
@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { expect, test, describe, it, vi } from "vitest"; import { expect, test, describe, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react"; import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import Toggle from "../../app/components/Toggle"; import Toggle from "../../app/components/Toggle";
@@ -120,7 +120,7 @@ describe("ToggleGroup Integration", () => {
}); });
it("handles state changes", async () => { it("handles state changes", async () => {
const { rerender } = render(<TestForm />); render(<TestForm />);
const toggleGroups = screen.getAllByRole("button"); const toggleGroups = screen.getAllByRole("button");
// Initially, left should be selected // Initially, left should be selected
@@ -182,7 +182,7 @@ describe("ToggleGroup Integration", () => {
}); });
it("handles rapid state changes", async () => { it("handles rapid state changes", async () => {
const { rerender } = render(<TestForm />); render(<TestForm />);
const toggleGroups = screen.getAllByRole("button"); const toggleGroups = screen.getAllByRole("button");
// Rapidly change states // Rapidly change states
@@ -1,6 +1,6 @@
import { render, screen, cleanup } from "@testing-library/react"; import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { vi, describe, test, expect, afterEach } from "vitest"; import { describe, test, expect, afterEach } from "vitest";
import HeroBanner from "../../app/components/HeroBanner"; import HeroBanner from "../../app/components/HeroBanner";
import NumberedCards from "../../app/components/NumberedCards"; import NumberedCards from "../../app/components/NumberedCards";
import RuleStack from "../../app/components/RuleStack"; import RuleStack from "../../app/components/RuleStack";
@@ -1,6 +1,6 @@
import { render, screen, cleanup } from "@testing-library/react"; import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { vi, describe, test, expect, afterEach } from "vitest"; import { describe, test, expect, afterEach } from "vitest";
import Header from "../../app/components/Header"; import Header from "../../app/components/Header";
import Footer from "../../app/components/Footer"; import Footer from "../../app/components/Footer";
@@ -160,7 +160,7 @@ describe("Page Flow Integration", () => {
); );
expect(cards.length).toBeGreaterThan(0); expect(cards.length).toBeGreaterThan(0);
}); });
// Check that all three cards are rendered // Check that all three cards are rendered
const cards = screen.getAllByText( const cards = screen.getAllByText(
/Document how your community|Build an operating manual|Get a link to your manual/, /Document how your community|Build an operating manual|Get a link to your manual/,
@@ -206,7 +206,7 @@ describe("Page Flow Integration", () => {
const headings = screen.getAllByRole("heading"); const headings = screen.getAllByRole("heading");
expect(headings.length).toBeGreaterThan(4); // Should have multiple headings expect(headings.length).toBeGreaterThan(4); // Should have multiple headings
}); });
// Check for proper heading hierarchy // Check for proper heading hierarchy
const headings = screen.getAllByRole("heading"); const headings = screen.getAllByRole("heading");
expect(headings.length).toBeGreaterThan(4); // Should have multiple headings expect(headings.length).toBeGreaterThan(4); // Should have multiple headings
@@ -129,7 +129,6 @@ describe("User Journey Integration", () => {
}); });
test("user explores the process through numbered cards", async () => { test("user explores the process through numbered cards", async () => {
const user = userEvent.setup();
render(<Page />); render(<Page />);
// Wait for dynamically imported NumberedCards component // Wait for dynamically imported NumberedCards component
@@ -153,7 +152,6 @@ describe("User Journey Integration", () => {
}); });
test("user accesses contact information through footer", async () => { test("user accesses contact information through footer", async () => {
const user = userEvent.setup();
render( render(
<div> <div>
<Header /> <Header />
@@ -179,7 +177,6 @@ describe("User Journey Integration", () => {
}); });
test("user explores features and benefits", async () => { test("user explores features and benefits", async () => {
const user = userEvent.setup();
render(<Page />); render(<Page />);
// Wait for dynamically imported FeatureGrid component // Wait for dynamically imported FeatureGrid component
@@ -201,7 +198,6 @@ describe("User Journey Integration", () => {
}); });
test("user interacts with logo wall and social proof", async () => { test("user interacts with logo wall and social proof", async () => {
const user = userEvent.setup();
render( render(
<div> <div>
<Page /> <Page />
@@ -232,7 +228,6 @@ describe("User Journey Integration", () => {
}); });
test("user completes the full journey from discovery to action", async () => { test("user completes the full journey from discovery to action", async () => {
const user = userEvent.setup();
render( render(
<div> <div>
<Header /> <Header />
@@ -278,7 +273,6 @@ describe("User Journey Integration", () => {
}); });
test("user can access all navigation options consistently", async () => { test("user can access all navigation options consistently", async () => {
const user = userEvent.setup();
render( render(
<div> <div>
<Header /> <Header />
+19 -9
View File
@@ -288,7 +288,7 @@ class WebPerformanceMonitor extends PerformanceMonitor {
/** /**
* Measure page load performance * Measure page load performance
*/ */
async measurePageLoad(url) { async measurePageLoad() {
return this.measureFunction("page_load", async () => { return this.measureFunction("page_load", async () => {
const start = performance.now(); const start = performance.now();
@@ -326,18 +326,26 @@ class PlaywrightPerformanceMonitor extends PerformanceMonitor {
// Navigate to the page // Navigate to the page
// Use "load" instead of "networkidle" to handle dynamically imported components // Use "load" instead of "networkidle" to handle dynamically imported components
// "networkidle" can timeout with code splitting as chunks load asynchronously // "networkidle" can timeout with code splitting as chunks load asynchronously
await this.page.goto(url, { await this.page.goto(url, {
waitUntil: "load", waitUntil: "load",
timeout: 60000, // 60 second timeout for slower networks timeout: 60000, // 60 second timeout for slower networks
}); });
} catch (error) { } catch (error) {
// Handle interstitial/blocking errors // Handle interstitial/blocking errors
if (error.message.includes("interstitial") || error.message.includes("prevented")) { if (
console.warn("Page load was blocked, attempting to continue:", error.message); error.message.includes("interstitial") ||
error.message.includes("prevented")
) {
console.warn(
"Page load was blocked, attempting to continue:",
error.message,
);
// Try to wait for the page to be in a usable state // Try to wait for the page to be in a usable state
try { try {
await this.page.waitForLoadState("domcontentloaded", { timeout: 10000 }); await this.page.waitForLoadState("domcontentloaded", {
} catch (e) { timeout: 10000,
});
} catch {
throw new Error(`Page failed to load: ${error.message}`); throw new Error(`Page failed to load: ${error.message}`);
} }
} else { } else {
@@ -349,9 +357,11 @@ class PlaywrightPerformanceMonitor extends PerformanceMonitor {
// This ensures code-split components have loaded // This ensures code-split components have loaded
try { try {
// Wait for main content sections that use dynamic imports // Wait for main content sections that use dynamic imports
await this.page.waitForSelector("section", { timeout: 10000 }).catch(() => { await this.page
// Ignore if sections don't appear - page might still be valid .waitForSelector("section", { timeout: 10000 })
}); .catch(() => {
// Ignore if sections don't appear - page might still be valid
});
} catch (error) { } catch (error) {
// Continue even if some components haven't loaded - we still want to measure performance // Continue even if some components haven't loaded - we still want to measure performance
console.warn("Some components may not have loaded:", error.message); console.warn("Some components may not have loaded:", error.message);
@@ -164,7 +164,6 @@ export const SingleSelectionInteraction = {
export const FormIntegration = { export const FormIntegration = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio"); const radioButtons = canvas.getAllByRole("radio");
// Should have hidden inputs for form submission // Should have hidden inputs for form submission
@@ -63,7 +63,6 @@ test.describe("RadioGroup Storybook Tests", () => {
test("interacts with controls", async ({ page }) => { test("interacts with controls", async ({ page }) => {
// Test mode control // Test mode control
await page.selectOption('[data-testid="mode-control"]', "inverse"); await page.selectOption('[data-testid="mode-control"]', "inverse");
const radioGroup = page.locator('[role="radiogroup"]');
const radioButtons = page.locator('[role="radio"]'); const radioButtons = page.locator('[role="radio"]');
// All radio buttons should have inverse styling // All radio buttons should have inverse styling
+2 -2
View File
@@ -60,7 +60,7 @@ vi.mock("../../app/components/ContentBanner", () => {
vi.mock("../../app/components/RelatedArticles", () => { vi.mock("../../app/components/RelatedArticles", () => {
return { return {
default: ({ relatedPosts, currentPostSlug }) => ( default: ({ relatedPosts }) => (
<div data-testid="related-articles"> <div data-testid="related-articles">
<h2>Related Articles</h2> <h2>Related Articles</h2>
{relatedPosts.map((post) => ( {relatedPosts.map((post) => (
@@ -200,7 +200,7 @@ describe("BlogPostPage", () => {
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId("related-articles")).toBeInTheDocument(); expect(screen.getByTestId("related-articles")).toBeInTheDocument();
}); });
expect(screen.getByText("Related Articles")).toBeInTheDocument(); expect(screen.getByText("Related Articles")).toBeInTheDocument();
expect(screen.getByTestId("related-related-1")).toBeInTheDocument(); expect(screen.getByTestId("related-related-1")).toBeInTheDocument();
expect(screen.getByTestId("related-related-2")).toBeInTheDocument(); expect(screen.getByTestId("related-related-2")).toBeInTheDocument();
+2 -2
View File
@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { render, screen, fireEvent } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { expect, test, describe, it, vi, beforeEach } from "vitest"; import { expect, describe, it, vi, beforeEach } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe"; import { axe, toHaveNoViolations } from "jest-axe";
import ContextMenu from "../../app/components/ContextMenu"; import ContextMenu from "../../app/components/ContextMenu";
import ContextMenuItem from "../../app/components/ContextMenuItem"; import ContextMenuItem from "../../app/components/ContextMenuItem";
+1 -2
View File
@@ -1,6 +1,5 @@
import { render, screen, cleanup } from "@testing-library/react"; import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import { describe, test, expect, afterEach } from "vitest";
import { vi, describe, test, expect, afterEach } from "vitest";
import FeatureGrid from "../../app/components/FeatureGrid"; import FeatureGrid from "../../app/components/FeatureGrid";
afterEach(() => { afterEach(() => {
-1
View File
@@ -1,6 +1,5 @@
import { describe, test, expect } from "vitest"; import { describe, test, expect } from "vitest";
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Footer from "../../app/components/Footer"; import Footer from "../../app/components/Footer";
describe("Footer", () => { describe("Footer", () => {
+1 -2
View File
@@ -1,6 +1,5 @@
import { describe, test, expect, vi, beforeEach } from "vitest"; import { describe, test, expect, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Header, { import Header, {
navigationItems, navigationItems,
avatarImages, avatarImages,
+1 -2
View File
@@ -1,6 +1,5 @@
import { render, screen, cleanup } from "@testing-library/react"; import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import { describe, test, expect, afterEach } from "vitest";
import { vi, describe, test, expect, afterEach } from "vitest";
import HeroBanner from "../../app/components/HeroBanner"; import HeroBanner from "../../app/components/HeroBanner";
afterEach(() => { afterEach(() => {
+1 -2
View File
@@ -1,6 +1,5 @@
import { render, screen, cleanup } from "@testing-library/react"; import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import { describe, test, expect, afterEach } from "vitest";
import { vi, describe, test, expect, afterEach } from "vitest";
import LogoWall from "../../app/components/LogoWall"; import LogoWall from "../../app/components/LogoWall";
afterEach(() => { afterEach(() => {
+1 -2
View File
@@ -1,6 +1,5 @@
import { render, screen, cleanup } from "@testing-library/react"; import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import { describe, test, expect, afterEach } from "vitest";
import { vi, describe, test, expect, afterEach } from "vitest";
import NumberedCards from "../../app/components/NumberedCards"; import NumberedCards from "../../app/components/NumberedCards";
afterEach(() => { afterEach(() => {
+10 -6
View File
@@ -100,7 +100,8 @@ describe("Page", () => {
// Wait for dynamically imported FeatureGrid component to load // Wait for dynamically imported FeatureGrid component to load
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getAllByText("We've got your back, every step of the way").length, screen.getAllByText("We've got your back, every step of the way")
.length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
expect( expect(
@@ -143,7 +144,8 @@ describe("Page", () => {
// FeatureGrid // FeatureGrid
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getAllByText("We've got your back, every step of the way").length, screen.getAllByText("We've got your back, every step of the way")
.length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
@@ -212,7 +214,7 @@ describe("Page", () => {
// Check all section titles (using getAllByText since there are multiple instances) // Check all section titles (using getAllByText since there are multiple instances)
expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0); expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0);
// Wait for dynamically imported components // Wait for dynamically imported components
await waitFor(() => { await waitFor(() => {
expect( expect(
@@ -221,7 +223,8 @@ describe("Page", () => {
}); });
await waitFor(() => { await waitFor(() => {
expect( expect(
screen.getAllByText("We've got your back, every step of the way").length, screen.getAllByText("We've got your back, every step of the way")
.length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
expect(screen.getAllByText("Still have questions?").length).toBeGreaterThan( expect(screen.getAllByText("Still have questions?").length).toBeGreaterThan(
@@ -236,7 +239,8 @@ describe("Page", () => {
await waitFor(() => { await waitFor(() => {
// Check all three numbered card items (using getAllByText since there are multiple instances) // Check all three numbered card items (using getAllByText since there are multiple instances)
expect( expect(
screen.getAllByText("Document how your community makes decisions").length, screen.getAllByText("Document how your community makes decisions")
.length,
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
expect( expect(
@@ -256,7 +260,7 @@ describe("Page", () => {
// Check subtitles (using getAllByText since there are multiple instances) // Check subtitles (using getAllByText since there are multiple instances)
expect(screen.getAllByText("with clarity").length).toBeGreaterThan(0); expect(screen.getAllByText("with clarity").length).toBeGreaterThan(0);
// Wait for dynamically imported components // Wait for dynamically imported components
await waitFor(() => { await waitFor(() => {
expect( expect(
-1
View File
@@ -1,5 +1,4 @@
import { render, screen, cleanup } from "@testing-library/react"; import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi, describe, test, expect, afterEach } from "vitest"; import { vi, describe, test, expect, afterEach } from "vitest";
import QuoteBlock from "../../app/components/QuoteBlock"; import QuoteBlock from "../../app/components/QuoteBlock";
+1 -1
View File
@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { render, screen, fireEvent } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import RadioButton from "../../app/components/RadioButton"; import RadioButton from "../../app/components/RadioButton";
+2 -2
View File
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, expect, vi, beforeEach, it } from "vitest";
import { render, screen, waitFor } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import RelatedArticles from "../../app/components/RelatedArticles"; import RelatedArticles from "../../app/components/RelatedArticles";
// Mock Next.js components // Mock Next.js components
+6 -3
View File
@@ -1,6 +1,7 @@
import { render, screen, cleanup } from "@testing-library/react"; import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { vi, describe, test, expect, afterEach } from "vitest"; import { vi, describe, test, expect, afterEach } from "vitest";
import { logger } from "../../lib/logger";
import RuleStack from "../../app/components/RuleStack"; import RuleStack from "../../app/components/RuleStack";
afterEach(() => { afterEach(() => {
@@ -99,16 +100,18 @@ describe("RuleStack Component", () => {
test("handles template click events", async () => { test("handles template click events", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); const debugSpy = vi
.spyOn(logger, "debug")
.mockImplementation(() => undefined);
render(<RuleStack />); render(<RuleStack />);
const consensusCard = screen.getByText("Consensus").closest("div"); const consensusCard = screen.getByText("Consensus").closest("div");
await user.click(consensusCard); await user.click(consensusCard);
expect(consoleSpy).toHaveBeenCalledWith("Consensus template clicked"); expect(debugSpy).toHaveBeenCalledWith("Consensus template clicked");
consoleSpy.mockRestore(); debugSpy.mockRestore();
}); });
test("renders with proper semantic structure", () => { test("renders with proper semantic structure", () => {
+2 -3
View File
@@ -1,7 +1,6 @@
import React from "react"; import { render, screen, waitFor } from "@testing-library/react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { expect, test, describe, it, vi } from "vitest"; import { expect, describe, it, vi } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe"; import { axe, toHaveNoViolations } from "jest-axe";
import Select from "../../app/components/Select"; import Select from "../../app/components/Select";
+1 -1
View File
@@ -1,4 +1,4 @@
import { expect, test, describe, it, vi } from "vitest"; import { expect, test, describe, vi } from "vitest";
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import TextArea from "../../app/components/TextArea"; import TextArea from "../../app/components/TextArea";
+1 -1
View File
@@ -1,4 +1,4 @@
import { expect, test, describe, it, vi } from "vitest"; import { expect, test, describe, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react"; import { render, screen, fireEvent } from "@testing-library/react";
import Toggle from "../../app/components/Toggle"; import Toggle from "../../app/components/Toggle";
+11 -5
View File
@@ -4,7 +4,7 @@ import { useClickOutside } from "../../../app/hooks/useClickOutside";
import { useRef } from "react"; import { useRef } from "react";
describe("useClickOutside", () => { describe("useClickOutside", () => {
let handler: ReturnType<typeof vi.fn>; let handler;
beforeEach(() => { beforeEach(() => {
handler = vi.fn(); handler = vi.fn();
@@ -26,7 +26,9 @@ describe("useClickOutside", () => {
result.current.current = div; result.current.current = div;
act(() => { act(() => {
document.body.dispatchEvent(new MouseEvent("mousedown", { bubbles: true })); document.body.dispatchEvent(
new MouseEvent("mousedown", { bubbles: true }),
);
}); });
expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledTimes(1);
@@ -55,14 +57,16 @@ describe("useClickOutside", () => {
}); });
test("does not call handler when disabled", () => { test("does not call handler when disabled", () => {
const { result } = renderHook(() => { renderHook(() => {
const ref = useRef(null); const ref = useRef(null);
useClickOutside([ref], handler, false); useClickOutside([ref], handler, false);
return ref; return ref;
}); });
act(() => { act(() => {
document.body.dispatchEvent(new MouseEvent("mousedown", { bubbles: true })); document.body.dispatchEvent(
new MouseEvent("mousedown", { bubbles: true }),
);
}); });
expect(handler).not.toHaveBeenCalled(); expect(handler).not.toHaveBeenCalled();
@@ -84,7 +88,9 @@ describe("useClickOutside", () => {
result.current.ref2.current = div2; result.current.ref2.current = div2;
act(() => { act(() => {
document.body.dispatchEvent(new MouseEvent("mousedown", { bubbles: true })); document.body.dispatchEvent(
new MouseEvent("mousedown", { bubbles: true }),
);
}); });
expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledTimes(1);
+1 -5
View File
@@ -1,9 +1,5 @@
import { defineConfig } from "vitest/config"; import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
@@ -64,7 +60,7 @@ export default defineConfig({
], ],
thresholds: { lines: 50, functions: 50, statements: 50, branches: 50 }, thresholds: { lines: 50, functions: 50, statements: 50, branches: 50 },
// Disable coverage collection in CI to prevent test failures // Disable coverage collection in CI to prevent test failures
enabled: !process.env.CI, enabled: !(typeof process !== "undefined" && process.env.CI),
}, },
pool: "threads", // Use threads for better performance pool: "threads", // Use threads for better performance
testTimeout: 60000, // 60s timeout for all tests testTimeout: 60000, // 60s timeout for all tests
+9 -8
View File
@@ -16,7 +16,7 @@ vi.mock("next/dynamic", () => {
return function DynamicComponent(props: any) { return function DynamicComponent(props: any) {
const [Component, setComponent] = React.useState(null); const [Component, setComponent] = React.useState(null);
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
React.useEffect(() => { React.useEffect(() => {
importFn() importFn()
.then((mod: any) => { .then((mod: any) => {
@@ -27,15 +27,15 @@ vi.mock("next/dynamic", () => {
setLoading(false); setLoading(false);
}); });
}, []); }, []);
if (loading && options?.loading) { if (loading && options?.loading) {
return options.loading(); return options.loading();
} }
if (Component) { if (Component) {
return React.createElement(Component, props); return React.createElement(Component, props);
} }
return null; return null;
}; };
}, },
@@ -49,18 +49,19 @@ Object.defineProperty(window, "matchMedia", {
// Parse the media query to determine if it matches // Parse the media query to determine if it matches
const minWidthMatch = query.match(/min-width:\s*(\d+)px/); const minWidthMatch = query.match(/min-width:\s*(\d+)px/);
const maxWidthMatch = query.match(/max-width:\s*(\d+)px/); const maxWidthMatch = query.match(/max-width:\s*(\d+)px/);
// Use window.innerWidth if set by tests, otherwise default to desktop (1200px) // Use window.innerWidth if set by tests, otherwise default to desktop (1200px)
// This allows tests to override viewport width by setting window.innerWidth // This allows tests to override viewport width by setting window.innerWidth
const viewportWidth = (typeof window !== "undefined" && window.innerWidth) || 1200; const viewportWidth =
(typeof window !== "undefined" && window.innerWidth) || 1200;
let matches = true; let matches = true;
if (minWidthMatch) { if (minWidthMatch) {
matches = viewportWidth >= parseInt(minWidthMatch[1], 10); matches = viewportWidth >= parseInt(minWidthMatch[1], 10);
} else if (maxWidthMatch) { } else if (maxWidthMatch) {
matches = viewportWidth <= parseInt(maxWidthMatch[1], 10); matches = viewportWidth <= parseInt(maxWidthMatch[1], 10);
} }
return { return {
matches, matches,
media: query, media: query,