npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

nextjs-app-hooks

v1.8.0

Published

A library of custom React hooks for simplified state management and streamlined component logic.

Readme

NextJS App Hooks

A comprehensive library of React hooks for Next.js applications(app router compatible), designed to simplify state management and streamline component logic.

npm version npm downloads MIT License

Features

  • 📱 Full TypeScript support with comprehensive type definitions
  • 🔄 SSR compatible with proper hydration support for Next.js
  • 🎯 Focused utilities for common UI patterns and browser APIs
  • 🧩 Modular design allowing for tree-shaking to minimize bundle size
  • 📚 Comprehensive documentation with examples for each hook
  • Performance optimized implementations

Installation

# npm
npm install nextjs-app-hooks

# yarn
yarn add nextjs-app-hooks

# pnpm
pnpm add nextjs-app-hooks

Contributing

Contributions are welcome! Here's how you can help improve this library:

Setup

  1. Fork the repository
  2. Clone your fork: git clone https://github.com/your-username/nextjs-app-hooks.git
  3. Install dependencies: npm install
  4. Create a branch for your changes: git checkout -b feature/your-feature-name

Development

  • Run npm run dev to start the development build with watch mode
  • Follow the existing code style and TypeScript patterns
  • Add appropriate JSDoc comments and type definitions
  • Ensure your changes are properly typed and exports are added to index.ts

Testing

  • Write tests for your hooks in the __tests__ directory
  • Run tests with npm test or npm run test:watch
  • Ensure all tests pass before submitting a PR

Pull Requests

  1. Update documentation for any new hooks or changes to existing ones
  2. Make sure your code passes all tests and linting
  3. Submit a PR with a clear description of the changes and any relevant issue numbers
  4. Wait for review and address any feedback

Adding a New Hook

When adding a new hook, please ensure it:

  • Has a clear, focused purpose
  • Works with SSR and Next.js App Router
  • Includes comprehensive TypeScript definitions
  • Includes detailed JSDoc comments with examples
  • Is exported from the main index file
  • Follows the naming conventions of existing hooks

Thank you for helping improve nextjs-app-hooks!

Usage

All hooks are exported from the package root:

"use client";

import { useIsBrowser, useDarkMode, useLocalStorage } from "nextjs-app-hooks";

function MyComponent() {
  const isBrowser = useIsBrowser();
  const isDarkMode = useDarkMode();
  const { value, setValue } = useLocalStorage("user-settings", {
    defaultValue: { theme: "light" },
  });

  // Your component logic
}

Hooks Overview

Environment Detection

| Hook | Description | | ---------------- | ---------------------------------------------------------- | | useIsBrowser | Detect if code is running in browser or server environment | | useIsServer | Detect if code is running on the server | | useHasRendered | Track whether component has rendered at least once |

Browser API Access

| Hook | Description | | ---------------------- | --------------------------------------------- | | useBattery | Access and monitor device battery status | | useClipboard | Copy text to clipboard with status tracking | | useCookie | Manage browser cookies with advanced features | | useGeolocation | Access and track device geolocation | | useLocalStorage | Manage localStorage with SSR support | | useSessionStorage | Manage sessionStorage with SSR support | | useNetwork | Monitor network connection status | | usePermission | Check and request browser permissions | | usePreferredLanguage | Detect and manage user's preferred language |

UI and Interaction

| Hook | Description | | ------------------------- | ---------------------------------------------------- | | useClickOutside | Detect clicks outside of a specified element | | useDebounce | Create debounced functions and values | | useHover | Track hover state of an element | | useIdle | Track user idle/active state | | useIntersectionObserver | Track element visibility using Intersection Observer | | useLockBodyScroll | Lock body scrolling for modals and drawers | | useLongPress | Detect long press gestures | | useMediaQuery | Respond to media queries with extensive options | | useMouse | Track mouse position and state |

Media Hooks

| Hook | Description | | ------------------------- | ---------------------------------------- | | useDarkMode | Check if the screen is in dark mode | | usePrefersReducedMotion | Check if the user prefers reduced motion | | useOrientation | Check current screen orientation | | useResponsive | Convenience hook for responsive design |

Detailed API Documentation

Environment Detection

