Plugin Directory

Changeset 3488009


Ignore:
Timestamp:
03/22/2026 02:43:36 AM (10 days ago)
Author:
fernandopimenta
Message:

Release v2.1.0 — autocomplete search, shortcode column, marketplace stats, dead code cleanup

Location:
pap-afiliados-pro/trunk
Files:
20 edited

Legend:

Unmodified
Added
Removed
  • pap-afiliados-pro/trunk/assets/css/papafpro-help.css

    r3482385 r3488009  
    138138    }
    139139}
     140
     141/* ==========================================================================
     142   SELECT FILTER (Autocomplete)
     143   ========================================================================== */
     144
     145.papafpro-select-wrapper {
     146    display: flex;
     147    flex-direction: column;
     148    gap: 4px;
     149}
     150
     151.papafpro-select-filter {
     152    width: 25em;
     153    max-width: 100%;
     154    padding: 4px 8px;
     155    border: 1px solid #8c8f94;
     156    border-radius: 3px;
     157    font-size: 13px;
     158    line-height: 1.5;
     159    box-sizing: border-box;
     160}
     161
     162.papafpro-select-filter:focus {
     163    border-color: #2271b1;
     164    box-shadow: 0 0 0 1px #2271b1;
     165    outline: none;
     166}
     167
     168@media screen and (max-width: 768px) {
     169    .papafpro-select-filter {
     170        width: 100%;
     171    }
     172}
     173
     174/* ==========================================================================
     175   PRODUCT SEARCH (Feature 3B)
     176   ========================================================================== */
     177
     178.papafpro-product-search {
     179    position: relative;
     180    max-width: 25em;
     181}
     182
     183.papafpro-product-search-input {
     184    width: 100%;
     185    padding: 4px 8px;
     186    border: 1px solid #8c8f94;
     187    border-radius: 3px;
     188    font-size: 13px;
     189    box-sizing: border-box;
     190}
     191
     192.papafpro-product-search-input:focus {
     193    border-color: #2271b1;
     194    box-shadow: 0 0 0 1px #2271b1;
     195    outline: none;
     196}
     197
     198/* Results dropdown */
     199.papafpro-product-results {
     200    position: absolute;
     201    top: 100%;
     202    left: 0;
     203    right: 0;
     204    max-height: 200px;
     205    overflow-y: auto;
     206    background: #fff;
     207    border: 1px solid #8c8f94;
     208    border-top: none;
     209    border-radius: 0 0 3px 3px;
     210    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
     211    z-index: 100;
     212}
     213
     214.papafpro-product-result-item {
     215    display: flex;
     216    align-items: center;
     217    gap: 8px;
     218    padding: 6px 8px;
     219    cursor: pointer;
     220    font-size: 13px;
     221}
     222
     223.papafpro-product-result-item:hover {
     224    background: #e8f4fd;
     225}
     226
     227.papafpro-product-result-name {
     228    flex: 1;
     229    overflow: hidden;
     230    text-overflow: ellipsis;
     231    white-space: nowrap;
     232}
     233
     234.papafpro-product-result-id {
     235    color: #787c82;
     236    font-size: 12px;
     237    flex-shrink: 0;
     238}
     239
     240.papafpro-product-result-marketplace {
     241    color: #2271b1;
     242    font-size: 11px;
     243    flex-shrink: 0;
     244}
     245
     246.papafpro-product-results-hint,
     247.papafpro-product-results-loading,
     248.papafpro-product-results-empty {
     249    padding: 8px;
     250    color: #787c82;
     251    font-size: 13px;
     252    font-style: italic;
     253}
     254
     255/* Chips (multi mode) */
     256.papafpro-product-chips {
     257    display: flex;
     258    flex-wrap: wrap;
     259    gap: 4px;
     260    margin-top: 6px;
     261}
     262
     263.papafpro-product-chip {
     264    display: inline-flex;
     265    align-items: center;
     266    gap: 4px;
     267    padding: 2px 6px;
     268    background: #f0f0f1;
     269    border: 1px solid #c3c4c7;
     270    border-radius: 3px;
     271    font-size: 12px;
     272    line-height: 1.5;
     273}
     274
     275.papafpro-product-chip-remove {
     276    background: none;
     277    border: none;
     278    cursor: pointer;
     279    padding: 0 2px;
     280    font-size: 14px;
     281    line-height: 1;
     282    color: #787c82;
     283}
     284
     285.papafpro-product-chip-remove:hover {
     286    color: #d63638;
     287}
     288
     289@media screen and (max-width: 768px) {
     290    .papafpro-product-search {
     291        max-width: 100%;
     292    }
     293}
  • pap-afiliados-pro/trunk/assets/css/papafpro-stats.css

    r3482385 r3488009  
    108108}
    109109
     110/* Chart empty state */
     111.papafpro-stats-chart-empty {
     112    text-align: center;
     113    color: #646970;
     114    font-style: italic;
     115    margin: 12px 0 0;
     116}
     117
    110118/* Detailed table */
    111119.papafpro-stats-table-container {
  • pap-afiliados-pro/trunk/assets/css/papafpro-template-builder.css

    r3482385 r3488009  
    264264}
    265265
     266.papafpro-preset-list thead th.papafpro-col-shortcode {
     267    width: 220px;
     268    text-align: left;
     269}
     270
    266271.papafpro-preset-list thead th.papafpro-col-actions {
    267272    width: 220px;
     
    285290.papafpro-preset-list tbody td.papafpro-col-id {
    286291    text-align: center;
     292}
     293
     294.papafpro-preset-list tbody td.papafpro-col-shortcode {
     295    white-space: nowrap;
     296}
     297
     298.papafpro-preset-list tbody td.papafpro-col-shortcode code {
     299    font-family: Consolas, Monaco, monospace;
     300    font-size: 12px;
     301    background: #f0f0f1;
     302    padding: 2px 6px;
     303    border-radius: 3px;
     304    user-select: all;
     305}
     306
     307.papafpro-preset-list .papafpro-copy-btn {
     308    background: none;
     309    border: none;
     310    cursor: pointer;
     311    padding: 2px 4px;
     312    margin-left: 4px;
     313    color: #2271b1;
     314    vertical-align: middle;
     315    line-height: 1;
     316}
     317
     318.papafpro-preset-list .papafpro-copy-btn:hover {
     319    color: #135e96;
     320}
     321
     322.papafpro-preset-list .papafpro-copy-btn .dashicons {
     323    font-size: 16px;
     324    width: 16px;
     325    height: 16px;
     326}
     327
     328.papafpro-preset-list .papafpro-copy-feedback {
     329    display: none;
     330    margin-left: 6px;
     331    color: #00a32a;
     332    font-size: 12px;
     333    vertical-align: middle;
    287334}
    288335
  • pap-afiliados-pro/trunk/assets/js/papafpro-help.js

    r3482385 r3488009  
    2323    var i18n       = papafproHelp.i18n || {};
    2424
     25    var searchDebounceTimer = null;
     26
    2527    // =========================================================================
    2628    // 1. GERADOR DE SHORTCODES
     
    6365                case 'preset':
    6466                    $input = buildPresetSelect( param );
     67                    break;
     68                case 'product_single':
     69                    $input = buildProductSearch( param, 'single' );
     70                    break;
     71                case 'product_multi':
     72                    $input = buildProductSearch( param, 'multi' );
    6573                    break;
    6674                case 'number':
     
    134142
    135143    /**
     144     * Add a text filter input above a select element.
     145     *
     146     * Filters options by partial match on visible text (case-insensitive).
     147     * Only added when the select has more than 5 non-placeholder options.
     148     *
     149     * @param {jQuery} $select The select element to filter.
     150     * @return {jQuery} The wrapper element (or original select if skipped).
     151     */
     152    function addFilterToSelect( $select ) {
     153        var optionCount = $select.find( 'option' ).not( '[value=""]' ).length;
     154        if ( optionCount <= 5 ) {
     155            return $select;
     156        }
     157
     158        var $wrapper = $( '<div>' ).addClass( 'papafpro-select-wrapper' );
     159        var $filter  = $( '<input>' )
     160            .attr( 'type', 'text' )
     161            .addClass( 'papafpro-select-filter' )
     162            .attr( 'placeholder', i18n.search || 'Search\u2026' );
     163
     164        $select.before( $wrapper );
     165        $wrapper.append( $filter ).append( $select );
     166
     167        var $allOptions = $select.find( 'option' ).clone();
     168
     169        $filter.on( 'input', function() {
     170            var query = $( this ).val().toLowerCase().trim();
     171            $select.empty();
     172
     173            if ( ! query ) {
     174                $select.append( $allOptions.clone() );
     175            } else {
     176                $allOptions.each( function() {
     177                    var $opt = $( this );
     178                    if ( $opt.val() === '' || $opt.text().toLowerCase().indexOf( query ) !== -1 ) {
     179                        $select.append( $opt.clone() );
     180                    }
     181                });
     182            }
     183
     184            var visibleOptions = $select.find( 'option' ).not( '[value=""]' ).length;
     185            if ( visibleOptions === 0 && query ) {
     186                $select.append(
     187                    $( '<option>' )
     188                        .attr( 'disabled', true )
     189                        .text( i18n.no_results || 'No results found' )
     190                );
     191            }
     192
     193            $select.val( '' );
     194            $select.trigger( 'change' );
     195        });
     196
     197        return $wrapper;
     198    }
     199
     200    /**
    136201     * Construir select de categorias.
    137202     *
     
    153218        });
    154219
     220        addFilterToSelect( $select );
     221
    155222        return $select;
    156223    }
     
    176243        });
    177244
     245        addFilterToSelect( $select );
     246
    178247        return $select;
    179248    }
     
    199268        });
    200269
     270        addFilterToSelect( $select );
     271
    201272        return $select;
     273    }
     274
     275    /**
     276     * Build a product search component (single or multi select).
     277     *
     278     * @param {Object} param Shortcode parameter definition.
     279     * @param {string} mode  'single' or 'multi'.
     280     * @return {jQuery} The search container element.
     281     */
     282    function buildProductSearch( param, mode ) {
     283        var $container = $( '<div>' ).addClass( 'papafpro-product-search' );
     284        var $hidden = $( '<input>' )
     285            .attr({
     286                type: 'hidden',
     287                id: 'papafpro-param-' + param.name,
     288                'data-param': param.name
     289            })
     290            .addClass( 'papafpro-generator-input' )
     291            .val( '' );
     292        var $searchInput = $( '<input>' )
     293            .attr({
     294                type: 'text',
     295                placeholder: i18n.search_products || 'Search products\u2026'
     296            })
     297            .addClass( 'papafpro-product-search-input' );
     298        var $results = $( '<div>' )
     299            .addClass( 'papafpro-product-results' )
     300            .hide();
     301        var $chips = ( mode === 'multi' )
     302            ? $( '<div>' ).addClass( 'papafpro-product-chips' )
     303            : null;
     304
     305        $container.append( $hidden ).append( $searchInput ).append( $results );
     306        if ( $chips ) {
     307            $container.append( $chips );
     308        }
     309
     310        // AbortController to cancel previous in-flight request.
     311        var currentController = null;
     312
     313        // Debounced search.
     314        $searchInput.on( 'input', function() {
     315            var query = $( this ).val().trim();
     316            var isNumeric = /^\d+$/.test( query );
     317            var minLength = isNumeric ? 1 : 2;
     318            clearTimeout( searchDebounceTimer );
     319
     320            if ( query.length < minLength ) {
     321                $results.hide().empty();
     322                if ( query.length > 0 && ! isNumeric ) {
     323                    $results.html(
     324                        $( '<div>' ).addClass( 'papafpro-product-results-hint' ).text(
     325                            i18n.min_chars || 'Type at least 2 characters'
     326                        )
     327                    ).show();
     328                }
     329                return;
     330            }
     331
     332            $results.html(
     333                $( '<div>' ).addClass( 'papafpro-product-results-loading' ).text(
     334                    i18n.searching || 'Searching\u2026'
     335                )
     336            ).show();
     337
     338            searchDebounceTimer = setTimeout( function() {
     339                // Cancel any previous in-flight request.
     340                if ( currentController ) {
     341                    currentController.abort();
     342                }
     343                currentController = new AbortController();
     344
     345                wp.apiFetch({
     346                    path: 'papafpro/v1/products/search?q=' + encodeURIComponent( query ) + '&limit=10',
     347                    signal: currentController.signal
     348                }).then( function( response ) {
     349                    $results.empty();
     350                    var products = response.data || response || [];
     351
     352                    if ( ! products.length ) {
     353                        $results.html(
     354                            $( '<div>' ).addClass( 'papafpro-product-results-empty' ).text(
     355                                i18n.no_products_found || 'No products found'
     356                            )
     357                        ).show();
     358                        return;
     359                    }
     360
     361                    products.forEach( function( product ) {
     362                        // Skip already-selected products in multi mode.
     363                        if ( mode === 'multi' ) {
     364                            var currentIds = $hidden.val().split( ',' ).filter( Boolean );
     365                            if ( currentIds.indexOf( String( product.id ) ) !== -1 ) {
     366                                return;
     367                            }
     368                        }
     369
     370                        var $item = $( '<div>' )
     371                            .addClass( 'papafpro-product-result-item' )
     372                            .attr( 'data-id', product.id );
     373                        var $name = $( '<span>' ).addClass( 'papafpro-product-result-name' );
     374                        $name[0].appendChild( document.createTextNode( product.title ) );
     375                        var $id = $( '<span>' ).addClass( 'papafpro-product-result-id' );
     376                        $id[0].appendChild( document.createTextNode( '#' + product.id ) );
     377                        $item.append( $name ).append( $id );
     378
     379                        if ( product.marketplace ) {
     380                            var $mp = $( '<span>' ).addClass( 'papafpro-product-result-marketplace' );
     381                            $mp[0].appendChild( document.createTextNode( product.marketplace ) );
     382                            $item.append( $mp );
     383                        }
     384
     385                        $item.on( 'click', function() {
     386                            selectProduct( product, $hidden, $searchInput, $results, $chips, mode );
     387                        });
     388
     389                        $results.append( $item );
     390                    });
     391                    $results.show();
     392                }).catch( function( error ) {
     393                    // Ignore AbortError — expected when cancelling previous requests.
     394                    if ( error && error.name === 'AbortError' ) {
     395                        return;
     396                    }
     397                    $results.hide().empty();
     398                });
     399            }, 450 );
     400        });
     401
     402        // Close results when clicking outside.
     403        $( document ).on( 'click', function( e ) {
     404            if ( ! $( e.target ).closest( $container ).length ) {
     405                $results.hide();
     406            }
     407        });
     408
     409        return $container;
     410    }
     411
     412    /**
     413     * Handle product selection from search results.
     414     *
     415     * @param {Object} product      The selected product object.
     416     * @param {jQuery} $hidden      The hidden input storing the value.
     417     * @param {jQuery} $searchInput The visible search input.
     418     * @param {jQuery} $results     The results dropdown.
     419     * @param {jQuery} $chips       The chips container (multi mode only).
     420     * @param {string} mode         'single' or 'multi'.
     421     */
     422    function selectProduct( product, $hidden, $searchInput, $results, $chips, mode ) {
     423        if ( mode === 'single' ) {
     424            $hidden.val( product.id );
     425            $searchInput.val( product.title + ' (#' + product.id + ')' );
     426            $results.hide().empty();
     427            $hidden.trigger( 'change' );
     428        } else {
     429            // Multi: add chip, accumulate ID.
     430            var currentIds = $hidden.val().split( ',' ).filter( Boolean );
     431            if ( currentIds.indexOf( String( product.id ) ) !== -1 ) {
     432                return; // Already selected.
     433            }
     434            currentIds.push( String( product.id ) );
     435            $hidden.val( currentIds.join( ',' ) );
     436
     437            var $chip = $( '<span>' ).addClass( 'papafpro-product-chip' );
     438            var $chipText = $( '<span>' ).addClass( 'papafpro-product-chip-text' );
     439            $chipText[0].appendChild( document.createTextNode( product.title + ' #' + product.id ) );
     440            var $chipRemove = $( '<button>' )
     441                .attr({
     442                    type: 'button',
     443                    'aria-label': i18n.remove || 'Remove',
     444                    'data-id': product.id
     445                })
     446                .addClass( 'papafpro-product-chip-remove' )
     447                .html( '&times;' );
     448
     449            $chipRemove.on( 'click', function() {
     450                var removeId = String( $( this ).attr( 'data-id' ) );
     451                var ids = $hidden.val().split( ',' ).filter( function( id ) {
     452                    return id !== removeId;
     453                });
     454                $hidden.val( ids.join( ',' ) );
     455                $chip.remove();
     456                $hidden.trigger( 'change' );
     457            });
     458
     459            $chip.append( $chipText ).append( $chipRemove );
     460            $chips.append( $chip );
     461
     462            // Clear search input and close results.
     463            $searchInput.val( '' );
     464            $results.hide().empty();
     465            $hidden.trigger( 'change' );
     466        }
    202467    }
    203468
  • pap-afiliados-pro/trunk/assets/js/papafpro-stats.js

    r3482385 r3488009  
    1313    var barChart = null;
    1414    var pieChart = null;
     15    var marketplaceChart = null;
    1516    var lineChart = null;
    1617
     
    3132            pieChart.destroy();
    3233            pieChart = null;
     34        }
     35        if (marketplaceChart) {
     36            marketplaceChart.destroy();
     37            marketplaceChart = null;
    3338        }
    3439        if (lineChart) {
     
    95100        }
    96101
    97         // 3. Linker Daily Trend (line).
     102        // 3. Clicks by Marketplace (doughnut).
     103        var mktCtx = document.getElementById('papafpro-chart-marketplace');
     104        if (mktCtx) {
     105            var mktData = data.byMarketplace || {};
     106            var mktLabels = Object.keys(mktData);
     107            var mktValues = mktLabels.map(function(k) {
     108                return mktData[k];
     109            });
     110
     111            if (mktLabels.length > 0) {
     112                $(mktCtx).show();
     113                $('#papafpro-marketplace-empty').hide();
     114                marketplaceChart = new Chart(mktCtx, {
     115                    type: 'doughnut',
     116                    data: {
     117                        labels: mktLabels.map(function(l) {
     118                            return l.charAt(0).toUpperCase() + l.slice(1);
     119                        }),
     120                        datasets: [{
     121                            data: mktValues,
     122                            backgroundColor: [
     123                                'rgba(255, 153, 0, 0.8)',
     124                                'rgba(255, 230, 0, 0.8)',
     125                                'rgba(238, 77, 45, 0.8)',
     126                                'rgba(0, 86, 179, 0.8)',
     127                                'rgba(161, 35, 142, 0.8)',
     128                                'rgba(100, 100, 100, 0.8)'
     129                            ]
     130                        }]
     131                    },
     132                    options: {
     133                        responsive: true,
     134                        maintainAspectRatio: false,
     135                        plugins: {
     136                            legend: { position: 'bottom' }
     137                        }
     138                    }
     139                });
     140            } else {
     141                $(mktCtx).hide();
     142                $('#papafpro-marketplace-empty').show();
     143            }
     144        }
     145
     146        // 4. Linker Daily Trend (line).
    98147        var lineCtx = document.getElementById('papafpro-chart-linker-trend');
    99148        if (lineCtx && data.linkerDaily && data.linkerDaily.length) {
     
    142191            var $emptyRow = $('<tr>');
    143192            $emptyRow.append(
    144                 $('<td>').attr('colspan', '4').text(
     193                $('<td>').attr('colspan', '5').text(
    145194                    papafproStatsData.i18n.noData || 'Nenhum dado encontrado.'
    146195                )
     
    159208            $tr.append($('<td>').text(row.page_url));
    160209            $tr.append($('<td>').text(source));
     210            $tr.append($('<td>').text(row.marketplace || '\u2014'));
    161211            $tr.append(
    162212                $('<td>').addClass('papafpro-stats-col-total').text(row.total_clicks)
  • pap-afiliados-pro/trunk/assets/js/papafpro-template-builder.js

    r3482385 r3488009  
    2020    var currentPresetId   = null;
    2121    var currentPresetName = null;
     22    var presetClipboard   = null;
    2223
    2324    /**
     
    482483        $tbody.empty();
    483484
     485        // Destroy previous ClipboardJS instance to avoid duplicate listeners.
     486        if ( presetClipboard ) {
     487            presetClipboard.destroy();
     488            presetClipboard = null;
     489        }
     490
    484491        // Empty state.
    485492        if ( ! presets.length ) {
    486493            $thead.append(
    487                 '<tr><th colspan="4"><em>Nenhum preset salvo.</em></th></tr>'
     494                '<tr><th colspan="5"><em>Nenhum preset salvo.</em></th></tr>'
    488495            );
    489496            return;
    490497        }
     498
     499        var i18n = papafproTemplateBuilder.i18n || {};
    491500
    492501        // Header row.
     
    495504            '<th class="papafpro-col-name">Name</th>' +
    496505            '<th class="papafpro-col-id">ID</th>' +
     506            '<th class="papafpro-col-shortcode">' + ( i18n.shortcode_header || 'Shortcode' ) + '</th>' +
    497507            '<th class="papafpro-col-actions">Actions</th>' +
    498508            '</tr>';
     
    524534            $cellId.append( $idBadge );
    525535
    526             // Column 4: Actions.
     536            // Column 4: Shortcode.
     537            var $cellShortcode = $( '<td>' ).addClass( 'papafpro-col-shortcode' );
     538            var shortcodeText = '[papafpro_preset id="' + preset.id + '"]';
     539            var $code = $( '<code>' ).text( shortcodeText );
     540            var $copyBtn = $( '<button>' )
     541                .attr( 'type', 'button' )
     542                .addClass( 'papafpro-copy-btn' )
     543                .attr( 'data-clipboard-text', shortcodeText )
     544                .attr( 'title', 'Copy' )
     545                .append( $( '<span>' ).addClass( 'dashicons dashicons-clipboard' ) );
     546            var $feedback = $( '<span>' )
     547                .addClass( 'papafpro-copy-feedback' )
     548                .hide();
     549            $cellShortcode.append( $code ).append( $copyBtn ).append( $feedback );
     550
     551            // Column 5: Actions.
    527552            var $cellActions = $( '<td>' ).addClass( 'papafpro-col-actions' );
    528553            var $actions = $( '<div>' ).addClass( 'papafpro-preset-actions' )
     
    542567
    543568            $row.append( $cellCheck ).append( $cellName )
    544                 .append( $cellId ).append( $cellActions );
     569                .append( $cellId ).append( $cellShortcode ).append( $cellActions );
    545570            $tbody.append( $row );
    546571        });
     572
     573        // Initialize ClipboardJS for shortcode copy buttons.
     574        if ( typeof ClipboardJS !== 'undefined' ) {
     575            presetClipboard = new ClipboardJS( '.papafpro-preset-list .papafpro-copy-btn' );
     576
     577            presetClipboard.on( 'success', function( e ) {
     578                var $btn      = $( e.trigger );
     579                var $feedback = $btn.siblings( '.papafpro-copy-feedback' );
     580                $feedback.text( i18n.copied || 'Copied!' ).show();
     581
     582                setTimeout( function() {
     583                    $feedback.fadeOut();
     584                }, 2000 );
     585
     586                e.clearSelection();
     587            });
     588
     589            presetClipboard.on( 'error', function( e ) {
     590                // Fallback: select code text for manual copy.
     591                var $code = $( e.trigger ).siblings( 'code' );
     592                if ( $code.length ) {
     593                    var range = document.createRange();
     594                    range.selectNodeContents( $code[0] );
     595                    var sel = window.getSelection();
     596                    sel.removeAllRanges();
     597                    sel.addRange( range );
     598                }
     599            });
     600        }
    547601    }
    548602
  • pap-afiliados-pro/trunk/includes/api/class-papafpro-linker-api.php

    r3482385 r3488009  
    144144                'sanitize_callback' => 'sanitize_text_field',
    145145                'validate_callback' => function ( $value ) {
    146                     return is_string( $value ) && strlen( trim( $value ) ) >= 2;
     146                    if ( ! is_string( $value ) ) {
     147                        return false;
     148                    }
     149                    $trimmed = trim( $value );
     150                    // Accept 1-char input if purely numeric (ID search).
     151                    if ( ctype_digit( $trimmed ) ) {
     152                        return strlen( $trimmed ) >= 1;
     153                    }
     154                    return strlen( $trimmed ) >= 2;
    147155                },
    148156            ),
     
    162170     * Handle GET /papafpro/v1/products/search.
    163171     *
    164      * Searches published products by title using WP_Query.
     172     * Searches published products by title substring (LIKE) instead
     173     * of MySQL fulltext ('s'), which is more predictable and works
     174     * with short terms (autocomplete use case).
     175     *
     176     * When the query is purely numeric, also searches by post ID
     177     * and returns the exact match as the first result.
    165178     *
    166179     * @since  2.0.0
     
    172185        $limit = $request->get_param( 'limit' );
    173186
     187        $results      = array();
     188        $existing_ids = array();
     189
     190        // If query is purely numeric, try to find the product by post ID first.
     191        if ( ctype_digit( trim( $q ) ) ) {
     192            $post = get_post( absint( $q ) );
     193            if ( $post
     194                && 'papafpro_product' === $post->post_type
     195                && 'publish' === $post->post_status
     196            ) {
     197                $results[]      = $this->format_product( $post->ID );
     198                $existing_ids[] = $post->ID;
     199            }
     200        }
     201
     202        // Title substring search via LIKE (replaces fulltext 's').
     203        global $wpdb;
     204        $like = '%' . $wpdb->esc_like( $q ) . '%';
     205
     206        $title_filter = function ( $where ) use ( $wpdb, $like ) {
     207            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $wpdb->posts is a trusted table name.
     208            $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_title LIKE %s", $like );
     209            return $where;
     210        };
     211
     212        add_filter( 'posts_where', $title_filter );
     213
    174214        $query = new WP_Query(
    175215            array(
     
    177217                'post_status'    => 'publish',
    178218                'posts_per_page' => $limit,
    179                 's'              => $q,
    180             )
    181         );
    182 
    183         $results = array();
     219                'orderby'        => 'title',
     220                'order'          => 'ASC',
     221            )
     222        );
     223
     224        remove_filter( 'posts_where', $title_filter );
    184225
    185226        if ( $query->have_posts() ) {
     
    188229                $post_id = get_the_ID();
    189230
    190                 $image_url = get_the_post_thumbnail_url( $post_id, 'thumbnail' );
    191                 if ( empty( $image_url ) ) {
    192                     $image_url = get_post_meta( $post_id, 'papafpro_image_url', true );
     231                // Skip duplicates (already added via ID search).
     232                if ( in_array( $post_id, $existing_ids, true ) ) {
     233                    continue;
    193234                }
    194235
    195                 $results[] = array(
    196                     'id'          => $post_id,
    197                     'title'       => get_the_title( $post_id ),
    198                     'url'         => get_post_meta( $post_id, 'papafpro_affiliate_link', true ),
    199                     'marketplace' => get_post_meta( $post_id, 'papafpro_marketplace', true ),
    200                     'image'       => $image_url ? $image_url : '',
    201                 );
     236                $results[]      = $this->format_product( $post_id );
     237                $existing_ids[] = $post_id;
    202238            }
    203239            wp_reset_postdata();
     
    209245                'data'    => $results,
    210246            )
     247        );
     248    }
     249
     250    /**
     251     * Format a product post into the search response structure.
     252     *
     253     * @since  2.1.0
     254     * @access private
     255     * @param  int $post_id Product post ID.
     256     * @return array Formatted product data.
     257     */
     258    private function format_product( $post_id ) {
     259        $image_url = get_the_post_thumbnail_url( $post_id, 'thumbnail' );
     260        if ( empty( $image_url ) ) {
     261            $image_url = get_post_meta( $post_id, 'papafpro_image_url', true );
     262        }
     263
     264        return array(
     265            'id'          => $post_id,
     266            'title'       => get_the_title( $post_id ),
     267            'url'         => get_post_meta( $post_id, 'papafpro_affiliate_link', true ),
     268            'marketplace' => get_post_meta( $post_id, 'papafpro_marketplace', true ),
     269            'image'       => $image_url ? $image_url : '',
    211270        );
    212271    }
  • pap-afiliados-pro/trunk/includes/class-papafpro-csv-import.php

    r3482385 r3488009  
    203203
    204204        // Remove BOM before encoding detection to prevent double-encoding.
    205         $content  = $this->remove_bom( $content );
     205        $content = $this->remove_bom( $content );
    206206
    207207        // Detect and convert encoding.
  • pap-afiliados-pro/trunk/includes/class-papafpro-help-page.php

    r3482385 r3488009  
    6363            'papafpro-help',
    6464            PAPAFPRO_PLUGIN_URL . 'assets/js/papafpro-help.js',
    65             array( 'jquery', 'clipboard' ),
     65            array( 'jquery', 'clipboard', 'wp-api-fetch' ),
    6666            PAPAFPRO_VERSION,
    6767            true
     
    154154            'presets'    => $presets_data,
    155155            'shortcodes' => $this->get_shortcode_definitions(),
     156            'rest_url'   => esc_url_raw( rest_url( 'papafpro/v1/' ) ),
     157            'rest_nonce' => wp_create_nonce( 'wp_rest' ),
    156158            'i18n'       => array(
    157                 'copied'      => __( 'Copied!', 'pap-afiliados-pro' ),
    158                 'copy_failed' => __( 'Failed to copy', 'pap-afiliados-pro' ),
    159                 'select_type' => __( 'Select a type', 'pap-afiliados-pro' ),
     159                'copied'            => __( 'Copied!', 'pap-afiliados-pro' ),
     160                'copy_failed'       => __( 'Failed to copy', 'pap-afiliados-pro' ),
     161                'select_type'       => __( 'Select a type', 'pap-afiliados-pro' ),
     162                'search'            => esc_html__( 'Search…', 'pap-afiliados-pro' ),
     163                'no_results'        => esc_html__( 'No results found', 'pap-afiliados-pro' ),
     164                'search_products'   => esc_html__( 'Search products…', 'pap-afiliados-pro' ),
     165                'searching'         => esc_html__( 'Searching…', 'pap-afiliados-pro' ),
     166                'no_products_found' => esc_html__( 'No products found', 'pap-afiliados-pro' ),
     167                'min_chars'         => esc_html__( 'Type at least 2 characters', 'pap-afiliados-pro' ),
     168                'remove'            => esc_html__( 'Remove', 'pap-afiliados-pro' ),
    160169            ),
    161170        );
     
    180189                    array(
    181190                        'name'        => 'id',
    182                         'type'        => 'number',
     191                        'type'        => 'product_single',
    183192                        'required'    => true,
    184                         'description' => __( 'Product ID', 'pap-afiliados-pro' ),
     193                        'description' => __( 'Search by name or ID', 'pap-afiliados-pro' ),
    185194                    ),
    186195                    array(
     
    200209                    array(
    201210                        'name'        => 'ids',
    202                         'type'        => 'text',
     211                        'type'        => 'product_multi',
    203212                        'required'    => true,
    204                         'description' => __( 'Comma-separated IDs', 'pap-afiliados-pro' ),
     213                        'description' => __( 'Search and select products', 'pap-afiliados-pro' ),
    205214                    ),
    206215                    array(
     
    298307            ),
    299308            array(
    300                 'name'             => 'papafpro_link',
    301                 'label'            => __( 'Inline Link', 'pap-afiliados-pro' ),
    302                 'description'      => __( 'Creates an inline text link with tracking.', 'pap-afiliados-pro' ),
    303                 'enclosing'        => true,
    304                 'params'           => array(
     309                'name'              => 'papafpro_link',
     310                'label'             => __( 'Inline Link', 'pap-afiliados-pro' ),
     311                'description'       => __( 'Creates an inline text link with tracking.', 'pap-afiliados-pro' ),
     312                'enclosing'         => true,
     313                'params'            => array(
    305314                    array(
    306315                        'name'        => 'product',
    307                         'type'        => 'number',
    308                         'required'    => false,
    309                         'description' => __( 'Registered product ID (fetches URL and marketplace automatically)', 'pap-afiliados-pro' ),
     316                        'type'        => 'product_single',
     317                        'required'    => false,
     318                        'description' => __( 'Search by name or ID (fetches URL and marketplace automatically)', 'pap-afiliados-pro' ),
    310319                    ),
    311320                    array(
  • pap-afiliados-pro/trunk/includes/class-papafpro-stats.php

    r3482385 r3488009  
    101101
    102102        $data = array(
    103             'total_clicks' => $this->get_total_clicks( $days ),
    104             'topProducts'  => $this->get_top_products( 10, $days ),
    105             'bySource'     => $this->get_clicks_by_source( $days ),
    106             'detailed'     => $this->get_detailed_clicks( $days, 50 ),
    107             'linker_total' => 0,
    108             'linkerDaily'  => array(),
     103            'total_clicks'  => $this->get_total_clicks( $days ),
     104            'topProducts'   => $this->get_top_products( 10, $days ),
     105            'bySource'      => $this->get_clicks_by_source( $days ),
     106            'byMarketplace' => $this->get_clicks_by_marketplace( $days ),
     107            'detailed'      => $this->get_detailed_clicks( $days, 50 ),
     108            'linker_total'  => 0,
     109            'linkerDaily'   => array(),
    109110        );
    110111
     
    314315
    315316    /**
     317     * Get click distribution by marketplace.
     318     *
     319     * Includes all click types (card + linker).
     320     *
     321     * @since  2.1.0
     322     * @param  int $days Number of days (0 = all time).
     323     * @return array Associative array ['Amazon' => N, 'Shopee' => N, ...].
     324     */
     325    public function get_clicks_by_marketplace( $days ) {
     326        global $wpdb;
     327
     328        if ( $days > 0 ) {
     329            $cache_key = 'papafpro_by_marketplace_' . $days;
     330            $results   = wp_cache_get( $cache_key, 'papafpro' );
     331            if ( false === $results ) {
     332                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table, no WP API available.
     333                $results = $wpdb->get_results(
     334                    $wpdb->prepare(
     335                        "SELECT marketplace, COUNT(*) as total FROM %i WHERE marketplace != '' AND click_date >= DATE_SUB(NOW(), INTERVAL %d DAY) GROUP BY marketplace ORDER BY total DESC",
     336                        $this->table_name,
     337                        $days
     338                    )
     339                );
     340                wp_cache_set( $cache_key, $results, 'papafpro' );
     341            }
     342        } else {
     343            $cache_key = 'papafpro_by_marketplace_all';
     344            $results   = wp_cache_get( $cache_key, 'papafpro' );
     345            if ( false === $results ) {
     346                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table, no WP API available.
     347                $results = $wpdb->get_results(
     348                    $wpdb->prepare(
     349                        "SELECT marketplace, COUNT(*) as total FROM %i WHERE marketplace != '' GROUP BY marketplace ORDER BY total DESC",
     350                        $this->table_name
     351                    )
     352                );
     353                wp_cache_set( $cache_key, $results, 'papafpro' );
     354            }
     355        }
     356
     357        $by_marketplace = array();
     358
     359        if ( is_array( $results ) ) {
     360            foreach ( $results as $row ) {
     361                $by_marketplace[ $row->marketplace ] = (int) $row->total;
     362            }
     363        }
     364
     365        return $by_marketplace;
     366    }
     367
     368    /**
    316369     * Get detailed click breakdown grouped by product, page, source, and type.
    317370     *
     
    319372     * @param  int $days  Number of days (0 = all time).
    320373     * @param  int $limit Maximum number of rows.
    321      * @return array Array of objects with product_id, product_title, page_url, click_source, click_type, total_clicks.
     374     * @return array Array of objects with product_id, product_title, page_url, click_source, click_type, marketplace, total_clicks.
    322375     */
    323376    public function get_detailed_clicks( $days, $limit = 50 ) {
     
    336389                $results = $wpdb->get_results(
    337390                    $wpdb->prepare(
    338                         'SELECT product_id, page_url, click_source, click_type, COUNT(*) as total_clicks FROM %i WHERE click_date >= DATE_SUB(NOW(), INTERVAL %d DAY) GROUP BY product_id, page_url, click_source, click_type ORDER BY total_clicks DESC LIMIT %d',
     391                        'SELECT product_id, page_url, click_source, click_type, marketplace, COUNT(*) as total_clicks FROM %i WHERE click_date >= DATE_SUB(NOW(), INTERVAL %d DAY) GROUP BY product_id, page_url, click_source, click_type, marketplace ORDER BY total_clicks DESC LIMIT %d',
    339392                        $this->table_name,
    340393                        $days,
     
    351404                $results = $wpdb->get_results(
    352405                    $wpdb->prepare(
    353                         'SELECT product_id, page_url, click_source, click_type, COUNT(*) as total_clicks FROM %i GROUP BY product_id, page_url, click_source, click_type ORDER BY total_clicks DESC LIMIT %d',
     406                        'SELECT product_id, page_url, click_source, click_type, marketplace, COUNT(*) as total_clicks FROM %i GROUP BY product_id, page_url, click_source, click_type, marketplace ORDER BY total_clicks DESC LIMIT %d',
    354407                        $this->table_name,
    355408                        $limit
     
    398451
    399452        $data = array(
    400             'total_clicks' => $this->get_total_clicks( $days ),
    401             'topProducts'  => $this->get_top_products( 10, $days ),
    402             'bySource'     => $this->get_clicks_by_source( $days ),
    403             'detailed'     => $this->get_detailed_clicks( $days, 50 ),
    404             'linker_total' => $linker_total,
    405             'linkerDaily'  => $linker_daily,
     453            'total_clicks'  => $this->get_total_clicks( $days ),
     454            'topProducts'   => $this->get_top_products( 10, $days ),
     455            'bySource'      => $this->get_clicks_by_source( $days ),
     456            'byMarketplace' => $this->get_clicks_by_marketplace( $days ),
     457            'detailed'      => $this->get_detailed_clicks( $days, 50 ),
     458            'linker_total'  => $linker_total,
     459            'linkerDaily'   => $linker_daily,
    406460        );
    407461
     
    466520            'papafproStatsData',
    467521            array(
    468                 'ajaxUrl'     => admin_url( 'admin-ajax.php' ),
    469                 'nonce'       => wp_create_nonce( 'papafpro_stats_nonce' ),
    470                 'topProducts' => $this->get_top_products( 10, $days ),
    471                 'bySource'    => $this->get_clicks_by_source( $days ),
    472                 'totalClicks' => $this->get_total_clicks( $days ),
    473                 'detailed'    => $this->get_detailed_clicks( $days, 50 ),
    474                 'linkerTotal' => $linker_total,
    475                 'linkerDaily' => $linker_daily,
    476                 'canClear'    => current_user_can( 'manage_options' ),
    477                 'i18n'        => array(
    478                     'clicks'       => __( 'Clicks', 'pap-afiliados-pro' ),
    479                     'linkerClicks' => __( 'Linker Clicks', 'pap-afiliados-pro' ),
    480                     'noData'       => __( 'No data found.', 'pap-afiliados-pro' ),
    481                     'confirmClear' => __( 'Are you sure you want to clear all statistics? This action cannot be undone.', 'pap-afiliados-pro' ),
     522                'ajaxUrl'       => admin_url( 'admin-ajax.php' ),
     523                'nonce'         => wp_create_nonce( 'papafpro_stats_nonce' ),
     524                'topProducts'   => $this->get_top_products( 10, $days ),
     525                'bySource'      => $this->get_clicks_by_source( $days ),
     526                'byMarketplace' => $this->get_clicks_by_marketplace( $days ),
     527                'totalClicks'   => $this->get_total_clicks( $days ),
     528                'detailed'      => $this->get_detailed_clicks( $days, 50 ),
     529                'linkerTotal'   => $linker_total,
     530                'linkerDaily'   => $linker_daily,
     531                'canClear'      => current_user_can( 'manage_options' ),
     532                'i18n'          => array(
     533                    'clicks'            => __( 'Clicks', 'pap-afiliados-pro' ),
     534                    'linkerClicks'      => __( 'Linker Clicks', 'pap-afiliados-pro' ),
     535                    'marketplace'       => __( 'Marketplace', 'pap-afiliados-pro' ),
     536                    'noMarketplaceData' => __( 'No marketplace data available', 'pap-afiliados-pro' ),
     537                    'noData'            => __( 'No data found.', 'pap-afiliados-pro' ),
     538                    'confirmClear'      => __( 'Are you sure you want to clear all statistics? This action cannot be undone.', 'pap-afiliados-pro' ),
    482539                ),
    483540            )
  • pap-afiliados-pro/trunk/includes/class-papafpro-template-builder.php

    r3482385 r3488009  
    10471047                'papafpro-template-builder',
    10481048                PAPAFPRO_PLUGIN_URL . 'assets/js/papafpro-template-builder.js',
    1049                 array( 'jquery', 'wp-color-picker' ),
     1049                array( 'jquery', 'wp-color-picker', 'clipboard' ),
    10501050                PAPAFPRO_VERSION,
    10511051                true
     
    10641064                        'save_settings_label' => __( 'Save Settings', 'pap-afiliados-pro' ),
    10651065                        'global_restored'     => __( 'Global settings restored.', 'pap-afiliados-pro' ),
     1066                        'shortcode_header'    => esc_html__( 'Shortcode', 'pap-afiliados-pro' ),
     1067                        'copied'              => esc_html__( 'Copied!', 'pap-afiliados-pro' ),
    10661068                    ),
    10671069                )
  • pap-afiliados-pro/trunk/includes/class-papafpro-template-css.php

    r3482385 r3488009  
    44 *
    55 * Responsável pela geração de CSS dinâmico dos cards de produto.
    6  * Gera variáveis CSS inline e sanitiza CSS customizado.
     6 * Gera variáveis CSS inline para o atributo style dos cards.
    77 *
    88 * @package    PAP_Afiliados_Pro
     
    4646
    4747        return trim( $vars );
    48     }
    49 
    50     /**
    51      * Sanitize CSS string removing dangerous patterns.
    52      *
    53      * NOTE: This method is currently not called in rendering pipeline.
    54      * Retained as utility for potential future use (e.g., Customizer integration).
    55      * The custom_css feature was removed per WordPress.org reviewer directive
    56      * prohibiting arbitrary CSS input from users.
    57      *
    58      * @since  2.0.0
    59      * @param  string $css Raw CSS string.
    60      * @return string Sanitized CSS.
    61      */
    62     public static function sanitize_css( $css ) {
    63         if ( empty( $css ) ) {
    64             return '';
    65         }
    66 
    67         // Remove padrões perigosos.
    68         $dangerous_patterns = array(
    69             '/expression\s*\(/i',           // CSS expression (IE).
    70             '/behavior\s*:/i',              // CSS behavior (IE).
    71             '/javascript\s*:/i',            // javascript: protocol.
    72             '/-moz-binding\s*:/i',          // Firefox XBL binding.
    73             '/url\s*\(\s*["\']?\s*data:/i', // data: URLs em CSS.
    74             '/@import/i',                   // @import pode carregar CSS externo.
    75         );
    76 
    77         foreach ( $dangerous_patterns as $pattern ) {
    78             $css = preg_replace( $pattern, '/* removed */', $css );
    79         }
    80 
    81         return wp_strip_all_tags( $css );
    8248    }
    8349
  • pap-afiliados-pro/trunk/includes/class-papafpro-template-render.php

    r3482385 r3488009  
    125125            }
    126126
    127             $output  = '<div class="' . esc_attr( $grid_classes ) . '"';
     127            $output = '<div class="' . esc_attr( $grid_classes ) . '"';
    128128            if ( $grid_style ) {
    129129                $output .= ' style="' . esc_attr( $grid_style ) . '"';
  • pap-afiliados-pro/trunk/includes/class-papafpro-utilities.php

    r3482385 r3488009  
    1414 * Classe Utilities - Funções auxiliares.
    1515 *
    16  * Funções reutilizáveis para sanitização, validação, formatação, etc.
     16 * Funções reutilizáveis para detecção de marketplace.
    1717 *
    1818 * @since 2.0.0
    1919 */
    2020class PAPAFPRO_Utilities {
    21 
    22     /**
    23      * Sanitizar array de settings.
    24      *
    25      * @since  2.0.0
    26      * @param  array $settings Array de configurações.
    27      * @return array           Array sanitizado.
    28      */
    29     public static function sanitize_settings( $settings ) {
    30         if ( ! is_array( $settings ) ) {
    31             return array();
    32         }
    33 
    34         $sanitized = array();
    35 
    36         // Strings.
    37         $string_fields = array(
    38             'button_text',
    39             'price_format',
    40             'no_price_text',
    41             'card_width',
    42             'card_height',
    43             'image_height',
    44             'border_radius',
    45             'shadow_size',
    46             'bg_color',
    47             'text_color',
    48             'title_color',
    49             'price_color',
    50             'button_bg_color',
    51             'button_text_color',
    52             'badge_bg_color',
    53             'badge_text_color',
    54             'title_font_size',
    55             'title_font_weight',
    56             'price_font_size',
    57             'price_font_weight',
    58             'button_font_size',
    59             'button_font_weight',
    60             'padding',
    61             'title_margin',
    62             'price_margin',
    63             'button_padding',
    64             'hover_transform',
    65             'hover_shadow',
    66             'badge_position',
    67             'badge_border_radius',
    68             'badge_padding',
    69             'badge_font_size',
    70         );
    71 
    72         foreach ( $string_fields as $field ) {
    73             if ( isset( $settings[ $field ] ) ) {
    74                 $sanitized[ $field ] = sanitize_text_field( wp_unslash( $settings[ $field ] ) );
    75             }
    76         }
    77 
    78         // Booleans.
    79         $bool_fields = array(
    80             'show_price',
    81             'clickable_title',
    82             'open_new_tab',
    83             'show_custom_badge',
    84             'show_marketplace',
    85             'enable_hover',
    86             'show_amazon_badge',
    87             'show_ml_badge',
    88             'show_shopee_badge',
    89             'show_aliexpress_badge',
    90             'show_magalu_badge',
    91             'keep_data_on_uninstall',
    92         );
    93 
    94         foreach ( $bool_fields as $field ) {
    95             if ( isset( $settings[ $field ] ) ) {
    96                 $sanitized[ $field ] = (bool) $settings[ $field ];
    97             }
    98         }
    99 
    100         return $sanitized;
    101     }
    102 
    103     /**
    104      * Sanitize custom CSS string removing dangerous patterns.
    105      *
    106      * NOTE: This method is currently not called in rendering pipeline.
    107      * Retained as utility for potential future use (e.g., Customizer integration).
    108      * The custom_css feature was removed per WordPress.org reviewer directive
    109      * prohibiting arbitrary CSS input from users.
    110      *
    111      * @since  2.0.0
    112      * @param  string $css CSS para sanitizar.
    113      * @return string      CSS sanitizado.
    114      */
    115     public static function sanitize_custom_css( $css ) {
    116         // Lista de propriedades proibidas.
    117         $forbidden = array(
    118             'expression',
    119             'javascript:',
    120             'vbscript:',
    121             'data:text/html',
    122             '-moz-binding',
    123             '@import',
    124         );
    125 
    126         // Remover propriedades perigosas.
    127         foreach ( $forbidden as $pattern ) {
    128             $css = str_ireplace( $pattern, '', $css );
    129         }
    130 
    131         // Remover tags HTML.
    132         $css = wp_strip_all_tags( $css );
    133 
    134         return $css;
    135     }
    13621
    13722    /**
     
    16348        return 'outros';
    16449    }
    165 
    166     /**
    167      * Formatar preço para exibição.
    168      *
    169      * @since  2.0.0
    170      * @param  string $price        Preço bruto (ex: "199.90").
    171      * @param  string $price_format Formato (ex: "R$ {valor}").
    172      * @return string               Preço formatado (ex: "R$ 199,90").
    173      */
    174     public static function format_price( $price, $price_format = 'R$ {valor}' ) {
    175         if ( empty( $price ) ) {
    176             return '';
    177         }
    178 
    179         // Converter ponto para vírgula (padrão BR).
    180         $price_formatted = str_replace( '.', ',', $price );
    181 
    182         // Aplicar formato.
    183         $output = str_replace( '{valor}', $price_formatted, $price_format );
    184 
    185         return esc_html( $output );
    186     }
    187 
    188     /**
    189      * Validar se URL é válida.
    190      *
    191      * @since  2.0.0
    192      * @param  string $url URL para validar.
    193      * @return bool        true se válida, false caso contrário.
    194      */
    195     public static function is_valid_url( $url ) {
    196         return ( filter_var( $url, FILTER_VALIDATE_URL ) !== false );
    197     }
    198 
    199     /**
    200      * Obter URL da página atual.
    201      *
    202      * @since  2.0.0
    203      * @return string URL da página atual (apenas path, sem domínio).
    204      */
    205     public static function get_current_page_url() {
    206         // Sanitizar SERVER vars.
    207         $request_uri = isset( $_SERVER['REQUEST_URI'] )
    208             ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) )
    209             : '';
    210 
    211         // Remover query string.
    212         $path = strtok( $request_uri, '?' );
    213 
    214         return esc_url_raw( $path );
    215     }
    216 
    217     /**
    218      * Converter array de settings para JSON.
    219      *
    220      * @since  2.0.0
    221      * @param  array $settings Array de settings.
    222      * @return string          JSON string.
    223      */
    224     public static function settings_to_json( $settings ) {
    225         return wp_json_encode( $settings, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
    226     }
    227 
    228     /**
    229      * Converter JSON para array de settings.
    230      *
    231      * @since  2.0.0
    232      * @param  string $json JSON string.
    233      * @return array        Array de settings.
    234      */
    235     public static function json_to_settings( $json ) {
    236         $settings = json_decode( $json, true );
    237 
    238         if ( ! is_array( $settings ) ) {
    239             return array();
    240         }
    241 
    242         return $settings;
    243     }
    24450}
  • pap-afiliados-pro/trunk/languages/pap-afiliados-pro-pt_BR.po

    r3482385 r3488009  
    66"Project-Id-Version: PAP Afiliados Pro 2.0.3\n"
    77"Report-Msgid-Bugs-To: https://pap-afiliados-pro.com.br\n"
    8 "POT-Creation-Date: 2026-03-13T00:00:00+00:00\n"
    9 "PO-Revision-Date: 2026-03-13 00:00+0000\n"
     8"POT-Creation-Date: 2026-03-20T00:00:00+00:00\n"
     9"PO-Revision-Date: 2026-03-20 00:00+0000\n"
    1010"Last-Translator: Fernando Pimenta\n"
    1111"Language-Team: Brazilian Portuguese\n"
     
    172172
    173173#: includes/integrations/elementor/class-papafpro-elementor-widgetphp:112
    174 #: includes/class-papafpro-help-pagephp:184
    175174msgid "Product ID"
    176175msgstr "ID do produto"
     176
     177#: includes/class-papafpro-help-pagephp:184
     178msgid "Search by name or ID"
     179msgstr "Buscar por nome ou ID"
    177180
    178181#: includes/integrations/elementor/class-papafpro-elementor-widgetphp:125
     
    682685
    683686#: includes/class-papafpro-products-adminphp:243
     687#: includes/class-papafpro-template-builderphp:1066
    684688msgid "Shortcode"
    685689msgstr "Shortcode"
     
    798802
    799803#: includes/class-papafpro-help-pagephp:157
     804#: includes/class-papafpro-template-builderphp:1067
    800805msgid "Copied!"
    801806msgstr "Copiado!"
     
    808813msgid "Select a type"
    809814msgstr "Selecione um tipo"
     815
     816#: includes/class-papafpro-help-pagephp:160
     817msgid "Search\u2026"
     818msgstr "Buscar\u2026"
     819
     820#: includes/class-papafpro-help-pagephp:161
     821msgid "No results found"
     822msgstr "Nenhum resultado encontrado"
     823
     824#: includes/class-papafpro-help-pagephp:162
     825msgid "Search products\u2026"
     826msgstr "Buscar produtos\u2026"
     827
     828#: includes/class-papafpro-help-pagephp:163
     829msgid "Searching\u2026"
     830msgstr "Buscando\u2026"
     831
     832#: includes/class-papafpro-help-pagephp:164
     833msgid "No products found"
     834msgstr "Nenhum produto encontrado"
     835
     836#: includes/class-papafpro-help-pagephp:165
     837msgid "Type at least 2 characters"
     838msgstr "Digite pelo menos 2 caracteres"
     839
     840#: includes/class-papafpro-help-pagephp:166
     841msgid "Remove"
     842msgstr "Remover"
    810843
    811844#: includes/class-papafpro-help-pagephp:178
     
    830863
    831864#: includes/class-papafpro-help-pagephp:204
    832 msgid "Comma-separated IDs"
    833 msgstr "IDs separados por vírgula"
     865msgid "Search and select products"
     866msgstr "Buscar e selecionar produtos"
    834867
    835868#: includes/class-papafpro-help-pagephp:210
     
    881914
    882915#: includes/class-papafpro-help-pagephp:309
    883 msgid "Registered product ID (fetches URL and marketplace automatically)"
    884 msgstr "ID do produto cadastrado (busca URL e marketplace automaticamente)"
     916msgid "Search by name or ID (fetches URL and marketplace automatically)"
     917msgstr "Buscar por nome ou ID (obtém URL e marketplace automaticamente)"
    885918
    886919#: includes/class-papafpro-help-pagephp:315
     
    16801713msgstr "Inserir Link"
    16811714
     1715#: includes/class-papafpro-stats.php views/papafpro-stats-page.php
     1716msgid "Clicks by Marketplace"
     1717msgstr "Cliques por Marketplace"
     1718
     1719#: includes/class-papafpro-stats.php views/papafpro-stats-page.php
     1720msgid "No marketplace data available"
     1721msgstr "Sem dados de marketplace"
     1722
     1723#: includes/class-papafpro-stats.php views/papafpro-stats-page.php
     1724msgid "Marketplace"
     1725msgstr "Marketplace"
     1726
  • pap-afiliados-pro/trunk/languages/pap-afiliados-pro.pot

    r3482385 r3488009  
    55"Project-Id-Version: PAP Afiliados Pro 2.0.3\n"
    66"Report-Msgid-Bugs-To: https://pap-afiliados-pro.com.br\n"
    7 "POT-Creation-Date: 2026-03-13T00:00:00+00:00\n"
     7"POT-Creation-Date: 2026-03-20T00:00:00+00:00\n"
    88"MIME-Version: 1.0\n"
    99"Content-Type: text/plain; charset=UTF-8\n"
     
    168168
    169169#: includes/integrations/elementor/class-papafpro-elementor-widgetphp:112
     170msgid "Product ID"
     171msgstr ""
     172
    170173#: includes/class-papafpro-help-pagephp:184
    171 msgid "Product ID"
     174msgid "Search by name or ID"
    172175msgstr ""
    173176
     
    678681
    679682#: includes/class-papafpro-products-adminphp:243
     683#: includes/class-papafpro-template-builderphp:1066
    680684msgid "Shortcode"
    681685msgstr ""
     
    794798
    795799#: includes/class-papafpro-help-pagephp:157
     800#: includes/class-papafpro-template-builderphp:1067
    796801msgid "Copied!"
    797802msgstr ""
     
    803808#: includes/class-papafpro-help-pagephp:159
    804809msgid "Select a type"
     810msgstr ""
     811
     812#: includes/class-papafpro-help-pagephp:160
     813msgid "Search\u2026"
     814msgstr ""
     815
     816#: includes/class-papafpro-help-pagephp:161
     817msgid "No results found"
     818msgstr ""
     819
     820#: includes/class-papafpro-help-pagephp:162
     821msgid "Search products\u2026"
     822msgstr ""
     823
     824#: includes/class-papafpro-help-pagephp:163
     825msgid "Searching\u2026"
     826msgstr ""
     827
     828#: includes/class-papafpro-help-pagephp:164
     829msgid "No products found"
     830msgstr ""
     831
     832#: includes/class-papafpro-help-pagephp:165
     833msgid "Type at least 2 characters"
     834msgstr ""
     835
     836#: includes/class-papafpro-help-pagephp:166
     837msgid "Remove"
    805838msgstr ""
    806839
     
    826859
    827860#: includes/class-papafpro-help-pagephp:204
    828 msgid "Comma-separated IDs"
     861msgid "Search and select products"
    829862msgstr ""
    830863
     
    877910
    878911#: includes/class-papafpro-help-pagephp:309
    879 msgid "Registered product ID (fetches URL and marketplace automatically)"
     912msgid "Search by name or ID (fetches URL and marketplace automatically)"
    880913msgstr ""
    881914
     
    16761709msgstr ""
    16771710
     1711#: includes/class-papafpro-stats.php views/papafpro-stats-page.php
     1712msgid "Clicks by Marketplace"
     1713msgstr ""
     1714
     1715#: includes/class-papafpro-stats.php views/papafpro-stats-page.php
     1716msgid "No marketplace data available"
     1717msgstr ""
     1718
     1719#: includes/class-papafpro-stats.php views/papafpro-stats-page.php
     1720msgid "Marketplace"
     1721msgstr ""
     1722
  • pap-afiliados-pro/trunk/pap-afiliados-pro.php

    r3482385 r3488009  
    44 * Plugin URI:        https://pap-afiliados-pro.com.br
    55 * Description:       Professional affiliate link management for Brazilian marketplaces (Amazon, Mercado Livre, Shopee, AliExpress, and others).
    6  * Version:           2.0.3
     6 * Version:           2.1.0
    77 * Requires at least: 6.2
    88 * Requires PHP:      7.4
     
    2424 * Constantes do plugin.
    2525 */
    26 define( 'PAPAFPRO_VERSION', '2.0.3' );
     26define( 'PAPAFPRO_VERSION', '2.1.0' );
    2727define( 'PAPAFPRO_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    2828define( 'PAPAFPRO_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
  • pap-afiliados-pro/trunk/readme.txt

    r3482385 r3488009  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 2.0.3
     7Stable tag: 2.1.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    130130
    131131== Changelog ==
     132
     133= 2.1.0 =
     134* Added: Shortcode column in presets list with one-click copy
     135* Added: Filter/search in shortcode generator dropdowns (5+ options)
     136* Added: Product search by name or ID in shortcode generator
     137* Added: Marketplace distribution chart in Statistics page
     138* Added: Marketplace column in detailed clicks table
     139* Improved: Product search endpoint performance (LIKE-based)
     140* Improved: Search request handling with abort on new input
    132141
    133142= 2.0.3 =
     
    176185== Upgrade Notice ==
    177186
     187= 2.1.0 =
     188New features: shortcode column in presets, filter/search in shortcode generator, product search by name/ID, and marketplace stats with doughnut chart.
     189
    178190= 2.0.3 =
    179191Fixes marketplace badge display for Mercado Livre and corrects hardcoded Portuguese strings for proper translation support.
  • pap-afiliados-pro/trunk/views/papafpro-stats-page.php

    r3482385 r3488009  
    7878        </div>
    7979        <div class="papafpro-stats-chart-container">
     80            <h3><?php esc_html_e( 'Clicks by Marketplace', 'pap-afiliados-pro' ); ?></h3>
     81            <div class="papafpro-stats-chart-wrap">
     82                <canvas id="papafpro-chart-marketplace"></canvas>
     83            </div>
     84            <p class="papafpro-stats-chart-empty" id="papafpro-marketplace-empty" style="display:none;">
     85                <?php esc_html_e( 'No marketplace data available', 'pap-afiliados-pro' ); ?>
     86            </p>
     87        </div>
     88        <div class="papafpro-stats-chart-container">
    8089            <h3><?php esc_html_e( 'Linker Trend', 'pap-afiliados-pro' ); ?></h3>
    8190            <div class="papafpro-stats-chart-wrap">
     
    94103                    <th><?php esc_html_e( 'Page', 'pap-afiliados-pro' ); ?></th>
    95104                    <th><?php esc_html_e( 'Source', 'pap-afiliados-pro' ); ?></th>
     105                    <th><?php esc_html_e( 'Marketplace', 'pap-afiliados-pro' ); ?></th>
    96106                    <th class="papafpro-stats-col-total"><?php esc_html_e( 'Total', 'pap-afiliados-pro' ); ?></th>
    97107                </tr>
     
    110120                    <?php endif; ?>
    111121                    </td>
     122                    <td><?php echo esc_html( ! empty( $papafpro_row->marketplace ) ? $papafpro_row->marketplace : '—' ); ?></td>
    112123                    <td class="papafpro-stats-col-total"><?php echo esc_html( number_format_i18n( $papafpro_row->total_clicks ) ); ?></td>
    113124                </tr>
     
    115126            <?php else : ?>
    116127                <tr>
    117                     <td colspan="4"><?php esc_html_e( 'No data found.', 'pap-afiliados-pro' ); ?></td>
     128                    <td colspan="5"><?php esc_html_e( 'No data found.', 'pap-afiliados-pro' ); ?></td>
    118129                </tr>
    119130            <?php endif; ?>
Note: See TracChangeset for help on using the changeset viewer.