/** * Shared bootstrap for the admin React entries (improvement-plan P2-2.2). * * Every `*-main.tsx` entry used to copy-paste the same pattern: * 1. read its `window.llmagnet*Data` payload (localized by PHP), * 2. feed it to `initMixpanel()` + `trackPageView()`, * 3. inside the component, read the payload again and render an error * fallback when missing. * * This module is now the single implementation: `initEntry()` covers the * pre-mount part, `useBootstrap()` the in-component part, and the * `fetchAdminShellBootstrap` / `applyAdminShellBootstrap` helpers cover the * admin-shell REST re-bootstrap used by client-side navigation. */ import { useEffect, useState } from 'react'; import { initMixpanel, trackPageView } from '../lib/mixpanel'; import type { AnalyticsData } from '../lib/mixpanel'; type WindowKey = keyof Window & string; /** Fields initMixpanel() consumes — present on payloads in varying subsets. */ interface MixpanelBootstrapFields { analyticsData?: AnalyticsData; settings?: { post_types?: string[] }; } /** Read a localized window payload, normalizing `undefined` to `null`. */ export function readBootstrapData(key: K): Window[K] | null { if (typeof window === 'undefined') { return null; } return (window[key] as Window[K] | undefined) ?? null; } /** * Pre-mount entry bootstrap: read the localized payload, initialize Mixpanel * with whatever telemetry context the payload carries, and track the page * view. Returns the payload (or null) for further pre-mount use. */ export function initEntry(key: K, pageName: string): Window[K] | null { const data = readBootstrapData(key); const fields = (data ?? {}) as MixpanelBootstrapFields; initMixpanel({ analyticsData: fields.analyticsData, settings: fields.settings, }); trackPageView(pageName); return data; } export interface UseBootstrapOptions { /** * When true the payload is read in an effect after the first render * (loading state shown first), matching the legacy App.tsx / * analytics-main.tsx behavior. Default: synchronous read on mount. */ deferred?: boolean; /** Error message surfaced when the payload is missing. */ missingMessage?: string; } export interface BootstrapState { data: T | null; setData: React.Dispatch>; isLoading: boolean; error: string | null; } /** * Read the entry's bootstrap payload from the window. * * Synchronous mode (default) resolves on mount — components remount with a * fresh payload when the admin shell swaps pages (key={wpPage}). Deferred * mode reproduces the legacy effect-based load with isLoading/error states. */ export function useBootstrap( key: K, options: UseBootstrapOptions = {}, ): BootstrapState> { type Payload = NonNullable; const { deferred = false, missingMessage = 'Data not found.' } = options; const [data, setData] = useState(() => deferred ? null : (readBootstrapData(key) as Payload | null), ); const [isLoading, setIsLoading] = useState(deferred); const [error, setError] = useState(null); useEffect(() => { if (!deferred) { return; } try { const value = readBootstrapData(key) as Payload | null; if (value) { setData(value); } else { console.error(`[LLMagnet] ${key} not found in window object`); setError(missingMessage); } } catch (err) { console.error(`[LLMagnet] Error loading ${key}:`, err); setError(missingMessage); } finally { setIsLoading(false); } // Bootstrap payloads are read once per mount by design. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return { data, setData, isLoading, error }; } // --------------------------------------------------------------------------- // Admin-shell REST bootstrap (client-side navigation between plugin pages) // --------------------------------------------------------------------------- export interface AdminShellBootstrapResponse { wpPage: string; globals: Record; wpApiSettings: { root: string; nonce: string }; /** Direct plugin URL to assets/react-build/js/ (trailing slash). */ assetsBaseUrl?: string; /** wp_page slug => direct entry bundle URL (from PHP). */ pageScripts?: Record; /** Stable shared chunk filenames => direct URLs. */ sharedChunks?: Record; version?: string; } /** Copy the server-rendered shell globals onto window (initial page load). */ export function applyShellGlobalsFromWindow(): void { const shell = window.llmagnetAdminShell; if (!shell?.globals) { return; } for (const [key, value] of Object.entries(shell.globals)) { (window as unknown as Record)[key] = value; } } /** Apply a REST bootstrap payload after client-side navigation. */ export function applyAdminShellBootstrap(payload: AdminShellBootstrapResponse): void { const previous = window.llmagnetAdminShell; window.llmagnetAdminShell = { wpPage: payload.wpPage, globals: payload.globals, assetsBaseUrl: payload.assetsBaseUrl ?? previous?.assetsBaseUrl, pageScripts: payload.pageScripts ?? previous?.pageScripts, sharedChunks: payload.sharedChunks ?? previous?.sharedChunks, version: payload.version ?? previous?.version, }; window.wpApiSettings = payload.wpApiSettings; for (const [key, value] of Object.entries(payload.globals)) { (window as unknown as Record)[key] = value; } } /** Fetch the bootstrap payload for a plugin admin page from the REST API. */ export async function fetchAdminShellBootstrap( wpPage: string, ): Promise { const root = window.wpApiSettings?.root?.replace(/\/$/, '') || ''; const nonce = window.wpApiSettings?.nonce || ''; const url = `${root}/llm-analytics/v1/admin-shell/bootstrap?wp_page=${encodeURIComponent(wpPage)}`; const res = await fetch(url, { headers: { 'X-WP-Nonce': nonce }, credentials: 'same-origin', }); if (!res.ok) { throw new Error(`Bootstrap HTTP ${res.status}`); } return (await res.json()) as AdminShellBootstrapResponse; }