useIsBrowser

function useIsBrowser(): boolean;

Determines if the code is running in the browser or on the server.

Example:

"use client";

import { useIsBrowser } from "nextjs-app-hooks";

export default function MyComponent() {
  const isBrowser = useIsBrowser();

  useEffect(() => {
    if (isBrowser) {
      // Safe to use browser APIs like localStorage, window, etc.
      localStorage.setItem("visited", "true");
    }
  }, [isBrowser]);

  return (
    <div>
      {isBrowser ? "Browser APIs are available" : "Running on the server"}
    </div>
  );
}

useIsServer

function useIsServer(): boolean;

Similar to useIsBrowser but returns true when running on the server.

Example:

"use client";

import { useIsServer } from "nextjs-app-hooks";

export default function MyComponent() {
  const isServer = useIsServer();

  return <div>{isServer ? "Rendering on server" : "Rendering on client"}</div>;
}

useHasRendered

function useHasRendered(): boolean;

Returns false on first render and true afterward, allowing for logic that should only run after initial render.

Example:

"use client";

import { useHasRendered } from "nextjs-app-hooks";

export default function MyComponent() {
  const hasRendered = useHasRendered();

  return (
    <div>{!hasRendered ? "First render" : "Component has rendered before"}</div>
  );
}

Browser API Hooks

useBattery

function useBattery(): BatteryHookState;

Access and monitor the device's battery status.

Example:

"use client";

import { useBattery, formatBatteryTime } from "nextjs-app-hooks";

export default function BatteryStatus() {
  const { battery, isSupported, isLoading, error } = useBattery();

  if (!isSupported) return <p>Battery API is not supported on this device</p>;
  if (isLoading) return <p>Loading battery information...</p>;
  if (error) return <p>Error: {error.message}</p>;
  if (!battery) return <p>No battery information available</p>;

  return (
    <div>
      <h2>Battery Status</h2>
      <p>Level: {(battery.level * 100).toFixed(0)}%</p>
      <p>Charging: {battery.charging ? "Yes" : "No"}</p>
      {battery.charging ? (
        <p>Full in: {formatBatteryTime(battery.chargingTime)}</p>
      ) : (
        <p>Time remaining: {formatBatteryTime(battery.dischargingTime)}</p>
      )}
    </div>
  );
}

useClipboard

function useClipboard(options?: UseClipboardOptions): UseClipboardReturn;

Copy text to clipboard with status tracking.

Example:

"use client";

import { useClipboard } from "nextjs-app-hooks";

export default function CopyButton() {
  const { copyToClipboard, isSuccess, status } = useClipboard();

  return (
    <button
      onClick={() => copyToClipboard("Text to copy")}
      className={isSuccess ? "bg-green-500" : "bg-blue-500"}
    >
      {status === "idle" && "Copy to clipboard"}
      {status === "copied" && "Copied!"}
      {status === "error" && "Failed to copy"}
    </button>
  );
}

useCookie

function useCookie<T = string>(
  key: string,
  options?: UseCookieOptions<T>
): UseCookieReturn<T>;

Manage browser cookies with advanced features.

Example:

"use client";

import { useCookie } from "nextjs-app-hooks";

export default function CookieConsent() {
  const {
    value: consent,
    setCookie,
    removeCookie,
    hasValue,
  } = useCookie("cookie-consent");

  return (
    <div>
      {!hasValue && (
        <div className="cookie-banner">
          <p>We use cookies to enhance your experience.</p>
          <div>
            <button
              onClick={() =>
                setCookie("accepted", { maxAge: 60 * 60 * 24 * 365 })
              }
            >
              Accept
            </button>
            <button onClick={() => setCookie("declined")}>Decline</button>
          </div>
        </div>
      )}

      {hasValue && (
        <button onClick={removeCookie}>Reset Cookie Preferences</button>
      )}
    </div>
  );
}

useGeolocation

function useGeolocation(options?: UseGeolocationOptions): UseGeolocationReturn;

Access and track the device's geolocation.

Example:

"use client";

import { useGeolocation } from "nextjs-app-hooks";

