import React, { useEffect, useState } from "react"; import type { LucideIcon } from "lucide-react"; import { ICON_MAP } from "./icon-map"; import { IconSelector } from "../components/ui/icon-selector"; import type { IconPickerValue } from "./icon-picker-types"; import type { MenuIconValue } from "./sidebar-menu-defaults"; interface MenuIconProps { /** Stored override — Lucide name string OR IconPickerValue object. */ icon: MenuIconValue | undefined; /** Lucide component used when no override / nothing renders. */ fallback: LucideIcon; className?: string; } function isPickerValue(value: unknown): value is IconPickerValue { return ( typeof value === "object" && value !== null && "type" in (value as Record) && "value" in (value as Record) ); } /** * Module-level cache + in-flight dedupe. The IconPicker stores image picks * as raw attachment IDs (e.g. "9") rather than URLs, so every consumer * needs to resolve them. Caching here avoids one fetch per render *and* * one fetch per consumer for the same attachment. */ const attachmentUrlCache = new Map(); const attachmentUrlInFlight = new Map>(); function resolveAttachmentUrl(id: string): Promise { const cached = attachmentUrlCache.get(id); if (cached !== undefined) return Promise.resolve(cached); const pending = attachmentUrlInFlight.get(id); if (pending) return pending; const apiUrl = (window as any).yatraAdmin?.apiUrl?.replace("/yatra/v1", "") ?? ""; if (!apiUrl) return Promise.resolve(""); const promise = fetch(`${apiUrl}/wp/v2/media/${id}`) .then((r) => r.json()) .then((data) => { const url: string = data?.source_url ?? ""; if (url) attachmentUrlCache.set(id, url); return url; }) .catch(() => "") .finally(() => { attachmentUrlInFlight.delete(id); }); attachmentUrlInFlight.set(id, promise); return promise; } /** * Renders an image override, resolving attachment IDs to source URLs on * first paint. Returns null while the URL is in flight so we don't show * a broken `` flash. */ const ImageOverride: React.FC<{ value: string; className: string }> = ({ value, className, }) => { const initial = value.startsWith("http://") || value.startsWith("https://") || value.startsWith("/") ? value : (attachmentUrlCache.get(value) ?? ""); const [resolved, setResolved] = useState(initial); useEffect(() => { if (resolved) return; if (!/^\d+$/.test(value)) return; let cancelled = false; resolveAttachmentUrl(value).then((url) => { if (!cancelled && url) setResolved(url); }); return () => { cancelled = true; }; }, [value, resolved]); if (!resolved) return null; return ( ); }; /** * Unified renderer for sidebar / menu-customization icon previews. * * Resolution order: * 1. IconPickerValue with type='image' → (attachment ID resolved * via the WP REST media endpoint) * 2. IconPickerValue with type='icon' → IconSelector (Yatra SVG or FA) * 3. string matching a known Lucide name → Lucide component * 4. otherwise → fallback Lucide */ export const MenuIcon: React.FC = ({ icon, fallback: Fallback, className = "h-4 w-4", }) => { if (isPickerValue(icon)) { if (icon.type === "image" && icon.value) { return ; } if (icon.type === "icon" && icon.value) { return ( ); } } if (typeof icon === "string" && icon !== "" && ICON_MAP[icon]) { const Lucide = ICON_MAP[icon]; return ; } return ; }; export default MenuIcon;