diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 3a5f2ce..235e3bf 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -491,20 +491,19 @@ jobs: # - run: npm run test:sb # env: { CI: true } - # Temporarily disabled - 523 pre-existing ESLint issues will be addressed in separate ticket - # lint: - # runs-on: [self-hosted, macos-latest] - # steps: - # - uses: actions/checkout@v4 - # - uses: actions/setup-node@v4 - # if: ${{ github.server_url == 'https://github.com' }} - # with: { node-version: 20, cache: npm } - # - uses: actions/setup-node@v4 - # if: ${{ github.server_url != 'https://github.com' || !github.server_url }} - # with: { node-version: 20 } - # - run: npm ci - # - run: npm run lint - # - run: npm exec prettier -- --check "**/*.{js,jsx,ts,tsx,json,css,md}" + lint: + runs-on: [self-hosted, macos-latest] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + if: ${{ github.server_url == 'https://github.com' }} + with: { node-version: 20, cache: npm } + - uses: actions/setup-node@v4 + if: ${{ github.server_url != 'https://github.com' || !github.server_url }} + with: { node-version: 20 } + - run: npm ci + - run: npm run lint + - run: npm exec prettier -- --check "**/*.{js,jsx,ts,tsx,json,css,md}" build: runs-on: [self-hosted, macos-latest] diff --git a/app/api/web-vitals/route.ts b/app/api/web-vitals/route.ts index 468ce50..a1beafe 100644 --- a/app/api/web-vitals/route.ts +++ b/app/api/web-vitals/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import fs from "fs"; import path from "path"; +import { logger } from "../../../lib/logger"; 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[]; } catch (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)); // Log for monitoring - console.log( + logger.info( `Web Vital received: ${metric} = ${data.value}ms (${data.rating})`, ); return NextResponse.json({ success: true }); } catch (error) { - console.error("Error processing web vital:", error); + logger.error("Error processing web vital:", error); return NextResponse.json( { error: "Internal server error" }, { status: 500 }, @@ -141,7 +142,7 @@ export async function GET() { return NextResponse.json({ metrics }); } catch (error) { - console.error("Error fetching web vitals:", error); + logger.error("Error fetching web vitals:", error); return NextResponse.json( { error: "Internal server error" }, { status: 500 }, diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx index a5571c1..ca857d7 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[slug]/page.tsx @@ -6,6 +6,7 @@ import { getAllBlogPosts as getAllPosts, type BlogPost, } from "../../../lib/content"; +import { logger } from "../../../lib/logger"; import ContentBanner from "../../components/ContentBanner"; import AskOrganizer from "../../components/AskOrganizer"; import { getAssetPath, ASSETS } from "../../../lib/assetUtils"; @@ -44,7 +45,7 @@ export async function generateStaticParams() { slug: post.slug, })); } catch (error) { - console.error("Error generating static params:", error); + logger.error("Error generating static params:", error); return []; } } @@ -87,7 +88,7 @@ export async function generateMetadata({ }, }; } catch (error) { - console.error("Error generating metadata:", error); + logger.error("Error generating metadata:", error); return { title: "Blog Post", description: "A blog post from our community.", @@ -162,7 +163,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/ErrorBoundary.tsx b/app/components/ErrorBoundary.tsx index 915a6a1..78896f9 100644 --- a/app/components/ErrorBoundary.tsx +++ b/app/components/ErrorBoundary.tsx @@ -1,6 +1,7 @@ "use client"; import React, { Component, type ReactNode } from "react"; +import { logger } from "../../lib/logger"; interface ErrorBoundaryProps { children: ReactNode; @@ -24,7 +25,7 @@ class ErrorBoundary extends Component { componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { // 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() { diff --git a/app/components/Input.tsx b/app/components/Input.tsx index 48c5c52..823cc60 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; } @@ -151,13 +151,12 @@ const Input = forwardRef( `.trim(); // Form field handlers with disabled state handling - const { handleChange, handleFocus, handleBlur } = useFormField< - HTMLInputElement - >(disabled, { - onChange, - onFocus, - onBlur, - }); + const { handleChange, handleFocus, handleBlur } = + useFormField(disabled, { + onChange, + onFocus, + onBlur, + }); return (
diff --git a/app/components/NumberedCard.tsx b/app/components/NumberedCard.tsx index d7466d6..317fca2 100644 --- a/app/components/NumberedCard.tsx +++ b/app/components/NumberedCard.tsx @@ -10,25 +10,23 @@ interface NumberedCardProps { iconColor?: string; } -const NumberedCard = memo( - ({ number, text, iconShape: _iconShape, iconColor: _iconColor }) => { - return ( -
- {/* Section Number - Top right (lg breakpoint) */} -
- -
- - {/* Card Content - Bottom left (lg breakpoint) */} -
-

- {text} -

-
+const NumberedCard = memo(({ number, text }) => { + return ( +
+ {/* Section Number - Top right (lg breakpoint) */} +
+
- ); - }, -); + + {/* Card Content - Bottom left (lg breakpoint) */} +
+

+ {text} +

+
+
+ ); +}); NumberedCard.displayName = "NumberedCard"; diff --git a/app/components/QuoteBlock.tsx b/app/components/QuoteBlock.tsx index 85b8aca..1eb1619 100644 --- a/app/components/QuoteBlock.tsx +++ b/app/components/QuoteBlock.tsx @@ -3,6 +3,7 @@ import { useState, memo } from "react"; import Image from "next/image"; import QuoteDecor from "./QuoteDecor"; +import { logger } from "../../lib/logger"; interface QuoteBlockProps { variant?: "compact" | "standard" | "extended"; @@ -13,7 +14,7 @@ interface QuoteBlockProps { avatarSrc?: string; id?: string; fallbackAvatarSrc?: string; - onError?: (error: { + onError?: (_error: { type: string; message: string; author?: string; @@ -109,7 +110,7 @@ const QuoteBlock = memo( // Error handling functions const handleImageError = (error: unknown) => { - console.warn( + logger.warn( `QuoteBlock: Failed to load avatar image for ${author}:`, error, ); @@ -135,7 +136,7 @@ const QuoteBlock = memo( // Validate required props if (!quote || !author) { - console.error("QuoteBlock: Missing required props (quote or author)"); + logger.error("QuoteBlock: Missing required props (quote or author)"); if (onError) { onError({ type: "missing_props", 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/RelatedArticles.tsx b/app/components/RelatedArticles.tsx index 300e36f..791b47f 100644 --- a/app/components/RelatedArticles.tsx +++ b/app/components/RelatedArticles.tsx @@ -108,7 +108,7 @@ const RelatedArticles = memo( } return ( -
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..d56880e 100644 --- a/app/components/RuleStack.tsx +++ b/app/components/RuleStack.tsx @@ -5,6 +5,7 @@ import Image from "next/image"; import RuleCard from "./RuleCard"; import Button from "./Button"; import { getAssetPath } from "../../lib/assetUtils"; +import { logger } from "../../lib/logger"; interface RuleStackProps { className?: string; @@ -13,12 +14,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; }; } } @@ -38,7 +39,7 @@ const RuleStack = memo(({ className = "" }) => { }); } } - console.log(`${templateName} template clicked`); + logger.debug(`${templateName} template clicked`); }; return ( 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..6511658 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; } @@ -155,13 +155,12 @@ const TextArea = forwardRef( `.trim(); // Form field handlers with disabled state handling - const { handleChange, handleFocus, handleBlur } = useFormField< - HTMLTextAreaElement - >(disabled, { - onChange, - onFocus, - onBlur, - }); + const { handleChange, handleFocus, handleBlur } = + useFormField(disabled, { + onChange, + onFocus, + onBlur, + }); return (
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/components/WebVitalsDashboard.tsx b/app/components/WebVitalsDashboard.tsx index 733a039..fa556a8 100644 --- a/app/components/WebVitalsDashboard.tsx +++ b/app/components/WebVitalsDashboard.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, memo } from "react"; +import { logger } from "../../lib/logger"; interface VitalData { value: number; @@ -50,7 +51,7 @@ const WebVitalsDashboard = memo(() => { const data = (await response.json()) as { metrics?: Metrics }; setMetrics(data.metrics || {}); } catch (error) { - console.error("Error fetching web vitals:", error); + logger.error("Error fetching web vitals:", error); } finally { setLoading(false); } diff --git a/app/hooks/index.ts b/app/hooks/index.ts index 8cf1fea..188245c 100644 --- a/app/hooks/index.ts +++ b/app/hooks/index.ts @@ -1,6 +1,6 @@ /** * Custom hooks for reusable component logic - * + * * This module exports all custom hooks used throughout the application. * Hooks encapsulate complex logic and state management that can be reused * across multiple components. @@ -12,7 +12,12 @@ export { useComponentId } from "./useComponentId"; export { useFormField } from "./useFormField"; export { useComponentStyles } from "./useComponentStyles"; export { useSchemaData } from "./useSchemaData"; -export { useMediaQuery, useIsMobile, useIsDesktop, BREAKPOINTS } from "./useMediaQuery"; +export { + useMediaQuery, + useIsMobile, + useIsDesktop, + BREAKPOINTS, +} from "./useMediaQuery"; export { useFormValidation, validationRules } from "./useFormValidation"; export type { SizeStyleConfig, 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..a4aa882 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; @@ -61,9 +61,7 @@ export interface UseComponentStylesOptions { * }); * ``` */ -export function useComponentStyles( - options: UseComponentStylesOptions, -): { +export function useComponentStyles(options: UseComponentStylesOptions): { sizeClasses: Record; stateClasses: Record; } { 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..a46abab 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 @@ -22,24 +22,30 @@ export const validationRules = { return emailRegex.test(value) ? null : "Please enter a valid email address"; }, - minLength: (min: number) => (value: string): string | null => { - if (!value) return null; - return value.length >= min - ? null - : `Must be at least ${min} characters long`; - }, + minLength: + (min: number) => + (value: string): string | null => { + if (!value) return null; + return value.length >= min + ? null + : `Must be at least ${min} characters long`; + }, - maxLength: (max: number) => (value: string): string | null => { - if (!value) return null; - return value.length <= max - ? null - : `Must be no more than ${max} characters long`; - }, + maxLength: + (max: number) => + (value: string): string | null => { + if (!value) return null; + return value.length <= max + ? null + : `Must be no more than ${max} characters long`; + }, - pattern: (regex: RegExp, message: string) => (value: string): string | null => { - if (!value) return null; - return regex.test(value) ? null : message; - }, + pattern: + (regex: RegExp, message: string) => + (value: string): string | null => { + if (!value) return null; + return regex.test(value) ? null : message; + }, }; /** @@ -182,13 +188,16 @@ export function useFormValidation(options: UseFormValidationOptions) { }, [initialValues]); // Set field value programmatically - const setValue = useCallback((name: string, value: string) => { - setValues((prev) => ({ ...prev, [name]: value })); - if (validateOnChange) { - const error = validateField(name, value); - setErrors((prev) => ({ ...prev, [name]: error })); - } - }, [validateOnChange, validateField]); + const setValue = useCallback( + (name: string, value: string) => { + setValues((prev) => ({ ...prev, [name]: value })); + if (validateOnChange) { + const error = validateField(name, value); + setErrors((prev) => ({ ...prev, [name]: error })); + } + }, + [validateOnChange, validateField], + ); return { values, 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/app/hooks/useSchemaData.ts b/app/hooks/useSchemaData.ts index 592bbcd..3eb1622 100644 --- a/app/hooks/useSchemaData.ts +++ b/app/hooks/useSchemaData.ts @@ -144,7 +144,12 @@ export function useSchemaData( type: "BreadcrumbList"; items: Array<{ name: string; url: string }>; }, -): SchemaOrganization | SchemaWebSite | SchemaHowTo | SchemaArticle | SchemaBreadcrumbList { +): + | SchemaOrganization + | SchemaWebSite + | SchemaHowTo + | SchemaArticle + | SchemaBreadcrumbList { return useMemo(() => { switch (config.type) { case "Organization": @@ -216,7 +221,9 @@ export function useSchemaData( "@id": config.mainEntityOfPage, }, }), - ...(config.articleSection && { articleSection: config.articleSection }), + ...(config.articleSection && { + articleSection: config.articleSection, + }), ...(config.keywords && { keywords: config.keywords }), } as SchemaArticle; diff --git a/docs/CUSTOM_HOOKS.md b/docs/CUSTOM_HOOKS.md index 73a62d5..facb73f 100644 --- a/docs/CUSTOM_HOOKS.md +++ b/docs/CUSTOM_HOOKS.md @@ -15,6 +15,7 @@ Detects clicks outside of specified elements. Useful for closing dropdowns, moda **Location:** `app/hooks/useClickOutside.ts` **Usage:** + ```tsx import { useClickOutside } from "../hooks"; @@ -26,6 +27,7 @@ useClickOutside([menuRef, buttonRef], () => setIsOpen(false), isOpen); ``` **Parameters:** + - `refs`: Array of refs to elements that should not trigger the callback - `handler`: Callback function to execute when clicking outside - `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` **Usage:** + ```tsx import { useAnalytics } from "../hooks"; @@ -66,6 +69,7 @@ trackCustomEvent( ``` **Returns:** + - `trackEvent`: Function to track standard analytics events - `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` **Usage:** + ```tsx import { useComponentId } from "../hooks"; @@ -89,10 +94,12 @@ const { id, labelId } = useComponentId("input", props.id); ``` **Parameters:** + - `prefix`: Prefix for the generated ID (e.g., "input", "select") - `providedId`: Optional ID provided via props (takes precedence) **Returns:** + - `id`: Component ID - `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` **Usage:** + ```tsx import { useFormField } from "../hooks"; @@ -117,18 +125,16 @@ const { handleChange, handleFocus, handleBlur } = useFormField(disabled, { }); // Use in component - +; ``` **Parameters:** + - `disabled`: Whether the field is disabled - `handlers`: Object containing onChange, onFocus, onBlur handlers **Returns:** + - `handleChange`: Wrapped onChange handler - `handleFocus`: Wrapped onFocus 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` **Usage:** + ```tsx 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` **Usage:** + ```tsx import { useSchemaData } from "../hooks"; @@ -205,10 +213,11 @@ const orgSchema = useSchemaData({