import {CardType} from "./cards"; import {Color} from "./Color"; import {__, getRegisteredCardRenderers} from "../globals"; import {ComponentMeta, ComponentType} from "./ComponentsMeta"; import {FC, ReactElement, ReactNode} from "react"; import {OfferData, TestableData} from "./store"; import {PopupWindowStateContext} from "./atoms"; export type CardRendererContext = 'testable' | 'tier-base' | 'tier-subchild' | 'offers' | string; export type CardRendererParameters = { color: Color, context: CardRendererContext, id: string, partialId?: string, localData?: TestableData | OfferData }; export type CardRenderer = { type?: string, icon: false | (({className}: { className: string, context?: 'card' | string }, color?: Color) => ReactElement), CustomCardComponent?: (parameters: CardRendererParameters, extraData?: Record) => false | FC<{ parameters: CardRendererParameters, extraData?: Record }>, ComputedValue: false | ((cardRendererParameters: CardRendererParameters) => ReactNode), AboveComputedValueTooltip?: (cardRendererParameters: CardRendererParameters) => ReactNode, AboveComputedValue?: (cardRendererParameters: CardRendererParameters) => ReactNode | object, BelowComputedValue?: (cardRendererParameters: CardRendererParameters) => ReactNode, OutsideComputedValue?: (cardRendererParameters: CardRendererParameters) => ReactNode, prefix?: string | boolean | ((scope: PopupWindowStateContext['scope']) => boolean | string), example?: string | ((scope: PopupWindowStateContext['scope']) => string | undefined), // for testables that can have fixed values (tiered branches) suggestedScopeMarker?: (suggestedScope: string) => ({ prefix?: string, label: string } | ReactNode); removeFixedValues?: (options: Options) => void, canRenderInSelectedSidebarPopup?: (options: Options, currentlySelectedType: string) => boolean, canShowAsAvailableWhileBeingAmongstSelected?: (options: Options, currentlySelectedType: string) => boolean, } const modules = import.meta.glob(['./cards/*.tsx', './cards/conditions/*.tsx', "./cards/stubs/*.tsx"], {eager: true}); export const cardRenderers: { filters: Record, conditions: Record, offers: Record } = { filters: { // [Categories.type!]: Categories, // [Quantity.type!]: Quantity, // [Products.type!]: Products, }, conditions: { // [UserRole.type!]: UserRole }, offers: {} } const addCardRenderer = (type: ComponentType, cardRenderer: CardRenderer) => { cardRenderers[type][cardRenderer.type!] = cardRenderer } Object.values(modules).forEach((module: any) => { let typeOfCard: ComponentType | undefined; // Find the type of card from the meta (filters, conditions, offers, etc.) const exports = Object.entries(module); exports.forEach(([key, value]) => { if (key.endsWith('Meta') && typeof value === 'object' && value !== null) { typeOfCard = (value as ComponentMeta).type } }); // loop again and now add the card renderer to the cardRenderers object // we have to loop again in case the renderer was exported before the meta, we don't want nasty bugs just because the order of exports is different // we can extract this into a function and then check in the first loop and return if found // but let's keep it simple for now exports.forEach(([key, value]) => { if (key.endsWith('Renderer') && typeof value === 'object' && value !== null) { const cardRenderer = value as CardRenderer; if (typeOfCard && cardRenderer.type) { addCardRenderer(typeOfCard, cardRenderer); } } }); }); /** * Add the external components */ for (const externalComponentType in getRegisteredCardRenderers()) { const externalCardRenderers = getRegisteredCardRenderers()[externalComponentType as ComponentType] || []; for (const externalCardRendererKey in externalCardRenderers) { const externalCardRenderer = externalCardRenderers[externalCardRendererKey]; addCardRenderer(externalComponentType as ComponentType, externalCardRenderer); } } /** * For templates, you always have to return two [string], even if you dont want the first or second one you should return an empty bracket. * * The first bracket has a secondary weight, it is lighter and smaller than the second one. * The second bracket has a primary weight, it is larger and bolder and is the same as the main name. * * Examples: * * "[Items in] [Specific] *" -> Items in Specific Categories * "[] [Specific] *" -> Specific Products * "[These] [] *" -> These Tags * */ export const getNamePrefixedForCardRenderer = (cardRenderer: CardRenderer, scope: PopupWindowStateContext['scope']): [string | '', string] => { let prefixedTemplate: string const defaultPrefixes = ['', ''] as [string, string] let customPrefix = typeof cardRenderer.prefix === 'function' ? cardRenderer.prefix(scope) : cardRenderer.prefix; if (!cardRenderer || customPrefix === false) { return defaultPrefixes; } else if (typeof customPrefix === 'string') { prefixedTemplate = customPrefix; } else { prefixedTemplate = __('[Items in] [Specific] *'); } const prefixChunks = [...prefixedTemplate.replace('*', '').matchAll(/\[(.*?)\]/g)].map(matches => matches[1]) return defaultPrefixes.map((defaultPrefix, index) => { if (prefixChunks[index]) { return prefixChunks[index].trim(); } return defaultPrefix; }) as [string, string] }