export default function LocationComponent() {
  const { position, error, isLoading, getPosition, isSupported } =
    useGeolocation({ enableHighAccuracy: true });

  if (!isSupported) {
    return <p>Geolocation is not supported in your browser.</p>;
  }

  return (
    <div>
      <button onClick={getPosition} disabled={isLoading}>
        {isLoading ? "Getting location..." : "Get My Location"}
      </button>

      {position && (
        <div>
          <p>Latitude: {position.latitude}</p>
          <p>Longitude: {position.longitude}</p>
          <p>Accuracy: {position.accuracy} meters</p>
        </div>
      )}

      {error && <p>Error: {error.message}</p>}
    </div>
  );
}

useLocalStorage

function useLocalStorage<T>(
  key: string,
  options?: UseLocalStorageOptions<T>
): UseLocalStorageReturn<T>;

Manage localStorage with SSR support.

Example:

"use client";

import { useLocalStorage } from "nextjs-app-hooks";

export default function UserPreferences() {
  const { value: theme, setValue: setTheme } = useLocalStorage("theme", {
    defaultValue: "light",
  });

  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        Toggle theme
      </button>
    </div>
  );
}

useSessionStorage

function useSessionStorage<T>(
  key: string,
  options?: UseSessionStorageOptions<T>
): UseSessionStorageReturn<T>;

Manage sessionStorage with SSR support.

Example:

"use client";

import { useSessionStorage } from "nextjs-app-hooks";

