From 86d7cff5d48485f63cc783190ddee1dc06bdc84a Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:51:27 -0700 Subject: [PATCH] Extract custom hooks for reusable logic --- app/components/AskOrganizer.tsx | 45 ++- app/components/Checkbox.tsx | 10 +- app/components/Input.tsx | 41 +-- app/components/NumberedCards.tsx | 29 +- app/components/RelatedArticles.tsx | 14 +- app/components/Select.tsx | 21 +- app/components/TextArea.tsx | 41 +-- app/hooks/index.ts | 33 ++ app/hooks/useAnalytics.ts | 90 ++++++ app/hooks/useClickOutside.ts | 52 +++ app/hooks/useComponentId.ts | 28 ++ app/hooks/useComponentStyles.ts | 111 +++++++ app/hooks/useFormField.ts | 71 +++++ app/hooks/useFormValidation.ts | 204 ++++++++++++ app/hooks/useMediaQuery.ts | 87 ++++++ app/hooks/useSchemaData.ts | 236 ++++++++++++++ docs/CUSTOM_HOOKS.md | 365 ++++++++++++++++++++++ tests/unit/AskOrganizer.test.jsx | 15 +- tests/unit/hooks/useClickOutside.test.jsx | 94 ++++++ tests/unit/hooks/useComponentId.test.jsx | 26 ++ tests/unit/hooks/useSchemaData.test.jsx | 118 +++++++ 21 files changed, 1590 insertions(+), 141 deletions(-) create mode 100644 app/hooks/index.ts create mode 100644 app/hooks/useAnalytics.ts create mode 100644 app/hooks/useClickOutside.ts create mode 100644 app/hooks/useComponentId.ts create mode 100644 app/hooks/useComponentStyles.ts create mode 100644 app/hooks/useFormField.ts create mode 100644 app/hooks/useFormValidation.ts create mode 100644 app/hooks/useMediaQuery.ts create mode 100644 app/hooks/useSchemaData.ts create mode 100644 docs/CUSTOM_HOOKS.md create mode 100644 tests/unit/hooks/useClickOutside.test.jsx create mode 100644 tests/unit/hooks/useComponentId.test.jsx create mode 100644 tests/unit/hooks/useSchemaData.test.jsx diff --git a/app/components/AskOrganizer.tsx b/app/components/AskOrganizer.tsx index 696d0b4..1f79f27 100644 --- a/app/components/AskOrganizer.tsx +++ b/app/components/AskOrganizer.tsx @@ -3,6 +3,7 @@ import { memo } from "react"; import ContentLockup from "./ContentLockup"; import Button from "./Button"; +import { useAnalytics } from "../hooks"; interface AskOrganizerProps { title?: string; @@ -22,16 +23,6 @@ interface AskOrganizerProps { }) => void; } -declare global { - interface Window { - gtag?: ( - command: string, - eventName: string, - params?: Record, - ) => void; - } -} - const AskOrganizer = memo( ({ title, @@ -43,30 +34,32 @@ const AskOrganizer = memo( variant = "centered", onContactClick, }) => { + const { trackEvent, trackCustomEvent } = useAnalytics(); + // Analytics tracking for contact button clicks const handleContactClick = ( _event: React.MouseEvent, ) => { - // Track contact button interaction - if (onContactClick) { - onContactClick({ - event: "contact_button_click", + // Track with standard analytics + trackEvent({ + event: "contact_button_click", + category: "engagement", + label: "ask_organizer", + component: "AskOrganizer", + variant, + }); + + // Also call custom callback if provided + trackCustomEvent( + "contact_button_click", + { component: "AskOrganizer", variant, buttonText, buttonHref, - timestamp: new Date().toISOString(), - }); - } - - // Additional analytics tracking (can be expanded) - if (typeof window !== "undefined" && window.gtag) { - window.gtag("event", "contact_button_click", { - event_category: "engagement", - event_label: "ask_organizer", - value: 1, - }); - } + }, + onContactClick, + ); }; // Variant-specific styling diff --git a/app/components/Checkbox.tsx b/app/components/Checkbox.tsx index 8008730..6a3806d 100644 --- a/app/components/Checkbox.tsx +++ b/app/components/Checkbox.tsx @@ -1,6 +1,7 @@ "use client"; -import { memo, useId } from "react"; +import { memo } from "react"; +import { useComponentId } from "../hooks"; interface CheckboxProps { checked?: boolean; @@ -95,8 +96,7 @@ const Checkbox = memo( }; // Generate unique ID for accessibility if not provided - const generatedId = useId(); - const checkboxId = id || `checkbox-${generatedId}`; + const { id: checkboxId, labelId } = useComponentId("checkbox", id); const accessibilityProps = { role: "checkbox" as const, @@ -104,7 +104,7 @@ const Checkbox = memo( ...(disabled && { "aria-disabled": true, tabIndex: -1 }), ...(!disabled && { tabIndex: 0 }), ...(ariaLabel && { "aria-label": ariaLabel }), - ...(label && !ariaLabel && { "aria-labelledby": `${checkboxId}-label` }), + ...(label && !ariaLabel && { "aria-labelledby": labelId }), id: checkboxId, ...props, }; @@ -151,7 +151,7 @@ const Checkbox = memo( {label && ( diff --git a/app/components/Input.tsx b/app/components/Input.tsx index 5c1ea96..48c5c52 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -1,6 +1,7 @@ "use client"; -import { memo, useCallback, forwardRef, useId } from "react"; +import { memo, forwardRef } from "react"; +import { useComponentId, useFormField } from "../hooks"; interface InputProps extends Omit< React.InputHTMLAttributes, @@ -43,8 +44,7 @@ const Input = forwardRef( ref, ) => { // Generate unique ID for accessibility if not provided - const generatedId = useId(); - const inputId = id || `input-${generatedId}`; + const { id: inputId, labelId } = useComponentId("input", id); // Size variants const sizeStyles: Record< @@ -150,37 +150,20 @@ const Input = forwardRef( ${className} `.trim(); - const handleChange = useCallback( - (e: React.ChangeEvent) => { - if (!disabled && onChange) { - onChange(e); - } - }, - [disabled, onChange], - ); - - const handleFocus = useCallback( - (e: React.FocusEvent) => { - if (!disabled && onFocus) { - onFocus(e); - } - }, - [disabled, onFocus], - ); - - const handleBlur = useCallback( - (e: React.FocusEvent) => { - if (!disabled && onBlur) { - onBlur(e); - } - }, - [disabled, onBlur], - ); + // Form field handlers with disabled state handling + const { handleChange, handleFocus, handleBlur } = useFormField< + HTMLInputElement + >(disabled, { + onChange, + onFocus, + onBlur, + }); return (
{label && (