/** * Remap4me widget. Compiled in-browser by @babel/standalone (presets: * react, typescript) - see includes/class-remap4me-shortcode.php. No * bundler: this is a single global script, not an ES module. */ interface Remap4meConfig { clientId: string; type: string; stage: string; lang: string; theme: 'light' | 'dark'; radius: boolean; accent_color: string; } interface Brand { id: number; name: string; active?: number; } interface Model { id: number; brand_id: number; namemodel: string; active?: number; } interface Generation { id: number; model_id: number; namefueltype: string; active?: number; } interface EngineVariant { id: number; model_id: number; fueltype_id: number; namevariant: string; active?: number; } interface FilterResponse { brands: Brand[]; models: Model[]; fueltypes: Generation[]; variants: EngineVariant[]; } interface DetailsResponse { id: number; namevariant: string; ecubrand: string; ecuversion: string; fuel: string; car_image: string; currency: number | string; dynoprint: number | string; oridinalpower: number; oridinaltorque: number; [ key: string ]: unknown; } ( function () { 'use strict'; var React = ( window as any ).React; var ReactDOM = ( window as any ).ReactDOM; var I18N = ( window as any ).Remap4meI18n || {}; var useState = React.useState; var useEffect = React.useEffect; var useCallback = React.useCallback; var createElement = React.createElement; var API_BASE = 'https://remap4.me/api/'; // details-new.php sends no CORS headers, so it can't be fetched // directly from a third-party site's browser context. Routed through // our own REST proxy instead - see class-remap4me-rest.php. var restConfig = ( window as any ).Remap4meRestConfig || {}; var DETAILS_URL = restConfig.detailsUrl || '/wp-json/remap4me/v1/details'; var OPTION_KEYS = [ 'AutotunerOBD', 'AutotunerBench', 'AutotunerBootmode', 'DPFOFF', 'EGROFF', 'GPFOFF', 'O2OFF', 'SCRADBLUEOFF', 'SPEEDLIMITOFF', 'STARTSTOPOFF', 'POPBANGS', 'POPCORN', 'Powergate' ]; function translate( lang: string, key: string ): string { var dict = I18N[ lang ] || I18N.en || {}; var fallback = I18N.en || {}; return dict[ key ] || fallback[ key ] || key; } function isActive( item: { active?: number } ): boolean { return item.active === undefined || item.active === null || Number( item.active ) === 1; } function fetchFilters( clientId: string, brandId: number, modelId: number, fueltypeId: number ): Promise { var params = new URLSearchParams( { client_id: clientId, brand_id: String( brandId ), model_id: String( modelId ), fueltype_id: String( fueltypeId ) } ); var url = API_BASE + '?' + params.toString(); return fetch( url ).then( function ( res ) { if ( ! res.ok ) { throw new Error( 'Request failed: ' + res.status ); } return res.json(); } ); } function fetchDetails( clientId: string, id: number ): Promise { var params = new URLSearchParams( { client_id: clientId, id: String( id ) } ); var url = DETAILS_URL + '?' + params.toString(); return fetch( url ).then( function ( res ) { if ( ! res.ok ) { throw new Error( 'Request failed: ' + res.status ); } return res.json(); } ).then( function ( data ) { return Array.isArray( data ) ? data[ 0 ] : data; } ); } function Select( props: { id: string; label: string; value: string; disabled: boolean; placeholder: string; options: Array<{ id: number; name: string }>; onChange: ( value: string ) => void; } ) { return createElement( 'div', { className: 'remap4me-field' }, createElement( 'label', { htmlFor: props.id }, props.label ), createElement( 'select', { id: props.id, className: 'remap4me-select', value: props.value, disabled: props.disabled, onChange: function ( e: any ) { props.onChange( e.target.value ); } }, createElement( 'option', { value: '' }, props.placeholder ), props.options.map( function ( opt ) { return createElement( 'option', { key: opt.id, value: String( opt.id ) }, opt.name ); } ) ) ); } function StatTile( props: { label: string; value: string; delta?: string; highlight?: boolean } ) { return createElement( 'div', { className: 'remap4me-stat' + ( props.highlight ? ' remap4me-stat--highlight' : '' ) }, createElement( 'div', { className: 'remap4me-stat-label' }, props.label ), createElement( 'div', { className: 'remap4me-stat-value' }, props.value, props.delta ? createElement( 'span', { className: 'remap4me-stat-delta' }, props.delta ) : null ) ); } function ResultsPanel( props: { lang: string; stage: string; details: DetailsResponse } ) { var t = function ( key: string ) { return translate( props.lang, key ); }; var details = props.details; var stageKey = props.stage || '1'; var powerKey = 'modificationpower' + ( stageKey === 'eco' ? '_eco' : stageKey ); var torqueKey = 'modificationtorque' + ( stageKey === 'eco' ? '_eco' : stageKey ); var priceKey = 'modprice' + ( stageKey === 'eco' ? '_eco' : stageKey ); var origPower = Number( details.oridinalpower ) || 0; var origTorque = Number( details.oridinaltorque ) || 0; var tunedPower = Number( details[ powerKey ] ) || 0; var tunedTorque = Number( details[ torqueKey ] ) || 0; var price = Number( details[ priceKey ] ) || 0; var currency = details.currency; var badges = OPTION_KEYS.filter( function ( key ) { return Number( details[ key ] ) === 1; } ); var hasDyno = details.dynoprint !== undefined && details.dynoprint !== null && details.dynoprint !== 0 && details.dynoprint !== '0' && details.dynoprint !== ''; return createElement( 'div', { className: 'remap4me-results' }, details.car_image ? createElement( 'img', { className: 'remap4me-image', src: details.car_image, alt: details.namevariant || '' } ) : null, createElement( 'div', { className: 'remap4me-results-header' }, createElement( 'span', { className: 'remap4me-results-title' }, t( 'results_title' ) ), details.namevariant ? createElement( 'span', null, '— ' + details.namevariant ) : null ), createElement( 'div', { className: 'remap4me-meta' }, details.ecubrand ? createElement( 'span', null, createElement( 'strong', null, t( 'ecubrand' ) + ': ' ), details.ecubrand ) : null, details.ecuversion ? createElement( 'span', null, createElement( 'strong', null, t( 'ecuversion' ) + ': ' ), details.ecuversion ) : null, details.fuel ? createElement( 'span', null, createElement( 'strong', null, t( 'fuel' ) + ': ' ), details.fuel ) : null ), createElement( 'div', { className: 'remap4me-stats' }, createElement( StatTile, { label: t( 'oridinalpower' ), value: origPower + ' ' + t( 'hp' ) } ), createElement( StatTile, { label: t( 'oridinaltorque' ), value: origTorque + ' ' + t( 'nm' ) } ), createElement( StatTile, { label: t( powerKey ) + ' (' + t( 'stage_label' ) + ' ' + stageKey + ')', value: ( tunedPower || origPower ) + ' ' + t( 'hp' ), delta: tunedPower && tunedPower > origPower ? '+' + ( tunedPower - origPower ) : undefined, highlight: true } ), createElement( StatTile, { label: t( torqueKey ) + ' (' + t( 'stage_label' ) + ' ' + stageKey + ')', value: ( tunedTorque || origTorque ) + ' ' + t( 'nm' ), delta: tunedTorque && tunedTorque > origTorque ? '+' + ( tunedTorque - origTorque ) : undefined, highlight: true } ), price > 0 ? createElement( StatTile, { label: t( priceKey ), value: price + ( typeof currency === 'string' && currency ? ' ' + currency : '' ) } ) : null, hasDyno ? createElement( StatTile, { label: t( 'dynoprint' ), value: String( details.dynoprint ) } ) : null ), badges.length ? createElement( 'div', null, createElement( 'div', { className: 'remap4me-stat-label', style: { marginBottom: '8px' } }, t( 'options_label' ) ), createElement( 'div', { className: 'remap4me-options' }, badges.map( function ( key ) { return createElement( 'span', { key: key, className: 'remap4me-badge' }, t( key ) ); } ) ) ) : null ); } function Widget( props: { config: Remap4meConfig } ) { var config = props.config; var lang = config.lang || 'en'; var t = useCallback( function ( key: string ) { return translate( lang, key ); }, [ lang ] ); var brandState = useState( [] ) as [ Brand[], ( v: Brand[] ) => void ]; var brands = brandState[ 0 ], setBrands = brandState[ 1 ]; var modelState = useState( [] ) as [ Model[], ( v: Model[] ) => void ]; var models = modelState[ 0 ], setModels = modelState[ 1 ]; var genState = useState( [] ) as [ Generation[], ( v: Generation[] ) => void ]; var generations = genState[ 0 ], setGenerations = genState[ 1 ]; var engineState = useState( [] ) as [ EngineVariant[], ( v: EngineVariant[] ) => void ]; var engines = engineState[ 0 ], setEngines = engineState[ 1 ]; var brandIdState = useState( '' ); var brandId = brandIdState[ 0 ], setBrandId = brandIdState[ 1 ]; var modelIdState = useState( '' ); var modelId = modelIdState[ 0 ], setModelId = modelIdState[ 1 ]; var genIdState = useState( '' ); var genId = genIdState[ 0 ], setGenId = genIdState[ 1 ]; var engineIdState = useState( '' ); var engineId = engineIdState[ 0 ], setEngineId = engineIdState[ 1 ]; type LoadingStage = 'idle' | 'brands' | 'models' | 'generations' | 'engines' | 'details'; var loadingState = useState( 'brands' ) as [ LoadingStage, ( v: LoadingStage ) => void ]; var loading = loadingState[ 0 ], setLoading = loadingState[ 1 ]; var errorState = useState( null ) as [ string | null, ( v: string | null ) => void ]; var error = errorState[ 0 ], setError = errorState[ 1 ]; var detailsState = useState( null ) as [ DetailsResponse | null, ( v: DetailsResponse | null ) => void ]; var details = detailsState[ 0 ], setDetails = detailsState[ 1 ]; useEffect( function () { var cancelled = false; setLoading( 'brands' ); setError( null ); fetchFilters( config.clientId, 0, 0, 0 ) .then( function ( data ) { if ( cancelled ) return; setBrands( ( data.brands || [] ).filter( isActive ) ); setLoading( 'idle' ); } ) .catch( function () { if ( cancelled ) return; setError( t( 'error' ) ); setLoading( 'idle' ); } ); return function () { cancelled = true; }; // eslint-disable-next-line }, [ config.clientId ] ); var onBrandChange = useCallback( function ( value: string ) { setBrandId( value ); setModelId( '' ); setGenId( '' ); setEngineId( '' ); setModels( [] ); setGenerations( [] ); setEngines( [] ); setDetails( null ); setError( null ); if ( ! value ) { return; } setLoading( 'models' ); fetchFilters( config.clientId, Number( value ), 0, 0 ) .then( function ( data ) { setModels( ( data.models || [] ).filter( isActive ) ); setLoading( 'idle' ); } ) .catch( function () { setError( t( 'error' ) ); setLoading( 'idle' ); } ); }, [ config.clientId, t ] ); var onModelChange = useCallback( function ( value: string ) { setModelId( value ); setGenId( '' ); setEngineId( '' ); setGenerations( [] ); setEngines( [] ); setDetails( null ); setError( null ); if ( ! value ) { return; } setLoading( 'generations' ); fetchFilters( config.clientId, Number( brandId ), Number( value ), 0 ) .then( function ( data ) { setGenerations( ( data.fueltypes || [] ).filter( isActive ) ); setLoading( 'idle' ); } ) .catch( function () { setError( t( 'error' ) ); setLoading( 'idle' ); } ); }, [ config.clientId, brandId, t ] ); var onGenChange = useCallback( function ( value: string ) { setGenId( value ); setEngineId( '' ); setEngines( [] ); setDetails( null ); setError( null ); if ( ! value ) { return; } setLoading( 'engines' ); fetchFilters( config.clientId, Number( brandId ), Number( modelId ), Number( value ) ) .then( function ( data ) { setEngines( ( data.variants || [] ).filter( isActive ) ); setLoading( 'idle' ); } ) .catch( function () { setError( t( 'error' ) ); setLoading( 'idle' ); } ); }, [ config.clientId, brandId, modelId, t ] ); var onEngineChange = useCallback( function ( value: string ) { setEngineId( value ); setDetails( null ); setError( null ); }, [] ); var onFind = useCallback( function () { if ( ! engineId ) { return; } setLoading( 'details' ); setError( null ); fetchDetails( config.clientId, Number( engineId ) ) .then( function ( data ) { if ( ! data ) { throw new Error( 'empty' ); } setDetails( data ); setLoading( 'idle' ); } ) .catch( function () { setError( t( 'error' ) ); setLoading( 'idle' ); } ); }, [ config.clientId, engineId, t ] ); var brandOptions = brands.map( function ( b ) { return { id: b.id, name: b.name }; } ); var modelOptions = models.map( function ( m ) { return { id: m.id, name: m.namemodel }; } ); var genOptions = generations.map( function ( g ) { return { id: g.id, name: g.namefueltype }; } ); var engineOptions = engines.map( function ( e ) { return { id: e.id, name: e.namevariant }; } ); var isBusy = loading !== 'idle'; return createElement( 'div', null, createElement( 'div', { className: 'remap4me-form' }, createElement( Select, { id: 'remap4me-brand', label: t( 'brands' ), value: brandId, disabled: isBusy && loading === 'brands', placeholder: t( 'select_brand' ), options: brandOptions, onChange: onBrandChange } ), createElement( Select, { id: 'remap4me-model', label: t( 'models' ), value: modelId, disabled: ! brandId || ( isBusy && loading === 'models' ), placeholder: t( 'select_model' ), options: modelOptions, onChange: onModelChange } ), createElement( Select, { id: 'remap4me-generation', label: t( 'fuel_types' ), value: genId, disabled: ! modelId || ( isBusy && loading === 'generations' ), placeholder: t( 'select_generation' ), options: genOptions, onChange: onGenChange } ), createElement( Select, { id: 'remap4me-engine', label: t( 'variants' ), value: engineId, disabled: ! genId || ( isBusy && loading === 'engines' ), placeholder: t( 'select_engine' ), options: engineOptions, onChange: onEngineChange } ), createElement( 'div', { className: 'remap4me-field' }, createElement( 'label', { className: 'powered-by', onClick: onFind }, 'Powered by ', createElement( 'a', { href:'https://www.remap4.me/', }, 'remap4.me' ), ), createElement( 'button', { id: 'find-btn', type: 'button', className: 'remap4me-btn', disabled: ! engineId || isBusy, onClick: onFind }, t( 'find' ) ) ) ), // loading === 'details' ? createElement( 'div', { className: 'remap4me-status' }, t( 'loading' ) ) : null, error ? createElement( 'div', { className: 'remap4me-status is-error' }, error ) : null, details ? createElement( ResultsPanel, { lang: lang, stage: config.stage, details: details } ) : null ); } function parseConfig( el: Element ): Remap4meConfig | null { var raw = el.getAttribute( 'data-remap4me' ); if ( ! raw ) { return null; } try { var parsed:Remap4meConfig = JSON.parse( raw ); return { clientId: parsed.clientId, type: parsed.type || 'car', stage: parsed.stage || '1', lang: ( parsed.lang || 'en' ).toLowerCase(), theme: parsed.theme === 'dark' ? 'dark' : 'light', radius: parsed.radius !== false, accent_color: parsed.accent_color || '' }; } catch ( e ) { return null; } } // Re-applies the appearance settings as CSS custom properties on the // widget root itself, so they still land even if a page builder strips // the inline style="" attribute the PHP shortcode also writes. function applyAppearance( el: HTMLElement, config: Remap4meConfig ) { if ( config.accent_color ) { el.style.setProperty( '--r4m-accent', config.accent_color ); } if ( config.radius === false ) { el.style.setProperty( '--r4m-radius', '0' ); el.style.setProperty( '--r4m-radius-sm', '0' ); } } function mountAll() { var nodes = document.querySelectorAll( '.remap4me-widget[data-remap4me]:not([data-remap4me-mounted])' ); nodes.forEach( function ( el ) { var config = parseConfig( el ); if ( ! config || ! config.clientId ) { return; } el.setAttribute( 'data-remap4me-mounted', '1' ); el.setAttribute( 'dir', ( I18N[ config.lang ] && I18N[ config.lang ].dir ) || 'ltr' ); applyAppearance( el as HTMLElement, config ); var root = ReactDOM.createRoot( el ); root.render( createElement( Widget, { config: config } ) ); } ); } if ( document.readyState === 'loading' ) { document.addEventListener( 'DOMContentLoaded', mountAll ); } else { mountAll(); } } )();