PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.13
Yatra – Travel Booking & Tour Operator Software v3.0.13
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / lib / menu-icon.tsx

menu-icon.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.13, at resources/js/lib/menu-icon.tsx

143 lines 4.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useEffect, useState } from "react";
2 import type { LucideIcon } from "lucide-react";
3 import { ICON_MAP } from "./icon-map";
4 import { IconSelector } from "../components/ui/icon-selector";
5 import type { IconPickerValue } from "./icon-picker-types";
6 import type { MenuIconValue } from "./sidebar-menu-defaults";
7
8 interface MenuIconProps {
9 /** Stored override — Lucide name string OR IconPickerValue object. */
10 icon: MenuIconValue | undefined;
11 /** Lucide component used when no override / nothing renders. */
12 fallback: LucideIcon;
13 className?: string;
14 }
15
16 function isPickerValue(value: unknown): value is IconPickerValue {
17 return (
18 typeof value === "object" &&
19 value !== null &&
20 "type" in (value as Record<string, unknown>) &&
21 "value" in (value as Record<string, unknown>)
22 );
23 }
24
25 /**
26 * Module-level cache + in-flight dedupe. The IconPicker stores image picks
27 * as raw attachment IDs (e.g. "9") rather than URLs, so every consumer
28 * needs to resolve them. Caching here avoids one fetch per render *and*
29 * one fetch per consumer for the same attachment.
30 */
31 const attachmentUrlCache = new Map<string, string>();
32 const attachmentUrlInFlight = new Map<string, Promise<string>>();
33
34 function resolveAttachmentUrl(id: string): Promise<string> {
35 const cached = attachmentUrlCache.get(id);
36 if (cached !== undefined) return Promise.resolve(cached);
37 const pending = attachmentUrlInFlight.get(id);
38 if (pending) return pending;
39
40 const apiUrl =
41 (window as any).yatraAdmin?.apiUrl?.replace("/yatra/v1", "") ?? "";
42 if (!apiUrl) return Promise.resolve("");
43
44 const promise = fetch(`${apiUrl}/wp/v2/media/${id}`)
45 .then((r) => r.json())
46 .then((data) => {
47 const url: string = data?.source_url ?? "";
48 if (url) attachmentUrlCache.set(id, url);
49 return url;
50 })
51 .catch(() => "")
52 .finally(() => {
53 attachmentUrlInFlight.delete(id);
54 });
55
56 attachmentUrlInFlight.set(id, promise);
57 return promise;
58 }
59
60 /**
61 * Renders an image override, resolving attachment IDs to source URLs on
62 * first paint. Returns null while the URL is in flight so we don't show
63 * a broken `<img src="9">` flash.
64 */
65 const ImageOverride: React.FC<{ value: string; className: string }> = ({
66 value,
67 className,
68 }) => {
69 const initial =
70 value.startsWith("http://") ||
71 value.startsWith("https://") ||
72 value.startsWith("/")
73 ? value
74 : (attachmentUrlCache.get(value) ?? "");
75
76 const [resolved, setResolved] = useState<string>(initial);
77
78 useEffect(() => {
79 if (resolved) return;
80 if (!/^\d+$/.test(value)) return;
81
82 let cancelled = false;
83 resolveAttachmentUrl(value).then((url) => {
84 if (!cancelled && url) setResolved(url);
85 });
86 return () => {
87 cancelled = true;
88 };
89 }, [value, resolved]);
90
91 if (!resolved) return null;
92 return (
93 <img
94 src={resolved}
95 alt=""
96 className={`${className} object-contain`}
97 aria-hidden="true"
98 />
99 );
100 };
101
102 /**
103 * Unified renderer for sidebar / menu-customization icon previews.
104 *
105 * Resolution order:
106 * 1. IconPickerValue with type='image' → <img> (attachment ID resolved
107 * via the WP REST media endpoint)
108 * 2. IconPickerValue with type='icon' → IconSelector (Yatra SVG or FA)
109 * 3. string matching a known Lucide name → Lucide component
110 * 4. otherwise → fallback Lucide
111 */
112 export const MenuIcon: React.FC<MenuIconProps> = ({
113 icon,
114 fallback: Fallback,
115 className = "h-4 w-4",
116 }) => {
117 if (isPickerValue(icon)) {
118 if (icon.type === "image" && icon.value) {
119 return <ImageOverride value={icon.value} className={className} />;
120 }
121 if (icon.type === "icon" && icon.value) {
122 return (
123 <IconSelector
124 iconName={icon.value}
125 provider={
126 (icon.provider as "yatra" | "fa-solid" | "fa-regular") ?? "yatra"
127 }
128 className={className}
129 />
130 );
131 }
132 }
133
134 if (typeof icon === "string" && icon !== "" && ICON_MAP[icon]) {
135 const Lucide = ICON_MAP[icon];
136 return <Lucide className={className} />;
137 }
138
139 return <Fallback className={className} />;
140 };
141
142 export default MenuIcon;
143