| @@ -1,239 +1,90 @@ | ||
| 1 | 1 | /** |
| 2 | - * Centralized Icon System | |
| 3 | - * | |
| 4 | - * This file provides a centralized way to manage icons across both React and PHP. | |
| 5 | - * Icons are defined in /includes/icons.json and loaded dynamically. | |
| 2 | + * Yatra icon library (includes/icons.json, expanded via scripts/sync-lucide-icons-json.mjs) | |
| 3 | + * + helpers for admin UI. Public PHP rendering uses yatra_svg_icon() / yatra_stored_picker_icon_markup(). | |
| 6 | 4 | */ |
| 7 | 5 | |
| 8 | 6 | import { createElement } from "react"; |
| 7 | +import type { SVGProps } from "react"; | |
| 8 | +import yatraIconsManifest from "@yatra/icons"; | |
| 9 | 9 | |
| 10 | -// Import all Lucide icons we use | |
| 11 | -import { | |
| 12 | - Activity, | |
| 13 | - UtensilsCrossed, | |
| 14 | - Building2, | |
| 15 | - Bus, | |
| 16 | - Moon, | |
| 17 | - Package, | |
| 18 | - Target, | |
| 19 | - Camera, | |
| 20 | - Mountain, | |
| 21 | - Waves, | |
| 22 | - Palette, | |
| 23 | - Plane, | |
| 24 | - Car, | |
| 25 | - Hotel, | |
| 26 | - Coffee, | |
| 27 | - Bed, | |
| 28 | - MapPin, | |
| 29 | - Footprints, | |
| 30 | - Eye, | |
| 31 | - Clock, | |
| 32 | - Calendar, | |
| 33 | - Image as ImageIcon, | |
| 34 | - Music, | |
| 35 | - Gamepad2, | |
| 36 | - BookOpen, | |
| 37 | - ShoppingBag, | |
| 38 | - Heart, | |
| 39 | - Star, | |
| 40 | - Zap, | |
| 41 | - Flame, | |
| 42 | - FileText, | |
| 43 | - Download, | |
| 44 | - Users, | |
| 45 | - Globe, | |
| 46 | - HelpCircle, | |
| 47 | - MessageCircle, | |
| 48 | - Check, | |
| 49 | - Info, | |
| 50 | -} from "lucide-react"; | |
| 51 | - | |
| 52 | -// Map icon names to Lucide components | |
| 53 | -const lucideIconMap: Record< | |
| 54 | - string, | |
| 55 | - React.ComponentType<React.SVGProps<SVGSVGElement>> | |
| 56 | -> = { | |
| 57 | - activity: Activity, | |
| 58 | - footprints: Footprints, | |
| 59 | - mountain: Mountain, | |
| 60 | - waves: Waves, | |
| 61 | - camera: Camera, | |
| 62 | - eye: Eye, | |
| 63 | - target: Target, | |
| 64 | - zap: Zap, | |
| 65 | - flame: Flame, | |
| 66 | - utensils: UtensilsCrossed, | |
| 67 | - coffee: Coffee, | |
| 68 | - hotel: Hotel, | |
| 69 | - bed: Bed, | |
| 70 | - building: Building2, | |
| 71 | - bus: Bus, | |
| 72 | - plane: Plane, | |
| 73 | - car: Car, | |
| 74 | - "map-pin": MapPin, | |
| 75 | - calendar: Calendar, | |
| 76 | - clock: Clock, | |
| 77 | - moon: Moon, | |
| 78 | - package: Package, | |
| 79 | - palette: Palette, | |
| 80 | - image: ImageIcon, | |
| 81 | - music: Music, | |
| 82 | - gamepad: Gamepad2, | |
| 83 | - book: BookOpen, | |
| 84 | - shopping: ShoppingBag, | |
| 85 | - heart: Heart, | |
| 86 | - star: Star, | |
| 87 | - "file-text": FileText, | |
| 88 | - download: Download, | |
| 89 | - users: Users, | |
| 90 | - globe: Globe, | |
| 91 | - "help-circle": HelpCircle, | |
| 92 | - "message-circle": MessageCircle, | |
| 93 | - check: Check, | |
| 94 | - info: Info, | |
| 95 | -}; | |
| 96 | - | |
| 97 | -export type IconName = keyof typeof lucideIconMap; | |
| 98 | - | |
| 99 | -export interface IconData { | |
| 10 | +export interface YatraManifestEntry { | |
| 100 | 11 | label: string; |
| 101 | - category: | |
| 102 | - | "activity" | |
| 103 | - | "travel" | |
| 104 | - | "food" | |
| 105 | - | "accommodation" | |
| 106 | - | "transport" | |
| 107 | - | "general"; | |
| 12 | + category: string; | |
| 108 | 13 | svg: string; |
| 109 | 14 | } |
| 110 | 15 | |
| 111 | -export interface IconOption { | |
| 112 | - name: IconName; | |
| 16 | +export type IconOption = { | |
| 17 | + name: string; | |
| 113 | 18 | label: string; |
| 114 | - component: React.ComponentType<React.SVGProps<SVGSVGElement>>; | |
| 115 | - category: | |
| 116 | - | "activity" | |
| 117 | - | "travel" | |
| 118 | - | "food" | |
| 119 | - | "accommodation" | |
| 120 | - | "transport" | |
| 121 | - | "general"; | |
| 122 | -} | |
| 19 | + category: string; | |
| 20 | + svg: string; | |
| 21 | +}; | |
| 123 | 22 | |
| 124 | -// Cache for loaded icons | |
| 125 | -let iconsCache: Record<string, IconData> | null = null; | |
| 23 | +/** @deprecated All string slugs from manifest are valid */ | |
| 24 | +export type IconName = string; | |
| 126 | 25 | |
| 127 | -/** | |
| 128 | - * Load icons from the centralized JSON file | |
| 129 | - */ | |
| 130 | -export async function loadIcons(): Promise<Record<string, IconData>> { | |
| 131 | - if (iconsCache !== null) { | |
| 132 | - return iconsCache; | |
| 133 | - } | |
| 134 | - | |
| 135 | - try { | |
| 136 | - // Load from the same JSON file that PHP uses | |
| 137 | - const response = await fetch( | |
| 138 | - "/wp-content/plugins/yatra/includes/icons.json", | |
| 139 | - ); | |
| 140 | - if (!response.ok) { | |
| 141 | - throw new Error("Failed to load icons"); | |
| 142 | - } | |
| 143 | - | |
| 144 | - const loadedIcons = await response.json(); | |
| 145 | - iconsCache = loadedIcons || {}; | |
| 146 | - return iconsCache as Record<string, IconData>; | |
| 147 | - } catch (error) { | |
| 148 | - console.warn("Failed to load centralized icons, using fallback:", error); | |
| 149 | - iconsCache = {}; | |
| 150 | - return iconsCache; | |
| 151 | - } | |
| 26 | +export function getYatraIconManifest(): Record<string, YatraManifestEntry> { | |
| 27 | + return yatraIconsManifest as Record<string, YatraManifestEntry>; | |
| 152 | 28 | } |
| 153 | 29 | |
| 154 | -/** | |
| 155 | - * Get all available icon options for the icon picker | |
| 156 | - */ | |
| 157 | 30 | export function getIconOptions(): IconOption[] { |
| 158 | - return Object.entries(lucideIconMap).map(([name, component]) => ({ | |
| 159 | - name: name as IconName, | |
| 160 | - label: name.charAt(0).toUpperCase() + name.slice(1).replace("-", " "), | |
| 161 | - component, | |
| 162 | - category: getCategoryForIcon(name as IconName), | |
| 163 | - })); | |
| 31 | + const m = yatraIconsManifest as Record<string, YatraManifestEntry>; | |
| 32 | + // Some manifest entries only carry `svg` (sync script left them without | |
| 33 | + // label/category). Without defensive defaults, IconPicker's search filter | |
| 34 | + // calls `.toLowerCase()` on undefined and crashes the whole panel. | |
| 35 | + // Derive a readable label from the slug ("hot-air-balloon" → "Hot Air | |
| 36 | + // Balloon") and fall back to a "general" bucket for the category so the | |
| 37 | + // search and category-filter UI still works. | |
| 38 | + const slugToLabel = (slug: string): string => | |
| 39 | + slug | |
| 40 | + .split(/[-_\s]+/) | |
| 41 | + .filter(Boolean) | |
| 42 | + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) | |
| 43 | + .join(" "); | |
| 44 | + return Object.entries(m).map(([name, meta]) => { | |
| 45 | + const safeMeta = (meta || {}) as Partial<YatraManifestEntry>; | |
| 46 | + return { | |
| 47 | + name, | |
| 48 | + label: | |
| 49 | + typeof safeMeta.label === "string" && safeMeta.label.length > 0 | |
| 50 | + ? safeMeta.label | |
| 51 | + : slugToLabel(name), | |
| 52 | + category: | |
| 53 | + typeof safeMeta.category === "string" && safeMeta.category.length > 0 | |
| 54 | + ? safeMeta.category | |
| 55 | + : "general", | |
| 56 | + svg: safeMeta.svg || "", | |
| 57 | + }; | |
| 58 | + }); | |
| 164 | 59 | } |
| 165 | 60 | |
| 166 | -/** | |
| 167 | - * Get category for an icon (fallback when JSON is not loaded) | |
| 168 | - */ | |
| 169 | -function getCategoryForIcon(iconName: IconName): IconOption["category"] { | |
| 170 | - const categoryMap: Record<string, IconOption["category"]> = { | |
| 171 | - activity: "activity", | |
| 172 | - footprints: "activity", | |
| 173 | - mountain: "activity", | |
| 174 | - waves: "activity", | |
| 175 | - camera: "activity", | |
| 176 | - eye: "activity", | |
| 177 | - target: "activity", | |
| 178 | - zap: "activity", | |
| 179 | - flame: "activity", | |
| 180 | - utensils: "food", | |
| 181 | - coffee: "food", | |
| 182 | - hotel: "accommodation", | |
| 183 | - bed: "accommodation", | |
| 184 | - building: "accommodation", | |
| 185 | - bus: "transport", | |
| 186 | - plane: "transport", | |
| 187 | - car: "transport", | |
| 188 | - "map-pin": "travel", | |
| 189 | - calendar: "travel", | |
| 190 | - clock: "travel", | |
| 191 | - moon: "general", | |
| 192 | - package: "general", | |
| 193 | - palette: "general", | |
| 194 | - image: "general", | |
| 195 | - music: "general", | |
| 196 | - gamepad: "general", | |
| 197 | - book: "general", | |
| 198 | - shopping: "general", | |
| 199 | - heart: "general", | |
| 200 | - star: "general", | |
| 201 | - "file-text": "general", | |
| 202 | - download: "general", | |
| 203 | - users: "general", | |
| 204 | - globe: "general", | |
| 205 | - "help-circle": "general", | |
| 206 | - "message-circle": "general", | |
| 207 | - check: "general", | |
| 208 | - info: "general", | |
| 209 | - }; | |
| 210 | - | |
| 211 | - return categoryMap[iconName] || "general"; | |
| 61 | +export function getYatraIconSvg(slug: string): string | null { | |
| 62 | + const m = yatraIconsManifest as Record<string, YatraManifestEntry>; | |
| 63 | + return m[slug]?.svg ?? null; | |
| 212 | 64 | } |
| 213 | 65 | |
| 214 | -/** | |
| 215 | - * Get React component for an icon name | |
| 216 | - */ | |
| 217 | -export function getIconComponent( | |
| 218 | - iconName: string, | |
| 219 | -): React.ComponentType<React.SVGProps<SVGSVGElement>> | null { | |
| 220 | - return lucideIconMap[iconName] || null; | |
| 66 | +/** @deprecated Prefer getYatraIconSvg + inline SVG; kept for narrow compatibility */ | |
| 67 | +export function getIconComponent(): null { | |
| 68 | + return null; | |
| 221 | 69 | } |
| 222 | 70 | |
| 223 | -/** | |
| 224 | - * Render an icon by name | |
| 225 | - */ | |
| 226 | 71 | export function renderIcon( |
| 227 | 72 | iconName: string, |
| 228 | - props: React.SVGProps<SVGSVGElement> = {}, | |
| 229 | -): React.ReactElement | null { | |
| 230 | - const IconComponent = getIconComponent(iconName); | |
| 231 | - if (!IconComponent) { | |
| 73 | + props: SVGProps<HTMLSpanElement> = {}, | |
| 74 | +): ReturnType<typeof createElement> | null { | |
| 75 | + const svg = getYatraIconSvg(iconName); | |
| 76 | + if (!svg) { | |
| 232 | 77 | return null; |
| 233 | 78 | } |
| 79 | + return createElement("span", { | |
| 80 | + ...props, | |
| 81 | + dangerouslySetInnerHTML: { __html: svg }, | |
| 82 | + }); | |
| 83 | +} | |
| 234 | 84 | |
| 235 | - return createElement(IconComponent, props); | |
| 85 | +export async function loadIcons(): Promise<Record<string, YatraManifestEntry>> { | |
| 86 | + return getYatraIconManifest(); | |
| 236 | 87 | } |
| 237 | 88 | |
| 238 | 89 | export default { |
| 239 | 90 | loadIcons, |
| @@ -239,5 +90,7 @@ | ||
| 239 | 90 | loadIcons, |
| 240 | 91 | getIconOptions, |
| 241 | 92 | getIconComponent, |
| 242 | 93 | renderIcon, |
| 94 | + getYatraIconSvg, | |
| 95 | + getYatraIconManifest, | |
| 243 | 96 | }; |