diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx index ece8c43..a5571c1 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[slug]/page.tsx @@ -1,15 +1,26 @@ import { notFound } from "next/navigation"; import type { Metadata } from "next"; +import dynamic from "next/dynamic"; import { getBlogPostBySlug, getAllBlogPosts as getAllPosts, type BlogPost, } from "../../../lib/content"; import ContentBanner from "../../components/ContentBanner"; -import RelatedArticles from "../../components/RelatedArticles"; import AskOrganizer from "../../components/AskOrganizer"; import { getAssetPath, ASSETS } from "../../../lib/assetUtils"; +// Code split RelatedArticles - blog-specific, below the fold +const RelatedArticles = dynamic( + () => import("../../components/RelatedArticles"), + { + loading: () => ( +
+ ), + ssr: true, + }, +); + // AskOrganizer data - same as index page const askOrganizerData = { title: "Still have questions?", 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 && (