diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx index a5571c1..ad8a368 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[slug]/page.tsx @@ -162,7 +162,11 @@ export default async function BlogPostPage({ params }: PageProps) { return scoredPosts .sort((a, b) => b.score - a.score) .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); diff --git a/app/components/AskOrganizer.tsx b/app/components/AskOrganizer.tsx index 1f79f27..a38f4ef 100644 --- a/app/components/AskOrganizer.tsx +++ b/app/components/AskOrganizer.tsx @@ -13,7 +13,7 @@ interface AskOrganizerProps { buttonHref?: string; className?: string; variant?: "centered" | "left-aligned" | "compact" | "inverse"; - onContactClick?: (data: { + onContactClick?: (_data: { event: string; component: string; variant: string; diff --git a/app/components/Button.tsx b/app/components/Button.tsx index 97a0082..e5bb220 100644 --- a/app/components/Button.tsx +++ b/app/components/Button.tsx @@ -14,7 +14,7 @@ interface ButtonProps extends React.ButtonHTMLAttributes { disabled?: boolean; type?: "button" | "submit" | "reset"; onClick?: ( - e: React.MouseEvent, + _e: React.MouseEvent, ) => void; href?: string; target?: string; diff --git a/app/components/Checkbox.tsx b/app/components/Checkbox.tsx index 6a3806d..97e8cd3 100644 --- a/app/components/Checkbox.tsx +++ b/app/components/Checkbox.tsx @@ -10,7 +10,7 @@ interface CheckboxProps { disabled?: boolean; label?: string; className?: string; - onChange?: (data: { + onChange?: (_data: { checked: boolean; value?: string; event: React.MouseEvent | React.KeyboardEvent; diff --git a/app/components/ContextMenuItem.tsx b/app/components/ContextMenuItem.tsx index 3ea0f2d..37c2aeb 100644 --- a/app/components/ContextMenuItem.tsx +++ b/app/components/ContextMenuItem.tsx @@ -9,7 +9,7 @@ interface ContextMenuItemProps extends React.HTMLAttributes { disabled?: boolean; className?: string; onClick?: ( - e: React.MouseEvent | React.KeyboardEvent, + _e: React.MouseEvent | React.KeyboardEvent, ) => void; size?: "small" | "medium" | "large"; } diff --git a/app/components/Input.tsx b/app/components/Input.tsx index 48c5c52..121e9f8 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -15,9 +15,9 @@ interface InputProps extends Omit< label?: string; placeholder?: string; value?: string; - onChange?: (e: React.ChangeEvent) => void; - onFocus?: (e: React.FocusEvent) => void; - onBlur?: (e: React.FocusEvent) => void; + onChange?: (_e: React.ChangeEvent) => void; + onFocus?: (_e: React.FocusEvent) => void; + onBlur?: (_e: React.FocusEvent) => void; className?: string; } diff --git a/app/components/NumberedCard.tsx b/app/components/NumberedCard.tsx index d7466d6..f3d836e 100644 --- a/app/components/NumberedCard.tsx +++ b/app/components/NumberedCard.tsx @@ -11,7 +11,7 @@ interface NumberedCardProps { } const NumberedCard = memo( - ({ number, text, iconShape: _iconShape, iconColor: _iconColor }) => { + ({ number, text }) => { return (
{/* Section Number - Top right (lg breakpoint) */} diff --git a/app/components/QuoteBlock.tsx b/app/components/QuoteBlock.tsx index 85b8aca..79f7bcd 100644 --- a/app/components/QuoteBlock.tsx +++ b/app/components/QuoteBlock.tsx @@ -13,7 +13,7 @@ interface QuoteBlockProps { avatarSrc?: string; id?: string; fallbackAvatarSrc?: string; - onError?: (error: { + onError?: (_error: { type: string; message: string; author?: string; diff --git a/app/components/RadioButton.tsx b/app/components/RadioButton.tsx index 20cc5f3..b9ebdfa 100644 --- a/app/components/RadioButton.tsx +++ b/app/components/RadioButton.tsx @@ -8,7 +8,7 @@ interface RadioButtonProps { state?: "default" | "hover" | "focus"; disabled?: boolean; label?: string; - onChange?: (data: { checked: boolean; value?: string }) => void; + onChange?: (_data: { checked: boolean; value?: string }) => void; id?: string; name?: string; value?: string; diff --git a/app/components/RadioGroup.tsx b/app/components/RadioGroup.tsx index 17aca61..c8d3cb1 100644 --- a/app/components/RadioGroup.tsx +++ b/app/components/RadioGroup.tsx @@ -12,7 +12,7 @@ interface RadioOption { interface RadioGroupProps { name?: string; value?: string; - onChange?: (data: { value: string }) => void; + onChange?: (_data: { value: string }) => void; mode?: "standard" | "inverse"; state?: "default" | "hover" | "focus"; disabled?: boolean; diff --git a/app/components/RuleCard.tsx b/app/components/RuleCard.tsx index 34cf8a7..d63c1d5 100644 --- a/app/components/RuleCard.tsx +++ b/app/components/RuleCard.tsx @@ -14,12 +14,12 @@ interface RuleCardProps { declare global { interface Window { gtag?: ( - command: string, - eventName: string, - params?: Record, + _command: string, + _eventName: string, + _params?: Record, ) => void; analytics?: { - track: (eventName: string, params?: Record) => void; + track: (_eventName: string, _params?: Record) => void; }; } } diff --git a/app/components/RuleStack.tsx b/app/components/RuleStack.tsx index b88d9e1..e98ce2e 100644 --- a/app/components/RuleStack.tsx +++ b/app/components/RuleStack.tsx @@ -13,12 +13,12 @@ interface RuleStackProps { declare global { interface Window { gtag?: ( - command: string, - eventName: string, - params?: Record, + _command: string, + _eventName: string, + _params?: Record, ) => void; analytics?: { - track: (eventName: string, params?: Record) => void; + track: (_eventName: string, _params?: Record) => void; }; } } diff --git a/app/components/Select.tsx b/app/components/Select.tsx index 5160218..0f4a608 100644 --- a/app/components/Select.tsx +++ b/app/components/Select.tsx @@ -33,7 +33,7 @@ interface SelectProps { className?: string; children?: React.ReactNode; value?: string; - onChange?: (data: { target: { value: string; text: string } }) => void; + onChange?: (_data: { target: { value: string; text: string } }) => void; options?: SelectOptionData[]; } @@ -115,10 +115,11 @@ const Select = forwardRef( const baseStyles = "w-full"; switch (size) { - case "small": + case "small": { const smallHeight = labelVariant === "horizontal" ? "h-[30px]" : "h-[32px]"; return `${baseStyles} ${smallHeight} pl-[12px] pr-[36px] py-[8px] text-[10px] leading-[14px]`; + } case "medium": return `${baseStyles} h-[36px] pl-[12px] pr-[36px] py-[8px] text-[14px] leading-[20px]`; case "large": diff --git a/app/components/SelectOption.tsx b/app/components/SelectOption.tsx index 796196d..fc66f43 100644 --- a/app/components/SelectOption.tsx +++ b/app/components/SelectOption.tsx @@ -8,7 +8,7 @@ interface SelectOptionProps { disabled?: boolean; className?: string; onClick?: ( - e: React.MouseEvent | React.KeyboardEvent, + _e: React.MouseEvent | React.KeyboardEvent, ) => void; size?: "small" | "medium" | "large"; } diff --git a/app/components/Switch.tsx b/app/components/Switch.tsx index e529279..331013d 100644 --- a/app/components/Switch.tsx +++ b/app/components/Switch.tsx @@ -6,12 +6,12 @@ interface SwitchProps extends Omit< > { checked?: boolean; onChange?: ( - e: + _e: | React.MouseEvent | React.KeyboardEvent, ) => void; - onFocus?: (e: React.FocusEvent) => void; - onBlur?: (e: React.FocusEvent) => void; + onFocus?: (_e: React.FocusEvent) => void; + onBlur?: (_e: React.FocusEvent) => void; state?: "default" | "hover" | "focus"; label?: string; className?: string; diff --git a/app/components/TextArea.tsx b/app/components/TextArea.tsx index e3e44d2..af75439 100644 --- a/app/components/TextArea.tsx +++ b/app/components/TextArea.tsx @@ -15,9 +15,9 @@ interface TextAreaProps extends Omit< label?: string; placeholder?: string; value?: string; - onChange?: (e: React.ChangeEvent) => void; - onFocus?: (e: React.FocusEvent) => void; - onBlur?: (e: React.FocusEvent) => void; + onChange?: (_e: React.ChangeEvent) => void; + onFocus?: (_e: React.FocusEvent) => void; + onBlur?: (_e: React.FocusEvent) => void; className?: string; rows?: number; } diff --git a/app/components/Toggle.tsx b/app/components/Toggle.tsx index a17760d..86dcebb 100644 --- a/app/components/Toggle.tsx +++ b/app/components/Toggle.tsx @@ -7,12 +7,12 @@ interface ToggleProps extends Omit< label?: string; checked?: boolean; onChange?: ( - e: + _e: | React.MouseEvent | React.KeyboardEvent, ) => void; - onFocus?: (e: React.FocusEvent) => void; - onBlur?: (e: React.FocusEvent) => void; + onFocus?: (_e: React.FocusEvent) => void; + onBlur?: (_e: React.FocusEvent) => void; disabled?: boolean; state?: "default" | "hover" | "focus"; showIcon?: boolean; diff --git a/app/components/ToggleGroup.tsx b/app/components/ToggleGroup.tsx index 313a53a..fde5e9d 100644 --- a/app/components/ToggleGroup.tsx +++ b/app/components/ToggleGroup.tsx @@ -11,12 +11,12 @@ interface ToggleGroupProps extends Omit< showText?: boolean; ariaLabel?: string; onChange?: ( - e: + _e: | React.MouseEvent | React.KeyboardEvent, ) => void; - onFocus?: (e: React.FocusEvent) => void; - onBlur?: (e: React.FocusEvent) => void; + onFocus?: (_e: React.FocusEvent) => void; + onBlur?: (_e: React.FocusEvent) => void; } const ToggleGroup = memo( diff --git a/app/hooks/useAnalytics.ts b/app/hooks/useAnalytics.ts index 9ba7b4d..7a95bfd 100644 --- a/app/hooks/useAnalytics.ts +++ b/app/hooks/useAnalytics.ts @@ -28,20 +28,20 @@ interface AnalyticsEvent { } interface UseAnalyticsReturn { - trackEvent: (event: AnalyticsEvent) => void; + trackEvent: (_event: AnalyticsEvent) => void; trackCustomEvent: ( - event: string, - data: Record, - callback?: (data: Record) => void, + _event: string, + _data: Record, + _callback?: (_data: Record) => void, ) => void; } declare global { interface Window { gtag?: ( - command: string, - eventName: string, - params?: Record, + _command: string, + _eventName: string, + _params?: Record, ) => void; } } @@ -64,7 +64,7 @@ export function useAnalytics(): UseAnalyticsReturn { const trackCustomEvent = ( event: string, data: Record, - callback?: (data: Record) => void, + callback?: (_data: Record) => void, ) => { // Execute custom callback if provided if (callback) { diff --git a/app/hooks/useClickOutside.ts b/app/hooks/useClickOutside.ts index 336fa02..34935ac 100644 --- a/app/hooks/useClickOutside.ts +++ b/app/hooks/useClickOutside.ts @@ -19,7 +19,7 @@ import { useEffect, RefObject } from "react"; */ export function useClickOutside( refs: Array>, - handler: (event: MouseEvent | TouchEvent) => void, + handler: (_event: MouseEvent | TouchEvent) => void, enabled: boolean = true, ): void { useEffect(() => { diff --git a/app/hooks/useComponentStyles.ts b/app/hooks/useComponentStyles.ts index ed013eb..477c3ed 100644 --- a/app/hooks/useComponentStyles.ts +++ b/app/hooks/useComponentStyles.ts @@ -24,7 +24,7 @@ export interface UseComponentStylesOptions { error?: boolean; sizeStyles: SizeStyleConfig; stateStyles: StateStyleConfig; - getStateStyles?: (params: { + getStateStyles?: (_params: { state?: string; disabled: boolean; error: boolean; diff --git a/app/hooks/useFormField.ts b/app/hooks/useFormField.ts index e377ca7..96e51ff 100644 --- a/app/hooks/useFormField.ts +++ b/app/hooks/useFormField.ts @@ -19,15 +19,15 @@ import { useCallback } from "react"; * ``` */ interface FormFieldHandlers { - onChange?: (e: React.ChangeEvent) => void; - onFocus?: (e: React.FocusEvent) => void; - onBlur?: (e: React.FocusEvent) => void; + onChange?: (_e: React.ChangeEvent) => void; + onFocus?: (_e: React.FocusEvent) => void; + onBlur?: (_e: React.FocusEvent) => void; } interface UseFormFieldReturn { - handleChange: (e: React.ChangeEvent) => void; - handleFocus: (e: React.FocusEvent) => void; - handleBlur: (e: React.FocusEvent) => void; + handleChange: (_e: React.ChangeEvent) => void; + handleFocus: (_e: React.FocusEvent) => void; + handleBlur: (_e: React.FocusEvent) => void; } export function useFormField( diff --git a/app/hooks/useFormValidation.ts b/app/hooks/useFormValidation.ts index 63982dd..8325ec7 100644 --- a/app/hooks/useFormValidation.ts +++ b/app/hooks/useFormValidation.ts @@ -3,7 +3,7 @@ import { useState, useCallback, useMemo } from "react"; /** * Validation rule function type */ -export type ValidationRule = (value: T) => string | null; +export type ValidationRule = (_value: T) => string | null; /** * Validation rules for common patterns diff --git a/app/hooks/useMediaQuery.ts b/app/hooks/useMediaQuery.ts index af81767..a04e921 100644 --- a/app/hooks/useMediaQuery.ts +++ b/app/hooks/useMediaQuery.ts @@ -51,6 +51,8 @@ export function useMediaQuery( } 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); // Create listener for changes diff --git a/eslint.config.mjs b/eslint.config.mjs index 654c98f..2eb8862 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -7,6 +7,8 @@ import tseslint from "@typescript-eslint/eslint-plugin"; import tsparser from "@typescript-eslint/parser"; import nextPlugin from "@next/eslint-plugin-next"; import globals from "globals"; +import react from "eslint-plugin-react"; +import reactHooks from "eslint-plugin-react-hooks"; const eslintConfig = [ // Base JavaScript recommended rules @@ -38,6 +40,7 @@ const eslintConfig = [ ...globals.node, ...globals.browser, ...globals.es2021, + React: "readonly", }, parserOptions: { 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 { @@ -55,6 +73,8 @@ const eslintConfig = [ ...globals.node, ...globals.browser, ...globals.es2021, + React: "readonly", + process: "readonly", }, parserOptions: { ecmaVersion: "latest", @@ -68,13 +88,33 @@ const eslintConfig = [ plugins: { "@typescript-eslint": tseslint, "@next/next": nextPlugin, + 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-eslint/no-unused-vars": [ "error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + "no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", }, ], "@typescript-eslint/no-explicit-any": "warn", @@ -92,6 +132,31 @@ const eslintConfig = [ "no-console": "warn", }, }, + // 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", + }, + }, ]; export default eslintConfig; diff --git a/next.config.mjs b/next.config.mjs index b985839..242f0c1 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,5 +1,6 @@ import createMDX from "@next/mdx"; +/* eslint-env node */ /** @type {import('next').NextConfig} */ const nextConfig = { // Performance optimizations diff --git a/package.json b/package.json index feafeaf..67bdeba 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dev": "next dev --turbopack", "build": "next build", "start": "next start", - "lint": "eslint . --ext .js,.jsx,.ts,.tsx", + "lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 9999", "postinstall": "npm rebuild lightningcss", "storybook": "storybook dev -p 6006", "storybook:local": "storybook dev -p 6006", diff --git a/scripts/performance-monitor.js b/scripts/performance-monitor.js index d03ebed..02e1a52 100644 --- a/scripts/performance-monitor.js +++ b/scripts/performance-monitor.js @@ -5,7 +5,6 @@ * Monitors Core Web Vitals and performance metrics */ -const { execSync } = require("child_process"); const fs = require("fs"); const path = require("path"); @@ -67,7 +66,7 @@ class PerformanceMonitor { execSync("curl -s http://localhost:3000 > /dev/null", { stdio: "pipe", }); - } catch (error) { + } catch { console.warn( "⚠️ Development server not running, skipping Lighthouse CI...", ); @@ -82,7 +81,7 @@ class PerformanceMonitor { // Parse Lighthouse results await this.parseLighthouseResults(); - } catch (error) { + } catch { console.warn("⚠️ Lighthouse CI failed, continuing with other metrics..."); } } diff --git a/scripts/test-performance.js b/scripts/test-performance.js index 6edf24d..d655e54 100644 --- a/scripts/test-performance.js +++ b/scripts/test-performance.js @@ -202,7 +202,7 @@ class PerformanceTester { execSync("curl -s http://localhost:3000 > /dev/null", { stdio: "pipe", }); - } catch (error) { + } catch { console.warn( "⚠️ Development server not running, skipping Lighthouse CI...", ); diff --git a/stories/Button.stories.js b/stories/Button.stories.js index 53d2cf8..248d9b9 100644 --- a/stories/Button.stories.js +++ b/stories/Button.stories.js @@ -54,25 +54,25 @@ export const Variants = { children: "Button", size: "large", }, - render: (args) => ( + render: (_args) => (
- - - - - -
@@ -92,22 +92,22 @@ export const Sizes = { children: "Button", variant: "default", }, - render: (args) => ( + render: (_args) => (
- - - - -
@@ -128,11 +128,11 @@ export const States = { size: "large", variant: "default", }, - render: (args) => ( + render: (_args) => (
- - +
diff --git a/stories/Checkbox.stories.js b/stories/Checkbox.stories.js index 52acb0c..b22d1af 100644 --- a/stories/Checkbox.stories.js +++ b/stories/Checkbox.stories.js @@ -5,9 +5,6 @@ import { CheckedInteraction, StandardInteraction, InverseInteraction, - KeyboardInteraction, - AccessibilityInteraction, - FormIntegration, } from "../tests/storybook/Checkbox.interactions.test"; export default { diff --git a/stories/ErrorBoundary.stories.js b/stories/ErrorBoundary.stories.js index 4290c0c..bb445ca 100644 --- a/stories/ErrorBoundary.stories.js +++ b/stories/ErrorBoundary.stories.js @@ -19,3 +19,22 @@ export default { }, }, }; + +export const Default = { + args: { + children:
Normal content
, + }, +}; + +export const WithError = { + render: () => { + const ThrowError = () => { + throw new Error("Test error for ErrorBoundary"); + }; + return ( + + + + ); + }, +}; diff --git a/stories/RadioButton.stories.js b/stories/RadioButton.stories.js index c99b5e9..67ffbc5 100644 --- a/stories/RadioButton.stories.js +++ b/stories/RadioButton.stories.js @@ -5,9 +5,6 @@ import { CheckedInteraction, StandardInteraction, InverseInteraction, - KeyboardInteraction, - AccessibilityInteraction, - FormIntegration, } from "../tests/storybook/RadioButton.interactions.test"; const meta = { diff --git a/stories/RadioGroup.stories.js b/stories/RadioGroup.stories.js index 86b88bc..5aaf488 100644 --- a/stories/RadioGroup.stories.js +++ b/stories/RadioGroup.stories.js @@ -5,10 +5,6 @@ import { StandardInteraction, InverseInteraction, InteractiveInteraction, - KeyboardInteraction, - AccessibilityInteraction, - SingleSelectionInteraction, - FormIntegration, } from "../tests/storybook/RadioGroup.interactions.test"; const meta = { diff --git a/stories/RuleCard.stories.js b/stories/RuleCard.stories.js index 07485de..bf52c35 100644 --- a/stories/RuleCard.stories.js +++ b/stories/RuleCard.stories.js @@ -57,7 +57,8 @@ export const Default = { }; export const AllVariants = { - render: (args) => ( + // eslint-disable-next-line no-unused-vars + render: (_args) => (
{ }); it("has proper focus management", async () => { - const user = userEvent.setup(); render( Item 1 @@ -249,7 +248,6 @@ describe("ContextMenu Components Accessibility", () => { }); it("maintains proper focus order", async () => { - const user = userEvent.setup(); render(); const items = screen.getAllByRole("menuitem"); @@ -340,7 +338,6 @@ describe("ContextMenu Components Accessibility", () => { }); it("announces selection state changes", async () => { - const user = userEvent.setup(); const { rerender } = render( Test Item diff --git a/tests/accessibility/Select.a11y.test.jsx b/tests/accessibility/Select.a11y.test.jsx index 2f73cd6..aae5b60 100644 --- a/tests/accessibility/Select.a11y.test.jsx +++ b/tests/accessibility/Select.a11y.test.jsx @@ -1,7 +1,7 @@ import React from "react"; import { render, screen, waitFor } from "@testing-library/react"; 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 Select from "../../app/components/Select"; @@ -136,7 +136,6 @@ describe("Select Component Accessibility", () => { describe("Screen Reader Support", () => { it("announces selected option", async () => { - const user = userEvent.setup(); render(