Extract custom hooks for reusable logic

This commit is contained in:
adilallo
2026-01-26 12:51:27 -07:00
parent f513aecacc
commit 86d7cff5d4
21 changed files with 1590 additions and 141 deletions
+19 -26
View File
@@ -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<string, unknown>,
) => void;
}
}
const AskOrganizer = memo<AskOrganizerProps>(
({
title,
@@ -43,30 +34,32 @@ const AskOrganizer = memo<AskOrganizerProps>(
variant = "centered",
onContactClick,
}) => {
const { trackEvent, trackCustomEvent } = useAnalytics();
// Analytics tracking for contact button clicks
const handleContactClick = (
_event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>,
) => {
// 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
+5 -5
View File
@@ -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<CheckboxProps>(
};
// 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<CheckboxProps>(
...(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<CheckboxProps>(
</span>
{label && (
<span
id={`${checkboxId}-label`}
id={labelId}
className="font-inter text-[14px] leading-[18px]"
style={{ color: labelColor }}
>
+12 -29
View File
@@ -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<HTMLInputElement>,
@@ -43,8 +44,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
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<HTMLInputElement, InputProps>(
${className}
`.trim();
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (!disabled && onChange) {
onChange(e);
}
},
[disabled, onChange],
);
const handleFocus = useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
if (!disabled && onFocus) {
onFocus(e);
}
},
[disabled, onFocus],
);
const handleBlur = useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
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 (
<div className={containerClasses}>
{label && (
<label
id={labelId}
htmlFor={inputId}
className={`${labelClasses} font-inter font-medium text-[var(--color-content-default-secondary)]`}
>
+12 -17
View File
@@ -1,9 +1,10 @@
"use client";
import { memo, useMemo } from "react";
import { memo } from "react";
import NumberedCard from "./NumberedCard";
import SectionHeader from "./SectionHeader";
import Button from "./Button";
import { useSchemaData } from "../hooks";
interface Card {
text: string;
@@ -18,22 +19,16 @@ interface NumberedCardsProps {
}
const NumberedCards = memo<NumberedCardsProps>(({ title, subtitle, cards }) => {
// Memoize schema data to prevent unnecessary re-computations
const schemaData = useMemo(
() => ({
"@context": "https://schema.org",
"@type": "HowTo",
name: title,
description: subtitle,
step: cards.map((card, index) => ({
"@type": "HowToStep",
position: index + 1,
name: card.text,
text: card.text,
})),
}),
[title, subtitle, cards],
);
// Generate schema data using hook
const schemaData = useSchemaData({
type: "HowTo",
name: title,
description: subtitle,
steps: cards.map((card) => ({
name: card.text,
text: card.text,
})),
});
return (
<>
+3 -11
View File
@@ -3,6 +3,7 @@
import { useState, useEffect, memo, useMemo, useCallback } from "react";
import ContentThumbnailTemplate from "./ContentThumbnailTemplate";
import type { BlogPost } from "../../lib/content";
import { useIsMobile } from "../hooks";
interface RelatedArticlesProps {
relatedPosts: BlogPost[];
@@ -20,7 +21,7 @@ const RelatedArticles = memo<RelatedArticlesProps>(
const [currentIndex, setCurrentIndex] = useState(0);
const [progress, setProgress] = useState(0);
const [isMobile, setIsMobile] = useState(true);
const isMobile = useIsMobile();
// Memoize the mouse down handler to prevent unnecessary re-renders
const handleMouseDown = useCallback(
@@ -72,16 +73,7 @@ const RelatedArticles = memo<RelatedArticlesProps>(
[currentIndex, progress],
);
// Check if we're on mobile (below lg breakpoint)
useEffect(() => {
const checkScreenSize = () => {
setIsMobile(window.innerWidth < 1024); // lg breakpoint is 1024px
};
checkScreenSize();
window.addEventListener("resize", checkScreenSize);
return () => window.removeEventListener("resize", checkScreenSize);
}, []);
// Mobile detection is now handled by useIsMobile hook
// Auto-advance every 3 seconds (only on mobile)
useEffect(() => {
+2 -19
View File
@@ -8,11 +8,11 @@ import React, {
useId,
useState,
useRef,
useEffect,
useCallback,
memo,
useImperativeHandle,
} from "react";
import { useClickOutside } from "../hooks";
import SelectDropdown from "./SelectDropdown";
import SelectOption from "./SelectOption";
@@ -71,24 +71,7 @@ const Select = forwardRef<HTMLButtonElement, SelectProps>(
);
// Handle click outside to close menu
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
menuRef.current &&
!menuRef.current.contains(event.target as Node) &&
selectRef.current &&
!selectRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
return () =>
document.removeEventListener("mousedown", handleClickOutside);
}
}, [isOpen]);
useClickOutside([menuRef, selectRef], () => setIsOpen(false), isOpen);
// Handle option selection
const handleOptionSelect = useCallback(
+12 -29
View File
@@ -1,6 +1,7 @@
"use client";
import { memo, useCallback, forwardRef, useId } from "react";
import { memo, forwardRef } from "react";
import { useComponentId, useFormField } from "../hooks";
interface TextAreaProps extends Omit<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
@@ -44,8 +45,7 @@ const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
ref,
) => {
// Generate unique ID for accessibility if not provided
const generatedId = useId();
const textareaId = id || `textarea-${generatedId}`;
const { id: textareaId, labelId } = useComponentId("textarea", id);
// Size variants with specific heights and radius for TextArea
const sizeStyles: Record<
@@ -154,37 +154,20 @@ const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
${className}
`.trim();
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (!disabled && onChange) {
onChange(e);
}
},
[disabled, onChange],
);
const handleFocus = useCallback(
(e: React.FocusEvent<HTMLTextAreaElement>) => {
if (!disabled && onFocus) {
onFocus(e);
}
},
[disabled, onFocus],
);
const handleBlur = useCallback(
(e: React.FocusEvent<HTMLTextAreaElement>) => {
if (!disabled && onBlur) {
onBlur(e);
}
},
[disabled, onBlur],
);
// Form field handlers with disabled state handling
const { handleChange, handleFocus, handleBlur } = useFormField<
HTMLTextAreaElement
>(disabled, {
onChange,
onFocus,
onBlur,
});
return (
<div className={containerClasses}>
{label && (
<label
id={labelId}
htmlFor={textareaId}
className={`${labelClasses} font-inter font-medium text-[var(--color-content-default-secondary)]`}
>