Extract custom hooks for reusable logic
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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)]`}
|
||||
>
|
||||
|
||||
@@ -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,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(() => {
|
||||
|
||||
@@ -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
@@ -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)]`}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user