Changeset 3488009
- Timestamp:
- 03/22/2026 02:43:36 AM (10 days ago)
- Location:
- pap-afiliados-pro/trunk
- Files:
-
- 20 edited
-
assets/css/papafpro-help.css (modified) (1 diff)
-
assets/css/papafpro-stats.css (modified) (1 diff)
-
assets/css/papafpro-template-builder.css (modified) (2 diffs)
-
assets/js/papafpro-help.js (modified) (6 diffs)
-
assets/js/papafpro-stats.js (modified) (5 diffs)
-
assets/js/papafpro-template-builder.js (modified) (5 diffs)
-
includes/api/class-papafpro-linker-api.php (modified) (6 diffs)
-
includes/class-papafpro-csv-import.php (modified) (1 diff)
-
includes/class-papafpro-help-page.php (modified) (5 diffs)
-
includes/class-papafpro-stats.php (modified) (7 diffs)
-
includes/class-papafpro-template-builder.php (modified) (2 diffs)
-
includes/class-papafpro-template-css.php (modified) (2 diffs)
-
includes/class-papafpro-template-render.php (modified) (1 diff)
-
includes/class-papafpro-utilities.php (modified) (2 diffs)
-
languages/pap-afiliados-pro-pt_BR.mo (modified) (previous)
-
languages/pap-afiliados-pro-pt_BR.po (modified) (8 diffs)
-
languages/pap-afiliados-pro.pot (modified) (8 diffs)
-
pap-afiliados-pro.php (modified) (2 diffs)
-
readme.txt (modified) (3 diffs)
-
views/papafpro-stats-page.php (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
pap-afiliados-pro/trunk/assets/css/papafpro-help.css
r3482385 r3488009 138 138 } 139 139 } 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 108 108 } 109 109 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 110 118 /* Detailed table */ 111 119 .papafpro-stats-table-container { -
pap-afiliados-pro/trunk/assets/css/papafpro-template-builder.css
r3482385 r3488009 264 264 } 265 265 266 .papafpro-preset-list thead th.papafpro-col-shortcode { 267 width: 220px; 268 text-align: left; 269 } 270 266 271 .papafpro-preset-list thead th.papafpro-col-actions { 267 272 width: 220px; … … 285 290 .papafpro-preset-list tbody td.papafpro-col-id { 286 291 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; 287 334 } 288 335 -
pap-afiliados-pro/trunk/assets/js/papafpro-help.js
r3482385 r3488009 23 23 var i18n = papafproHelp.i18n || {}; 24 24 25 var searchDebounceTimer = null; 26 25 27 // ========================================================================= 26 28 // 1. GERADOR DE SHORTCODES … … 63 65 case 'preset': 64 66 $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' ); 65 73 break; 66 74 case 'number': … … 134 142 135 143 /** 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 /** 136 201 * Construir select de categorias. 137 202 * … … 153 218 }); 154 219 220 addFilterToSelect( $select ); 221 155 222 return $select; 156 223 } … … 176 243 }); 177 244 245 addFilterToSelect( $select ); 246 178 247 return $select; 179 248 } … … 199 268 }); 200 269 270 addFilterToSelect( $select ); 271 201 272 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( '×' ); 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 } 202 467 } 203 468 -
pap-afiliados-pro/trunk/assets/js/papafpro-stats.js
r3482385 r3488009 13 13 var barChart = null; 14 14 var pieChart = null; 15 var marketplaceChart = null; 15 16 var lineChart = null; 16 17 … … 31 32 pieChart.destroy(); 32 33 pieChart = null; 34 } 35 if (marketplaceChart) { 36 marketplaceChart.destroy(); 37 marketplaceChart = null; 33 38 } 34 39 if (lineChart) { … … 95 100 } 96 101 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). 98 147 var lineCtx = document.getElementById('papafpro-chart-linker-trend'); 99 148 if (lineCtx && data.linkerDaily && data.linkerDaily.length) { … … 142 191 var $emptyRow = $('<tr>'); 143 192 $emptyRow.append( 144 $('<td>').attr('colspan', ' 4').text(193 $('<td>').attr('colspan', '5').text( 145 194 papafproStatsData.i18n.noData || 'Nenhum dado encontrado.' 146 195 ) … … 159 208 $tr.append($('<td>').text(row.page_url)); 160 209 $tr.append($('<td>').text(source)); 210 $tr.append($('<td>').text(row.marketplace || '\u2014')); 161 211 $tr.append( 162 212 $('<td>').addClass('papafpro-stats-col-total').text(row.total_clicks) -
pap-afiliados-pro/trunk/assets/js/papafpro-template-builder.js
r3482385 r3488009 20 20 var currentPresetId = null; 21 21 var currentPresetName = null; 22 var presetClipboard = null; 22 23 23 24 /** … … 482 483 $tbody.empty(); 483 484 485 // Destroy previous ClipboardJS instance to avoid duplicate listeners. 486 if ( presetClipboard ) { 487 presetClipboard.destroy(); 488 presetClipboard = null; 489 } 490 484 491 // Empty state. 485 492 if ( ! presets.length ) { 486 493 $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>' 488 495 ); 489 496 return; 490 497 } 498 499 var i18n = papafproTemplateBuilder.i18n || {}; 491 500 492 501 // Header row. … … 495 504 '<th class="papafpro-col-name">Name</th>' + 496 505 '<th class="papafpro-col-id">ID</th>' + 506 '<th class="papafpro-col-shortcode">' + ( i18n.shortcode_header || 'Shortcode' ) + '</th>' + 497 507 '<th class="papafpro-col-actions">Actions</th>' + 498 508 '</tr>'; … … 524 534 $cellId.append( $idBadge ); 525 535 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. 527 552 var $cellActions = $( '<td>' ).addClass( 'papafpro-col-actions' ); 528 553 var $actions = $( '<div>' ).addClass( 'papafpro-preset-actions' ) … … 542 567 543 568 $row.append( $cellCheck ).append( $cellName ) 544 .append( $cellId ).append( $cell Actions );569 .append( $cellId ).append( $cellShortcode ).append( $cellActions ); 545 570 $tbody.append( $row ); 546 571 }); 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 } 547 601 } 548 602 -
pap-afiliados-pro/trunk/includes/api/class-papafpro-linker-api.php
r3482385 r3488009 144 144 'sanitize_callback' => 'sanitize_text_field', 145 145 '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; 147 155 }, 148 156 ), … … 162 170 * Handle GET /papafpro/v1/products/search. 163 171 * 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. 165 178 * 166 179 * @since 2.0.0 … … 172 185 $limit = $request->get_param( 'limit' ); 173 186 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 174 214 $query = new WP_Query( 175 215 array( … … 177 217 'post_status' => 'publish', 178 218 '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 ); 184 225 185 226 if ( $query->have_posts() ) { … … 188 229 $post_id = get_the_ID(); 189 230 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; 193 234 } 194 235 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; 202 238 } 203 239 wp_reset_postdata(); … … 209 245 'data' => $results, 210 246 ) 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 : '', 211 270 ); 212 271 } -
pap-afiliados-pro/trunk/includes/class-papafpro-csv-import.php
r3482385 r3488009 203 203 204 204 // Remove BOM before encoding detection to prevent double-encoding. 205 $content = $this->remove_bom( $content );205 $content = $this->remove_bom( $content ); 206 206 207 207 // Detect and convert encoding. -
pap-afiliados-pro/trunk/includes/class-papafpro-help-page.php
r3482385 r3488009 63 63 'papafpro-help', 64 64 PAPAFPRO_PLUGIN_URL . 'assets/js/papafpro-help.js', 65 array( 'jquery', 'clipboard' ),65 array( 'jquery', 'clipboard', 'wp-api-fetch' ), 66 66 PAPAFPRO_VERSION, 67 67 true … … 154 154 'presets' => $presets_data, 155 155 'shortcodes' => $this->get_shortcode_definitions(), 156 'rest_url' => esc_url_raw( rest_url( 'papafpro/v1/' ) ), 157 'rest_nonce' => wp_create_nonce( 'wp_rest' ), 156 158 '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' ), 160 169 ), 161 170 ); … … 180 189 array( 181 190 'name' => 'id', 182 'type' => ' number',191 'type' => 'product_single', 183 192 'required' => true, 184 'description' => __( ' ProductID', 'pap-afiliados-pro' ),193 'description' => __( 'Search by name or ID', 'pap-afiliados-pro' ), 185 194 ), 186 195 array( … … 200 209 array( 201 210 'name' => 'ids', 202 'type' => ' text',211 'type' => 'product_multi', 203 212 'required' => true, 204 'description' => __( ' Comma-separated IDs', 'pap-afiliados-pro' ),213 'description' => __( 'Search and select products', 'pap-afiliados-pro' ), 205 214 ), 206 215 array( … … 298 307 ), 299 308 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( 305 314 array( 306 315 'name' => 'product', 307 'type' => ' number',308 'required' => false, 309 'description' => __( ' Registered productID (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' ), 310 319 ), 311 320 array( -
pap-afiliados-pro/trunk/includes/class-papafpro-stats.php
r3482385 r3488009 101 101 102 102 $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(), 109 110 ); 110 111 … … 314 315 315 316 /** 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 /** 316 369 * Get detailed click breakdown grouped by product, page, source, and type. 317 370 * … … 319 372 * @param int $days Number of days (0 = all time). 320 373 * @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. 322 375 */ 323 376 public function get_detailed_clicks( $days, $limit = 50 ) { … … 336 389 $results = $wpdb->get_results( 337 390 $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', 339 392 $this->table_name, 340 393 $days, … … 351 404 $results = $wpdb->get_results( 352 405 $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', 354 407 $this->table_name, 355 408 $limit … … 398 451 399 452 $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, 406 460 ); 407 461 … … 466 520 'papafproStatsData', 467 521 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' ), 482 539 ), 483 540 ) -
pap-afiliados-pro/trunk/includes/class-papafpro-template-builder.php
r3482385 r3488009 1047 1047 'papafpro-template-builder', 1048 1048 PAPAFPRO_PLUGIN_URL . 'assets/js/papafpro-template-builder.js', 1049 array( 'jquery', 'wp-color-picker' ),1049 array( 'jquery', 'wp-color-picker', 'clipboard' ), 1050 1050 PAPAFPRO_VERSION, 1051 1051 true … … 1064 1064 'save_settings_label' => __( 'Save Settings', 'pap-afiliados-pro' ), 1065 1065 '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' ), 1066 1068 ), 1067 1069 ) -
pap-afiliados-pro/trunk/includes/class-papafpro-template-css.php
r3482385 r3488009 4 4 * 5 5 * 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. 7 7 * 8 8 * @package PAP_Afiliados_Pro … … 46 46 47 47 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 directive56 * prohibiting arbitrary CSS input from users.57 *58 * @since 2.0.059 * @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 );82 48 } 83 49 -
pap-afiliados-pro/trunk/includes/class-papafpro-template-render.php
r3482385 r3488009 125 125 } 126 126 127 $output = '<div class="' . esc_attr( $grid_classes ) . '"';127 $output = '<div class="' . esc_attr( $grid_classes ) . '"'; 128 128 if ( $grid_style ) { 129 129 $output .= ' style="' . esc_attr( $grid_style ) . '"'; -
pap-afiliados-pro/trunk/includes/class-papafpro-utilities.php
r3482385 r3488009 14 14 * Classe Utilities - Funções auxiliares. 15 15 * 16 * Funções reutilizáveis para sanitização, validação, formatação, etc.16 * Funções reutilizáveis para detecção de marketplace. 17 17 * 18 18 * @since 2.0.0 19 19 */ 20 20 class PAPAFPRO_Utilities { 21 22 /**23 * Sanitizar array de settings.24 *25 * @since 2.0.026 * @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 directive109 * prohibiting arbitrary CSS input from users.110 *111 * @since 2.0.0112 * @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 }136 21 137 22 /** … … 163 48 return 'outros'; 164 49 } 165 166 /**167 * Formatar preço para exibição.168 *169 * @since 2.0.0170 * @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.0192 * @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.0203 * @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.0221 * @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.0232 * @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 }244 50 } -
pap-afiliados-pro/trunk/languages/pap-afiliados-pro-pt_BR.po
r3482385 r3488009 6 6 "Project-Id-Version: PAP Afiliados Pro 2.0.3\n" 7 7 "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- 1300: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" 10 10 "Last-Translator: Fernando Pimenta\n" 11 11 "Language-Team: Brazilian Portuguese\n" … … 172 172 173 173 #: includes/integrations/elementor/class-papafpro-elementor-widgetphp:112 174 #: includes/class-papafpro-help-pagephp:184175 174 msgid "Product ID" 176 175 msgstr "ID do produto" 176 177 #: includes/class-papafpro-help-pagephp:184 178 msgid "Search by name or ID" 179 msgstr "Buscar por nome ou ID" 177 180 178 181 #: includes/integrations/elementor/class-papafpro-elementor-widgetphp:125 … … 682 685 683 686 #: includes/class-papafpro-products-adminphp:243 687 #: includes/class-papafpro-template-builderphp:1066 684 688 msgid "Shortcode" 685 689 msgstr "Shortcode" … … 798 802 799 803 #: includes/class-papafpro-help-pagephp:157 804 #: includes/class-papafpro-template-builderphp:1067 800 805 msgid "Copied!" 801 806 msgstr "Copiado!" … … 808 813 msgid "Select a type" 809 814 msgstr "Selecione um tipo" 815 816 #: includes/class-papafpro-help-pagephp:160 817 msgid "Search\u2026" 818 msgstr "Buscar\u2026" 819 820 #: includes/class-papafpro-help-pagephp:161 821 msgid "No results found" 822 msgstr "Nenhum resultado encontrado" 823 824 #: includes/class-papafpro-help-pagephp:162 825 msgid "Search products\u2026" 826 msgstr "Buscar produtos\u2026" 827 828 #: includes/class-papafpro-help-pagephp:163 829 msgid "Searching\u2026" 830 msgstr "Buscando\u2026" 831 832 #: includes/class-papafpro-help-pagephp:164 833 msgid "No products found" 834 msgstr "Nenhum produto encontrado" 835 836 #: includes/class-papafpro-help-pagephp:165 837 msgid "Type at least 2 characters" 838 msgstr "Digite pelo menos 2 caracteres" 839 840 #: includes/class-papafpro-help-pagephp:166 841 msgid "Remove" 842 msgstr "Remover" 810 843 811 844 #: includes/class-papafpro-help-pagephp:178 … … 830 863 831 864 #: includes/class-papafpro-help-pagephp:204 832 msgid " Comma-separated IDs"833 msgstr " IDs separados por vírgula"865 msgid "Search and select products" 866 msgstr "Buscar e selecionar produtos" 834 867 835 868 #: includes/class-papafpro-help-pagephp:210 … … 881 914 882 915 #: includes/class-papafpro-help-pagephp:309 883 msgid " Registered productID (fetches URL and marketplace automatically)"884 msgstr " ID do produto cadastrado (buscaURL e marketplace automaticamente)"916 msgid "Search by name or ID (fetches URL and marketplace automatically)" 917 msgstr "Buscar por nome ou ID (obtém URL e marketplace automaticamente)" 885 918 886 919 #: includes/class-papafpro-help-pagephp:315 … … 1680 1713 msgstr "Inserir Link" 1681 1714 1715 #: includes/class-papafpro-stats.php views/papafpro-stats-page.php 1716 msgid "Clicks by Marketplace" 1717 msgstr "Cliques por Marketplace" 1718 1719 #: includes/class-papafpro-stats.php views/papafpro-stats-page.php 1720 msgid "No marketplace data available" 1721 msgstr "Sem dados de marketplace" 1722 1723 #: includes/class-papafpro-stats.php views/papafpro-stats-page.php 1724 msgid "Marketplace" 1725 msgstr "Marketplace" 1726 -
pap-afiliados-pro/trunk/languages/pap-afiliados-pro.pot
r3482385 r3488009 5 5 "Project-Id-Version: PAP Afiliados Pro 2.0.3\n" 6 6 "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" 8 8 "MIME-Version: 1.0\n" 9 9 "Content-Type: text/plain; charset=UTF-8\n" … … 168 168 169 169 #: includes/integrations/elementor/class-papafpro-elementor-widgetphp:112 170 msgid "Product ID" 171 msgstr "" 172 170 173 #: includes/class-papafpro-help-pagephp:184 171 msgid " ProductID"174 msgid "Search by name or ID" 172 175 msgstr "" 173 176 … … 678 681 679 682 #: includes/class-papafpro-products-adminphp:243 683 #: includes/class-papafpro-template-builderphp:1066 680 684 msgid "Shortcode" 681 685 msgstr "" … … 794 798 795 799 #: includes/class-papafpro-help-pagephp:157 800 #: includes/class-papafpro-template-builderphp:1067 796 801 msgid "Copied!" 797 802 msgstr "" … … 803 808 #: includes/class-papafpro-help-pagephp:159 804 809 msgid "Select a type" 810 msgstr "" 811 812 #: includes/class-papafpro-help-pagephp:160 813 msgid "Search\u2026" 814 msgstr "" 815 816 #: includes/class-papafpro-help-pagephp:161 817 msgid "No results found" 818 msgstr "" 819 820 #: includes/class-papafpro-help-pagephp:162 821 msgid "Search products\u2026" 822 msgstr "" 823 824 #: includes/class-papafpro-help-pagephp:163 825 msgid "Searching\u2026" 826 msgstr "" 827 828 #: includes/class-papafpro-help-pagephp:164 829 msgid "No products found" 830 msgstr "" 831 832 #: includes/class-papafpro-help-pagephp:165 833 msgid "Type at least 2 characters" 834 msgstr "" 835 836 #: includes/class-papafpro-help-pagephp:166 837 msgid "Remove" 805 838 msgstr "" 806 839 … … 826 859 827 860 #: includes/class-papafpro-help-pagephp:204 828 msgid " Comma-separated IDs"861 msgid "Search and select products" 829 862 msgstr "" 830 863 … … 877 910 878 911 #: includes/class-papafpro-help-pagephp:309 879 msgid " Registered productID (fetches URL and marketplace automatically)"912 msgid "Search by name or ID (fetches URL and marketplace automatically)" 880 913 msgstr "" 881 914 … … 1676 1709 msgstr "" 1677 1710 1711 #: includes/class-papafpro-stats.php views/papafpro-stats-page.php 1712 msgid "Clicks by Marketplace" 1713 msgstr "" 1714 1715 #: includes/class-papafpro-stats.php views/papafpro-stats-page.php 1716 msgid "No marketplace data available" 1717 msgstr "" 1718 1719 #: includes/class-papafpro-stats.php views/papafpro-stats-page.php 1720 msgid "Marketplace" 1721 msgstr "" 1722 -
pap-afiliados-pro/trunk/pap-afiliados-pro.php
r3482385 r3488009 4 4 * Plugin URI: https://pap-afiliados-pro.com.br 5 5 * Description: Professional affiliate link management for Brazilian marketplaces (Amazon, Mercado Livre, Shopee, AliExpress, and others). 6 * Version: 2. 0.36 * Version: 2.1.0 7 7 * Requires at least: 6.2 8 8 * Requires PHP: 7.4 … … 24 24 * Constantes do plugin. 25 25 */ 26 define( 'PAPAFPRO_VERSION', '2. 0.3' );26 define( 'PAPAFPRO_VERSION', '2.1.0' ); 27 27 define( 'PAPAFPRO_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 28 28 define( 'PAPAFPRO_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); -
pap-afiliados-pro/trunk/readme.txt
r3482385 r3488009 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 2. 0.37 Stable tag: 2.1.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 130 130 131 131 == 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 132 141 133 142 = 2.0.3 = … … 176 185 == Upgrade Notice == 177 186 187 = 2.1.0 = 188 New features: shortcode column in presets, filter/search in shortcode generator, product search by name/ID, and marketplace stats with doughnut chart. 189 178 190 = 2.0.3 = 179 191 Fixes 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 78 78 </div> 79 79 <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"> 80 89 <h3><?php esc_html_e( 'Linker Trend', 'pap-afiliados-pro' ); ?></h3> 81 90 <div class="papafpro-stats-chart-wrap"> … … 94 103 <th><?php esc_html_e( 'Page', 'pap-afiliados-pro' ); ?></th> 95 104 <th><?php esc_html_e( 'Source', 'pap-afiliados-pro' ); ?></th> 105 <th><?php esc_html_e( 'Marketplace', 'pap-afiliados-pro' ); ?></th> 96 106 <th class="papafpro-stats-col-total"><?php esc_html_e( 'Total', 'pap-afiliados-pro' ); ?></th> 97 107 </tr> … … 110 120 <?php endif; ?> 111 121 </td> 122 <td><?php echo esc_html( ! empty( $papafpro_row->marketplace ) ? $papafpro_row->marketplace : '—' ); ?></td> 112 123 <td class="papafpro-stats-col-total"><?php echo esc_html( number_format_i18n( $papafpro_row->total_clicks ) ); ?></td> 113 124 </tr> … … 115 126 <?php else : ?> 116 127 <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> 118 129 </tr> 119 130 <?php endif; ?>
Note: See TracChangeset
for help on using the changeset viewer.