| 1 |
( function ( $ ) { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var TIER_LABEL = { foundational: 'Fix first', 'config-health': 'Quick wins', opportunity: 'Opportunities' }; |
| 5 |
var TIER_ORDER = [ 'foundational', 'config-health', 'opportunity' ]; |
| 6 |
var OPP_VISIBLE = 5; // collapse opportunities beyond this many. |
| 7 |
var OPP_PIN = 'newsletter'; // always keep this opportunity in the visible set (promote if below the fold). |
| 8 |
// PLACEHOLDER personalization hints (until the backend personalizes detail copy per featureId). |
| 9 |
var AFTER_OPTIN = { recurring: 'quantified after opt-in', 'fee-relief': 'personalized after opt-in' }; |
| 10 |
|
| 11 |
// Allow only http(s) (or scheme-less) URLs into an href. cta.url and recommendedPlan.url come from |
| 12 |
// the remote recommendations feed, so a `javascript:` or `data:` URL from a compromised or spoofed |
| 13 |
// endpoint would otherwise execute inside wp-admin with the logged-in admin's privileges. |
| 14 |
// |
| 15 |
// Deliberately NOT implemented with the URL constructor and a catch that returns '#': where URL is |
| 16 |
// unavailable that silently kills every CTA on the page. |
| 17 |
function safeHref( url ) { |
| 18 |
if ( ! url ) { return '#'; } |
| 19 |
// Strip whitespace and control characters anywhere in the string before testing the scheme: |
| 20 |
// browsers ignore them when resolving, so "java\tscript:" and " javascript:" both execute. |
| 21 |
var candidate = String( url ).replace( /[\s\x00-\x1f\x7f]/g, '' ); |
| 22 |
// A scheme is present only if a colon precedes any /, ? or #. Scheme-relative ("//host/x") and |
| 23 |
// path-relative URLs carry no scheme of their own and inherit the admin origin, so they're fine. |
| 24 |
if ( /^[a-z][a-z0-9+.\-]*:/i.test( candidate ) ) { |
| 25 |
return /^https?:/i.test( candidate ) ? url : '#'; |
| 26 |
} |
| 27 |
return url; |
| 28 |
} |
| 29 |
|
| 30 |
// Upgrade recs point at the plugin's tagged upgrade URL; config-fix recs open the relevant in-plugin |
| 31 |
// screen (cfg.internal); everything else falls back to the rec's own (external) CTA link. |
| 32 |
function ctaHref( rec, cfg ) { |
| 33 |
if ( 'upgrade' === rec.action && cfg.upgrade_url ) { |
| 34 |
return cfg.upgrade_url + ( cfg.upgrade_url.indexOf( '?' ) > -1 ? '&' : '?' ) + 'utm_content=' + encodeURIComponent( rec.featureId || '' ); |
| 35 |
} |
| 36 |
if ( ctaIsInternal( rec, cfg ) ) { |
| 37 |
return cfg.internal[ rec.name ]; |
| 38 |
} |
| 39 |
// Growth/how-to recs have no in-plugin toggle; point them at a specific doc article |
| 40 |
// (cfg.docs) instead of the engine's generic /documentation/ landing page. |
| 41 |
if ( cfg.docs && rec.name && cfg.docs[ rec.name ] ) { |
| 42 |
return cfg.docs[ rec.name ]; |
| 43 |
} |
| 44 |
return rec.cta && rec.cta.url ? rec.cta.url : '#'; |
| 45 |
} |
| 46 |
|
| 47 |
// True when a rec maps to an in-plugin admin screen (opens in the same tab, not a new one). |
| 48 |
function ctaIsInternal( rec, cfg ) { |
| 49 |
return 'upgrade' !== rec.action && !! ( cfg.internal && rec.name && cfg.internal[ rec.name ] ); |
| 50 |
} |
| 51 |
|
| 52 |
// Score gain badge. Uses the backend's rec.scoreGain when present; until then, a PLACEHOLDER |
| 53 |
// derived from priority so only the fixable tiers (foundational / config-health) show points. |
| 54 |
function scoreGain( rec ) { |
| 55 |
if ( rec.scoreGain != null ) { return rec.scoreGain; } |
| 56 |
if ( 'foundational' === rec.tier ) { return Math.max( 10, Math.min( 20, Math.round( ( rec.priority || 0 ) / 55 ) ) ); } |
| 57 |
if ( 'config-health' === rec.tier ) { return Math.max( 4, Math.min( 12, Math.round( ( rec.priority || 0 ) / 70 ) ) ); } |
| 58 |
return 0; |
| 59 |
} |
| 60 |
|
| 61 |
// Fallback Font Awesome icon per recommendation category, used when a rec has no bundled addon icon. |
| 62 |
var CAT_ICON = { |
| 63 |
payments: 'fa-credit-card', |
| 64 |
donations: 'fa-bullhorn', |
| 65 |
'donor-experience': 'fa-heart', |
| 66 |
growth: 'fa-line-chart', |
| 67 |
compliance: 'fa-shield' |
| 68 |
}; |
| 69 |
// More specific Font Awesome icons, keyed by rec featureId (opportunities without a bundled icon) |
| 70 |
// or by config-fix check name (those recs have an empty featureId; their name is the check id). |
| 71 |
var FEATURE_ICON = { |
| 72 |
'seo-aioseo': 'fa-search', |
| 73 |
offline: 'fa-money', |
| 74 |
campaigns: 'fa-bullhorn', |
| 75 |
gateways: 'fa-credit-card', |
| 76 |
'test-mode-on': 'fa-flask', |
| 77 |
'no-gateway': 'fa-credit-card', |
| 78 |
'single-gateway': 'fa-credit-card', |
| 79 |
'offline-off': 'fa-money', |
| 80 |
'receipt-email-off': 'fa-envelope', |
| 81 |
'recurring-no-capable-gateway': 'fa-refresh', |
| 82 |
'no-published-campaign': 'fa-bullhorn', |
| 83 |
'no-donation-yet': 'fa-heart', |
| 84 |
'campaigns-no-goal': 'fa-bullseye', |
| 85 |
'spam-off-with-donations': 'fa-shield', |
| 86 |
'low-avg-gift': 'fa-line-chart', |
| 87 |
'stalled-momentum': 'fa-clock-o' |
| 88 |
}; |
| 89 |
|
| 90 |
// Build the left-hand icon for a recommendation: the bundled addon icon when we have one, otherwise |
| 91 |
// a category/feature Font Awesome icon. Config-fix recs have an empty featureId, so fall back to name. |
| 92 |
function recIcon( r, cfg ) { |
| 93 |
var $icon = $( '<span class="charitable-sa-rec-icon" aria-hidden="true"></span>' ); |
| 94 |
var url = cfg.rec_icons && r.featureId && cfg.rec_icons[ r.featureId ]; |
| 95 |
if ( url ) { |
| 96 |
$icon.append( $( '<img alt="" />' ).attr( 'src', url ) ); |
| 97 |
} else { |
| 98 |
var key = r.featureId || r.name; |
| 99 |
var fa = FEATURE_ICON[ key ] || CAT_ICON[ r.category ] || 'fa-lightbulb-o'; |
| 100 |
$icon.addClass( 'charitable-sa-rec-icon--fa' ).append( $( '<i class="fa"></i>' ).addClass( fa ) ); |
| 101 |
} |
| 102 |
return $icon; |
| 103 |
} |
| 104 |
|
| 105 |
function renderRec( r, tier, cfg, complete ) { |
| 106 |
var $c = $( '<div class="charitable-sa-rec tier-' + tier + '"></div>' ); |
| 107 |
$c.append( recIcon( r, cfg ) ); |
| 108 |
var $main = $( '<div class="charitable-sa-rec-main"></div>' ); |
| 109 |
var $h = $( '<strong></strong>' ).text( r.headline ); |
| 110 |
var gain = scoreGain( r ); |
| 111 |
if ( gain > 0 ) { $h.append( $( '<span class="charitable-sa-gain"></span>' ).text( '+' + gain + ' pts' ) ); } |
| 112 |
$main.append( $h ); |
| 113 |
var $d = $( '<div class="charitable-sa-detail"></div>' ).text( r.detail ); |
| 114 |
// The "quantified/personalized after opt-in" note only makes sense before opt-in; once the |
| 115 |
// score is complete the detail is already personalized, so suppress it. |
| 116 |
if ( ! complete && AFTER_OPTIN[ r.featureId ] ) { $d.append( $( '<em class="charitable-sa-after"></em>' ).text( ' (' + AFTER_OPTIN[ r.featureId ] + ')' ) ); } |
| 117 |
$main.append( $d ); |
| 118 |
$c.append( $main ); |
| 119 |
if ( r.cta && r.cta.label ) { |
| 120 |
var $cta = $( '<a class="button"></a>' ).attr( 'href', safeHref( ctaHref( r, cfg ) ) ).text( r.cta.label ); |
| 121 |
// Internal (in-plugin) links open in the same tab; external links open in a new tab. |
| 122 |
if ( ! ctaIsInternal( r, cfg ) ) { $cta.attr( 'target', '_blank' ).attr( 'rel', 'noopener noreferrer' ); } |
| 123 |
$c.append( $cta ); |
| 124 |
} |
| 125 |
return $c; |
| 126 |
} |
| 127 |
|
| 128 |
function bandColor( band ) { return band === 'great' ? '#5AA152' : band === 'good' ? '#dba617' : '#d63638'; } |
| 129 |
function bandLabel( band ) { return band === 'great' ? 'Great' : band === 'good' ? 'Good' : 'Needs work'; } |
| 130 |
function dimColor( v ) { return v >= 70 ? '#5AA152' : v >= 40 ? '#dba617' : '#d63638'; } |
| 131 |
var DIM_LABEL = { activation: 'Activation', health: 'Health', growth: 'Growth', optimization: 'Optimization' }; |
| 132 |
// Plain-language explanation of each scored area, shown in a tooltip on the "?" icon beside the label. |
| 133 |
var DIM_TIP = { |
| 134 |
activation: 'Getting live and bringing in your first donations.', |
| 135 |
health: 'Core setup best practices: receipts, campaign goals, and more than one way to give.', |
| 136 |
growth: 'Recurring giving, list building, and steady donation momentum.', |
| 137 |
optimization: 'Getting more from every gift: larger average gifts, covered fees, and repeat donors.' |
| 138 |
}; |
| 139 |
|
| 140 |
function renderScore( score, $out ) { |
| 141 |
if ( ! score ) { return; } |
| 142 |
var pct = Math.max( 0, Math.min( 100, score.value || 0 ) ); |
| 143 |
var col = bandColor( score.band ); |
| 144 |
var $hero = $( '<div class="charitable-sa-score charitable-sa-score--live"></div>' ); |
| 145 |
// Start the ring empty and the number at 0; animateScore() fills them on load (see below). |
| 146 |
var $ring = $( '<div class="charitable-sa-ring"></div>' ) |
| 147 |
.attr( 'style', 'background: conic-gradient(' + col + ' 0 0%, #edeef0 0% 100%);' ); |
| 148 |
var $ringNum = $( '<b></b>' ).text( '0' ); |
| 149 |
$ring.append( $( '<span class="charitable-sa-ring-v"></span>' ).append( $ringNum ).append( $( '<s></s>' ).text( '/ 100' ) ) ); |
| 150 |
$hero.append( $ring ); |
| 151 |
var bars = []; // { $el, target } - filled while building the dimension bars, animated on load. |
| 152 |
|
| 153 |
var $meta = $( '<div class="charitable-sa-score-meta"></div>' ); |
| 154 |
var $title = $( '<h3 class="charitable-sa-score-title"></h3>' ).text( 'Fundraising Score ' ); |
| 155 |
if ( score.complete ) { |
| 156 |
$title.append( $( '<span class="charitable-sa-band"></span>' ).text( bandLabel( score.band ) ).attr( 'style', 'background:' + col + ';' ) ); |
| 157 |
} else { |
| 158 |
var done = [ 'activation', 'health', 'growth', 'optimization' ].filter( function ( k ) { |
| 159 |
return score.dimensions && score.dimensions[ k ] && score.dimensions[ k ].complete; |
| 160 |
} ).length; |
| 161 |
$title.append( $( '<span class="charitable-sa-estpill"></span>' ).text( 'Estimated · ' + done + ' of 4 areas' ) ); |
| 162 |
} |
| 163 |
$meta.append( $title ); |
| 164 |
|
| 165 |
if ( ( score.potential || 0 ) > ( score.value || 0 ) ) { |
| 166 |
$meta.append( $( '<div class="charitable-sa-score-desc"></div>' ).text( 'Reaches ' + score.potential + ' if you complete the items below.' ) ); |
| 167 |
} else if ( ! score.complete ) { |
| 168 |
$meta.append( $( '<div class="charitable-sa-score-desc"></div>' ).text( 'Share your fundraising stats below to complete your Growth & Optimization scores.' ) ); |
| 169 |
} |
| 170 |
|
| 171 |
var $dims = $( '<div class="charitable-sa-dims"></div>' ); |
| 172 |
[ 'activation', 'health', 'growth', 'optimization' ].forEach( function ( k ) { |
| 173 |
var d = ( score.dimensions && score.dimensions[ k ] ) || {}; |
| 174 |
var $dim = $( '<div class="charitable-sa-dim"></div>' ); |
| 175 |
var $dimN = $( '<span class="charitable-sa-dim-n"></span>' ).text( DIM_LABEL[ k ] ); |
| 176 |
$dimN.append( |
| 177 |
$( '<span class="charitable-sa-tip" tabindex="0" role="img"></span>' ) |
| 178 |
.attr( 'aria-label', DIM_LABEL[ k ] + ': ' + DIM_TIP[ k ] ) |
| 179 |
.attr( 'data-tip', DIM_TIP[ k ] ) |
| 180 |
.text( '?' ) |
| 181 |
); |
| 182 |
$dim.append( $dimN ); |
| 183 |
var $bar = $( '<span class="charitable-sa-dim-bar"></span>' ); |
| 184 |
if ( d.complete ) { |
| 185 |
var barVal = Math.max( 0, Math.min( 100, d.value || 0 ) ); |
| 186 |
var $barFill = $( '<i></i>' ).attr( 'style', 'width:0%; background:' + dimColor( d.value || 0 ) + ';' ); |
| 187 |
bars.push( { $el: $barFill, target: barVal } ); |
| 188 |
$bar.append( $barFill ); |
| 189 |
} else { |
| 190 |
$dim.addClass( 'charitable-sa-dim--locked' ); |
| 191 |
} |
| 192 |
$dim.append( $bar ); |
| 193 |
if ( ! d.complete ) { $dim.append( $( '<span class="charitable-sa-dim-hint"></span>' ).text( 'Share stats to score' ) ); } |
| 194 |
$dims.append( $dim ); |
| 195 |
} ); |
| 196 |
$meta.append( $dims ); |
| 197 |
$hero.append( $meta ); |
| 198 |
$out.append( $hero ); |
| 199 |
animateScore( $ring, $ringNum, col, pct, bars ); |
| 200 |
} |
| 201 |
|
| 202 |
// Animate the score ring (conic fill), its number (count up), and the dimension bars from zero to |
| 203 |
// their values when the report renders. Honors prefers-reduced-motion, defers until the tab is |
| 204 |
// visible (rAF is paused while hidden), and always guarantees a correct final state. |
| 205 |
function animateScore( $ring, $ringNum, col, pct, bars ) { |
| 206 |
var done = false; |
| 207 |
function setRing( v ) { $ring.attr( 'style', 'background: conic-gradient(' + col + ' 0 ' + v + '%, #edeef0 ' + v + '% 100%);' ); } |
| 208 |
function finalState() { |
| 209 |
if ( done ) { return; } |
| 210 |
done = true; |
| 211 |
setRing( pct ); $ringNum.text( pct ); |
| 212 |
bars.forEach( function ( b ) { b.$el.css( 'width', b.target + '%' ); } ); |
| 213 |
} |
| 214 |
var reduce = window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches; |
| 215 |
if ( reduce || ! window.requestAnimationFrame ) { finalState(); return; } |
| 216 |
|
| 217 |
function run() { |
| 218 |
if ( done ) { return; } |
| 219 |
var DURATION = 900, start = null; |
| 220 |
function frame( ts ) { |
| 221 |
if ( done ) { return; } |
| 222 |
if ( start === null ) { start = ts; } |
| 223 |
var t = Math.min( 1, ( ts - start ) / DURATION ); |
| 224 |
var e = 1 - Math.pow( 1 - t, 3 ); // easeOutCubic |
| 225 |
setRing( pct * e ); |
| 226 |
$ringNum.text( Math.round( pct * e ) ); |
| 227 |
bars.forEach( function ( b ) { b.$el.css( 'width', ( b.target * e ) + '%' ); } ); |
| 228 |
if ( t < 1 ) { window.requestAnimationFrame( frame ); } else { finalState(); } |
| 229 |
} |
| 230 |
window.requestAnimationFrame( frame ); |
| 231 |
window.setTimeout( finalState, DURATION + 600 ); // safety net if rAF gets throttled mid-run |
| 232 |
} |
| 233 |
|
| 234 |
if ( document.hidden ) { |
| 235 |
// rAF is paused while the tab is hidden; animate the first time it becomes visible, but still |
| 236 |
// guarantee a correct final state in case the tab is never viewed. |
| 237 |
var onShow = function () { |
| 238 |
if ( ! document.hidden ) { document.removeEventListener( 'visibilitychange', onShow ); run(); } |
| 239 |
}; |
| 240 |
document.addEventListener( 'visibilitychange', onShow ); |
| 241 |
window.setTimeout( finalState, 5000 ); |
| 242 |
} else { |
| 243 |
run(); |
| 244 |
} |
| 245 |
} |
| 246 |
|
| 247 |
function renderReport( report, $out, cfg ) { |
| 248 |
$out.empty(); |
| 249 |
var recs = ( report && report.recommendations ) || []; |
| 250 |
var complete = !! ( report && report.score && report.score.complete ); |
| 251 |
var byTier = {}; |
| 252 |
recs.forEach( function ( r ) { ( byTier[ r.tier ] = byTier[ r.tier ] || [] ).push( r ); } ); |
| 253 |
|
| 254 |
// Score hero first - top of the report. |
| 255 |
renderScore( report && report.score, $out ); |
| 256 |
|
| 257 |
// Recommended plan second. |
| 258 |
if ( report && report.recommendedPlan ) { |
| 259 |
var p = report.recommendedPlan; |
| 260 |
var $plan = $( '<div class="charitable-sa-plan"></div>' ); |
| 261 |
$plan.append( $( '<strong></strong>' ).text( 'Recommended plan: ' + p.label ) ); |
| 262 |
var unlocks = ( p.unlocks && p.unlocks.length ) ? 'Unlocks ' + p.unlocks.join( ', ' ) + '. ' : ''; |
| 263 |
var tail = ( report.score && report.score.potential > report.score.value ) |
| 264 |
? ' Acting on these would lift your score toward ' + report.score.potential + '.' |
| 265 |
: ''; |
| 266 |
$plan.append( $( '<div></div>' ).text( unlocks + tail ) ); |
| 267 |
$plan.append( $( '<a class="button button-primary" style="margin-top:8px;"></a>' ).attr( 'href', safeHref( p.url ) ).attr( 'target', '_blank' ).attr( 'rel', 'noopener noreferrer' ).text( 'See ' + p.label ) ); |
| 268 |
$out.append( $plan ); |
| 269 |
} |
| 270 |
|
| 271 |
TIER_ORDER.forEach( function ( tier ) { |
| 272 |
if ( ! byTier[ tier ] ) { return; } |
| 273 |
var list = byTier[ tier ]; |
| 274 |
// Promote the pinned opportunity into the last visible slot if it sorted below the fold. |
| 275 |
if ( 'opportunity' === tier && OPP_PIN ) { |
| 276 |
var pinIdx = -1; |
| 277 |
for ( var j = 0; j < list.length; j++ ) { if ( OPP_PIN === list[ j ].featureId ) { pinIdx = j; break; } } |
| 278 |
if ( pinIdx >= OPP_VISIBLE ) { list.splice( OPP_VISIBLE - 1, 0, list.splice( pinIdx, 1 )[ 0 ] ); } |
| 279 |
} |
| 280 |
var $g = $( '<div class="charitable-sa-group"></div>' ); |
| 281 |
$g.append( $( '<h3></h3>' ).text( TIER_LABEL[ tier ] || tier ) ); |
| 282 |
|
| 283 |
var visible = ( 'opportunity' === tier ) ? Math.min( OPP_VISIBLE, list.length ) : list.length; |
| 284 |
for ( var i = 0; i < visible; i++ ) { $g.append( renderRec( list[ i ], tier, cfg, complete ) ); } |
| 285 |
|
| 286 |
if ( 'opportunity' === tier && list.length > visible ) { |
| 287 |
var hidden = list.slice( visible ); |
| 288 |
var $more = $( '<p class="charitable-sa-more"></p>' ).text( '+ ' + hidden.length + ' more opportunities' ); |
| 289 |
$more.on( 'click', function () { |
| 290 |
hidden.forEach( function ( r ) { $more.before( renderRec( r, tier, cfg, complete ) ); } ); |
| 291 |
$more.remove(); |
| 292 |
} ); |
| 293 |
$g.append( $more ); |
| 294 |
} |
| 295 |
$out.append( $g ); |
| 296 |
} ); |
| 297 |
|
| 298 |
if ( ! recs.length && ! ( report && report.recommendedPlan ) ) { |
| 299 |
$out.append( $( '<p></p>' ).text( 'Your setup looks great - no recommendations right now.' ) ); |
| 300 |
} |
| 301 |
} |
| 302 |
|
| 303 |
function appendRefresh( $out, cfg, agoText, refreshOk, waitText ) { |
| 304 |
if ( $out.find( '.charitable-sa-refresh-row' ).length ) { return; } |
| 305 |
var $row = $( '<p class="charitable-sa-refresh-row"></p>' ); |
| 306 |
if ( agoText ) { $row.append( $( '<span class="charitable-sa-analyzed"></span>' ).text( agoText + '. ' ) ); } |
| 307 |
if ( refreshOk ) { |
| 308 |
var label = ( cfg.i18n && cfg.i18n.refresh ) || 'Refresh'; |
| 309 |
var $link = $( '<button type="button" class="button-link charitable-sa-refresh"></button>' ).text( label ); |
| 310 |
$link.on( 'click', function () { |
| 311 |
$( '#charitable-site-analysis-run' ).data( 'refresh', 1 ).trigger( 'click' ); |
| 312 |
} ); |
| 313 |
$row.append( $link ); |
| 314 |
} else { |
| 315 |
// Within the once-a-day refresh cooldown: show when it's available instead of an active link. |
| 316 |
var msg = waitText |
| 317 |
? ( ( cfg.i18n && cfg.i18n.refresh_wait ) || 'You can refresh again in %s' ).replace( '%s', waitText ) |
| 318 |
: ( ( cfg.i18n && cfg.i18n.refresh_soon ) || 'You can refresh again tomorrow' ); |
| 319 |
$row.append( $( '<span class="charitable-sa-refresh-wait"></span>' ).text( msg ) ); |
| 320 |
} |
| 321 |
$out.append( $row ); |
| 322 |
} |
| 323 |
|
| 324 |
$( function () { |
| 325 |
var cfg = window.charitable_site_analysis || {}; |
| 326 |
$( '#charitable-site-analysis-run' ).on( 'click', function () { |
| 327 |
var $btn = $( this ), |
| 328 |
$spin = $( '#charitable-site-analysis-spinner' ), |
| 329 |
$err = $( '#charitable-site-analysis-error' ), |
| 330 |
$out = $( '#charitable-site-analysis-results' ); |
| 331 |
$err.hide(); |
| 332 |
var refresh = $btn.data( 'refresh' ) ? 1 : 0; |
| 333 |
$btn.removeData( 'refresh' ); |
| 334 |
$out.empty(); |
| 335 |
// On refresh the pre-run block (and its spinner) is hidden, so show a loading state inside the |
| 336 |
// results area instead of leaving it blank while the API responds. |
| 337 |
if ( refresh ) { |
| 338 |
$out.append( $( '<div class="charitable-sa-loading"></div>' ) |
| 339 |
.append( $( '<span class="spinner is-active"></span>' ) ) |
| 340 |
.append( $( '<span></span>' ).text( ( cfg.i18n && cfg.i18n.running ) || 'Analyzing your site…' ) ) ); |
| 341 |
} |
| 342 |
$btn.prop( 'disabled', true ); |
| 343 |
$spin.addClass( 'is-active' ); |
| 344 |
// The usage toggle is absent when site-wide usage tracking is already on -> treat as consented. |
| 345 |
var $consent = $( '#charitable-site-analysis-consent' ); |
| 346 |
var consent = ( 0 === $consent.length || $consent.is( ':checked' ) ) ? 1 : 0; |
| 347 |
$.post( cfg.ajax_url, { |
| 348 |
action: 'charitable_site_analysis', |
| 349 |
nonce: cfg.nonce, |
| 350 |
consent: consent, |
| 351 |
refresh: refresh |
| 352 |
} ).done( function ( res ) { |
| 353 |
if ( res && res.success ) { |
| 354 |
$( '.charitable-site-analysis' ).addClass( 'charitable-sa-has-results' ); |
| 355 |
renderReport( res.data, $out, cfg ); |
| 356 |
// A fresh analysis just ran, so the refresh cooldown restarts - disable it until reload. |
| 357 |
appendRefresh( $out, cfg, '', false, '' ); |
| 358 |
} else { |
| 359 |
var msg = ( res && res.data && res.data.message ) || ( cfg.i18n && cfg.i18n.error ) || 'Error.'; |
| 360 |
$err.show().find( 'p' ).text( msg ); |
| 361 |
if ( res && res.data && res.data.stale ) { |
| 362 |
$( '.charitable-site-analysis' ).addClass( 'charitable-sa-has-results' ); |
| 363 |
renderReport( res.data.stale, $out, cfg ); |
| 364 |
// The fetch failed (stale shown), so let the user retry; the server still rate-limits. |
| 365 |
appendRefresh( $out, cfg, '', true, '' ); |
| 366 |
} |
| 367 |
} |
| 368 |
} ).fail( function () { |
| 369 |
$err.show().find( 'p' ).text( ( cfg.i18n && cfg.i18n.error ) || 'Error.' ); |
| 370 |
} ).always( function () { |
| 371 |
$out.find( '.charitable-sa-loading' ).remove(); |
| 372 |
$btn.prop( 'disabled', false ); |
| 373 |
$spin.removeClass( 'is-active' ); |
| 374 |
} ); |
| 375 |
} ); |
| 376 |
|
| 377 |
// On load, render the last cached report (if any) so returning to the tab shows the last |
| 378 |
// analysis instead of the pre-run state. Refresh re-runs against the live API. |
| 379 |
if ( cfg.cached ) { |
| 380 |
var $cachedOut = $( '#charitable-site-analysis-results' ); |
| 381 |
$( '.charitable-site-analysis' ).addClass( 'charitable-sa-has-results' ); |
| 382 |
renderReport( cfg.cached, $cachedOut, cfg ); |
| 383 |
appendRefresh( $cachedOut, cfg, cfg.cached_ago, cfg.refresh_ok, cfg.refresh_wait ); |
| 384 |
} |
| 385 |
} ); |
| 386 |
} )( jQuery ); |
| 387 |
|