export default function FormWithSessionPersistence() {
  const {
    value: formData,
    setValue: setFormData,
    removeValue: clearForm,
  } = useSessionStorage("form_data", {
    defaultValue: { name: "", email: "", message: "" },
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData((prev) => ({ ...prev, [name]: value }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    // Submit form data
    console.log("Submitting:", formData);
    clearForm();
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Name"
      />
      <input
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <textarea
        name="message"
        value={formData.message}
        onChange={handleChange}
        placeholder="Message"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

useNetwork

function useNetwork(options?: UseNetworkOptions): UseNetworkReturn;

Monitor network connection status.

Example:

"use client";

import { useNetwork } from "nextjs-app-hooks";

export default function NetworkStatus() {
  const network = useNetwork({
    autoReconnect: true,
    onOffline: () => console.log("Connection lost"),
    onOnline: () => console.log("Connection restored"),
  });

  return (
    <div className={network.online ? "online" : "offline"}>
      <h1>Network Status</h1>
      <p>Status: {network.online ? "Online" : "Offline"}</p>
      <p>Connection: {network.connection.type}</p>
      <p>Speed: {network.connection.effectiveType}</p>

      {!network.online && (
        <button onClick={network.reconnect} disabled={network.reconnecting}>
          {network.reconnecting ? "Reconnecting..." : "Reconnect Manually"}
        </button>
      )}
    </div>
  );
}

usePermission

function usePermission(
  name: PermissionName,
  options?: UsePermissionOptions
): UsePermissionReturn;

Check and request browser permissions.

Example:

"use client";

import { usePermission } from "nextjs-app-hooks";

export default function CameraComponent() {
  const { state, isGranted, request, error } = usePermission("camera");

  return (
    <div>
      <p>Camera permission: {state}</p>

      {!isGranted && (
        <button onClick={request} disabled={state === "denied"}>
          {state === "denied"
            ? "Permission denied (check browser settings)"
            : "Request camera access"}
        </button>
      )}

      {isGranted && <div className="camera-view">Camera is available</div>}

      {error && <p className="error">Error: {error.message}</p>}
    </div>
  );
}

usePreferredLanguage

function usePreferredLanguage(
  options?: UsePreferredLanguageOptions
): UsePreferredLanguageReturn;

Detect and manage user's preferred language.

Example:

"use client";

import { usePreferredLanguage } from "nextjs-app-hooks";

export default function LanguageSwitcher() {
  const { language, setLanguage, resetToSystemDefault } = usePreferredLanguage({
    supportedLanguages: ["en", "fr", "es", "de"],
    onChange: (lang) => console.log(`Language changed to ${lang}`),
  });

  const languages = [
    { code: "en", name: "English" },
    { code: "fr", name: "Français" },
    { code: "es", name: "Español" },
    { code: "de", name: "Deutsch" },
  ];

  return (
    <div className="language-switcher">
      <select value={language} onChange={(e) => setLanguage(e.target.value)}>
        {languages.map((lang) => (
          <option key={lang.code} value={lang.code}>
            {lang.name}
          </option>
        ))}
      </select>
      <button onClick={resetToSystemDefault}>Reset to System Default</button>
    </div>
  );
}

UI and Interaction Hooks

useClickOutside

function useClickOutside<T extends HTMLElement = HTMLElement>(
  onClickOutside: (event: MouseEvent | TouchEvent) => void,
  options?: UseClickOutsideOptions
): RefObject<T | null>;

Detect clicks outside of a specified element.

Example:

"use client";

import { useClickOutside } from "nextjs-app-hooks";

export default function Modal({ isOpen, onClose, children }) {
  const modalRef = useClickOutside(() => {
    if (isOpen) onClose();
  });

  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div ref={modalRef} className="modal-content">
        {children}
        <button onClick={onClose}>Close</button>
      </div>
    </div>
  );
}

useDebounce

function useDebounce<T extends (...args: any[]) => any>(
  fn: T,
  options?: UseDebounceOptions
): [
  (...args: Parameters<T>) => void,
  () => void,
  boolean,
  () => ReturnType<T> | undefined
];

function useDebounceValue<T>(
  value: T,
  delay?: number,
  options?: Omit<UseDebounceOptions, "delay">
): T;

Create debounced functions and values.

Example:

"use client";

import { useDebounce, useDebounceValue } from "nextjs-app-hooks";

export default function SearchInput() {
  const [searchTerm, setSearchTerm] = useState("");
  const debouncedSearchTerm = useDebounceValue(searchTerm, 300);

  // Use debounced value for API calls
  useEffect(() => {
    if (debouncedSearchTerm) {
      searchAPI(debouncedSearchTerm);
    }
  }, [debouncedSearchTerm]);

  // Using debounced function
  const searchAPI = async (term) => {
    // API call logic
    console.log("Searching for:", term);
  };

  const [debouncedSearch, cancelSearch, isPending] = useDebounce(searchAPI, {
    delay: 300,
    trackPending: true,
  });

  const handleInputChange = (e) => {
    const value = e.target.value;
    setSearchTerm(value);
    debouncedSearch(value);
  };

  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={handleInputChange}
        placeholder="Search..."
      />
      {isPending && <span>Searching...</span>}
    </div>
  );
}

useHover

function useHover<T extends HTMLElement = HTMLDivElement>(
  options?: UseHoverOptions
): UseHoverReturn<T>;

Track hover state of an element.

Example:

"use client";

import { useHover } from "nextjs-app-hooks";

export default function HoverCard() {
  const { hoverRef, isHovered } =
    useHover <
    HTMLDivElement >
    {
      enterDelay: 100,
      leaveDelay: 300,
    };

  return (
    <div ref={hoverRef} className={`card ${isHovered ? "card-hovered" : ""}`}>
      Hover over me!
      {isHovered && <div className="tooltip">Hello there!</div>}
    </div>
  );
}

useIdle

function useIdle(options?: UseIdleOptions): UseIdleReturn;

Track user idle/active state.

Example:

"use client";

import { useIdle } from "nextjs-app-hooks";

export default function IdleDetectionExample() {
  const { isIdle, reset, idleTime, remainingTime } = useIdle({
    idleTime: 60000, // 1 minute
    onIdle: () => console.log("User is idle"),
    onActive: () => console.log("User is active"),
  });

  return (
    <div>
      <p>User is currently {isIdle ? "idle" : "active"}</p>
      <p>Time since last activity: {Math.floor(idleTime / 1000)}s</p>
      <p>Time until idle: {Math.ceil(remainingTime / 1000)}s</p>

      {isIdle && <button onClick={reset}>I'm still here!</button>}
    </div>
  );
}

useIntersectionObserver

function useIntersectionObserver<T extends Element = HTMLDivElement>(
  options?: UseIntersectionObserverOptions
): UseIntersectionObserverReturn<T>;

// Simplified version
function useInView<T extends Element = HTMLDivElement>(
  options?: UseInViewOptions
): UseInViewReturn<T>;

Track element visibility using the Intersection Observer API.

Example:

"use client";

import { useInView } from "nextjs-app-hooks";

export default function LazyImage() {
  const { ref, inView } = useInView({
    threshold: 0.1,
    triggerOnce: true,
  });

  return (
    <div ref={ref} className="image-container">
      {inView ? (
        <img src="https://example.com/image.jpg" alt="Lazy loaded" />
      ) : (
        <div className="placeholder" />
      )}
    </div>
  );
}

useLockBodyScroll

function useLockBodyScroll(
  initialLocked?: boolean,
  options?: UseLockBodyScrollOptions
): UseLockBodyScrollReturn;

Lock body scrolling for modals, drawers, etc.

Example:

"use client";

import { useLockBodyScroll } from "nextjs-app-hooks";

export default function Modal({ isOpen, onClose, children }) {
  // Lock scrolling when modal is open
  useLockBodyScroll(isOpen);

  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div className="modal-content">
        {children}
        <button onClick={onClose}>Close</button>
      </div>
    </div>
  );
}

useLongPress

function useLongPress(
  callback?: ((e: React.MouseEvent | React.TouchEvent) => void) | null,
  options?: UseLongPressOptions
): UseLongPressReturn;

Detect long press gestures.

Example:

"use client";

import { useLongPress } from "nextjs-app-hooks";

export default function LongPressButton() {
  const onLongPress = () => alert("Long pressed!");

  const { handlers, isLongPressed } = useLongPress(onLongPress, {
    delay: 500,
    onPressStart: () => console.log("Press started"),
    onLongPressEnd: () => console.log("Long press ended"),
  });

  return (
    <button {...handlers} className={isLongPressed ? "active" : ""}>
      Press and hold me
    </button>
  );
}

useMediaQuery

function useMediaQuery(
  features: MediaQueryFeatures | string,
  options?: UseMediaQueryOptions
): UseMediaQueryReturn;

Respond to media queries with extensive options.

Example:

"use client";

import { useMediaQuery, useDarkMode } from "nextjs-app-hooks";

export default function ResponsiveComponent() {
  const isMobile = useMediaQuery({ maxWidth: "sm" });
  const isTablet = useMediaQuery({ minWidth: "md", maxWidth: "lg" });
  const isDesktop = useMediaQuery({ minWidth: "xl" });

  // Detect dark mode
  const isDarkMode = useDarkMode();

  return (
    <div className={isDarkMode ? "dark-theme" : "light-theme"}>
      {isMobile.matches && <MobileView />}
      {isTablet.matches && <TabletView />}
      {isDesktop.matches && <DesktopView />}
    </div>
  );
}

useMouse

function useMouse(options?: UseMouseOptions): UseMouseReturn;

Track mouse position and state.

Example:

"use client";

import { useMouse } from "nextjs-app-hooks";

export default function MouseTracker() {
  const mouse = useMouse();

  return (
    <div style={{ height: "300px", border: "1px solid black" }}>
      <p>
        Mouse position: {mouse.position.x}, {mouse.position.y}
      </p>
      <p>Left button: {mouse.buttons.left ? "Pressed" : "Released"}</p>
      <p>Inside element: {mouse.isInside ? "Yes" : "No"}</p>
    </div>
  );
}

Advanced Examples

Theme Switcher with Persistence

"use client";

import { useState, useEffect } from "react";
import { useDarkMode, useLocalStorage, useMediaQuery } from "nextjs-app-hooks";

export default function ThemeSwitcher() {
  // Get OS theme preference
  const systemPrefersDark = useDarkMode();

  // Load theme from localStorage
  const { value: savedTheme, setValue: saveTheme } = useLocalStorage("theme", {
    defaultValue: "system",
  });

  // Derived theme state
  const [theme, setTheme] = useState(savedTheme);
  const [isDark, setIsDark] = useState(false);

  // Update the active theme based on system preference and user selection
  useEffect(() => {
    const isDarkTheme =
      theme === "dark" || (theme === "system" && systemPrefersDark);

    setIsDark(isDarkTheme);

    // Update document class for CSS theme switching
    if (isDarkTheme) {
      document.documentElement.classList.add("dark");
    } else {
      document.documentElement.classList.remove("dark");
    }
  }, [theme, systemPrefersDark]);

  // Handle theme changes
  const handleThemeChange = (newTheme) => {
    setTheme(newTheme);
    saveTheme(newTheme);
  };

  return (
    <div className="theme-switcher">
      <h2>Theme Settings</h2>

      <div className="theme-options">
        <button
          onClick={() => handleThemeChange("light")}
          className={theme === "light" ? "active" : ""}
        >
          Light
        </button>

        <button
          onClick={() => handleThemeChange("dark")}
          className={theme === "dark" ? "active" : ""}
        >
          Dark
        </button>

        <button
          onClick={() => handleThemeChange("system")}
          className={theme === "system" ? "active" : ""}
        >
          System ({systemPrefersDark ? "Dark" : "Light"})
        </button>
      </div>

      <div className="current-theme">
        Currently using: <strong>{isDark ? "Dark" : "Light"}</strong> theme
      </div>
    </div>
  );
}

Form with Session Persistence and Validation

"use client";

import { useState } from "react";
import { useSessionStorage, useNetwork } from "nextjs-app-hooks";

export default function PersistentForm() {
  const {
    value: formData,
    setValue: setFormData,
    removeValue: clearForm,
  } = useSessionStorage("contact_form", {
    defaultValue: {
      name: "",
      email: "",
      message: "",
    },
  });

  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [isSuccess, setIsSuccess] = useState(false);

  const network = useNetwork();

  const validateForm = () => {
    const newErrors = {};

    if (!formData.name.trim()) {
      newErrors.name = "Name is required";
    }

    if (!formData.email.trim()) {
      newErrors.email = "Email is required";
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = "Email is invalid";
    }

    if (!formData.message.trim()) {
      newErrors.message = "Message is required";
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData((prev) => ({ ...prev, [name]: value }));

    // Clear error when user starts typing
    if (errors[name]) {
      setErrors((prev) => ({ ...prev, [name]: "" }));
    }
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    if (!validateForm()) {
      return;
    }

    if (!network.online) {
      alert(
        "You are offline. Please try again when you have internet connection."
      );
      return;
    }

    setIsSubmitting(true);

    try {
      // Simulate API call
      await new Promise((resolve) => setTimeout(resolve, 1500));

      // Success
      setIsSuccess(true);
      clearForm();
    } catch (error) {
      console.error("Error submitting form:", error);
      alert("Failed to submit the form. Please try again.");
    } finally {
      setIsSubmitting(false);
    }
  };

  if (isSuccess) {
    return (
      <div className="success-message">
        <h2>Thank you for your message!</h2>
        <p>We'll get back to you soon.</p>
        <button onClick={() => setIsSuccess(false)}>
          Send another message
        </button>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit} className="contact-form">
      <div className="form-group">
        <label htmlFor="name">Name</label>
        <input
          id="name"
          name="name"
          type="text"
          value={formData.name}
          onChange={handleChange}
          className={errors.name ? "error" : ""}
        />
        {errors.name && <div className="error-message">{errors.name}</div>}
      </div>

      <div className="form-group">
        <label htmlFor="email">Email</label>
        <input
          id="email"
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
          className={errors.email ? "error" : ""}
        />
        {errors.email && <div className="error-message">{errors.email}</div>}
      </div>

      <div className="form-group">
        <label htmlFor="message">Message</label>
        <textarea
          id="message"
          name="message"
          value={formData.message}
          onChange={handleChange}
          className={errors.message ? "error" : ""}
          rows={4}
        />
        {errors.message && (
          <div className="error-message">{errors.message}</div>
        )}
      </div>

      <div className="form-actions">
        <button type="submit" className="submit-button" disabled={isSubmitting}>
          {isSubmitting ? "Sending..." : "Send Message"}
        </button>

        <button
          type="button"
          className="clear-button"
          onClick={clearForm}
          disabled={isSubmitting}
        >
          Clear Form
        </button>
      </div>

      {!network.online && (
        <div className="network-warning">
          You appear to be offline. The form will be saved but cannot be
          submitted until you're back online.
        </div>
      )}
    </form>
  );
}