@opensite/hooks
Advanced tools
| import { useCallback, useEffect, useMemo, useState } from "react"; | ||
| // --------------------------------------------------------------------------- | ||
| // Detection internals | ||
| // --------------------------------------------------------------------------- | ||
| /** | ||
| * CSS media query targeting devices whose *primary* pointing device is coarse | ||
| * and lacks hover — the W3C Interaction Media Features (Level 4) definition of | ||
| * a touch-first device. | ||
| * | ||
| * Evaluated via `matchMedia` and subscribed to with a `change` listener so | ||
| * convertible laptops, detachable tablets, and foldables react to input-mode | ||
| * switches in real time. | ||
| * | ||
| * @see https://www.w3.org/TR/mediaqueries-4/#mf-interaction | ||
| */ | ||
| const TOUCH_MEDIA_QUERY = "(hover: none) and (pointer: coarse)"; | ||
| /** | ||
| * Synchronously evaluate touch capability using a two-tier strategy. | ||
| * | ||
| * **Tier 1 — Interaction Media Features** (`pointer: coarse` + `hover: none`). | ||
| * Most accurate on modern browsers (Chrome 41+, Firefox 64+, Safari 9+) and | ||
| * correctly classifies hybrid devices by inspecting only the *primary* input. | ||
| * | ||
| * **Tier 2 — Legacy touch-API probe** (`ontouchstart` on window, | ||
| * `navigator.maxTouchPoints`). Covers older Android WebView and pre-Chromium | ||
| * Edge. This probe can produce false positives on some desktop touchscreens, | ||
| * but it is only reached when Tier 1 is inconclusive. | ||
| * | ||
| * The function must only be called in a browser context — callers are | ||
| * responsible for guarding with `typeof window !== "undefined"`. | ||
| */ | ||
| function detectTouchCapability() { | ||
| // Tier 1: Interaction Media Features (wide support since ~2018). | ||
| if (typeof window.matchMedia === "function") { | ||
| const mql = window.matchMedia(TOUCH_MEDIA_QUERY); | ||
| // `mql.media` is set to `"not all"` when the browser does not understand | ||
| // the query at all — in that case we fall through to the legacy probe. | ||
| if (mql.media !== "not all") { | ||
| return mql.matches ? "touch" : "desktop"; | ||
| } | ||
| } | ||
| // Tier 2: legacy touch-API probe. | ||
| if ("ontouchstart" in window || | ||
| (typeof navigator !== "undefined" && | ||
| typeof navigator.maxTouchPoints === "number" && | ||
| navigator.maxTouchPoints > 0)) { | ||
| return "touch"; | ||
| } | ||
| return "desktop"; | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Hook | ||
| // --------------------------------------------------------------------------- | ||
| /** | ||
| * Detect whether the device's primary input mechanism is touch-based. | ||
| * | ||
| * Uses a layered detection strategy combining CSS Interaction Media Features | ||
| * (`pointer: coarse`, `hover: none`) with legacy touch-API probing | ||
| * (`ontouchstart`, `maxTouchPoints`) for maximum browser coverage. | ||
| * | ||
| * ### SSR safety | ||
| * | ||
| * The hook returns `"unknown"` (or a caller-supplied `defaultDeviceType`) on | ||
| * the server **and** during the first client render, so the server and client | ||
| * always produce identical markup — no hydration mismatch. After the commit | ||
| * phase, a `useEffect` evaluates synchronously and subscribes to `matchMedia` | ||
| * change events. | ||
| * | ||
| * ### Dynamic updates | ||
| * | ||
| * On browsers that support Interaction Media Features, the hook listens for | ||
| * `change` events on the `(hover: none) and (pointer: coarse)` query. This | ||
| * means convertible laptops, detachable tablets, and foldables will | ||
| * automatically re-classify when the user switches input modes — no polling | ||
| * required. | ||
| * | ||
| * ### Performance | ||
| * | ||
| * - Detection runs once on mount (one `matchMedia` call) — no per-render work. | ||
| * - The `change` listener is passive and only fires on actual input-mode | ||
| * transitions (rare). | ||
| * - The returned object is memoized; `deviceType` must change for the | ||
| * reference to update. | ||
| * - The `recheck` callback is `useCallback`-stable for the hook's lifetime. | ||
| * | ||
| * @param options - Optional configuration. See {@link UseIsTouchDeviceOptions}. | ||
| * @returns A memoized {@link UseIsTouchDeviceResult} object. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * import { useIsTouchDevice } from "@opensite/hooks/useIsTouchDevice"; | ||
| * | ||
| * function Toolbar() { | ||
| * const { isTouchDevice } = useIsTouchDevice(); | ||
| * return isTouchDevice ? <TouchToolbar /> : <DesktopToolbar />; | ||
| * } | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // With a server-side hint to eliminate layout shift | ||
| * function Page({ ssrDeviceType }: { ssrDeviceType: DeviceType }) { | ||
| * const { deviceType } = useIsTouchDevice({ | ||
| * defaultDeviceType: ssrDeviceType, | ||
| * }); | ||
| * // ... | ||
| * } | ||
| * ``` | ||
| */ | ||
| export function useIsTouchDevice(options = {}) { | ||
| const { defaultDeviceType = "unknown" } = options; | ||
| // SSR-safe initial state — never accesses browser APIs at initialization. | ||
| const [deviceType, setDeviceType] = useState(defaultDeviceType); | ||
| // Stable imperative re-evaluation callback. Guarded so it is a safe no-op | ||
| // if accidentally called during SSR (e.g. in a shared util). | ||
| const recheck = useCallback(() => { | ||
| if (typeof window === "undefined") | ||
| return; | ||
| setDeviceType(detectTouchCapability()); | ||
| }, []); | ||
| useEffect(() => { | ||
| if (typeof window === "undefined") | ||
| return; | ||
| // Synchronous first evaluation — updates state in the same commit so | ||
| // consumers see the real value on the very first paint after hydration. | ||
| setDeviceType(detectTouchCapability()); | ||
| // Subscribe to media-query changes so hybrid/convertible devices update | ||
| // when the user switches between touch and pointer input modes. | ||
| if (typeof window.matchMedia !== "function") | ||
| return; | ||
| const mql = window.matchMedia(TOUCH_MEDIA_QUERY); | ||
| // If the browser doesn't understand the query there's nothing to listen | ||
| // for — the legacy probe result from above is our final answer. | ||
| if (mql.media === "not all") | ||
| return; | ||
| const onChange = (event) => { | ||
| setDeviceType(event.matches ? "touch" : "desktop"); | ||
| }; | ||
| mql.addEventListener("change", onChange); | ||
| return () => { | ||
| mql.removeEventListener("change", onChange); | ||
| }; | ||
| }, []); | ||
| // Memoize the return object so consumers that destructure or pass the whole | ||
| // result into dependency arrays don't trigger spurious re-renders. | ||
| return useMemo(() => ({ | ||
| deviceType, | ||
| isTouchDevice: deviceType === "touch", | ||
| recheck, | ||
| }), [deviceType, recheck]); | ||
| } |
| /** | ||
| * Granular classification of the detected primary input device. | ||
| * | ||
| * - `"unknown"` – returned during SSR and before the first client-side | ||
| * evaluation completes. Consumers should treat this as "not yet determined" | ||
| * and render a safe, non-committal default (e.g. show both touch and pointer | ||
| * affordances, or a loading skeleton). | ||
| * - `"touch"` – the primary input is a coarse pointer with no hover capability | ||
| * (phones, most tablets in touch mode). | ||
| * - `"desktop"` – the primary input is a fine pointer with hover support | ||
| * (mouse, trackpad, stylus with hover). | ||
| */ | ||
| export type DeviceType = "unknown" | "touch" | "desktop"; | ||
| /** | ||
| * Configuration options for {@link useIsTouchDevice}. | ||
| */ | ||
| export interface UseIsTouchDeviceOptions { | ||
| /** | ||
| * Value returned during SSR and before the first client-side detection. | ||
| * | ||
| * Defaults to `"unknown"`. Override with `"touch"` or `"desktop"` when you | ||
| * have server-side hints (e.g. User-Agent parsing via middleware) and want to | ||
| * avoid a visible layout shift after hydration. | ||
| */ | ||
| defaultDeviceType?: DeviceType; | ||
| } | ||
| /** | ||
| * Shape returned by {@link useIsTouchDevice}. | ||
| * | ||
| * The object reference is memoized with `useMemo` so it remains referentially | ||
| * stable across re-renders when `deviceType` has not changed — safe to include | ||
| * in downstream dependency arrays without triggering spurious effects. | ||
| */ | ||
| export interface UseIsTouchDeviceResult { | ||
| /** Granular device classification (`"unknown"` | `"touch"` | `"desktop"`). */ | ||
| deviceType: DeviceType; | ||
| /** Convenience boolean — `true` when `deviceType === "touch"`. */ | ||
| isTouchDevice: boolean; | ||
| /** | ||
| * Re-run touch detection imperatively. | ||
| * | ||
| * Useful after external events that the hook cannot observe automatically | ||
| * (e.g. a WebHID device connect, or a custom "mode switch" UI toggle). | ||
| * The callback reference is stable across the lifetime of the hook. | ||
| */ | ||
| recheck: () => void; | ||
| } | ||
| /** | ||
| * Detect whether the device's primary input mechanism is touch-based. | ||
| * | ||
| * Uses a layered detection strategy combining CSS Interaction Media Features | ||
| * (`pointer: coarse`, `hover: none`) with legacy touch-API probing | ||
| * (`ontouchstart`, `maxTouchPoints`) for maximum browser coverage. | ||
| * | ||
| * ### SSR safety | ||
| * | ||
| * The hook returns `"unknown"` (or a caller-supplied `defaultDeviceType`) on | ||
| * the server **and** during the first client render, so the server and client | ||
| * always produce identical markup — no hydration mismatch. After the commit | ||
| * phase, a `useEffect` evaluates synchronously and subscribes to `matchMedia` | ||
| * change events. | ||
| * | ||
| * ### Dynamic updates | ||
| * | ||
| * On browsers that support Interaction Media Features, the hook listens for | ||
| * `change` events on the `(hover: none) and (pointer: coarse)` query. This | ||
| * means convertible laptops, detachable tablets, and foldables will | ||
| * automatically re-classify when the user switches input modes — no polling | ||
| * required. | ||
| * | ||
| * ### Performance | ||
| * | ||
| * - Detection runs once on mount (one `matchMedia` call) — no per-render work. | ||
| * - The `change` listener is passive and only fires on actual input-mode | ||
| * transitions (rare). | ||
| * - The returned object is memoized; `deviceType` must change for the | ||
| * reference to update. | ||
| * - The `recheck` callback is `useCallback`-stable for the hook's lifetime. | ||
| * | ||
| * @param options - Optional configuration. See {@link UseIsTouchDeviceOptions}. | ||
| * @returns A memoized {@link UseIsTouchDeviceResult} object. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * import { useIsTouchDevice } from "@opensite/hooks/useIsTouchDevice"; | ||
| * | ||
| * function Toolbar() { | ||
| * const { isTouchDevice } = useIsTouchDevice(); | ||
| * return isTouchDevice ? <TouchToolbar /> : <DesktopToolbar />; | ||
| * } | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // With a server-side hint to eliminate layout shift | ||
| * function Page({ ssrDeviceType }: { ssrDeviceType: DeviceType }) { | ||
| * const { deviceType } = useIsTouchDevice({ | ||
| * defaultDeviceType: ssrDeviceType, | ||
| * }); | ||
| * // ... | ||
| * } | ||
| * ``` | ||
| */ | ||
| export declare function useIsTouchDevice(options?: UseIsTouchDeviceOptions): UseIsTouchDeviceResult; |
| import { useCallback, useEffect, useMemo, useState } from "react"; | ||
| // --------------------------------------------------------------------------- | ||
| // Detection internals | ||
| // --------------------------------------------------------------------------- | ||
| /** | ||
| * CSS media query targeting devices whose *primary* pointing device is coarse | ||
| * and lacks hover — the W3C Interaction Media Features (Level 4) definition of | ||
| * a touch-first device. | ||
| * | ||
| * Evaluated via `matchMedia` and subscribed to with a `change` listener so | ||
| * convertible laptops, detachable tablets, and foldables react to input-mode | ||
| * switches in real time. | ||
| * | ||
| * @see https://www.w3.org/TR/mediaqueries-4/#mf-interaction | ||
| */ | ||
| const TOUCH_MEDIA_QUERY = "(hover: none) and (pointer: coarse)"; | ||
| /** | ||
| * Synchronously evaluate touch capability using a two-tier strategy. | ||
| * | ||
| * **Tier 1 — Interaction Media Features** (`pointer: coarse` + `hover: none`). | ||
| * Most accurate on modern browsers (Chrome 41+, Firefox 64+, Safari 9+) and | ||
| * correctly classifies hybrid devices by inspecting only the *primary* input. | ||
| * | ||
| * **Tier 2 — Legacy touch-API probe** (`ontouchstart` on window, | ||
| * `navigator.maxTouchPoints`). Covers older Android WebView and pre-Chromium | ||
| * Edge. This probe can produce false positives on some desktop touchscreens, | ||
| * but it is only reached when Tier 1 is inconclusive. | ||
| * | ||
| * The function must only be called in a browser context — callers are | ||
| * responsible for guarding with `typeof window !== "undefined"`. | ||
| */ | ||
| function detectTouchCapability() { | ||
| // Tier 1: Interaction Media Features (wide support since ~2018). | ||
| if (typeof window.matchMedia === "function") { | ||
| const mql = window.matchMedia(TOUCH_MEDIA_QUERY); | ||
| // `mql.media` is set to `"not all"` when the browser does not understand | ||
| // the query at all — in that case we fall through to the legacy probe. | ||
| if (mql.media !== "not all") { | ||
| return mql.matches ? "touch" : "desktop"; | ||
| } | ||
| } | ||
| // Tier 2: legacy touch-API probe. | ||
| if ("ontouchstart" in window || | ||
| (typeof navigator !== "undefined" && | ||
| typeof navigator.maxTouchPoints === "number" && | ||
| navigator.maxTouchPoints > 0)) { | ||
| return "touch"; | ||
| } | ||
| return "desktop"; | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Hook | ||
| // --------------------------------------------------------------------------- | ||
| /** | ||
| * Detect whether the device's primary input mechanism is touch-based. | ||
| * | ||
| * Uses a layered detection strategy combining CSS Interaction Media Features | ||
| * (`pointer: coarse`, `hover: none`) with legacy touch-API probing | ||
| * (`ontouchstart`, `maxTouchPoints`) for maximum browser coverage. | ||
| * | ||
| * ### SSR safety | ||
| * | ||
| * The hook returns `"unknown"` (or a caller-supplied `defaultDeviceType`) on | ||
| * the server **and** during the first client render, so the server and client | ||
| * always produce identical markup — no hydration mismatch. After the commit | ||
| * phase, a `useEffect` evaluates synchronously and subscribes to `matchMedia` | ||
| * change events. | ||
| * | ||
| * ### Dynamic updates | ||
| * | ||
| * On browsers that support Interaction Media Features, the hook listens for | ||
| * `change` events on the `(hover: none) and (pointer: coarse)` query. This | ||
| * means convertible laptops, detachable tablets, and foldables will | ||
| * automatically re-classify when the user switches input modes — no polling | ||
| * required. | ||
| * | ||
| * ### Performance | ||
| * | ||
| * - Detection runs once on mount (one `matchMedia` call) — no per-render work. | ||
| * - The `change` listener is passive and only fires on actual input-mode | ||
| * transitions (rare). | ||
| * - The returned object is memoized; `deviceType` must change for the | ||
| * reference to update. | ||
| * - The `recheck` callback is `useCallback`-stable for the hook's lifetime. | ||
| * | ||
| * @param options - Optional configuration. See {@link UseIsTouchDeviceOptions}. | ||
| * @returns A memoized {@link UseIsTouchDeviceResult} object. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * import { useIsTouchDevice } from "@opensite/hooks/useIsTouchDevice"; | ||
| * | ||
| * function Toolbar() { | ||
| * const { isTouchDevice } = useIsTouchDevice(); | ||
| * return isTouchDevice ? <TouchToolbar /> : <DesktopToolbar />; | ||
| * } | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // With a server-side hint to eliminate layout shift | ||
| * function Page({ ssrDeviceType }: { ssrDeviceType: DeviceType }) { | ||
| * const { deviceType } = useIsTouchDevice({ | ||
| * defaultDeviceType: ssrDeviceType, | ||
| * }); | ||
| * // ... | ||
| * } | ||
| * ``` | ||
| */ | ||
| export function useIsTouchDevice(options = {}) { | ||
| const { defaultDeviceType = "unknown" } = options; | ||
| // SSR-safe initial state — never accesses browser APIs at initialization. | ||
| const [deviceType, setDeviceType] = useState(defaultDeviceType); | ||
| // Stable imperative re-evaluation callback. Guarded so it is a safe no-op | ||
| // if accidentally called during SSR (e.g. in a shared util). | ||
| const recheck = useCallback(() => { | ||
| if (typeof window === "undefined") | ||
| return; | ||
| setDeviceType(detectTouchCapability()); | ||
| }, []); | ||
| useEffect(() => { | ||
| if (typeof window === "undefined") | ||
| return; | ||
| // Synchronous first evaluation — updates state in the same commit so | ||
| // consumers see the real value on the very first paint after hydration. | ||
| setDeviceType(detectTouchCapability()); | ||
| // Subscribe to media-query changes so hybrid/convertible devices update | ||
| // when the user switches between touch and pointer input modes. | ||
| if (typeof window.matchMedia !== "function") | ||
| return; | ||
| const mql = window.matchMedia(TOUCH_MEDIA_QUERY); | ||
| // If the browser doesn't understand the query there's nothing to listen | ||
| // for — the legacy probe result from above is our final answer. | ||
| if (mql.media === "not all") | ||
| return; | ||
| const onChange = (event) => { | ||
| setDeviceType(event.matches ? "touch" : "desktop"); | ||
| }; | ||
| mql.addEventListener("change", onChange); | ||
| return () => { | ||
| mql.removeEventListener("change", onChange); | ||
| }; | ||
| }, []); | ||
| // Memoize the return object so consumers that destructure or pass the whole | ||
| // result into dependency arrays don't trigger spurious re-renders. | ||
| return useMemo(() => ({ | ||
| deviceType, | ||
| isTouchDevice: deviceType === "touch", | ||
| recheck, | ||
| }), [deviceType, recheck]); | ||
| } |
| import { useCallback, useEffect, useMemo, useState } from "react"; | ||
| import { useMediaQuery } from "./useMediaQuery.js"; | ||
| // --------------------------------------------------------------------------- | ||
| // Constants | ||
| // --------------------------------------------------------------------------- | ||
| /** | ||
| * Tailwind CSS v4 default breakpoints (min-width values in pixels). | ||
| * @see https://tailwindcss.com/docs/responsive-design | ||
| */ | ||
| const DEFAULT_BREAKPOINTS = { | ||
| sm: 640, | ||
| md: 768, | ||
| lg: 1024, | ||
| xl: 1280, | ||
| "2xl": 1536, | ||
| }; | ||
| /** | ||
| * Default mapping of Tailwind breakpoints to semantic screen types. | ||
| * | ||
| * - default, sm → MOBILE | ||
| * - md → TABLET | ||
| * - lg, xl, 2xl → DESKTOP | ||
| */ | ||
| const DEFAULT_SCREEN_TYPE_MAPPING = { | ||
| default: "MOBILE", | ||
| sm: "MOBILE", | ||
| md: "TABLET", | ||
| lg: "DESKTOP", | ||
| xl: "DESKTOP", | ||
| "2xl": "DESKTOP", | ||
| }; | ||
| // --------------------------------------------------------------------------- | ||
| // Helpers | ||
| // --------------------------------------------------------------------------- | ||
| /** | ||
| * Get current viewport dimensions. | ||
| * Returns `{ width: 0, height: 0 }` during SSR. | ||
| */ | ||
| function getViewportDimensions() { | ||
| if (typeof window === "undefined") { | ||
| return { width: 0, height: 0 }; | ||
| } | ||
| return { | ||
| width: window.innerWidth, | ||
| height: window.innerHeight, | ||
| }; | ||
| } | ||
| /** | ||
| * Determine the current Tailwind breakpoint based on viewport width. | ||
| * Uses mobile-first logic: returns the largest breakpoint that matches. | ||
| */ | ||
| function calculateTailwindSize(width, breakpoints) { | ||
| // Check breakpoints from largest to smallest (mobile-first) | ||
| if (width >= breakpoints["2xl"]) | ||
| return "2xl"; | ||
| if (width >= breakpoints.xl) | ||
| return "xl"; | ||
| if (width >= breakpoints.lg) | ||
| return "lg"; | ||
| if (width >= breakpoints.md) | ||
| return "md"; | ||
| if (width >= breakpoints.sm) | ||
| return "sm"; | ||
| return "default"; | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Hook | ||
| // --------------------------------------------------------------------------- | ||
| /** | ||
| * Track viewport dimensions and compute Tailwind breakpoint / screen type. | ||
| * | ||
| * Provides real-time viewport width and height, along with derived values for | ||
| * the current Tailwind CSS breakpoint (`tailwindSize`) and a semantic screen | ||
| * type classification (`screenType`) for layout decisions. | ||
| * | ||
| * ### SSR Safety | ||
| * | ||
| * The hook returns safe defaults during SSR (`width: 0`, `height: 0`, | ||
| * `tailwindSize: "default"`, `screenType: "UNKNOWN"`) to prevent hydration | ||
| * mismatches. After mount, values update to reflect the actual viewport. | ||
| * | ||
| * ### Performance | ||
| * | ||
| * - Uses `useMediaQuery` internally for efficient breakpoint detection via | ||
| * CSS media queries (no polling). | ||
| * - Viewport dimensions update on window `resize` events. | ||
| * - The returned object is memoized; values must change for the reference | ||
| * to update. | ||
| * | ||
| * @param options - Optional configuration. See {@link UseScreenOptions}. | ||
| * @returns A memoized {@link UseScreenResult} object. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * import { useScreen } from "@opensite/hooks/useScreen"; | ||
| * | ||
| * function ResponsiveLayout() { | ||
| * const { screenType, tailwindSize, width } = useScreen(); | ||
| * | ||
| * return ( | ||
| * <div> | ||
| * <p>Viewport: {width}px ({tailwindSize})</p> | ||
| * {screenType === "MOBILE" && <MobileNav />} | ||
| * {screenType === "TABLET" && <TabletNav />} | ||
| * {screenType === "DESKTOP" && <DesktopNav />} | ||
| * </div> | ||
| * ); | ||
| * } | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // With custom breakpoints | ||
| * const { screenType } = useScreen({ | ||
| * breakpoints: { sm: 600, md: 900, lg: 1200, xl: 1400, "2xl": 1800 }, | ||
| * }); | ||
| * ``` | ||
| */ | ||
| export function useScreen(options = {}) { | ||
| const { breakpoints: customBreakpoints, screenTypeMapping: customMapping, defaultScreenType = "UNKNOWN", defaultTailwindSize = "default", } = options; | ||
| // Merge custom config with defaults | ||
| const breakpoints = useMemo(() => ({ ...DEFAULT_BREAKPOINTS, ...customBreakpoints }), [customBreakpoints]); | ||
| const screenTypeMapping = useMemo(() => ({ ...DEFAULT_SCREEN_TYPE_MAPPING, ...customMapping }), [customMapping]); | ||
| // Track viewport dimensions | ||
| const [dimensions, setDimensions] = useState(() => getViewportDimensions()); | ||
| // Use media queries for breakpoint detection (more reliable than width alone | ||
| // for edge cases and provides instant updates via matchMedia) | ||
| const isSm = useMediaQuery(`(min-width: ${breakpoints.sm}px)`); | ||
| const isMd = useMediaQuery(`(min-width: ${breakpoints.md}px)`); | ||
| const isLg = useMediaQuery(`(min-width: ${breakpoints.lg}px)`); | ||
| const isXl = useMediaQuery(`(min-width: ${breakpoints.xl}px)`); | ||
| const is2xl = useMediaQuery(`(min-width: ${breakpoints["2xl"]}px)`); | ||
| // Stable refresh callback | ||
| const refresh = useCallback(() => { | ||
| if (typeof window === "undefined") | ||
| return; | ||
| setDimensions(getViewportDimensions()); | ||
| }, []); | ||
| // Subscribe to resize events for dimension tracking | ||
| useEffect(() => { | ||
| if (typeof window === "undefined") | ||
| return; | ||
| // Initial measurement | ||
| setDimensions(getViewportDimensions()); | ||
| const handleResize = () => { | ||
| setDimensions(getViewportDimensions()); | ||
| }; | ||
| window.addEventListener("resize", handleResize); | ||
| return () => window.removeEventListener("resize", handleResize); | ||
| }, []); | ||
| // Compute tailwindSize from media query results (most reliable) | ||
| const tailwindSize = useMemo(() => { | ||
| // During SSR, all media queries return false | ||
| if (!isSm && !isMd && !isLg && !isXl && !is2xl) { | ||
| // Check if we're on client with actual dimensions | ||
| if (dimensions.width > 0) { | ||
| return calculateTailwindSize(dimensions.width, breakpoints); | ||
| } | ||
| return defaultTailwindSize; | ||
| } | ||
| // Determine from media queries (largest matching breakpoint wins) | ||
| if (is2xl) | ||
| return "2xl"; | ||
| if (isXl) | ||
| return "xl"; | ||
| if (isLg) | ||
| return "lg"; | ||
| if (isMd) | ||
| return "md"; | ||
| if (isSm) | ||
| return "sm"; | ||
| return "default"; | ||
| }, [ | ||
| isSm, | ||
| isMd, | ||
| isLg, | ||
| isXl, | ||
| is2xl, | ||
| dimensions.width, | ||
| breakpoints, | ||
| defaultTailwindSize, | ||
| ]); | ||
| // Derive screen type from tailwind size | ||
| const screenType = useMemo(() => { | ||
| // During SSR before detection | ||
| if (tailwindSize === defaultTailwindSize && dimensions.width === 0) { | ||
| return defaultScreenType; | ||
| } | ||
| return screenTypeMapping[tailwindSize]; | ||
| }, [ | ||
| tailwindSize, | ||
| screenTypeMapping, | ||
| dimensions.width, | ||
| defaultScreenType, | ||
| defaultTailwindSize, | ||
| ]); | ||
| // Memoize the return object for stable references | ||
| return useMemo(() => ({ | ||
| width: dimensions.width, | ||
| height: dimensions.height, | ||
| tailwindSize, | ||
| screenType, | ||
| refresh, | ||
| }), [dimensions.width, dimensions.height, tailwindSize, screenType, refresh]); | ||
| } |
| /** | ||
| * Tailwind CSS breakpoint identifiers. | ||
| * | ||
| * - `"default"` – viewport width below the smallest defined breakpoint (< 640px) | ||
| * - `"sm"` – small devices (>= 640px) | ||
| * - `"md"` – medium devices (>= 768px) | ||
| * - `"lg"` – large devices (>= 1024px) | ||
| * - `"xl"` – extra large devices (>= 1280px) | ||
| * - `"2xl"` – 2x extra large devices (>= 1536px) | ||
| */ | ||
| export type TailwindSize = "default" | "sm" | "md" | "lg" | "xl" | "2xl"; | ||
| /** | ||
| * Semantic screen type classification for layout decisions. | ||
| * | ||
| * - `"MOBILE"` – phone-sized viewports (default + sm breakpoints) | ||
| * - `"TABLET"` – tablet-sized viewports (md breakpoint) | ||
| * - `"DESKTOP"` – desktop-sized viewports (lg, xl, 2xl breakpoints) | ||
| * - `"UNKNOWN"` – returned during SSR before client-side detection | ||
| */ | ||
| export type ScreenType = "UNKNOWN" | "MOBILE" | "TABLET" | "DESKTOP"; | ||
| /** | ||
| * Breakpoint configuration with pixel thresholds. | ||
| * | ||
| * Values represent the minimum width (in pixels) for each breakpoint. | ||
| * Follows Tailwind CSS v4 defaults. | ||
| */ | ||
| export interface ScreenBreakpoints { | ||
| /** Minimum width for `sm` breakpoint. Default: 640 */ | ||
| sm: number; | ||
| /** Minimum width for `md` breakpoint. Default: 768 */ | ||
| md: number; | ||
| /** Minimum width for `lg` breakpoint. Default: 1024 */ | ||
| lg: number; | ||
| /** Minimum width for `xl` breakpoint. Default: 1280 */ | ||
| xl: number; | ||
| /** Minimum width for `2xl` breakpoint. Default: 1536 */ | ||
| "2xl": number; | ||
| } | ||
| /** | ||
| * Mapping of Tailwind breakpoints to semantic screen types. | ||
| */ | ||
| export interface ScreenTypeMapping { | ||
| /** Screen type for default (< sm) viewport. Default: "MOBILE" */ | ||
| default: ScreenType; | ||
| /** Screen type for sm viewport. Default: "MOBILE" */ | ||
| sm: ScreenType; | ||
| /** Screen type for md viewport. Default: "TABLET" */ | ||
| md: ScreenType; | ||
| /** Screen type for lg viewport. Default: "DESKTOP" */ | ||
| lg: ScreenType; | ||
| /** Screen type for xl viewport. Default: "DESKTOP" */ | ||
| xl: ScreenType; | ||
| /** Screen type for 2xl viewport. Default: "DESKTOP" */ | ||
| "2xl": ScreenType; | ||
| } | ||
| /** | ||
| * Configuration options for {@link useScreen}. | ||
| */ | ||
| export interface UseScreenOptions { | ||
| /** | ||
| * Custom breakpoint pixel thresholds. | ||
| * Partial object that merges with Tailwind v4 defaults. | ||
| */ | ||
| breakpoints?: Partial<ScreenBreakpoints>; | ||
| /** | ||
| * Custom mapping of breakpoints to screen types. | ||
| * Partial object that merges with defaults. | ||
| */ | ||
| screenTypeMapping?: Partial<ScreenTypeMapping>; | ||
| /** | ||
| * Default screen type returned during SSR and before detection. | ||
| * Defaults to `"UNKNOWN"`. | ||
| */ | ||
| defaultScreenType?: ScreenType; | ||
| /** | ||
| * Default Tailwind size returned during SSR and before detection. | ||
| * Defaults to `"default"`. | ||
| */ | ||
| defaultTailwindSize?: TailwindSize; | ||
| } | ||
| /** | ||
| * Shape returned by {@link useScreen}. | ||
| * | ||
| * The object reference is memoized with `useMemo` so it remains referentially | ||
| * stable across re-renders when values have not changed. | ||
| */ | ||
| export interface UseScreenResult { | ||
| /** Current viewport width in pixels. `0` during SSR. */ | ||
| width: number; | ||
| /** Current viewport height in pixels. `0` during SSR. */ | ||
| height: number; | ||
| /** | ||
| * Current Tailwind CSS breakpoint identifier. | ||
| * Determined by the largest matching `min-width` breakpoint. | ||
| */ | ||
| tailwindSize: TailwindSize; | ||
| /** | ||
| * Semantic screen type for layout decisions. | ||
| * Maps `tailwindSize` to a simplified classification. | ||
| */ | ||
| screenType: ScreenType; | ||
| /** Re-measure viewport dimensions on demand. */ | ||
| refresh: () => void; | ||
| } | ||
| /** | ||
| * Track viewport dimensions and compute Tailwind breakpoint / screen type. | ||
| * | ||
| * Provides real-time viewport width and height, along with derived values for | ||
| * the current Tailwind CSS breakpoint (`tailwindSize`) and a semantic screen | ||
| * type classification (`screenType`) for layout decisions. | ||
| * | ||
| * ### SSR Safety | ||
| * | ||
| * The hook returns safe defaults during SSR (`width: 0`, `height: 0`, | ||
| * `tailwindSize: "default"`, `screenType: "UNKNOWN"`) to prevent hydration | ||
| * mismatches. After mount, values update to reflect the actual viewport. | ||
| * | ||
| * ### Performance | ||
| * | ||
| * - Uses `useMediaQuery` internally for efficient breakpoint detection via | ||
| * CSS media queries (no polling). | ||
| * - Viewport dimensions update on window `resize` events. | ||
| * - The returned object is memoized; values must change for the reference | ||
| * to update. | ||
| * | ||
| * @param options - Optional configuration. See {@link UseScreenOptions}. | ||
| * @returns A memoized {@link UseScreenResult} object. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * import { useScreen } from "@opensite/hooks/useScreen"; | ||
| * | ||
| * function ResponsiveLayout() { | ||
| * const { screenType, tailwindSize, width } = useScreen(); | ||
| * | ||
| * return ( | ||
| * <div> | ||
| * <p>Viewport: {width}px ({tailwindSize})</p> | ||
| * {screenType === "MOBILE" && <MobileNav />} | ||
| * {screenType === "TABLET" && <TabletNav />} | ||
| * {screenType === "DESKTOP" && <DesktopNav />} | ||
| * </div> | ||
| * ); | ||
| * } | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // With custom breakpoints | ||
| * const { screenType } = useScreen({ | ||
| * breakpoints: { sm: 600, md: 900, lg: 1200, xl: 1400, "2xl": 1800 }, | ||
| * }); | ||
| * ``` | ||
| */ | ||
| export declare function useScreen(options?: UseScreenOptions): UseScreenResult; |
| import { useCallback, useEffect, useMemo, useState } from "react"; | ||
| import { useMediaQuery } from "./useMediaQuery.js"; | ||
| // --------------------------------------------------------------------------- | ||
| // Constants | ||
| // --------------------------------------------------------------------------- | ||
| /** | ||
| * Tailwind CSS v4 default breakpoints (min-width values in pixels). | ||
| * @see https://tailwindcss.com/docs/responsive-design | ||
| */ | ||
| const DEFAULT_BREAKPOINTS = { | ||
| sm: 640, | ||
| md: 768, | ||
| lg: 1024, | ||
| xl: 1280, | ||
| "2xl": 1536, | ||
| }; | ||
| /** | ||
| * Default mapping of Tailwind breakpoints to semantic screen types. | ||
| * | ||
| * - default, sm → MOBILE | ||
| * - md → TABLET | ||
| * - lg, xl, 2xl → DESKTOP | ||
| */ | ||
| const DEFAULT_SCREEN_TYPE_MAPPING = { | ||
| default: "MOBILE", | ||
| sm: "MOBILE", | ||
| md: "TABLET", | ||
| lg: "DESKTOP", | ||
| xl: "DESKTOP", | ||
| "2xl": "DESKTOP", | ||
| }; | ||
| // --------------------------------------------------------------------------- | ||
| // Helpers | ||
| // --------------------------------------------------------------------------- | ||
| /** | ||
| * Get current viewport dimensions. | ||
| * Returns `{ width: 0, height: 0 }` during SSR. | ||
| */ | ||
| function getViewportDimensions() { | ||
| if (typeof window === "undefined") { | ||
| return { width: 0, height: 0 }; | ||
| } | ||
| return { | ||
| width: window.innerWidth, | ||
| height: window.innerHeight, | ||
| }; | ||
| } | ||
| /** | ||
| * Determine the current Tailwind breakpoint based on viewport width. | ||
| * Uses mobile-first logic: returns the largest breakpoint that matches. | ||
| */ | ||
| function calculateTailwindSize(width, breakpoints) { | ||
| // Check breakpoints from largest to smallest (mobile-first) | ||
| if (width >= breakpoints["2xl"]) | ||
| return "2xl"; | ||
| if (width >= breakpoints.xl) | ||
| return "xl"; | ||
| if (width >= breakpoints.lg) | ||
| return "lg"; | ||
| if (width >= breakpoints.md) | ||
| return "md"; | ||
| if (width >= breakpoints.sm) | ||
| return "sm"; | ||
| return "default"; | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Hook | ||
| // --------------------------------------------------------------------------- | ||
| /** | ||
| * Track viewport dimensions and compute Tailwind breakpoint / screen type. | ||
| * | ||
| * Provides real-time viewport width and height, along with derived values for | ||
| * the current Tailwind CSS breakpoint (`tailwindSize`) and a semantic screen | ||
| * type classification (`screenType`) for layout decisions. | ||
| * | ||
| * ### SSR Safety | ||
| * | ||
| * The hook returns safe defaults during SSR (`width: 0`, `height: 0`, | ||
| * `tailwindSize: "default"`, `screenType: "UNKNOWN"`) to prevent hydration | ||
| * mismatches. After mount, values update to reflect the actual viewport. | ||
| * | ||
| * ### Performance | ||
| * | ||
| * - Uses `useMediaQuery` internally for efficient breakpoint detection via | ||
| * CSS media queries (no polling). | ||
| * - Viewport dimensions update on window `resize` events. | ||
| * - The returned object is memoized; values must change for the reference | ||
| * to update. | ||
| * | ||
| * @param options - Optional configuration. See {@link UseScreenOptions}. | ||
| * @returns A memoized {@link UseScreenResult} object. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * import { useScreen } from "@opensite/hooks/useScreen"; | ||
| * | ||
| * function ResponsiveLayout() { | ||
| * const { screenType, tailwindSize, width } = useScreen(); | ||
| * | ||
| * return ( | ||
| * <div> | ||
| * <p>Viewport: {width}px ({tailwindSize})</p> | ||
| * {screenType === "MOBILE" && <MobileNav />} | ||
| * {screenType === "TABLET" && <TabletNav />} | ||
| * {screenType === "DESKTOP" && <DesktopNav />} | ||
| * </div> | ||
| * ); | ||
| * } | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // With custom breakpoints | ||
| * const { screenType } = useScreen({ | ||
| * breakpoints: { sm: 600, md: 900, lg: 1200, xl: 1400, "2xl": 1800 }, | ||
| * }); | ||
| * ``` | ||
| */ | ||
| export function useScreen(options = {}) { | ||
| const { breakpoints: customBreakpoints, screenTypeMapping: customMapping, defaultScreenType = "UNKNOWN", defaultTailwindSize = "default", } = options; | ||
| // Merge custom config with defaults | ||
| const breakpoints = useMemo(() => ({ ...DEFAULT_BREAKPOINTS, ...customBreakpoints }), [customBreakpoints]); | ||
| const screenTypeMapping = useMemo(() => ({ ...DEFAULT_SCREEN_TYPE_MAPPING, ...customMapping }), [customMapping]); | ||
| // Track viewport dimensions | ||
| const [dimensions, setDimensions] = useState(() => getViewportDimensions()); | ||
| // Use media queries for breakpoint detection (more reliable than width alone | ||
| // for edge cases and provides instant updates via matchMedia) | ||
| const isSm = useMediaQuery(`(min-width: ${breakpoints.sm}px)`); | ||
| const isMd = useMediaQuery(`(min-width: ${breakpoints.md}px)`); | ||
| const isLg = useMediaQuery(`(min-width: ${breakpoints.lg}px)`); | ||
| const isXl = useMediaQuery(`(min-width: ${breakpoints.xl}px)`); | ||
| const is2xl = useMediaQuery(`(min-width: ${breakpoints["2xl"]}px)`); | ||
| // Stable refresh callback | ||
| const refresh = useCallback(() => { | ||
| if (typeof window === "undefined") | ||
| return; | ||
| setDimensions(getViewportDimensions()); | ||
| }, []); | ||
| // Subscribe to resize events for dimension tracking | ||
| useEffect(() => { | ||
| if (typeof window === "undefined") | ||
| return; | ||
| // Initial measurement | ||
| setDimensions(getViewportDimensions()); | ||
| const handleResize = () => { | ||
| setDimensions(getViewportDimensions()); | ||
| }; | ||
| window.addEventListener("resize", handleResize); | ||
| return () => window.removeEventListener("resize", handleResize); | ||
| }, []); | ||
| // Compute tailwindSize from media query results (most reliable) | ||
| const tailwindSize = useMemo(() => { | ||
| // During SSR, all media queries return false | ||
| if (!isSm && !isMd && !isLg && !isXl && !is2xl) { | ||
| // Check if we're on client with actual dimensions | ||
| if (dimensions.width > 0) { | ||
| return calculateTailwindSize(dimensions.width, breakpoints); | ||
| } | ||
| return defaultTailwindSize; | ||
| } | ||
| // Determine from media queries (largest matching breakpoint wins) | ||
| if (is2xl) | ||
| return "2xl"; | ||
| if (isXl) | ||
| return "xl"; | ||
| if (isLg) | ||
| return "lg"; | ||
| if (isMd) | ||
| return "md"; | ||
| if (isSm) | ||
| return "sm"; | ||
| return "default"; | ||
| }, [ | ||
| isSm, | ||
| isMd, | ||
| isLg, | ||
| isXl, | ||
| is2xl, | ||
| dimensions.width, | ||
| breakpoints, | ||
| defaultTailwindSize, | ||
| ]); | ||
| // Derive screen type from tailwind size | ||
| const screenType = useMemo(() => { | ||
| // During SSR before detection | ||
| if (tailwindSize === defaultTailwindSize && dimensions.width === 0) { | ||
| return defaultScreenType; | ||
| } | ||
| return screenTypeMapping[tailwindSize]; | ||
| }, [ | ||
| tailwindSize, | ||
| screenTypeMapping, | ||
| dimensions.width, | ||
| defaultScreenType, | ||
| defaultTailwindSize, | ||
| ]); | ||
| // Memoize the return object for stable references | ||
| return useMemo(() => ({ | ||
| width: dimensions.width, | ||
| height: dimensions.height, | ||
| tailwindSize, | ||
| screenType, | ||
| refresh, | ||
| }), [dimensions.width, dimensions.height, tailwindSize, screenType, refresh]); | ||
| } |
@@ -1,2 +0,2 @@ | ||
| !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).OpensiteHooks={},e.React)}(this,function(e,t){"use strict";const n="undefined"!=typeof window?t.useLayoutEffect:t.useEffect;function r(e,r,o={}){const i=t.useRef(e),u=t.useRef(null),c=t.useRef(null),s=t.useRef(null),a=o.leading??!1,l=o.trailing??!0,d=o.maxWait,m=Math.max(0,r);n(()=>{i.current=e},[e]);const f=t.useCallback(()=>{u.current&&(clearTimeout(u.current),u.current=null),c.current&&(clearTimeout(c.current),c.current=null)},[]),p=t.useCallback(()=>{if(!s.current)return;const e=s.current;s.current=null,i.current(...e)},[]),w=t.useCallback((...e)=>{s.current=e;if(a&&null===u.current&&null===c.current&&p(),u.current&&clearTimeout(u.current),l&&(u.current=setTimeout(()=>{u.current=null,s.current&&p(),c.current&&(clearTimeout(c.current),c.current=null)},m)),null!=d&&l&&!c.current){const e=Math.max(0,d);c.current=setTimeout(()=>{c.current=null,u.current&&(clearTimeout(u.current),u.current=null),s.current&&p()},e)}},[p,a,l,d,m]),b=t.useCallback(()=>{f(),s.current=null},[f]),g=t.useCallback(()=>{s.current&&(f(),p())},[f,p]);return t.useEffect(()=>()=>b(),[b]),{debouncedCallback:w,cancel:b,flush:g}}function o(e,n,o){const[i,u]=t.useState(e),{debouncedCallback:c,cancel:s}=r(e=>{u(e)},n,o);return t.useEffect(()=>{c(e)},[c,e]),t.useEffect(()=>()=>s(),[s]),i}function i(e){return(null==e?void 0:e.ownerDocument)??document}const u=new Map([["instagram.com","instagram"],["www.instagram.com","instagram"],["instagr.am","instagram"],["www.instagr.am","instagram"],["linkedin.com","linkedin"],["www.linkedin.com","linkedin"],["ca.linkedin.com","linkedin"],["uk.linkedin.com","linkedin"],["in.linkedin.com","linkedin"],["lnkd.in","linkedin"],["google.com","google"],["www.google.com","google"],["maps.google.com","google"],["goo.gl","google"],["maps.app.goo.gl","google"],["g.co","google"],["facebook.com","facebook"],["www.facebook.com","facebook"],["m.facebook.com","facebook"],["fb.com","facebook"],["fb.me","facebook"],["on.fb.me","facebook"],["tiktok.com","tiktok"],["www.tiktok.com","tiktok"],["m.tiktok.com","tiktok"],["vm.tiktok.com","tiktok"],["vt.tiktok.com","tiktok"],["youtube.com","youtube"],["www.youtube.com","youtube"],["m.youtube.com","youtube"],["youtu.be","youtube"],["yelp.com","yelp"],["www.yelp.com","yelp"],["m.yelp.com","yelp"],["spotify.com","spotify"],["www.spotify.com","spotify"],["open.spotify.com","spotify"],["play.spotify.com","spotify"],["spoti.fi","spotify"],["spotify.link","spotify"],["apple.com","apple"],["www.apple.com","apple"],["music.apple.com","apple"],["podcasts.apple.com","apple"],["apps.apple.com","apple"],["itunes.apple.com","apple"],["x.com","x"],["www.x.com","x"],["twitter.com","x"],["www.twitter.com","x"],["t.co","x"],["github.com","github"],["www.github.com","github"],["gist.github.com","github"],["raw.githubusercontent.com","github"],["github.io","github"],["discord.com","discord"],["www.discord.com","discord"],["discord.gg","discord"],["discordapp.com","discord"],["discordapp.net","discord"],["discord.new","discord"],["discord.gift","discord"],["discord.gifts","discord"],["dis.gd","discord"],["snapchat.com","snapchat"],["www.snapchat.com","snapchat"],["snap.com","snapchat"],["www.snap.com","snapchat"],["story.snapchat.com","snapchat"],["web.snapchat.com","snapchat"],["dev.to","dev"],["www.dev.to","dev"],["substack.com","substack"],["www.substack.com","substack"],["reddit.com","reddit"],["www.reddit.com","reddit"],["old.reddit.com","reddit"],["new.reddit.com","reddit"],["i.redd.it","reddit"],["v.redd.it","reddit"],["redd.it","reddit"],["preview.redd.it","reddit"],["pinterest.com","pinterest"],["www.pinterest.com","pinterest"],["pin.it","pinterest"],["in.pinterest.com","pinterest"],["br.pinterest.com","pinterest"],["uk.pinterest.com","pinterest"],["threads.net","threads"],["www.threads.net","threads"],["threads.com","threads"],["www.threads.com","threads"],["twitch.tv","twitch"],["www.twitch.tv","twitch"],["m.twitch.tv","twitch"],["whatsapp.com","whatsapp"],["www.whatsapp.com","whatsapp"],["wa.me","whatsapp"],["web.whatsapp.com","whatsapp"],["telegram.org","telegram"],["www.telegram.org","telegram"],["t.me","telegram"],["telegram.me","telegram"],["telegram.dog","telegram"],["medium.com","medium"],["www.medium.com","medium"],["patreon.com","patreon"],["www.patreon.com","patreon"],["onlyfans.com","onlyfans"],["www.onlyfans.com","onlyfans"],["eventbrite.com","eventbrite"],["www.eventbrite.com","eventbrite"],["eventbrite.co.uk","eventbrite"],["eventbrite.com.au","eventbrite"],["eventbrite.ca","eventbrite"],["eventbrite.de","eventbrite"],["eventbrite.fr","eventbrite"],["eventbrite.es","eventbrite"],["eventbrite.it","eventbrite"],["eventbrite.ie","eventbrite"],["eventbrite.nl","eventbrite"],["eventbrite.co.nz","eventbrite"],["eventbriteapi.com","eventbrite"],["evbuc.com","eventbrite"],["npmjs.com","npmjs"],["www.npmjs.com","npmjs"],["npmjs.org","npmjs"],["www.npmjs.org","npmjs"],["registry.npmjs.org","npmjs"],["registry.npmjs.com","npmjs"],["replicate.npmjs.com","npmjs"],["crates.io","crates"],["www.crates.io","crates"],["rubygems.org","rubygems"],["www.rubygems.org","rubygems"],["behance.net","behance"],["www.behance.net","behance"],["portfolio.behance.net","behance"],["mir-s3-cdn-cf.behance.net","behance"],["dribbble.com","dribbble"],["www.dribbble.com","dribbble"],["drbl.in","dribbble"]]);function c(e,r,o,i){const u=t.useRef(r);n(()=>{u.current=r},[r]),t.useEffect(()=>{const t="undefined"!=typeof Window&&o instanceof Window,n="undefined"!=typeof Document&&o instanceof Document,r=void 0===o?"undefined"!=typeof window?window:null:t||n||"undefined"!=typeof HTMLElement&&o instanceof HTMLElement?o:(c=o)&&"object"==typeof c&&"current"in c?o.current:null;var c;if(!(null==r?void 0:r.addEventListener))return;const s=e=>{const t=u.current;"function"==typeof t?t(e):t.handleEvent(e)};return r.addEventListener(e,s,i),()=>{r.removeEventListener(e,s,i)}},[e,o,i])}function s(){const[e,n]=t.useState(!1);return t.useEffect(()=>{n(!0)},[]),e}function a(e){const[r,o]=t.useState(()=>e instanceof Map||Array.isArray(e)?new Map(e):new Map),i=t.useRef(r);n(()=>{i.current=r},[r]);const u=t.useMemo(()=>({set:(e,t)=>{o(n=>{const r=new Map(n);return r.set(e,t),r})},setAll:e=>{o((Map,new Map(e)))},remove:e=>{o(t=>{const n=new Map(t);return n.delete(e),n})},clear:()=>o(new Map),get:e=>i.current.get(e),has:e=>i.current.has(e)}),[]);return[r,u]}const l="https://octane.buzz";function d(e){const t=function(e){return e.replace(/\/+$/,"")}(e.baseUrl??l),n=new URLSearchParams;return e.apiKey&&n.set("api_key",e.apiKey),n.set("url",e.url),`${t}/api/v1/extract/${e.endpoint}?${n.toString()}`}async function m(e){var t;if(!e.url||0===e.url.trim().length)return{ok:!1,error:{message:"URL is required."}};const n=d(e);try{const t=await fetch(n,{method:"GET",signal:e.signal});let r=null;try{r=await t.json()}catch{r=null}if(!t.ok){const e=r;return{ok:!1,error:{message:(null==e?void 0:e.error)??`Request failed with status ${t.status}.`,status:(null==e?void 0:e.status)??t.status,raw:r}}}return{ok:!0,response:r}}catch(r){return(null==(t=e.signal)?void 0:t.aborted)?{ok:!1,error:{message:"Request aborted."}}:{ok:!1,error:{message:r instanceof Error?r.message:"Request failed unexpectedly.",raw:r}}}}function f(e){const{endpoint:n,options:i,selectData:u,shouldSkip:c}=e,d=s(),[f,p]=t.useState({loading:!1}),[,w]=a(),b=i.cache??true,g=i.enabled??true,v=i.debounceMs??250,y=i.refreshDebounceMs??150,h=o(t.useMemo(()=>{var e;return(null==(e=i.url)?void 0:e.trim())??""},[i.url]),v),k=t.useRef(0),E=t.useRef(0),[C,T]=t.useState(0),{debouncedCallback:x,cancel:S}=r(()=>{k.current+=1,T(k.current)},y),M=t.useCallback(()=>{x()},[x]);t.useEffect(()=>()=>S(),[S]);const R=t.useMemo(()=>{if(!h)return"";const e=i.baseUrl??l,t=i.apiKey??"";return`${n}:${e}:${t}:${h}`},[n,i.apiKey,i.baseUrl,h]),L=t.useRef(null);return t.useEffect(()=>{var e;if(!d)return;if(!g||!h)return void p({loading:!1});if(null==c?void 0:c(h))return void p({loading:!1});const t=C!==E.current;if(t&&(E.current=C),b&&!t){const e=w.get(R);if(e)return void p({loading:!1,data:e.data,raw:e.raw,meta:e.meta})}null==(e=L.current)||e.abort();const r=new AbortController;return L.current=r,p(e=>({...e,loading:!0,error:void 0})),m({endpoint:n,url:h,apiKey:i.apiKey,baseUrl:i.baseUrl,signal:r.signal}).then(e=>{if(r.signal.aborted)return;if(!e.ok)return void p(t=>({...t,loading:!1,error:e.error}));const t=e.response,n=function(e){const{requestedUrl:t,finalUrl:n,url:r,normalizedUrl:o,status:i,contentType:u,fetchedAt:c,bodyBytes:s,bodyTruncated:a,maxBodyBytes:l,cache:d}=e;return{requestedUrl:t,finalUrl:n,url:r,normalizedUrl:o,status:i,contentType:u,fetchedAt:c,bodyBytes:s,bodyTruncated:a,maxBodyBytes:l,cache:d}}(t),o=function(e){const{requestedUrl:t,finalUrl:n,url:r,normalizedUrl:o,status:i,contentType:u,fetchedAt:c,bodyBytes:s,bodyTruncated:a,maxBodyBytes:l,cache:d,...m}=e;return m}(t),i=u(o,t,n);b&&w.set(R,{data:i,raw:t,meta:n}),p({loading:!1,data:i,raw:t,meta:n})}).catch(e=>{r.signal.aborted||p(t=>({...t,loading:!1,error:{message:e instanceof Error?e.message:"Request failed unexpectedly.",raw:e}}))}),()=>{r.abort()}},[w,b,h,g,n,d,i.apiKey,i.baseUrl,R,C,u,c]),t.useMemo(()=>({...f,refresh:M}),[M,f])}const p=[/search\.google\.com\/local\/reviews/i,/google\.com\/maps\/place/i,/maps\.google\.com/i,/opentable\.com/i],w=(...e)=>{for(const t of e)if("string"==typeof t&&t.trim().length>0)return t},b=e=>{if(e)try{return new URL(e).hostname}catch{return}};e.buildWebsiteExtractorUrl=d,e.fetchWebsiteExtractor=m,e.useBoolean=function(e=!1){const[n,r]=t.useState(e),o=t.useCallback(()=>r(!0),[]),i=t.useCallback(()=>r(!1),[]),u=t.useCallback(()=>r(e=>!e),[]);return t.useMemo(()=>({value:n,setValue:r,setTrue:o,setFalse:i,toggle:u}),[n,r,o,i,u])},e.useCopyToClipboard=function(e={}){const n=e.resetDelay??2e3,r=t.useRef(null),[o,i]=t.useState(null),u=t.useMemo(()=>!("undefined"==typeof navigator||!navigator.clipboard)||"undefined"!=typeof document&&("function"==typeof document.queryCommandSupported&&document.queryCommandSupported("copy")),[]),c=t.useCallback(()=>{r.current&&clearTimeout(r.current),r.current=setTimeout(()=>{i(null)},n)},[n]),s=t.useCallback(async e=>{if(!u)return!1;const t="undefined"!=typeof navigator&&!!navigator.clipboard;try{if(t)await navigator.clipboard.writeText(e);else if("undefined"!=typeof document){const t=document.createElement("textarea");t.value=e,t.setAttribute("readonly",""),t.style.position="fixed",t.style.left="-9999px",t.style.top="0",document.body.appendChild(t),t.focus(),t.select();const n=document.execCommand("copy");if(document.body.removeChild(t),!n)return!1}return i(e),c(),!0}catch{return!1}},[u,c]);return t.useEffect(()=>()=>{r.current&&clearTimeout(r.current)},[]),{copy:s,copiedText:o,isSupported:u}},e.useDebounceCallback=r,e.useDebounceValue=o,e.useEventListener=c,e.useHover=function(e){const[n,r]=t.useState(!1),o=t.useCallback(()=>{r(!0)},[]),i=t.useCallback(()=>{r(!1)},[]);return c("pointerenter",o,e),c("pointerleave",i,e),n},e.useIsClient=s,e.useIsomorphicLayoutEffect=n,e.useLocalStorage=function(e,n,r={}){const{initializeWithValue:o=!0,serialize:i=JSON.stringify,deserialize:u=JSON.parse,listenToStorageChanges:c=!0}=r,s=t.useRef(n),a=t.useCallback(()=>{if("undefined"==typeof window)return s.current;if(!o)return s.current;try{const t=window.localStorage.getItem(e);return t?u(t):s.current}catch{return s.current}},[u,o,e]),[l,d]=t.useState(()=>a()),m=t.useCallback(t=>{d(n=>{const r="function"==typeof t?t(n):t;if("undefined"!=typeof window)try{window.localStorage.setItem(e,i(r))}catch{}return r})},[e,i]);return t.useEffect(()=>{d(a())},[a]),t.useEffect(()=>{if("undefined"==typeof window||!c)return;const t=t=>{if(t.key===e)if(null!==t.newValue)try{d(u(t.newValue))}catch{d(s.current)}else d(s.current)};return window.addEventListener("storage",t),()=>window.removeEventListener("storage",t)},[u,e,c]),[l,m]},e.useMap=a,e.useMediaQuery=function(e,n={}){const[r,o]=t.useState(()=>"undefined"==typeof window||"function"!=typeof window.matchMedia?n.defaultValue??!1:window.matchMedia(e).matches);return t.useEffect(()=>{if("undefined"==typeof window||"function"!=typeof window.matchMedia)return;const t=window.matchMedia(e),n=e=>{o(e.matches)};return o(t.matches),t.addEventListener?(t.addEventListener("change",n),()=>t.removeEventListener("change",n)):(t.addListener(n),()=>t.removeListener(n))},[e]),r},e.useOnClickOutside=function(e,r,o,u){const c=t.useRef(r);n(()=>{c.current=r},[r]),t.useEffect(()=>{if("undefined"==typeof document)return;const t="undefined"!=typeof window&&void 0!==window.PointerEvent,n=o??(t?"pointerdown":"mousedown"),r=Array.isArray(e)?e:[e];let s=document;for(const e of r)if(e.current){const t=i(e.current);if(t!==document){s=t;break}}const a=e=>{const t=e.target;if("undefined"==typeof Node||!(t instanceof Node))return;r.some(e=>{const n=e.current;return!!n&&n.contains(t)})||c.current(e)};return s.addEventListener(n,a,u),()=>{s.removeEventListener(n,a,u)}},[o,u,e])},e.useOpenGraphExtractor=function(e){const n=t.useMemo(()=>e.skipPatterns??p,[e.skipPatterns]),r=t.useCallback(e=>n.some(t=>t.test(e)),[n]),o=t.useCallback((e,t,n)=>{var r,o,i;const{openGraph:u,htmlInferred:c,hybridGraph:s}=e,a=w(null==u?void 0:u.description,null==s?void 0:s.description,null==c?void 0:c.description),l=w(null==u?void 0:u.title,null==s?void 0:s.title,null==c?void 0:c.title),d=w(null==u?void 0:u.site_name,null==s?void 0:s.site_name,null==c?void 0:c.site_name),m=w(null==s?void 0:s.favicon,null==c?void 0:c.favicon),f=w((null==(r=null==u?void 0:u.image)?void 0:r.url)??void 0,(null==s?void 0:s.image)??void 0,(null==c?void 0:c.image)??void 0,null==(o=null==c?void 0:c.images)?void 0:o[0]),p=w((null==(i=null==u?void 0:u.video)?void 0:i.url)??void 0,(null==s?void 0:s.video)??void 0),g=w(null==s?void 0:s.videoType,null==c?void 0:c.videoType),v=w(n.url,(null==u?void 0:u.url)??void 0,(null==s?void 0:s.url)??void 0,(null==c?void 0:c.url)??void 0,n.finalUrl,n.normalizedUrl,n.requestedUrl)??"";return{description:a,favicon:m,image:f,video:p,videoType:g,siteName:d,title:l,url:v,siteHost:b(v)}},[]);return f({endpoint:"open-graph",options:e,selectData:o,shouldSkip:r})},e.usePlatformFromUrl=function(e){return t.useMemo(()=>{if(!e||"string"!=typeof e)return"unknown";const t=e.trim();if(!t)return"unknown";try{const e=new URL(t).hostname.toLowerCase(),n=u.get(e);return n||(e.endsWith(".substack.com")?"substack":e.endsWith(".github.io")?"github":e.includes("pinterest.com")?"pinterest":e.includes("eventbrite.")?"eventbrite":e.endsWith(".medium.com")?"medium":e.endsWith(".behance.net")?"behance":"unknown")}catch{return"unknown"}},[e])},e.usePrevious=function(e){const r=t.useRef();return n(()=>{r.current=e},[e]),r.current},e.useResizeObserver=function(e,r,o){const i=t.useRef(r),u=t.useRef(null),[c,s]=t.useState(null);return n(()=>{i.current=r},[r]),t.useEffect(()=>{if("undefined"==typeof ResizeObserver)return;const t="undefined"!=typeof Element&&e instanceof Element?e:(n=e)&&"object"==typeof n&&"current"in n?e.current:null;var n;if(!t)return;const r=new ResizeObserver(e=>{const t=e[0];u.current=t,i.current?i.current(t):s(t)});return r.observe(t,o),()=>r.disconnect()},[o,e]),i.current?u.current:c},e.useSessionStorage=function(e,n,r={}){const{initializeWithValue:o=!0,serialize:i=JSON.stringify,deserialize:u=JSON.parse,listenToStorageChanges:c=!1}=r,s=t.useRef(n),a=t.useCallback(()=>{if("undefined"==typeof window)return s.current;if(!o)return s.current;try{const t=window.sessionStorage.getItem(e);return t?u(t):s.current}catch{return s.current}},[u,o,e]),[l,d]=t.useState(()=>a()),m=t.useCallback(t=>{d(n=>{const r="function"==typeof t?t(n):t;if("undefined"!=typeof window)try{window.sessionStorage.setItem(e,i(r))}catch{}return r})},[e,i]);return t.useEffect(()=>{d(a())},[a]),t.useEffect(()=>{if("undefined"==typeof window||!c)return;const t=t=>{if(t.key===e)if(null!==t.newValue)try{d(u(t.newValue))}catch{d(s.current)}else d(s.current)};return window.addEventListener("storage",t),()=>window.removeEventListener("storage",t)},[u,e,c]),[l,m]},e.useThrottle=function(e,n,r={}){const o=r.leading??!0,i=r.trailing??!0,u=Math.max(0,n),[c,s]=t.useState(e),a=t.useRef(0),l=t.useRef(null),d=t.useRef(null);return t.useEffect(()=>{if(0===u)return void s(e);const t=Date.now();if(0===a.current)return a.current=t,o?void s(e):void(i&&!l.current&&(d.current=e,l.current=setTimeout(()=>{l.current=null,null!==d.current&&(s(d.current),d.current=null,a.current=Date.now())},u)));const n=t-a.current;if(n>=u&&o)return s(e),a.current=t,void(d.current=null);if(i&&(d.current=e,!l.current)){const e=Math.max(u-n,0);l.current=setTimeout(()=>{l.current=null,null!==d.current&&(s(d.current),d.current=null,a.current=Date.now())},e)}},[o,i,e,u]),t.useEffect(()=>()=>{l.current&&clearTimeout(l.current)},[]),c},e.useWebsiteLinksExtractor=function(e){return f({endpoint:"links",options:e,selectData:t.useCallback(e=>({totalLinks:e.totalLinks??0,uniqueDomains:e.uniqueDomains??0,links:e.links??[]}),[])})},e.useWebsiteMetaExtractor=function(e){return f({endpoint:"meta",options:e,selectData:t.useCallback(e=>({title:e.title??void 0,description:e.description??void 0,language:e.language??void 0,canonicalUrl:e.canonicalUrl??void 0,feedUrl:e.feedUrl??null,textContentLength:e.textContentLength??void 0,metaTags:e.metaTags??{}}),[])})},e.useWebsiteRssExtractor=function(e){return f({endpoint:"rss",options:e,selectData:t.useCallback(e=>({feedUrl:e.feedUrl??null,feeds:e.feeds??[]}),[])})},e.useWebsiteSchemaExtractor=function(e){return f({endpoint:"schema",options:e,selectData:t.useCallback(e=>({schema:e.schema??[],schemaTypes:e.schemaTypes??[]}),[])})},Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})}); | ||
| !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).OpensiteHooks={},e.React)}(this,function(e,t){"use strict";const n="undefined"!=typeof window?t.useLayoutEffect:t.useEffect;function r(e,r,o={}){const i=t.useRef(e),u=t.useRef(null),c=t.useRef(null),s=t.useRef(null),a=o.leading??!1,l=o.trailing??!0,d=o.maxWait,f=Math.max(0,r);n(()=>{i.current=e},[e]);const m=t.useCallback(()=>{u.current&&(clearTimeout(u.current),u.current=null),c.current&&(clearTimeout(c.current),c.current=null)},[]),p=t.useCallback(()=>{if(!s.current)return;const e=s.current;s.current=null,i.current(...e)},[]),w=t.useCallback((...e)=>{s.current=e;if(a&&null===u.current&&null===c.current&&p(),u.current&&clearTimeout(u.current),l&&(u.current=setTimeout(()=>{u.current=null,s.current&&p(),c.current&&(clearTimeout(c.current),c.current=null)},f)),null!=d&&l&&!c.current){const e=Math.max(0,d);c.current=setTimeout(()=>{c.current=null,u.current&&(clearTimeout(u.current),u.current=null),s.current&&p()},e)}},[p,a,l,d,f]),b=t.useCallback(()=>{m(),s.current=null},[m]),h=t.useCallback(()=>{s.current&&(m(),p())},[m,p]);return t.useEffect(()=>()=>b(),[b]),{debouncedCallback:w,cancel:b,flush:h}}function o(e,n,o){const[i,u]=t.useState(e),{debouncedCallback:c,cancel:s}=r(e=>{u(e)},n,o);return t.useEffect(()=>{c(e)},[c,e]),t.useEffect(()=>()=>s(),[s]),i}function i(e){return(null==e?void 0:e.ownerDocument)??document}function u(e,n={}){const[r,o]=t.useState(()=>"undefined"==typeof window||"function"!=typeof window.matchMedia?n.defaultValue??!1:window.matchMedia(e).matches);return t.useEffect(()=>{if("undefined"==typeof window||"function"!=typeof window.matchMedia)return;const t=window.matchMedia(e),n=e=>{o(e.matches)};return o(t.matches),t.addEventListener("change",n),()=>t.removeEventListener("change",n)},[e]),r}const c=new Map([["instagram.com","instagram"],["www.instagram.com","instagram"],["instagr.am","instagram"],["www.instagr.am","instagram"],["linkedin.com","linkedin"],["www.linkedin.com","linkedin"],["ca.linkedin.com","linkedin"],["uk.linkedin.com","linkedin"],["in.linkedin.com","linkedin"],["lnkd.in","linkedin"],["google.com","google"],["www.google.com","google"],["maps.google.com","google"],["goo.gl","google"],["maps.app.goo.gl","google"],["g.co","google"],["facebook.com","facebook"],["www.facebook.com","facebook"],["m.facebook.com","facebook"],["fb.com","facebook"],["fb.me","facebook"],["on.fb.me","facebook"],["tiktok.com","tiktok"],["www.tiktok.com","tiktok"],["m.tiktok.com","tiktok"],["vm.tiktok.com","tiktok"],["vt.tiktok.com","tiktok"],["youtube.com","youtube"],["www.youtube.com","youtube"],["m.youtube.com","youtube"],["youtu.be","youtube"],["yelp.com","yelp"],["www.yelp.com","yelp"],["m.yelp.com","yelp"],["spotify.com","spotify"],["www.spotify.com","spotify"],["open.spotify.com","spotify"],["play.spotify.com","spotify"],["spoti.fi","spotify"],["spotify.link","spotify"],["apple.com","apple"],["www.apple.com","apple"],["music.apple.com","apple"],["podcasts.apple.com","apple"],["apps.apple.com","apple"],["itunes.apple.com","apple"],["x.com","x"],["www.x.com","x"],["twitter.com","x"],["www.twitter.com","x"],["t.co","x"],["github.com","github"],["www.github.com","github"],["gist.github.com","github"],["raw.githubusercontent.com","github"],["github.io","github"],["discord.com","discord"],["www.discord.com","discord"],["discord.gg","discord"],["discordapp.com","discord"],["discordapp.net","discord"],["discord.new","discord"],["discord.gift","discord"],["discord.gifts","discord"],["dis.gd","discord"],["snapchat.com","snapchat"],["www.snapchat.com","snapchat"],["snap.com","snapchat"],["www.snap.com","snapchat"],["story.snapchat.com","snapchat"],["web.snapchat.com","snapchat"],["dev.to","dev"],["www.dev.to","dev"],["substack.com","substack"],["www.substack.com","substack"],["reddit.com","reddit"],["www.reddit.com","reddit"],["old.reddit.com","reddit"],["new.reddit.com","reddit"],["i.redd.it","reddit"],["v.redd.it","reddit"],["redd.it","reddit"],["preview.redd.it","reddit"],["pinterest.com","pinterest"],["www.pinterest.com","pinterest"],["pin.it","pinterest"],["in.pinterest.com","pinterest"],["br.pinterest.com","pinterest"],["uk.pinterest.com","pinterest"],["threads.net","threads"],["www.threads.net","threads"],["threads.com","threads"],["www.threads.com","threads"],["twitch.tv","twitch"],["www.twitch.tv","twitch"],["m.twitch.tv","twitch"],["whatsapp.com","whatsapp"],["www.whatsapp.com","whatsapp"],["wa.me","whatsapp"],["web.whatsapp.com","whatsapp"],["telegram.org","telegram"],["www.telegram.org","telegram"],["t.me","telegram"],["telegram.me","telegram"],["telegram.dog","telegram"],["medium.com","medium"],["www.medium.com","medium"],["patreon.com","patreon"],["www.patreon.com","patreon"],["onlyfans.com","onlyfans"],["www.onlyfans.com","onlyfans"],["eventbrite.com","eventbrite"],["www.eventbrite.com","eventbrite"],["eventbrite.co.uk","eventbrite"],["eventbrite.com.au","eventbrite"],["eventbrite.ca","eventbrite"],["eventbrite.de","eventbrite"],["eventbrite.fr","eventbrite"],["eventbrite.es","eventbrite"],["eventbrite.it","eventbrite"],["eventbrite.ie","eventbrite"],["eventbrite.nl","eventbrite"],["eventbrite.co.nz","eventbrite"],["eventbriteapi.com","eventbrite"],["evbuc.com","eventbrite"],["npmjs.com","npmjs"],["www.npmjs.com","npmjs"],["npmjs.org","npmjs"],["www.npmjs.org","npmjs"],["registry.npmjs.org","npmjs"],["registry.npmjs.com","npmjs"],["replicate.npmjs.com","npmjs"],["crates.io","crates"],["www.crates.io","crates"],["rubygems.org","rubygems"],["www.rubygems.org","rubygems"],["behance.net","behance"],["www.behance.net","behance"],["portfolio.behance.net","behance"],["mir-s3-cdn-cf.behance.net","behance"],["dribbble.com","dribbble"],["www.dribbble.com","dribbble"],["drbl.in","dribbble"]]);function s(e,r,o,i){const u=t.useRef(r);n(()=>{u.current=r},[r]),t.useEffect(()=>{const t="undefined"!=typeof Window&&o instanceof Window,n="undefined"!=typeof Document&&o instanceof Document,r=void 0===o?"undefined"!=typeof window?window:null:t||n||"undefined"!=typeof HTMLElement&&o instanceof HTMLElement?o:(c=o)&&"object"==typeof c&&"current"in c?o.current:null;var c;if(!(null==r?void 0:r.addEventListener))return;const s=e=>{const t=u.current;"function"==typeof t?t(e):t.handleEvent(e)};return r.addEventListener(e,s,i),()=>{r.removeEventListener(e,s,i)}},[e,o,i])}function a(){const[e,n]=t.useState(!1);return t.useEffect(()=>{n(!0)},[]),e}function l(e){const[r,o]=t.useState(()=>e instanceof Map||Array.isArray(e)?new Map(e):new Map),i=t.useRef(r);n(()=>{i.current=r},[r]);const u=t.useMemo(()=>({set:(e,t)=>{o(n=>{const r=new Map(n);return r.set(e,t),r})},setAll:e=>{o((Map,new Map(e)))},remove:e=>{o(t=>{const n=new Map(t);return n.delete(e),n})},clear:()=>o(new Map),get:e=>i.current.get(e),has:e=>i.current.has(e)}),[]);return[r,u]}const d="https://octane.buzz";function f(e){const t=function(e){return e.replace(/\/+$/,"")}(e.baseUrl??d),n=new URLSearchParams;return e.apiKey&&n.set("api_key",e.apiKey),n.set("url",e.url),`${t}/api/v1/extract/${e.endpoint}?${n.toString()}`}async function m(e){var t;if(!e.url||0===e.url.trim().length)return{ok:!1,error:{message:"URL is required."}};const n=f(e);try{const t=await fetch(n,{method:"GET",signal:e.signal});let r=null;try{r=await t.json()}catch{r=null}if(!t.ok){const e=r;return{ok:!1,error:{message:(null==e?void 0:e.error)??`Request failed with status ${t.status}.`,status:(null==e?void 0:e.status)??t.status,raw:r}}}return{ok:!0,response:r}}catch(r){return(null==(t=e.signal)?void 0:t.aborted)?{ok:!1,error:{message:"Request aborted."}}:{ok:!1,error:{message:r instanceof Error?r.message:"Request failed unexpectedly.",raw:r}}}}function p(e){const{endpoint:n,options:i,selectData:u,shouldSkip:c}=e,s=a(),[f,p]=t.useState({loading:!1}),[,w]=l(),b=i.cache??true,h=i.enabled??true,g=i.debounceMs??250,v=i.refreshDebounceMs??150,y=o(t.useMemo(()=>{var e;return(null==(e=i.url)?void 0:e.trim())??""},[i.url]),g),k=t.useRef(0),E=t.useRef(0),[x,T]=t.useState(0),{debouncedCallback:M,cancel:S}=r(()=>{k.current+=1,T(k.current)},v),C=t.useCallback(()=>{M()},[M]);t.useEffect(()=>()=>S(),[S]);const L=t.useMemo(()=>{if(!y)return"";const e=i.baseUrl??d,t=i.apiKey??"";return`${n}:${e}:${t}:${y}`},[n,i.apiKey,i.baseUrl,y]),R=t.useRef(null);return t.useEffect(()=>{var e;if(!s)return;if(!h||!y)return void p({loading:!1});if(null==c?void 0:c(y))return void p({loading:!1});const t=x!==E.current;if(t&&(E.current=x),b&&!t){const e=w.get(L);if(e)return void p({loading:!1,data:e.data,raw:e.raw,meta:e.meta})}null==(e=R.current)||e.abort();const r=new AbortController;return R.current=r,p(e=>({...e,loading:!0,error:void 0})),m({endpoint:n,url:y,apiKey:i.apiKey,baseUrl:i.baseUrl,signal:r.signal}).then(e=>{if(r.signal.aborted)return;if(!e.ok)return void p(t=>({...t,loading:!1,error:e.error}));const t=e.response,n=function(e){const{requestedUrl:t,finalUrl:n,url:r,normalizedUrl:o,status:i,contentType:u,fetchedAt:c,bodyBytes:s,bodyTruncated:a,maxBodyBytes:l,cache:d}=e;return{requestedUrl:t,finalUrl:n,url:r,normalizedUrl:o,status:i,contentType:u,fetchedAt:c,bodyBytes:s,bodyTruncated:a,maxBodyBytes:l,cache:d}}(t),o=function(e){const{requestedUrl:t,finalUrl:n,url:r,normalizedUrl:o,status:i,contentType:u,fetchedAt:c,bodyBytes:s,bodyTruncated:a,maxBodyBytes:l,cache:d,...f}=e;return f}(t),i=u(o,t,n);b&&w.set(L,{data:i,raw:t,meta:n}),p({loading:!1,data:i,raw:t,meta:n})}).catch(e=>{r.signal.aborted||p(t=>({...t,loading:!1,error:{message:e instanceof Error?e.message:"Request failed unexpectedly.",raw:e}}))}),()=>{r.abort()}},[w,b,y,h,n,s,i.apiKey,i.baseUrl,L,x,u,c]),t.useMemo(()=>({...f,refresh:C}),[C,f])}const w=[/search\.google\.com\/local\/reviews/i,/google\.com\/maps\/place/i,/maps\.google\.com/i,/opentable\.com/i],b=(...e)=>{for(const t of e)if("string"==typeof t&&t.trim().length>0)return t},h=e=>{if(e)try{return new URL(e).hostname}catch{return}};const g="(hover: none) and (pointer: coarse)";function v(){if("function"==typeof window.matchMedia){const e=window.matchMedia(g);if("not all"!==e.media)return e.matches?"touch":"desktop"}return"ontouchstart"in window||"undefined"!=typeof navigator&&"number"==typeof navigator.maxTouchPoints&&navigator.maxTouchPoints>0?"touch":"desktop"}const y={sm:640,md:768,lg:1024,xl:1280,"2xl":1536},k={default:"MOBILE",sm:"MOBILE",md:"TABLET",lg:"DESKTOP",xl:"DESKTOP","2xl":"DESKTOP"};function E(){return"undefined"==typeof window?{width:0,height:0}:{width:window.innerWidth,height:window.innerHeight}}e.buildWebsiteExtractorUrl=f,e.fetchWebsiteExtractor=m,e.useBoolean=function(e=!1){const[n,r]=t.useState(e),o=t.useCallback(()=>r(!0),[]),i=t.useCallback(()=>r(!1),[]),u=t.useCallback(()=>r(e=>!e),[]);return t.useMemo(()=>({value:n,setValue:r,setTrue:o,setFalse:i,toggle:u}),[n,r,o,i,u])},e.useCopyToClipboard=function(e={}){const n=e.resetDelay??2e3,r=t.useRef(null),[o,i]=t.useState(null),u=t.useMemo(()=>!("undefined"==typeof navigator||!navigator.clipboard)||"undefined"!=typeof document&&("function"==typeof document.queryCommandSupported&&document.queryCommandSupported("copy")),[]),c=t.useCallback(()=>{r.current&&clearTimeout(r.current),r.current=setTimeout(()=>{i(null)},n)},[n]),s=t.useCallback(async e=>{if(!u)return!1;const t="undefined"!=typeof navigator&&!!navigator.clipboard;try{if(t)await navigator.clipboard.writeText(e);else if("undefined"!=typeof document){const t=document.createElement("textarea");t.value=e,t.setAttribute("readonly",""),t.style.position="fixed",t.style.left="-9999px",t.style.top="0",document.body.appendChild(t),t.focus(),t.select();const n=document.execCommand("copy");if(document.body.removeChild(t),!n)return!1}return i(e),c(),!0}catch{return!1}},[u,c]);return t.useEffect(()=>()=>{r.current&&clearTimeout(r.current)},[]),{copy:s,copiedText:o,isSupported:u}},e.useDebounceCallback=r,e.useDebounceValue=o,e.useEventListener=s,e.useHover=function(e){const[n,r]=t.useState(!1),o=t.useCallback(()=>{r(!0)},[]),i=t.useCallback(()=>{r(!1)},[]);return s("pointerenter",o,e),s("pointerleave",i,e),n},e.useIsClient=a,e.useIsTouchDevice=function(e={}){const{defaultDeviceType:n="unknown"}=e,[r,o]=t.useState(n),i=t.useCallback(()=>{"undefined"!=typeof window&&o(v())},[]);return t.useEffect(()=>{if("undefined"==typeof window)return;if(o(v()),"function"!=typeof window.matchMedia)return;const e=window.matchMedia(g);if("not all"===e.media)return;const t=e=>{o(e.matches?"touch":"desktop")};return e.addEventListener("change",t),()=>{e.removeEventListener("change",t)}},[]),t.useMemo(()=>({deviceType:r,isTouchDevice:"touch"===r,recheck:i}),[r,i])},e.useIsomorphicLayoutEffect=n,e.useLocalStorage=function(e,n,r={}){const{initializeWithValue:o=!0,serialize:i=JSON.stringify,deserialize:u=JSON.parse,listenToStorageChanges:c=!0}=r,s=t.useRef(n),a=t.useCallback(()=>{if("undefined"==typeof window)return s.current;if(!o)return s.current;try{const t=window.localStorage.getItem(e);return t?u(t):s.current}catch{return s.current}},[u,o,e]),[l,d]=t.useState(()=>a()),f=t.useCallback(t=>{d(n=>{const r="function"==typeof t?t(n):t;if("undefined"!=typeof window)try{window.localStorage.setItem(e,i(r))}catch{}return r})},[e,i]);return t.useEffect(()=>{d(a())},[a]),t.useEffect(()=>{if("undefined"==typeof window||!c)return;const t=t=>{if(t.key===e)if(null!==t.newValue)try{d(u(t.newValue))}catch{d(s.current)}else d(s.current)};return window.addEventListener("storage",t),()=>window.removeEventListener("storage",t)},[u,e,c]),[l,f]},e.useMap=l,e.useMediaQuery=u,e.useOnClickOutside=function(e,r,o,u){const c=t.useRef(r);n(()=>{c.current=r},[r]),t.useEffect(()=>{if("undefined"==typeof document)return;const t="undefined"!=typeof window&&void 0!==window.PointerEvent,n=o??(t?"pointerdown":"mousedown"),r=Array.isArray(e)?e:[e];let s=document;for(const e of r)if(e.current){const t=i(e.current);if(t!==document){s=t;break}}const a=e=>{const t=e.target;if("undefined"==typeof Node||!(t instanceof Node))return;r.some(e=>{const n=e.current;return!!n&&n.contains(t)})||c.current(e)};return s.addEventListener(n,a,u),()=>{s.removeEventListener(n,a,u)}},[o,u,e])},e.useOpenGraphExtractor=function(e){const n=t.useMemo(()=>e.skipPatterns??w,[e.skipPatterns]),r=t.useCallback(e=>n.some(t=>t.test(e)),[n]),o=t.useCallback((e,t,n)=>{var r,o,i;const{openGraph:u,htmlInferred:c,hybridGraph:s}=e,a=b(null==u?void 0:u.description,null==s?void 0:s.description,null==c?void 0:c.description),l=b(null==u?void 0:u.title,null==s?void 0:s.title,null==c?void 0:c.title),d=b(null==u?void 0:u.site_name,null==s?void 0:s.site_name,null==c?void 0:c.site_name),f=b(null==s?void 0:s.favicon,null==c?void 0:c.favicon),m=b((null==(r=null==u?void 0:u.image)?void 0:r.url)??void 0,(null==s?void 0:s.image)??void 0,(null==c?void 0:c.image)??void 0,null==(o=null==c?void 0:c.images)?void 0:o[0]),p=b((null==(i=null==u?void 0:u.video)?void 0:i.url)??void 0,(null==s?void 0:s.video)??void 0),w=b(null==s?void 0:s.videoType,null==c?void 0:c.videoType),g=b(n.url,(null==u?void 0:u.url)??void 0,(null==s?void 0:s.url)??void 0,(null==c?void 0:c.url)??void 0,n.finalUrl,n.normalizedUrl,n.requestedUrl)??"";return{description:a,favicon:f,image:m,video:p,videoType:w,siteName:d,title:l,url:g,siteHost:h(g)}},[]);return p({endpoint:"open-graph",options:e,selectData:o,shouldSkip:r})},e.usePlatformFromUrl=function(e){return t.useMemo(()=>{if(!e||"string"!=typeof e)return"unknown";const t=e.trim();if(!t)return"unknown";try{const e=new URL(t).hostname.toLowerCase(),n=c.get(e);return n||(e.endsWith(".substack.com")?"substack":e.endsWith(".github.io")?"github":e.includes("pinterest.com")?"pinterest":e.includes("eventbrite.")?"eventbrite":e.endsWith(".medium.com")?"medium":e.endsWith(".behance.net")?"behance":"unknown")}catch{return"unknown"}},[e])},e.usePrevious=function(e){const r=t.useRef();return n(()=>{r.current=e},[e]),r.current},e.useResizeObserver=function(e,r,o){const i=t.useRef(r),u=t.useRef(null),[c,s]=t.useState(null);return n(()=>{i.current=r},[r]),t.useEffect(()=>{if("undefined"==typeof ResizeObserver)return;const t="undefined"!=typeof Element&&e instanceof Element?e:(n=e)&&"object"==typeof n&&"current"in n?e.current:null;var n;if(!t)return;const r=new ResizeObserver(e=>{const t=e[0];u.current=t,i.current?i.current(t):s(t)});return r.observe(t,o),()=>r.disconnect()},[o,e]),i.current?u.current:c},e.useScreen=function(e={}){const{breakpoints:n,screenTypeMapping:r,defaultScreenType:o="UNKNOWN",defaultTailwindSize:i="default"}=e,c=t.useMemo(()=>({...y,...n}),[n]),s=t.useMemo(()=>({...k,...r}),[r]),[a,l]=t.useState(()=>E()),d=u(`(min-width: ${c.sm}px)`),f=u(`(min-width: ${c.md}px)`),m=u(`(min-width: ${c.lg}px)`),p=u(`(min-width: ${c.xl}px)`),w=u(`(min-width: ${c["2xl"]}px)`),b=t.useCallback(()=>{"undefined"!=typeof window&&l(E())},[]);t.useEffect(()=>{if("undefined"==typeof window)return;l(E());const e=()=>{l(E())};return window.addEventListener("resize",e),()=>window.removeEventListener("resize",e)},[]);const h=t.useMemo(()=>d||f||m||p||w?w?"2xl":p?"xl":m?"lg":f?"md":d?"sm":"default":a.width>0?function(e,t){return e>=t["2xl"]?"2xl":e>=t.xl?"xl":e>=t.lg?"lg":e>=t.md?"md":e>=t.sm?"sm":"default"}(a.width,c):i,[d,f,m,p,w,a.width,c,i]),g=t.useMemo(()=>h===i&&0===a.width?o:s[h],[h,s,a.width,o,i]);return t.useMemo(()=>({width:a.width,height:a.height,tailwindSize:h,screenType:g,refresh:b}),[a.width,a.height,h,g,b])},e.useSessionStorage=function(e,n,r={}){const{initializeWithValue:o=!0,serialize:i=JSON.stringify,deserialize:u=JSON.parse,listenToStorageChanges:c=!1}=r,s=t.useRef(n),a=t.useCallback(()=>{if("undefined"==typeof window)return s.current;if(!o)return s.current;try{const t=window.sessionStorage.getItem(e);return t?u(t):s.current}catch{return s.current}},[u,o,e]),[l,d]=t.useState(()=>a()),f=t.useCallback(t=>{d(n=>{const r="function"==typeof t?t(n):t;if("undefined"!=typeof window)try{window.sessionStorage.setItem(e,i(r))}catch{}return r})},[e,i]);return t.useEffect(()=>{d(a())},[a]),t.useEffect(()=>{if("undefined"==typeof window||!c)return;const t=t=>{if(t.key===e)if(null!==t.newValue)try{d(u(t.newValue))}catch{d(s.current)}else d(s.current)};return window.addEventListener("storage",t),()=>window.removeEventListener("storage",t)},[u,e,c]),[l,f]},e.useThrottle=function(e,n,r={}){const o=r.leading??!0,i=r.trailing??!0,u=Math.max(0,n),[c,s]=t.useState(e),a=t.useRef(0),l=t.useRef(null),d=t.useRef(null);return t.useEffect(()=>{if(0===u)return void s(e);const t=Date.now();if(0===a.current)return a.current=t,o?void s(e):void(i&&!l.current&&(d.current=e,l.current=setTimeout(()=>{l.current=null,null!==d.current&&(s(d.current),d.current=null,a.current=Date.now())},u)));const n=t-a.current;if(n>=u&&o)return s(e),a.current=t,void(d.current=null);if(i&&(d.current=e,!l.current)){const e=Math.max(u-n,0);l.current=setTimeout(()=>{l.current=null,null!==d.current&&(s(d.current),d.current=null,a.current=Date.now())},e)}},[o,i,e,u]),t.useEffect(()=>()=>{l.current&&clearTimeout(l.current)},[]),c},e.useWebsiteLinksExtractor=function(e){return p({endpoint:"links",options:e,selectData:t.useCallback(e=>({totalLinks:e.totalLinks??0,uniqueDomains:e.uniqueDomains??0,links:e.links??[]}),[])})},e.useWebsiteMetaExtractor=function(e){return p({endpoint:"meta",options:e,selectData:t.useCallback(e=>({title:e.title??void 0,description:e.description??void 0,language:e.language??void 0,canonicalUrl:e.canonicalUrl??void 0,feedUrl:e.feedUrl??null,textContentLength:e.textContentLength??void 0,metaTags:e.metaTags??{}}),[])})},e.useWebsiteRssExtractor=function(e){return p({endpoint:"rss",options:e,selectData:t.useCallback(e=>({feedUrl:e.feedUrl??null,feeds:e.feeds??[]}),[])})},e.useWebsiteSchemaExtractor=function(e){return p({endpoint:"schema",options:e,selectData:t.useCallback(e=>({schema:e.schema??[],schemaTypes:e.schemaTypes??[]}),[])})},Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})}); | ||
| //# sourceMappingURL=opensite-hooks.umd.js.map |
@@ -1,2 +0,2 @@ | ||
| !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).OpensiteHooks={},e.React)}(this,function(e,t){"use strict";const n="undefined"!=typeof window?t.useLayoutEffect:t.useEffect;function r(e,r,o={}){const i=t.useRef(e),u=t.useRef(null),c=t.useRef(null),s=t.useRef(null),a=o.leading??!1,l=o.trailing??!0,d=o.maxWait,m=Math.max(0,r);n(()=>{i.current=e},[e]);const f=t.useCallback(()=>{u.current&&(clearTimeout(u.current),u.current=null),c.current&&(clearTimeout(c.current),c.current=null)},[]),p=t.useCallback(()=>{if(!s.current)return;const e=s.current;s.current=null,i.current(...e)},[]),w=t.useCallback((...e)=>{s.current=e;if(a&&null===u.current&&null===c.current&&p(),u.current&&clearTimeout(u.current),l&&(u.current=setTimeout(()=>{u.current=null,s.current&&p(),c.current&&(clearTimeout(c.current),c.current=null)},m)),null!=d&&l&&!c.current){const e=Math.max(0,d);c.current=setTimeout(()=>{c.current=null,u.current&&(clearTimeout(u.current),u.current=null),s.current&&p()},e)}},[p,a,l,d,m]),b=t.useCallback(()=>{f(),s.current=null},[f]),g=t.useCallback(()=>{s.current&&(f(),p())},[f,p]);return t.useEffect(()=>()=>b(),[b]),{debouncedCallback:w,cancel:b,flush:g}}function o(e,n,o){const[i,u]=t.useState(e),{debouncedCallback:c,cancel:s}=r(e=>{u(e)},n,o);return t.useEffect(()=>{c(e)},[c,e]),t.useEffect(()=>()=>s(),[s]),i}function i(e){return(null==e?void 0:e.ownerDocument)??document}const u=new Map([["instagram.com","instagram"],["www.instagram.com","instagram"],["instagr.am","instagram"],["www.instagr.am","instagram"],["linkedin.com","linkedin"],["www.linkedin.com","linkedin"],["ca.linkedin.com","linkedin"],["uk.linkedin.com","linkedin"],["in.linkedin.com","linkedin"],["lnkd.in","linkedin"],["google.com","google"],["www.google.com","google"],["maps.google.com","google"],["goo.gl","google"],["maps.app.goo.gl","google"],["g.co","google"],["facebook.com","facebook"],["www.facebook.com","facebook"],["m.facebook.com","facebook"],["fb.com","facebook"],["fb.me","facebook"],["on.fb.me","facebook"],["tiktok.com","tiktok"],["www.tiktok.com","tiktok"],["m.tiktok.com","tiktok"],["vm.tiktok.com","tiktok"],["vt.tiktok.com","tiktok"],["youtube.com","youtube"],["www.youtube.com","youtube"],["m.youtube.com","youtube"],["youtu.be","youtube"],["yelp.com","yelp"],["www.yelp.com","yelp"],["m.yelp.com","yelp"],["spotify.com","spotify"],["www.spotify.com","spotify"],["open.spotify.com","spotify"],["play.spotify.com","spotify"],["spoti.fi","spotify"],["spotify.link","spotify"],["apple.com","apple"],["www.apple.com","apple"],["music.apple.com","apple"],["podcasts.apple.com","apple"],["apps.apple.com","apple"],["itunes.apple.com","apple"],["x.com","x"],["www.x.com","x"],["twitter.com","x"],["www.twitter.com","x"],["t.co","x"],["github.com","github"],["www.github.com","github"],["gist.github.com","github"],["raw.githubusercontent.com","github"],["github.io","github"],["discord.com","discord"],["www.discord.com","discord"],["discord.gg","discord"],["discordapp.com","discord"],["discordapp.net","discord"],["discord.new","discord"],["discord.gift","discord"],["discord.gifts","discord"],["dis.gd","discord"],["snapchat.com","snapchat"],["www.snapchat.com","snapchat"],["snap.com","snapchat"],["www.snap.com","snapchat"],["story.snapchat.com","snapchat"],["web.snapchat.com","snapchat"],["dev.to","dev"],["www.dev.to","dev"],["substack.com","substack"],["www.substack.com","substack"],["reddit.com","reddit"],["www.reddit.com","reddit"],["old.reddit.com","reddit"],["new.reddit.com","reddit"],["i.redd.it","reddit"],["v.redd.it","reddit"],["redd.it","reddit"],["preview.redd.it","reddit"],["pinterest.com","pinterest"],["www.pinterest.com","pinterest"],["pin.it","pinterest"],["in.pinterest.com","pinterest"],["br.pinterest.com","pinterest"],["uk.pinterest.com","pinterest"],["threads.net","threads"],["www.threads.net","threads"],["threads.com","threads"],["www.threads.com","threads"],["twitch.tv","twitch"],["www.twitch.tv","twitch"],["m.twitch.tv","twitch"],["whatsapp.com","whatsapp"],["www.whatsapp.com","whatsapp"],["wa.me","whatsapp"],["web.whatsapp.com","whatsapp"],["telegram.org","telegram"],["www.telegram.org","telegram"],["t.me","telegram"],["telegram.me","telegram"],["telegram.dog","telegram"],["medium.com","medium"],["www.medium.com","medium"],["patreon.com","patreon"],["www.patreon.com","patreon"],["onlyfans.com","onlyfans"],["www.onlyfans.com","onlyfans"],["eventbrite.com","eventbrite"],["www.eventbrite.com","eventbrite"],["eventbrite.co.uk","eventbrite"],["eventbrite.com.au","eventbrite"],["eventbrite.ca","eventbrite"],["eventbrite.de","eventbrite"],["eventbrite.fr","eventbrite"],["eventbrite.es","eventbrite"],["eventbrite.it","eventbrite"],["eventbrite.ie","eventbrite"],["eventbrite.nl","eventbrite"],["eventbrite.co.nz","eventbrite"],["eventbriteapi.com","eventbrite"],["evbuc.com","eventbrite"],["npmjs.com","npmjs"],["www.npmjs.com","npmjs"],["npmjs.org","npmjs"],["www.npmjs.org","npmjs"],["registry.npmjs.org","npmjs"],["registry.npmjs.com","npmjs"],["replicate.npmjs.com","npmjs"],["crates.io","crates"],["www.crates.io","crates"],["rubygems.org","rubygems"],["www.rubygems.org","rubygems"],["behance.net","behance"],["www.behance.net","behance"],["portfolio.behance.net","behance"],["mir-s3-cdn-cf.behance.net","behance"],["dribbble.com","dribbble"],["www.dribbble.com","dribbble"],["drbl.in","dribbble"]]);function c(e,r,o,i){const u=t.useRef(r);n(()=>{u.current=r},[r]),t.useEffect(()=>{const t="undefined"!=typeof Window&&o instanceof Window,n="undefined"!=typeof Document&&o instanceof Document,r=void 0===o?"undefined"!=typeof window?window:null:t||n||"undefined"!=typeof HTMLElement&&o instanceof HTMLElement?o:(c=o)&&"object"==typeof c&&"current"in c?o.current:null;var c;if(!(null==r?void 0:r.addEventListener))return;const s=e=>{const t=u.current;"function"==typeof t?t(e):t.handleEvent(e)};return r.addEventListener(e,s,i),()=>{r.removeEventListener(e,s,i)}},[e,o,i])}function s(){const[e,n]=t.useState(!1);return t.useEffect(()=>{n(!0)},[]),e}function a(e){const[r,o]=t.useState(()=>e instanceof Map||Array.isArray(e)?new Map(e):new Map),i=t.useRef(r);n(()=>{i.current=r},[r]);const u=t.useMemo(()=>({set:(e,t)=>{o(n=>{const r=new Map(n);return r.set(e,t),r})},setAll:e=>{o((Map,new Map(e)))},remove:e=>{o(t=>{const n=new Map(t);return n.delete(e),n})},clear:()=>o(new Map),get:e=>i.current.get(e),has:e=>i.current.has(e)}),[]);return[r,u]}const l="https://octane.buzz";function d(e){const t=function(e){return e.replace(/\/+$/,"")}(e.baseUrl??l),n=new URLSearchParams;return e.apiKey&&n.set("api_key",e.apiKey),n.set("url",e.url),`${t}/api/v1/extract/${e.endpoint}?${n.toString()}`}async function m(e){var t;if(!e.url||0===e.url.trim().length)return{ok:!1,error:{message:"URL is required."}};const n=d(e);try{const t=await fetch(n,{method:"GET",signal:e.signal});let r=null;try{r=await t.json()}catch{r=null}if(!t.ok){const e=r;return{ok:!1,error:{message:(null==e?void 0:e.error)??`Request failed with status ${t.status}.`,status:(null==e?void 0:e.status)??t.status,raw:r}}}return{ok:!0,response:r}}catch(r){return(null==(t=e.signal)?void 0:t.aborted)?{ok:!1,error:{message:"Request aborted."}}:{ok:!1,error:{message:r instanceof Error?r.message:"Request failed unexpectedly.",raw:r}}}}function f(e){const{endpoint:n,options:i,selectData:u,shouldSkip:c}=e,d=s(),[f,p]=t.useState({loading:!1}),[,w]=a(),b=i.cache??true,g=i.enabled??true,v=i.debounceMs??250,y=i.refreshDebounceMs??150,h=o(t.useMemo(()=>{var e;return(null==(e=i.url)?void 0:e.trim())??""},[i.url]),v),k=t.useRef(0),E=t.useRef(0),[C,T]=t.useState(0),{debouncedCallback:x,cancel:S}=r(()=>{k.current+=1,T(k.current)},y),M=t.useCallback(()=>{x()},[x]);t.useEffect(()=>()=>S(),[S]);const R=t.useMemo(()=>{if(!h)return"";const e=i.baseUrl??l,t=i.apiKey??"";return`${n}:${e}:${t}:${h}`},[n,i.apiKey,i.baseUrl,h]),L=t.useRef(null);return t.useEffect(()=>{var e;if(!d)return;if(!g||!h)return void p({loading:!1});if(null==c?void 0:c(h))return void p({loading:!1});const t=C!==E.current;if(t&&(E.current=C),b&&!t){const e=w.get(R);if(e)return void p({loading:!1,data:e.data,raw:e.raw,meta:e.meta})}null==(e=L.current)||e.abort();const r=new AbortController;return L.current=r,p(e=>({...e,loading:!0,error:void 0})),m({endpoint:n,url:h,apiKey:i.apiKey,baseUrl:i.baseUrl,signal:r.signal}).then(e=>{if(r.signal.aborted)return;if(!e.ok)return void p(t=>({...t,loading:!1,error:e.error}));const t=e.response,n=function(e){const{requestedUrl:t,finalUrl:n,url:r,normalizedUrl:o,status:i,contentType:u,fetchedAt:c,bodyBytes:s,bodyTruncated:a,maxBodyBytes:l,cache:d}=e;return{requestedUrl:t,finalUrl:n,url:r,normalizedUrl:o,status:i,contentType:u,fetchedAt:c,bodyBytes:s,bodyTruncated:a,maxBodyBytes:l,cache:d}}(t),o=function(e){const{requestedUrl:t,finalUrl:n,url:r,normalizedUrl:o,status:i,contentType:u,fetchedAt:c,bodyBytes:s,bodyTruncated:a,maxBodyBytes:l,cache:d,...m}=e;return m}(t),i=u(o,t,n);b&&w.set(R,{data:i,raw:t,meta:n}),p({loading:!1,data:i,raw:t,meta:n})}).catch(e=>{r.signal.aborted||p(t=>({...t,loading:!1,error:{message:e instanceof Error?e.message:"Request failed unexpectedly.",raw:e}}))}),()=>{r.abort()}},[w,b,h,g,n,d,i.apiKey,i.baseUrl,R,C,u,c]),t.useMemo(()=>({...f,refresh:M}),[M,f])}const p=[/search\.google\.com\/local\/reviews/i,/google\.com\/maps\/place/i,/maps\.google\.com/i,/opentable\.com/i],w=(...e)=>{for(const t of e)if("string"==typeof t&&t.trim().length>0)return t},b=e=>{if(e)try{return new URL(e).hostname}catch{return}};e.buildWebsiteExtractorUrl=d,e.fetchWebsiteExtractor=m,e.useBoolean=function(e=!1){const[n,r]=t.useState(e),o=t.useCallback(()=>r(!0),[]),i=t.useCallback(()=>r(!1),[]),u=t.useCallback(()=>r(e=>!e),[]);return t.useMemo(()=>({value:n,setValue:r,setTrue:o,setFalse:i,toggle:u}),[n,r,o,i,u])},e.useCopyToClipboard=function(e={}){const n=e.resetDelay??2e3,r=t.useRef(null),[o,i]=t.useState(null),u=t.useMemo(()=>!("undefined"==typeof navigator||!navigator.clipboard)||"undefined"!=typeof document&&("function"==typeof document.queryCommandSupported&&document.queryCommandSupported("copy")),[]),c=t.useCallback(()=>{r.current&&clearTimeout(r.current),r.current=setTimeout(()=>{i(null)},n)},[n]),s=t.useCallback(async e=>{if(!u)return!1;const t="undefined"!=typeof navigator&&!!navigator.clipboard;try{if(t)await navigator.clipboard.writeText(e);else if("undefined"!=typeof document){const t=document.createElement("textarea");t.value=e,t.setAttribute("readonly",""),t.style.position="fixed",t.style.left="-9999px",t.style.top="0",document.body.appendChild(t),t.focus(),t.select();const n=document.execCommand("copy");if(document.body.removeChild(t),!n)return!1}return i(e),c(),!0}catch{return!1}},[u,c]);return t.useEffect(()=>()=>{r.current&&clearTimeout(r.current)},[]),{copy:s,copiedText:o,isSupported:u}},e.useDebounceCallback=r,e.useDebounceValue=o,e.useEventListener=c,e.useHover=function(e){const[n,r]=t.useState(!1),o=t.useCallback(()=>{r(!0)},[]),i=t.useCallback(()=>{r(!1)},[]);return c("pointerenter",o,e),c("pointerleave",i,e),n},e.useIsClient=s,e.useIsomorphicLayoutEffect=n,e.useLocalStorage=function(e,n,r={}){const{initializeWithValue:o=!0,serialize:i=JSON.stringify,deserialize:u=JSON.parse,listenToStorageChanges:c=!0}=r,s=t.useRef(n),a=t.useCallback(()=>{if("undefined"==typeof window)return s.current;if(!o)return s.current;try{const t=window.localStorage.getItem(e);return t?u(t):s.current}catch{return s.current}},[u,o,e]),[l,d]=t.useState(()=>a()),m=t.useCallback(t=>{d(n=>{const r="function"==typeof t?t(n):t;if("undefined"!=typeof window)try{window.localStorage.setItem(e,i(r))}catch{}return r})},[e,i]);return t.useEffect(()=>{d(a())},[a]),t.useEffect(()=>{if("undefined"==typeof window||!c)return;const t=t=>{if(t.key===e)if(null!==t.newValue)try{d(u(t.newValue))}catch{d(s.current)}else d(s.current)};return window.addEventListener("storage",t),()=>window.removeEventListener("storage",t)},[u,e,c]),[l,m]},e.useMap=a,e.useMediaQuery=function(e,n={}){const[r,o]=t.useState(()=>"undefined"==typeof window||"function"!=typeof window.matchMedia?n.defaultValue??!1:window.matchMedia(e).matches);return t.useEffect(()=>{if("undefined"==typeof window||"function"!=typeof window.matchMedia)return;const t=window.matchMedia(e),n=e=>{o(e.matches)};return o(t.matches),t.addEventListener?(t.addEventListener("change",n),()=>t.removeEventListener("change",n)):(t.addListener(n),()=>t.removeListener(n))},[e]),r},e.useOnClickOutside=function(e,r,o,u){const c=t.useRef(r);n(()=>{c.current=r},[r]),t.useEffect(()=>{if("undefined"==typeof document)return;const t="undefined"!=typeof window&&void 0!==window.PointerEvent,n=o??(t?"pointerdown":"mousedown"),r=Array.isArray(e)?e:[e];let s=document;for(const e of r)if(e.current){const t=i(e.current);if(t!==document){s=t;break}}const a=e=>{const t=e.target;if("undefined"==typeof Node||!(t instanceof Node))return;r.some(e=>{const n=e.current;return!!n&&n.contains(t)})||c.current(e)};return s.addEventListener(n,a,u),()=>{s.removeEventListener(n,a,u)}},[o,u,e])},e.useOpenGraphExtractor=function(e){const n=t.useMemo(()=>e.skipPatterns??p,[e.skipPatterns]),r=t.useCallback(e=>n.some(t=>t.test(e)),[n]),o=t.useCallback((e,t,n)=>{var r,o,i;const{openGraph:u,htmlInferred:c,hybridGraph:s}=e,a=w(null==u?void 0:u.description,null==s?void 0:s.description,null==c?void 0:c.description),l=w(null==u?void 0:u.title,null==s?void 0:s.title,null==c?void 0:c.title),d=w(null==u?void 0:u.site_name,null==s?void 0:s.site_name,null==c?void 0:c.site_name),m=w(null==s?void 0:s.favicon,null==c?void 0:c.favicon),f=w((null==(r=null==u?void 0:u.image)?void 0:r.url)??void 0,(null==s?void 0:s.image)??void 0,(null==c?void 0:c.image)??void 0,null==(o=null==c?void 0:c.images)?void 0:o[0]),p=w((null==(i=null==u?void 0:u.video)?void 0:i.url)??void 0,(null==s?void 0:s.video)??void 0),g=w(null==s?void 0:s.videoType,null==c?void 0:c.videoType),v=w(n.url,(null==u?void 0:u.url)??void 0,(null==s?void 0:s.url)??void 0,(null==c?void 0:c.url)??void 0,n.finalUrl,n.normalizedUrl,n.requestedUrl)??"";return{description:a,favicon:m,image:f,video:p,videoType:g,siteName:d,title:l,url:v,siteHost:b(v)}},[]);return f({endpoint:"open-graph",options:e,selectData:o,shouldSkip:r})},e.usePlatformFromUrl=function(e){return t.useMemo(()=>{if(!e||"string"!=typeof e)return"unknown";const t=e.trim();if(!t)return"unknown";try{const e=new URL(t).hostname.toLowerCase(),n=u.get(e);return n||(e.endsWith(".substack.com")?"substack":e.endsWith(".github.io")?"github":e.includes("pinterest.com")?"pinterest":e.includes("eventbrite.")?"eventbrite":e.endsWith(".medium.com")?"medium":e.endsWith(".behance.net")?"behance":"unknown")}catch{return"unknown"}},[e])},e.usePrevious=function(e){const r=t.useRef();return n(()=>{r.current=e},[e]),r.current},e.useResizeObserver=function(e,r,o){const i=t.useRef(r),u=t.useRef(null),[c,s]=t.useState(null);return n(()=>{i.current=r},[r]),t.useEffect(()=>{if("undefined"==typeof ResizeObserver)return;const t="undefined"!=typeof Element&&e instanceof Element?e:(n=e)&&"object"==typeof n&&"current"in n?e.current:null;var n;if(!t)return;const r=new ResizeObserver(e=>{const t=e[0];u.current=t,i.current?i.current(t):s(t)});return r.observe(t,o),()=>r.disconnect()},[o,e]),i.current?u.current:c},e.useSessionStorage=function(e,n,r={}){const{initializeWithValue:o=!0,serialize:i=JSON.stringify,deserialize:u=JSON.parse,listenToStorageChanges:c=!1}=r,s=t.useRef(n),a=t.useCallback(()=>{if("undefined"==typeof window)return s.current;if(!o)return s.current;try{const t=window.sessionStorage.getItem(e);return t?u(t):s.current}catch{return s.current}},[u,o,e]),[l,d]=t.useState(()=>a()),m=t.useCallback(t=>{d(n=>{const r="function"==typeof t?t(n):t;if("undefined"!=typeof window)try{window.sessionStorage.setItem(e,i(r))}catch{}return r})},[e,i]);return t.useEffect(()=>{d(a())},[a]),t.useEffect(()=>{if("undefined"==typeof window||!c)return;const t=t=>{if(t.key===e)if(null!==t.newValue)try{d(u(t.newValue))}catch{d(s.current)}else d(s.current)};return window.addEventListener("storage",t),()=>window.removeEventListener("storage",t)},[u,e,c]),[l,m]},e.useThrottle=function(e,n,r={}){const o=r.leading??!0,i=r.trailing??!0,u=Math.max(0,n),[c,s]=t.useState(e),a=t.useRef(0),l=t.useRef(null),d=t.useRef(null);return t.useEffect(()=>{if(0===u)return void s(e);const t=Date.now();if(0===a.current)return a.current=t,o?void s(e):void(i&&!l.current&&(d.current=e,l.current=setTimeout(()=>{l.current=null,null!==d.current&&(s(d.current),d.current=null,a.current=Date.now())},u)));const n=t-a.current;if(n>=u&&o)return s(e),a.current=t,void(d.current=null);if(i&&(d.current=e,!l.current)){const e=Math.max(u-n,0);l.current=setTimeout(()=>{l.current=null,null!==d.current&&(s(d.current),d.current=null,a.current=Date.now())},e)}},[o,i,e,u]),t.useEffect(()=>()=>{l.current&&clearTimeout(l.current)},[]),c},e.useWebsiteLinksExtractor=function(e){return f({endpoint:"links",options:e,selectData:t.useCallback(e=>({totalLinks:e.totalLinks??0,uniqueDomains:e.uniqueDomains??0,links:e.links??[]}),[])})},e.useWebsiteMetaExtractor=function(e){return f({endpoint:"meta",options:e,selectData:t.useCallback(e=>({title:e.title??void 0,description:e.description??void 0,language:e.language??void 0,canonicalUrl:e.canonicalUrl??void 0,feedUrl:e.feedUrl??null,textContentLength:e.textContentLength??void 0,metaTags:e.metaTags??{}}),[])})},e.useWebsiteRssExtractor=function(e){return f({endpoint:"rss",options:e,selectData:t.useCallback(e=>({feedUrl:e.feedUrl??null,feeds:e.feeds??[]}),[])})},e.useWebsiteSchemaExtractor=function(e){return f({endpoint:"schema",options:e,selectData:t.useCallback(e=>({schema:e.schema??[],schemaTypes:e.schemaTypes??[]}),[])})},Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})}); | ||
| !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).OpensiteHooks={},e.React)}(this,function(e,t){"use strict";const n="undefined"!=typeof window?t.useLayoutEffect:t.useEffect;function r(e,r,o={}){const i=t.useRef(e),u=t.useRef(null),c=t.useRef(null),s=t.useRef(null),a=o.leading??!1,l=o.trailing??!0,d=o.maxWait,f=Math.max(0,r);n(()=>{i.current=e},[e]);const m=t.useCallback(()=>{u.current&&(clearTimeout(u.current),u.current=null),c.current&&(clearTimeout(c.current),c.current=null)},[]),p=t.useCallback(()=>{if(!s.current)return;const e=s.current;s.current=null,i.current(...e)},[]),w=t.useCallback((...e)=>{s.current=e;if(a&&null===u.current&&null===c.current&&p(),u.current&&clearTimeout(u.current),l&&(u.current=setTimeout(()=>{u.current=null,s.current&&p(),c.current&&(clearTimeout(c.current),c.current=null)},f)),null!=d&&l&&!c.current){const e=Math.max(0,d);c.current=setTimeout(()=>{c.current=null,u.current&&(clearTimeout(u.current),u.current=null),s.current&&p()},e)}},[p,a,l,d,f]),b=t.useCallback(()=>{m(),s.current=null},[m]),h=t.useCallback(()=>{s.current&&(m(),p())},[m,p]);return t.useEffect(()=>()=>b(),[b]),{debouncedCallback:w,cancel:b,flush:h}}function o(e,n,o){const[i,u]=t.useState(e),{debouncedCallback:c,cancel:s}=r(e=>{u(e)},n,o);return t.useEffect(()=>{c(e)},[c,e]),t.useEffect(()=>()=>s(),[s]),i}function i(e){return(null==e?void 0:e.ownerDocument)??document}function u(e,n={}){const[r,o]=t.useState(()=>"undefined"==typeof window||"function"!=typeof window.matchMedia?n.defaultValue??!1:window.matchMedia(e).matches);return t.useEffect(()=>{if("undefined"==typeof window||"function"!=typeof window.matchMedia)return;const t=window.matchMedia(e),n=e=>{o(e.matches)};return o(t.matches),t.addEventListener("change",n),()=>t.removeEventListener("change",n)},[e]),r}const c=new Map([["instagram.com","instagram"],["www.instagram.com","instagram"],["instagr.am","instagram"],["www.instagr.am","instagram"],["linkedin.com","linkedin"],["www.linkedin.com","linkedin"],["ca.linkedin.com","linkedin"],["uk.linkedin.com","linkedin"],["in.linkedin.com","linkedin"],["lnkd.in","linkedin"],["google.com","google"],["www.google.com","google"],["maps.google.com","google"],["goo.gl","google"],["maps.app.goo.gl","google"],["g.co","google"],["facebook.com","facebook"],["www.facebook.com","facebook"],["m.facebook.com","facebook"],["fb.com","facebook"],["fb.me","facebook"],["on.fb.me","facebook"],["tiktok.com","tiktok"],["www.tiktok.com","tiktok"],["m.tiktok.com","tiktok"],["vm.tiktok.com","tiktok"],["vt.tiktok.com","tiktok"],["youtube.com","youtube"],["www.youtube.com","youtube"],["m.youtube.com","youtube"],["youtu.be","youtube"],["yelp.com","yelp"],["www.yelp.com","yelp"],["m.yelp.com","yelp"],["spotify.com","spotify"],["www.spotify.com","spotify"],["open.spotify.com","spotify"],["play.spotify.com","spotify"],["spoti.fi","spotify"],["spotify.link","spotify"],["apple.com","apple"],["www.apple.com","apple"],["music.apple.com","apple"],["podcasts.apple.com","apple"],["apps.apple.com","apple"],["itunes.apple.com","apple"],["x.com","x"],["www.x.com","x"],["twitter.com","x"],["www.twitter.com","x"],["t.co","x"],["github.com","github"],["www.github.com","github"],["gist.github.com","github"],["raw.githubusercontent.com","github"],["github.io","github"],["discord.com","discord"],["www.discord.com","discord"],["discord.gg","discord"],["discordapp.com","discord"],["discordapp.net","discord"],["discord.new","discord"],["discord.gift","discord"],["discord.gifts","discord"],["dis.gd","discord"],["snapchat.com","snapchat"],["www.snapchat.com","snapchat"],["snap.com","snapchat"],["www.snap.com","snapchat"],["story.snapchat.com","snapchat"],["web.snapchat.com","snapchat"],["dev.to","dev"],["www.dev.to","dev"],["substack.com","substack"],["www.substack.com","substack"],["reddit.com","reddit"],["www.reddit.com","reddit"],["old.reddit.com","reddit"],["new.reddit.com","reddit"],["i.redd.it","reddit"],["v.redd.it","reddit"],["redd.it","reddit"],["preview.redd.it","reddit"],["pinterest.com","pinterest"],["www.pinterest.com","pinterest"],["pin.it","pinterest"],["in.pinterest.com","pinterest"],["br.pinterest.com","pinterest"],["uk.pinterest.com","pinterest"],["threads.net","threads"],["www.threads.net","threads"],["threads.com","threads"],["www.threads.com","threads"],["twitch.tv","twitch"],["www.twitch.tv","twitch"],["m.twitch.tv","twitch"],["whatsapp.com","whatsapp"],["www.whatsapp.com","whatsapp"],["wa.me","whatsapp"],["web.whatsapp.com","whatsapp"],["telegram.org","telegram"],["www.telegram.org","telegram"],["t.me","telegram"],["telegram.me","telegram"],["telegram.dog","telegram"],["medium.com","medium"],["www.medium.com","medium"],["patreon.com","patreon"],["www.patreon.com","patreon"],["onlyfans.com","onlyfans"],["www.onlyfans.com","onlyfans"],["eventbrite.com","eventbrite"],["www.eventbrite.com","eventbrite"],["eventbrite.co.uk","eventbrite"],["eventbrite.com.au","eventbrite"],["eventbrite.ca","eventbrite"],["eventbrite.de","eventbrite"],["eventbrite.fr","eventbrite"],["eventbrite.es","eventbrite"],["eventbrite.it","eventbrite"],["eventbrite.ie","eventbrite"],["eventbrite.nl","eventbrite"],["eventbrite.co.nz","eventbrite"],["eventbriteapi.com","eventbrite"],["evbuc.com","eventbrite"],["npmjs.com","npmjs"],["www.npmjs.com","npmjs"],["npmjs.org","npmjs"],["www.npmjs.org","npmjs"],["registry.npmjs.org","npmjs"],["registry.npmjs.com","npmjs"],["replicate.npmjs.com","npmjs"],["crates.io","crates"],["www.crates.io","crates"],["rubygems.org","rubygems"],["www.rubygems.org","rubygems"],["behance.net","behance"],["www.behance.net","behance"],["portfolio.behance.net","behance"],["mir-s3-cdn-cf.behance.net","behance"],["dribbble.com","dribbble"],["www.dribbble.com","dribbble"],["drbl.in","dribbble"]]);function s(e,r,o,i){const u=t.useRef(r);n(()=>{u.current=r},[r]),t.useEffect(()=>{const t="undefined"!=typeof Window&&o instanceof Window,n="undefined"!=typeof Document&&o instanceof Document,r=void 0===o?"undefined"!=typeof window?window:null:t||n||"undefined"!=typeof HTMLElement&&o instanceof HTMLElement?o:(c=o)&&"object"==typeof c&&"current"in c?o.current:null;var c;if(!(null==r?void 0:r.addEventListener))return;const s=e=>{const t=u.current;"function"==typeof t?t(e):t.handleEvent(e)};return r.addEventListener(e,s,i),()=>{r.removeEventListener(e,s,i)}},[e,o,i])}function a(){const[e,n]=t.useState(!1);return t.useEffect(()=>{n(!0)},[]),e}function l(e){const[r,o]=t.useState(()=>e instanceof Map||Array.isArray(e)?new Map(e):new Map),i=t.useRef(r);n(()=>{i.current=r},[r]);const u=t.useMemo(()=>({set:(e,t)=>{o(n=>{const r=new Map(n);return r.set(e,t),r})},setAll:e=>{o((Map,new Map(e)))},remove:e=>{o(t=>{const n=new Map(t);return n.delete(e),n})},clear:()=>o(new Map),get:e=>i.current.get(e),has:e=>i.current.has(e)}),[]);return[r,u]}const d="https://octane.buzz";function f(e){const t=function(e){return e.replace(/\/+$/,"")}(e.baseUrl??d),n=new URLSearchParams;return e.apiKey&&n.set("api_key",e.apiKey),n.set("url",e.url),`${t}/api/v1/extract/${e.endpoint}?${n.toString()}`}async function m(e){var t;if(!e.url||0===e.url.trim().length)return{ok:!1,error:{message:"URL is required."}};const n=f(e);try{const t=await fetch(n,{method:"GET",signal:e.signal});let r=null;try{r=await t.json()}catch{r=null}if(!t.ok){const e=r;return{ok:!1,error:{message:(null==e?void 0:e.error)??`Request failed with status ${t.status}.`,status:(null==e?void 0:e.status)??t.status,raw:r}}}return{ok:!0,response:r}}catch(r){return(null==(t=e.signal)?void 0:t.aborted)?{ok:!1,error:{message:"Request aborted."}}:{ok:!1,error:{message:r instanceof Error?r.message:"Request failed unexpectedly.",raw:r}}}}function p(e){const{endpoint:n,options:i,selectData:u,shouldSkip:c}=e,s=a(),[f,p]=t.useState({loading:!1}),[,w]=l(),b=i.cache??true,h=i.enabled??true,g=i.debounceMs??250,v=i.refreshDebounceMs??150,y=o(t.useMemo(()=>{var e;return(null==(e=i.url)?void 0:e.trim())??""},[i.url]),g),k=t.useRef(0),E=t.useRef(0),[x,T]=t.useState(0),{debouncedCallback:M,cancel:S}=r(()=>{k.current+=1,T(k.current)},v),C=t.useCallback(()=>{M()},[M]);t.useEffect(()=>()=>S(),[S]);const L=t.useMemo(()=>{if(!y)return"";const e=i.baseUrl??d,t=i.apiKey??"";return`${n}:${e}:${t}:${y}`},[n,i.apiKey,i.baseUrl,y]),R=t.useRef(null);return t.useEffect(()=>{var e;if(!s)return;if(!h||!y)return void p({loading:!1});if(null==c?void 0:c(y))return void p({loading:!1});const t=x!==E.current;if(t&&(E.current=x),b&&!t){const e=w.get(L);if(e)return void p({loading:!1,data:e.data,raw:e.raw,meta:e.meta})}null==(e=R.current)||e.abort();const r=new AbortController;return R.current=r,p(e=>({...e,loading:!0,error:void 0})),m({endpoint:n,url:y,apiKey:i.apiKey,baseUrl:i.baseUrl,signal:r.signal}).then(e=>{if(r.signal.aborted)return;if(!e.ok)return void p(t=>({...t,loading:!1,error:e.error}));const t=e.response,n=function(e){const{requestedUrl:t,finalUrl:n,url:r,normalizedUrl:o,status:i,contentType:u,fetchedAt:c,bodyBytes:s,bodyTruncated:a,maxBodyBytes:l,cache:d}=e;return{requestedUrl:t,finalUrl:n,url:r,normalizedUrl:o,status:i,contentType:u,fetchedAt:c,bodyBytes:s,bodyTruncated:a,maxBodyBytes:l,cache:d}}(t),o=function(e){const{requestedUrl:t,finalUrl:n,url:r,normalizedUrl:o,status:i,contentType:u,fetchedAt:c,bodyBytes:s,bodyTruncated:a,maxBodyBytes:l,cache:d,...f}=e;return f}(t),i=u(o,t,n);b&&w.set(L,{data:i,raw:t,meta:n}),p({loading:!1,data:i,raw:t,meta:n})}).catch(e=>{r.signal.aborted||p(t=>({...t,loading:!1,error:{message:e instanceof Error?e.message:"Request failed unexpectedly.",raw:e}}))}),()=>{r.abort()}},[w,b,y,h,n,s,i.apiKey,i.baseUrl,L,x,u,c]),t.useMemo(()=>({...f,refresh:C}),[C,f])}const w=[/search\.google\.com\/local\/reviews/i,/google\.com\/maps\/place/i,/maps\.google\.com/i,/opentable\.com/i],b=(...e)=>{for(const t of e)if("string"==typeof t&&t.trim().length>0)return t},h=e=>{if(e)try{return new URL(e).hostname}catch{return}};const g="(hover: none) and (pointer: coarse)";function v(){if("function"==typeof window.matchMedia){const e=window.matchMedia(g);if("not all"!==e.media)return e.matches?"touch":"desktop"}return"ontouchstart"in window||"undefined"!=typeof navigator&&"number"==typeof navigator.maxTouchPoints&&navigator.maxTouchPoints>0?"touch":"desktop"}const y={sm:640,md:768,lg:1024,xl:1280,"2xl":1536},k={default:"MOBILE",sm:"MOBILE",md:"TABLET",lg:"DESKTOP",xl:"DESKTOP","2xl":"DESKTOP"};function E(){return"undefined"==typeof window?{width:0,height:0}:{width:window.innerWidth,height:window.innerHeight}}e.buildWebsiteExtractorUrl=f,e.fetchWebsiteExtractor=m,e.useBoolean=function(e=!1){const[n,r]=t.useState(e),o=t.useCallback(()=>r(!0),[]),i=t.useCallback(()=>r(!1),[]),u=t.useCallback(()=>r(e=>!e),[]);return t.useMemo(()=>({value:n,setValue:r,setTrue:o,setFalse:i,toggle:u}),[n,r,o,i,u])},e.useCopyToClipboard=function(e={}){const n=e.resetDelay??2e3,r=t.useRef(null),[o,i]=t.useState(null),u=t.useMemo(()=>!("undefined"==typeof navigator||!navigator.clipboard)||"undefined"!=typeof document&&("function"==typeof document.queryCommandSupported&&document.queryCommandSupported("copy")),[]),c=t.useCallback(()=>{r.current&&clearTimeout(r.current),r.current=setTimeout(()=>{i(null)},n)},[n]),s=t.useCallback(async e=>{if(!u)return!1;const t="undefined"!=typeof navigator&&!!navigator.clipboard;try{if(t)await navigator.clipboard.writeText(e);else if("undefined"!=typeof document){const t=document.createElement("textarea");t.value=e,t.setAttribute("readonly",""),t.style.position="fixed",t.style.left="-9999px",t.style.top="0",document.body.appendChild(t),t.focus(),t.select();const n=document.execCommand("copy");if(document.body.removeChild(t),!n)return!1}return i(e),c(),!0}catch{return!1}},[u,c]);return t.useEffect(()=>()=>{r.current&&clearTimeout(r.current)},[]),{copy:s,copiedText:o,isSupported:u}},e.useDebounceCallback=r,e.useDebounceValue=o,e.useEventListener=s,e.useHover=function(e){const[n,r]=t.useState(!1),o=t.useCallback(()=>{r(!0)},[]),i=t.useCallback(()=>{r(!1)},[]);return s("pointerenter",o,e),s("pointerleave",i,e),n},e.useIsClient=a,e.useIsTouchDevice=function(e={}){const{defaultDeviceType:n="unknown"}=e,[r,o]=t.useState(n),i=t.useCallback(()=>{"undefined"!=typeof window&&o(v())},[]);return t.useEffect(()=>{if("undefined"==typeof window)return;if(o(v()),"function"!=typeof window.matchMedia)return;const e=window.matchMedia(g);if("not all"===e.media)return;const t=e=>{o(e.matches?"touch":"desktop")};return e.addEventListener("change",t),()=>{e.removeEventListener("change",t)}},[]),t.useMemo(()=>({deviceType:r,isTouchDevice:"touch"===r,recheck:i}),[r,i])},e.useIsomorphicLayoutEffect=n,e.useLocalStorage=function(e,n,r={}){const{initializeWithValue:o=!0,serialize:i=JSON.stringify,deserialize:u=JSON.parse,listenToStorageChanges:c=!0}=r,s=t.useRef(n),a=t.useCallback(()=>{if("undefined"==typeof window)return s.current;if(!o)return s.current;try{const t=window.localStorage.getItem(e);return t?u(t):s.current}catch{return s.current}},[u,o,e]),[l,d]=t.useState(()=>a()),f=t.useCallback(t=>{d(n=>{const r="function"==typeof t?t(n):t;if("undefined"!=typeof window)try{window.localStorage.setItem(e,i(r))}catch{}return r})},[e,i]);return t.useEffect(()=>{d(a())},[a]),t.useEffect(()=>{if("undefined"==typeof window||!c)return;const t=t=>{if(t.key===e)if(null!==t.newValue)try{d(u(t.newValue))}catch{d(s.current)}else d(s.current)};return window.addEventListener("storage",t),()=>window.removeEventListener("storage",t)},[u,e,c]),[l,f]},e.useMap=l,e.useMediaQuery=u,e.useOnClickOutside=function(e,r,o,u){const c=t.useRef(r);n(()=>{c.current=r},[r]),t.useEffect(()=>{if("undefined"==typeof document)return;const t="undefined"!=typeof window&&void 0!==window.PointerEvent,n=o??(t?"pointerdown":"mousedown"),r=Array.isArray(e)?e:[e];let s=document;for(const e of r)if(e.current){const t=i(e.current);if(t!==document){s=t;break}}const a=e=>{const t=e.target;if("undefined"==typeof Node||!(t instanceof Node))return;r.some(e=>{const n=e.current;return!!n&&n.contains(t)})||c.current(e)};return s.addEventListener(n,a,u),()=>{s.removeEventListener(n,a,u)}},[o,u,e])},e.useOpenGraphExtractor=function(e){const n=t.useMemo(()=>e.skipPatterns??w,[e.skipPatterns]),r=t.useCallback(e=>n.some(t=>t.test(e)),[n]),o=t.useCallback((e,t,n)=>{var r,o,i;const{openGraph:u,htmlInferred:c,hybridGraph:s}=e,a=b(null==u?void 0:u.description,null==s?void 0:s.description,null==c?void 0:c.description),l=b(null==u?void 0:u.title,null==s?void 0:s.title,null==c?void 0:c.title),d=b(null==u?void 0:u.site_name,null==s?void 0:s.site_name,null==c?void 0:c.site_name),f=b(null==s?void 0:s.favicon,null==c?void 0:c.favicon),m=b((null==(r=null==u?void 0:u.image)?void 0:r.url)??void 0,(null==s?void 0:s.image)??void 0,(null==c?void 0:c.image)??void 0,null==(o=null==c?void 0:c.images)?void 0:o[0]),p=b((null==(i=null==u?void 0:u.video)?void 0:i.url)??void 0,(null==s?void 0:s.video)??void 0),w=b(null==s?void 0:s.videoType,null==c?void 0:c.videoType),g=b(n.url,(null==u?void 0:u.url)??void 0,(null==s?void 0:s.url)??void 0,(null==c?void 0:c.url)??void 0,n.finalUrl,n.normalizedUrl,n.requestedUrl)??"";return{description:a,favicon:f,image:m,video:p,videoType:w,siteName:d,title:l,url:g,siteHost:h(g)}},[]);return p({endpoint:"open-graph",options:e,selectData:o,shouldSkip:r})},e.usePlatformFromUrl=function(e){return t.useMemo(()=>{if(!e||"string"!=typeof e)return"unknown";const t=e.trim();if(!t)return"unknown";try{const e=new URL(t).hostname.toLowerCase(),n=c.get(e);return n||(e.endsWith(".substack.com")?"substack":e.endsWith(".github.io")?"github":e.includes("pinterest.com")?"pinterest":e.includes("eventbrite.")?"eventbrite":e.endsWith(".medium.com")?"medium":e.endsWith(".behance.net")?"behance":"unknown")}catch{return"unknown"}},[e])},e.usePrevious=function(e){const r=t.useRef();return n(()=>{r.current=e},[e]),r.current},e.useResizeObserver=function(e,r,o){const i=t.useRef(r),u=t.useRef(null),[c,s]=t.useState(null);return n(()=>{i.current=r},[r]),t.useEffect(()=>{if("undefined"==typeof ResizeObserver)return;const t="undefined"!=typeof Element&&e instanceof Element?e:(n=e)&&"object"==typeof n&&"current"in n?e.current:null;var n;if(!t)return;const r=new ResizeObserver(e=>{const t=e[0];u.current=t,i.current?i.current(t):s(t)});return r.observe(t,o),()=>r.disconnect()},[o,e]),i.current?u.current:c},e.useScreen=function(e={}){const{breakpoints:n,screenTypeMapping:r,defaultScreenType:o="UNKNOWN",defaultTailwindSize:i="default"}=e,c=t.useMemo(()=>({...y,...n}),[n]),s=t.useMemo(()=>({...k,...r}),[r]),[a,l]=t.useState(()=>E()),d=u(`(min-width: ${c.sm}px)`),f=u(`(min-width: ${c.md}px)`),m=u(`(min-width: ${c.lg}px)`),p=u(`(min-width: ${c.xl}px)`),w=u(`(min-width: ${c["2xl"]}px)`),b=t.useCallback(()=>{"undefined"!=typeof window&&l(E())},[]);t.useEffect(()=>{if("undefined"==typeof window)return;l(E());const e=()=>{l(E())};return window.addEventListener("resize",e),()=>window.removeEventListener("resize",e)},[]);const h=t.useMemo(()=>d||f||m||p||w?w?"2xl":p?"xl":m?"lg":f?"md":d?"sm":"default":a.width>0?function(e,t){return e>=t["2xl"]?"2xl":e>=t.xl?"xl":e>=t.lg?"lg":e>=t.md?"md":e>=t.sm?"sm":"default"}(a.width,c):i,[d,f,m,p,w,a.width,c,i]),g=t.useMemo(()=>h===i&&0===a.width?o:s[h],[h,s,a.width,o,i]);return t.useMemo(()=>({width:a.width,height:a.height,tailwindSize:h,screenType:g,refresh:b}),[a.width,a.height,h,g,b])},e.useSessionStorage=function(e,n,r={}){const{initializeWithValue:o=!0,serialize:i=JSON.stringify,deserialize:u=JSON.parse,listenToStorageChanges:c=!1}=r,s=t.useRef(n),a=t.useCallback(()=>{if("undefined"==typeof window)return s.current;if(!o)return s.current;try{const t=window.sessionStorage.getItem(e);return t?u(t):s.current}catch{return s.current}},[u,o,e]),[l,d]=t.useState(()=>a()),f=t.useCallback(t=>{d(n=>{const r="function"==typeof t?t(n):t;if("undefined"!=typeof window)try{window.sessionStorage.setItem(e,i(r))}catch{}return r})},[e,i]);return t.useEffect(()=>{d(a())},[a]),t.useEffect(()=>{if("undefined"==typeof window||!c)return;const t=t=>{if(t.key===e)if(null!==t.newValue)try{d(u(t.newValue))}catch{d(s.current)}else d(s.current)};return window.addEventListener("storage",t),()=>window.removeEventListener("storage",t)},[u,e,c]),[l,f]},e.useThrottle=function(e,n,r={}){const o=r.leading??!0,i=r.trailing??!0,u=Math.max(0,n),[c,s]=t.useState(e),a=t.useRef(0),l=t.useRef(null),d=t.useRef(null);return t.useEffect(()=>{if(0===u)return void s(e);const t=Date.now();if(0===a.current)return a.current=t,o?void s(e):void(i&&!l.current&&(d.current=e,l.current=setTimeout(()=>{l.current=null,null!==d.current&&(s(d.current),d.current=null,a.current=Date.now())},u)));const n=t-a.current;if(n>=u&&o)return s(e),a.current=t,void(d.current=null);if(i&&(d.current=e,!l.current)){const e=Math.max(u-n,0);l.current=setTimeout(()=>{l.current=null,null!==d.current&&(s(d.current),d.current=null,a.current=Date.now())},e)}},[o,i,e,u]),t.useEffect(()=>()=>{l.current&&clearTimeout(l.current)},[]),c},e.useWebsiteLinksExtractor=function(e){return p({endpoint:"links",options:e,selectData:t.useCallback(e=>({totalLinks:e.totalLinks??0,uniqueDomains:e.uniqueDomains??0,links:e.links??[]}),[])})},e.useWebsiteMetaExtractor=function(e){return p({endpoint:"meta",options:e,selectData:t.useCallback(e=>({title:e.title??void 0,description:e.description??void 0,language:e.language??void 0,canonicalUrl:e.canonicalUrl??void 0,feedUrl:e.feedUrl??null,textContentLength:e.textContentLength??void 0,metaTags:e.metaTags??{}}),[])})},e.useWebsiteRssExtractor=function(e){return p({endpoint:"rss",options:e,selectData:t.useCallback(e=>({feedUrl:e.feedUrl??null,feeds:e.feeds??[]}),[])})},e.useWebsiteSchemaExtractor=function(e){return p({endpoint:"schema",options:e,selectData:t.useCallback(e=>({schema:e.schema??[],schemaTypes:e.schemaTypes??[]}),[])})},Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})}); | ||
| //# sourceMappingURL=opensite-hooks.umd.js.map |
@@ -23,3 +23,5 @@ export { useBoolean } from "./useBoolean.js"; | ||
| export { useWebsiteRssExtractor } from "./useWebsiteRssExtractor.js"; | ||
| export { useIsTouchDevice } from "./useIsTouchDevice.js"; | ||
| export { useScreen } from "./useScreen.js"; | ||
| export { fetchWebsiteExtractor } from "./websiteExtractorService.js"; | ||
| export { buildWebsiteExtractorUrl } from "./websiteExtractorService.js"; |
| export { useBoolean } from "./useBoolean.js"; | ||
| export type { UseBooleanResult } from "./useBoolean.js"; | ||
| export { useDebounceCallback } from "./useDebounceCallback.js"; | ||
| export type { DebounceOptions, DebouncedCallback } from "./useDebounceCallback.js"; | ||
| export type { DebounceOptions, DebouncedCallback, } from "./useDebounceCallback.js"; | ||
| export { useDebounceValue } from "./useDebounceValue.js"; | ||
@@ -37,4 +37,8 @@ export { useLocalStorage } from "./useLocalStorage.js"; | ||
| export type { WebsiteRssFeed, WebsiteRssPayload, WebsiteRssResponse, } from "./useWebsiteRssExtractor.js"; | ||
| export { useIsTouchDevice } from "./useIsTouchDevice.js"; | ||
| export type { DeviceType, UseIsTouchDeviceOptions, UseIsTouchDeviceResult, } from "./useIsTouchDevice.js"; | ||
| export { useScreen } from "./useScreen.js"; | ||
| export type { ScreenBreakpoints, ScreenType, ScreenTypeMapping, TailwindSize, UseScreenOptions, UseScreenResult, } from "./useScreen.js"; | ||
| export { fetchWebsiteExtractor } from "./websiteExtractorService.js"; | ||
| export { buildWebsiteExtractorUrl } from "./websiteExtractorService.js"; | ||
| export type { WebsiteExtractCacheMeta, WebsiteExtractMeta, WebsiteExtractorError, WebsiteExtractorOptions, WebsiteExtractorRequest, WebsiteExtractorResponse, WebsiteExtractorResult, WebsiteExtractorState, } from "./websiteExtractorTypes.js"; |
@@ -23,3 +23,5 @@ export { useBoolean } from "./useBoolean.js"; | ||
| export { useWebsiteRssExtractor } from "./useWebsiteRssExtractor.js"; | ||
| export { useIsTouchDevice } from "./useIsTouchDevice.js"; | ||
| export { useScreen } from "./useScreen.js"; | ||
| export { fetchWebsiteExtractor } from "./websiteExtractorService.js"; | ||
| export { buildWebsiteExtractorUrl } from "./websiteExtractorService.js"; |
| import { useEffect, useState } from "react"; | ||
| export function useMediaQuery(query, options = {}) { | ||
| const [matches, setMatches] = useState(() => { | ||
| if (typeof window === "undefined" || typeof window.matchMedia !== "function") { | ||
| if (typeof window === "undefined" || | ||
| typeof window.matchMedia !== "function") { | ||
| return options.defaultValue ?? false; | ||
@@ -10,3 +11,4 @@ } | ||
| useEffect(() => { | ||
| if (typeof window === "undefined" || typeof window.matchMedia !== "function") { | ||
| if (typeof window === "undefined" || | ||
| typeof window.matchMedia !== "function") { | ||
| return; | ||
@@ -19,10 +21,6 @@ } | ||
| setMatches(mediaQueryList.matches); | ||
| if (mediaQueryList.addEventListener) { | ||
| mediaQueryList.addEventListener("change", handler); | ||
| return () => mediaQueryList.removeEventListener("change", handler); | ||
| } | ||
| mediaQueryList.addListener(handler); | ||
| return () => mediaQueryList.removeListener(handler); | ||
| mediaQueryList.addEventListener("change", handler); | ||
| return () => mediaQueryList.removeEventListener("change", handler); | ||
| }, [query]); | ||
| return matches; | ||
| } |
| import { useEffect, useState } from "react"; | ||
| export function useMediaQuery(query, options = {}) { | ||
| const [matches, setMatches] = useState(() => { | ||
| if (typeof window === "undefined" || typeof window.matchMedia !== "function") { | ||
| if (typeof window === "undefined" || | ||
| typeof window.matchMedia !== "function") { | ||
| return options.defaultValue ?? false; | ||
@@ -10,3 +11,4 @@ } | ||
| useEffect(() => { | ||
| if (typeof window === "undefined" || typeof window.matchMedia !== "function") { | ||
| if (typeof window === "undefined" || | ||
| typeof window.matchMedia !== "function") { | ||
| return; | ||
@@ -19,10 +21,6 @@ } | ||
| setMatches(mediaQueryList.matches); | ||
| if (mediaQueryList.addEventListener) { | ||
| mediaQueryList.addEventListener("change", handler); | ||
| return () => mediaQueryList.removeEventListener("change", handler); | ||
| } | ||
| mediaQueryList.addListener(handler); | ||
| return () => mediaQueryList.removeListener(handler); | ||
| mediaQueryList.addEventListener("change", handler); | ||
| return () => mediaQueryList.removeEventListener("change", handler); | ||
| }, [query]); | ||
| return matches; | ||
| } |
+11
-1
| { | ||
| "name": "@opensite/hooks", | ||
| "version": "2.0.8", | ||
| "version": "2.1.0", | ||
| "description": "Performance-first React hooks for UI state, storage, events, and responsive behavior with tree-shakable exports.", | ||
@@ -76,2 +76,7 @@ "keywords": [ | ||
| }, | ||
| "./useIsTouchDevice": { | ||
| "import": "./dist/core/useIsTouchDevice.js", | ||
| "require": "./dist/core/useIsTouchDevice.cjs", | ||
| "types": "./dist/core/useIsTouchDevice.d.ts" | ||
| }, | ||
| "./useIsomorphicLayoutEffect": { | ||
@@ -122,2 +127,7 @@ "import": "./dist/core/useIsomorphicLayoutEffect.js", | ||
| }, | ||
| "./useScreen": { | ||
| "import": "./dist/core/useScreen.js", | ||
| "require": "./dist/core/useScreen.cjs", | ||
| "types": "./dist/core/useScreen.d.ts" | ||
| }, | ||
| "./useSessionStorage": { | ||
@@ -124,0 +134,0 @@ "import": "./dist/core/useSessionStorage.js", |
+2
-0
@@ -95,2 +95,3 @@  | ||
| | [`useMediaQuery`](./docs/useMediaQuery.md) | Reactive CSS media query matching | [View](./docs/useMediaQuery.md) | | ||
| | [`useScreen`](./docs/useScreen.md) | Viewport dimensions with Tailwind breakpoint and screen type detection | [View](./docs/useScreen.md) | | ||
| | **Utilities** | | | | ||
@@ -100,2 +101,3 @@ | [`useCopyToClipboard`](./docs/useCopyToClipboard.md) | Copy text to clipboard with feedback state | [View](./docs/useCopyToClipboard.md) | | ||
| | [`useIsClient`](./docs/useIsClient.md) | Detect client-side vs server-side rendering | [View](./docs/useIsClient.md) | | ||
| | [`useIsTouchDevice`](./docs/useIsTouchDevice.md) | Detect touch-primary input with dynamic hybrid device updates | [View](./docs/useIsTouchDevice.md) | | ||
| | [`useIsomorphicLayoutEffect`](./docs/useIsomorphicLayoutEffect.md) | SSR-safe useLayoutEffect | [View](./docs/useIsomorphicLayoutEffect.md) | | ||
@@ -102,0 +104,0 @@ | **Website Extractors** | | | |
Sorry, the diff of this file is too big to display
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
310262
26.01%93
6.9%4104
31.83%238
0.85%4
-20%