window.CGP_PAGE={"pages": "https://charliesgnarlyparty.com/wp-content/uploads/2026/09/", "ext": "webp", "merch": "https://charliesgnarlyparty.com/wp-content/plugins/charlies-gnarly-party/assets/merch/", "cart": "", "shop": "https://charliesgnarlyparty.com/shop/"}; document.documentElement.classList.add("cgp-landing"); if(document.body){document.body.classList.add("cgp-landing");} else{document.addEventListener("DOMContentLoaded",function(){document.body.classList.add("cgp-landing");});} (function(){ "use strict"; /* ========================================================= 1. THE BOOK. A real open book: odd page left, even right. ========================================================= */ var TOTAL = 35; // PDF page 1 is the front cover and page 2 is the BACK cover, so page 2 // belongs at the end. Pages 3 and 4 are the copyright and title pages: // front matter nobody wants to click through, so the story opens straight // onto printed page 2 at PDF page 5. From there the parity holds all the // way down, odd on the left and even on the right, and page 35 (the end) // has no partner. var FRONT = 1, BACK = 2, STORY = 5, LAST = 35; function spreadViews(){ var v = [[FRONT]]; for(var i=STORY;i<=33;i+=2) v.push([i,i+1]); v.push([LAST]); v.push([BACK]); return v; } function singleViews(){ var v = [[FRONT]]; for(var i=STORY;i<=LAST;i++) v.push([i]); v.push([BACK]); return v; } // the number printed on the page itself, which runs three behind the PDF function printed(n){ return n - 3; } var wide = window.matchMedia('(min-width:860px)'); var views = wide.matches ? spreadViews() : singleViews(); var vi = 0; var book = document.getElementById('book'); var leafL = document.getElementById('leafL'); var leafR = document.getElementById('leafR'); var imgL = document.getElementById('imgL'); var imgR = document.getElementById('imgR'); var prevBtn = document.getElementById('prev'); var nextBtn = document.getElementById('next'); var bar = document.getElementById('bar'); var flipper = document.getElementById('flipper'); var flipFront = document.getElementById('flipFront'); var flipBack = document.getElementById('flipBack'); var readerBox = document.getElementById('readerBox'); var reduce = window.matchMedia('(prefers-reduced-motion: reduce)'); var FLIP_MS = 620; var busy = false, lastPair = null; var PAGES = (window.CGP_PAGE && CGP_PAGE.pages) || 'pages/'; var PAGE_EXT = (window.CGP_PAGE && CGP_PAGE.ext) || 'jpg'; function src(n){ return PAGES + 'p-' + String(n).padStart(2,'0') + '.' + PAGE_EXT; } // Warm the next couple of views so turning feels instant, without // pulling all 35 pages down on first paint. function prefetch(){ for(var k=vi+1;k<=Math.min(vi+2,views.length-1);k++){ views[k].forEach(function(n){ new Image().src = src(n); }); } } function paintLeaves(pair){ book.classList.toggle('single', pair.length === 1); imgL.src = src(pair[0]); imgL.alt = 'Charlie’s Gnarly Party, page ' + printed(pair[0]); if(pair.length > 1){ leafR.hidden = false; imgR.src = src(pair[1]); imgR.alt = 'Charlie’s Gnarly Party, page ' + printed(pair[1]); }else{ leafR.hidden = true; imgR.removeAttribute('src'); imgR.alt = ''; } } function cue(cls){ [leafL,leafR].forEach(function(l){ l.classList.remove('soft','slide'); void l.offsetWidth; l.classList.add(cls); }); } // A spread turning into another spread gets the real thing: the leaf you // grabbed rotates on the spine, its back face carrying the page underneath. function turnLeaf(from, to, dir){ var fwd = dir > 0; flipFront.src = src(fwd ? from[1] : from[0]); flipBack.src = src(fwd ? to[0] : to[1]); // the stationary half holds still while the far half is already revealed book.classList.remove('single'); leafR.hidden = false; imgL.src = src(fwd ? from[0] : to[0]); imgR.src = src(fwd ? to[1] : from[1]); busy = true; flipper.hidden = false; flipper.classList.toggle('rev', !fwd); flipper.classList.remove('run'); void flipper.offsetWidth; flipper.classList.add('run'); setTimeout(function(){ flipper.classList.remove('run'); flipper.hidden = true; paintLeaves(to); busy = false; }, FLIP_MS); } function render(dir){ var pair = views[vi]; var from = lastPair; lastPair = pair; book.classList.toggle('back', dir < 0); var canFlip = from && from.length === 2 && pair.length === 2 && !reduce.matches; if(canFlip){ turnLeaf(from, pair, dir); }else{ paintLeaves(pair); if(from) cue(views.length > 20 ? 'slide' : 'soft'); } bar.style.width = ((vi+1) / views.length * 100) + '%'; bar.parentNode.setAttribute('aria-valuenow', vi+1); bar.parentNode.setAttribute('aria-valuemax', views.length); prevBtn.disabled = vi === 0; nextBtn.disabled = vi === views.length - 1; prefetch(); if(vi === views.length - 1) showEnd(); } function go(d){ if(busy) return; var n = vi + d; if(n < 0 || n >= views.length) return; vi = n; render(d); } prevBtn.addEventListener('click', function(){ go(-1); }); nextBtn.addEventListener('click', function(){ go(1); }); book.addEventListener('click', function(e){ var r = book.getBoundingClientRect(); go(e.clientX - r.left < r.width/2 ? -1 : 1); }); document.addEventListener('keydown', function(e){ if(document.getElementById('funnel').classList.contains('open')) return; if(document.getElementById('contact').classList.contains('open')) return; if(e.key === 'ArrowLeft') go(-1); if(e.key === 'ArrowRight') go(1); }); var x0=null,y0=null; book.addEventListener('touchstart', function(e){ x0 = e.touches[0].clientX; y0 = e.touches[0].clientY; }, {passive:true}); book.addEventListener('touchend', function(e){ if(x0===null) return; var dx = e.changedTouches[0].clientX - x0, dy = e.changedTouches[0].clientY - y0; if(Math.abs(dx) > 45 && Math.abs(dx) > Math.abs(dy)) go(dx < 0 ? 1 : -1); x0 = y0 = null; }, {passive:true}); // Keep the reader's place when the layout flips between one and two pages. function relayout(){ var current = views[vi][0]; views = wide.matches ? spreadViews() : singleViews(); lastPair = null; vi = 0; for(var i=0;iThat is the part you are actually buying. Their name goes inside the front cover, by hand.

' + '' + ''; readerBox.appendChild(el); el.querySelector('[data-restart]').addEventListener('click', function(){ el.remove(); endShown = false; vi = 0; render(-1); }); el.querySelector('.js-start').addEventListener('click', function(){ el.remove(); endShown = false; }); } render(1); /* ========================================================= 2. THE FUNNEL. One CTA, five steps, upsell and downsell. ========================================================= */ var PRICE = { hardcover:25, signed:30, personalized:35, extraCopy:21, extraCopyRrp:25, upgrade:10, shirt:19, shirtRrp:25, stickers:3, stickersRrp:4, giftWrap:4, oto1:39, oto1Down:18, oto2Shirt:17, oto2Stickers:2, ship:5, freeShipAt:50 }; var EDITION_LABEL = { hardcover:'The plain hardcover', signed:'Signed by David', personalized:'Personalized copy' }; function freshCart(){ return { edition:'personalized', qty:1, name:'', note:'', shirt:null, stickers:false, stickersOto:false, email:'', ship:{name:'',a1:'',a2:'',city:'',region:'',post:'',country:'United States'}, direct:false, wrap:false, pay:'card', orderNo:'', paid:0, savedAtPay:0, postAdds:[] }; } var cart = freshCart(); var SHIRTS = [ { id:'kevin', label:'Krazy 4 K Kevin', sub:'Men’s tee', img:(window.CGP_PAGE?CGP_PAGE.merch:'merch/')+'Kevin-Front-S.webp' }, { id:'molly', label:'Molly', sub:'Women’s tee', img:(window.CGP_PAGE?CGP_PAGE.merch:'merch/')+'Molly-Front-S.webp' }, { id:'lucy', label:'Lucy LSD', sub:'Women’s tank', img:(window.CGP_PAGE?CGP_PAGE.merch:'merch/')+'Lucy-Front-S.webp' } ]; var SHOT = { hardcover:(window.CGP_PAGE?CGP_PAGE.merch:'merch/')+'CGP-Hardcover-1.webp', signed:(window.CGP_PAGE?CGP_PAGE.merch:'merch/')+'CGP-Hard-Signed-BookSq.webp', personalized:(window.CGP_PAGE?CGP_PAGE.merch:'merch/')+'CGP-Hard-PersonalSq.webp', stickers:(window.CGP_PAGE?CGP_PAGE.merch:'merch/')+'CGP-Sticker-Pack.webp' }; function photo(src, alt){ return '
' + alt + '
'; } var SIZES = ['S','M','L','XL','2XL']; function lines(){ var out = []; out.push({ n:EDITION_LABEL[cart.edition], sub:cart.edition === 'personalized' && cart.name ? 'Written inside for ' + cart.name : '', v:PRICE[cart.edition] }); if(cart.qty > 1){ out.push({ n:(cart.qty-1) + ' extra ' + (cart.qty-1 === 1 ? 'copy' : 'copies'), sub:'$' + PRICE.extraCopy + ' each instead of $' + PRICE.extraCopyRrp, v:(cart.qty-1) * PRICE.extraCopy, rm:function(){ cart.qty = 1; } }); } if(cart.shirt){ var s = SHIRTS.filter(function(x){ return x.id === cart.shirt.style; })[0]; out.push({ n:s.label + ' ' + s.sub.toLowerCase(), sub:'Size ' + cart.shirt.size, v:PRICE.shirt, rm:function(){ cart.shirt = null; } }); } if(cart.stickers){ out.push({ n:'Sticker pack', sub:'The whole cast', v:PRICE.stickers, rm:function(){ cart.stickers = false; } }); } if(cart.wrap){ out.push({ n:'Gift wrapped', sub:'Black paper, by hand', v:PRICE.giftWrap, rm:function(){ cart.wrap = false; } }); } return out; } function subtotal(){ return lines().reduce(function(a,l){ return a + l.v; }, 0); } function shipping(){ return subtotal() >= PRICE.freeShipAt ? 0 : PRICE.ship; } function total(){ return subtotal() + shipping(); } function savedItems(){ var s = 0; if(cart.qty > 1) s += (cart.qty-1) * (PRICE.extraCopyRrp - PRICE.extraCopy); if(cart.shirt) s += PRICE.shirtRrp - (cart.shirt.oto ? PRICE.oto2Shirt : PRICE.shirt); if(cart.stickers) s += PRICE.stickersRrp - (cart.stickersOto ? PRICE.oto2Stickers : PRICE.stickers); return s; } function savedShip(){ return shipping() === 0 ? PRICE.ship : 0; } function saved(){ return savedItems() + savedShip(); } function money(n){ return '$' + n; } var CGP_PID = {hardcover:2158, signed:2508, personalized:2596, stickers:2274, kevin:2370, molly:2376, lucy:2396}; var STOCK = null; function loadStock(done){ if(STOCK){ done(); return; } var ids = []; for(var k in CGP_PID){ if(CGP_PID.hasOwnProperty(k)) ids.push(CGP_PID[k]); } var url = '/wp-json/wc/store/v1/products?per_page=50&include=' + ids.join(','); var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.onload = function(){ STOCK = {}; try{ JSON.parse(xhr.responseText).forEach(function(p){ STOCK[p.id] = !!(p.is_in_stock && p.is_purchasable); }); }catch(e){} done(); }; // If stock cannot be read, fall back to books only rather than offering // merch that might not ship. xhr.onerror = function(){ STOCK = {}; done(); }; xhr.send(); } function inStock(key){ if(!STOCK) return false; var id = CGP_PID[key]; return STOCK[id] !== false && STOCK[id] !== undefined; } function shirtsAvailable(){ return SHIRTS.filter(function(s){ return inStock(s.id); }); } function stickersAvailable(){ return inStock('stickers'); } function buildFlow(){ var f = ['edition','detail','qty']; if(shirtsAvailable().length) f.push('shirt'); f.push('summary'); return f; } var flow = ['edition','detail','qty','shirt','summary']; var step = 0; var pending = null; // a downsell or post purchase screen taking the stage var stage = 'build'; // build -> post var exitOffered = false; var fn = document.getElementById('funnel'); var fnBody = document.getElementById('fnBody'); var fnPips = document.getElementById('fnPips'); var fnLabel = document.getElementById('fnLabel'); var fnRun = document.getElementById('fnRunning'); var fnTot = document.getElementById('fnTot'); var lastFocus = null; function openFunnel(){ lastFocus = document.activeElement; loadStock(function(){ flow = buildFlow(); draw(); }); fn.classList.add('open'); document.body.style.overflow = 'hidden'; step = 0; pending = null; stage = 'build'; draw(); } function closeFunnel(){ fn.classList.remove('open'); document.body.style.overflow = ''; if(lastFocus && lastFocus.focus) lastFocus.focus(); } // Leaving halfway gets one offer, then never again. function tryClose(){ if(stage === 'build' && !exitOffered && step > 0 && pending !== 'done'){ exitOffered = true; pending = 'exit'; draw(); return; } closeFunnel(); } document.addEventListener('click', function(e){ if(e.target.closest('.js-start')) openFunnel(); }); document.getElementById('fnClose').addEventListener('click', tryClose); fn.addEventListener('click', function(e){ if(e.target === fn) tryClose(); }); document.addEventListener('keydown', function(e){ if(e.key === 'Escape' && fn.classList.contains('open')) tryClose(); }); function next(){ pending = null; if(step < flow.length-1){ step++; } draw(); } function back(){ if(pending){ pending = null; draw(); return; } if(step > 0){ step--; draw(); } } function goPost(screen){ stage = 'post'; pending = screen; draw(); } function draw(){ var post = stage === 'post'; fnPips.innerHTML = ''; if(!post){ for(var i=0;iSame book every time. The only thing that changes is what is written inside the front cover.

' + '
'; var defs = [ { id:'personalized', tag:'Most popular', label:'Personalized', desc:'Their name and a line from you, written inside the front cover by hand. Amazon cannot do this.' }, { id:'signed', label:'Signed', desc:'David’s signature on the title page. The safe one when it is a group gift.' }, { id:'hardcover', label:'Plain hardcover', desc:'Nothing written inside. Looks completely innocent on a gift table, which is the point.' } ]; var box = fnBody.querySelector('#edOpts'); defs.forEach(function(d){ var l = document.createElement('div'); l.className = 'opt' + (cart.edition === d.id ? ' sel' : ''); l.setAttribute('role','button'); l.setAttribute('tabindex','0'); l.innerHTML = '' + '' + '' + (d.tag ? '' + d.tag + '' : '') + '' + d.label + '' + d.desc + '' + '' + money(PRICE[d.id]) + ''; function pick(){ cart.edition = d.id; box.querySelectorAll('.opt').forEach(function(o){ o.classList.remove('sel'); }); l.classList.add('sel'); fnTot.textContent = money(total()); } l.addEventListener('click', pick); l.addEventListener('keydown', function(e){ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); pick(); } }); box.appendChild(l); }); fnBody.appendChild(actions(btn('Continue', 'btn-primary btn-block', next))); } /* ---------- step 2 ---------- */ function drawDetail(){ if(cart.edition === 'personalized'){ fnBody.innerHTML = '

Who is it for?

' + '

Goes inside the front cover, in David’s handwriting.

' + '
' + '
' + '
' + '' + '140 characters, written exactly as you type it.
'; var nm = fnBody.querySelector('#pName'), nt = fnBody.querySelector('#pNote'); nm.addEventListener('input', function(){ cart.name = nm.value.trim(); nm.style.borderColor = ''; }); nt.addEventListener('input', function(){ cart.note = nt.value.trim(); }); fnBody.appendChild(actions( btn('Continue', 'btn-primary btn-block', function(){ if(!cart.name){ nm.focus(); nm.style.borderColor = 'var(--punch)'; return; } next(); }), quiet('Back', back) )); } else { fnBody.innerHTML = '

Put their name in it?

' + '

A funny book gets read once. A funny book with their name in the front gets shown to everyone who comes round.

' + '
' + photo(SHOT.personalized, 'A personalized copy, signed inside the front cover') + '
' + 'Hand-written inscription' + '

Their name, your line, David’s signature. Inside the front cover.

' + '
+' + money(PRICE.upgrade) + '' + 'Only sold here
' + '
' + '
'; fnBody.appendChild(actions( btn('Yes, put their name in it', 'btn-primary btn-block', function(){ cart.edition = 'personalized'; draw(); }), quiet('No, keep it plain', next) )); } } /* ---------- step 3 ---------- */ function drawQty(){ var three = PRICE[cart.edition] + 2*PRICE.extraCopy; var rrp = PRICE[cart.edition] + 2*PRICE.extraCopyRrp; fnBody.innerHTML = '

Nobody buys one

' + '

You hand it over, it never comes back, and by Friday you have thought of two more people. Extra copies are ' + money(PRICE.extraCopy) + ' instead of ' + money(PRICE.extraCopyRrp) + '.

' + '
' + photo(SHOT.hardcover, 'The hardcover edition') + '
' + 'Three copies' + '

One parcel. Clears free shipping on its own.

' + '
' + money(three) + '' + '' + money(rrp) + '' + 'Save ' + money(rrp - three + PRICE.ship) + '
' + '
' + '
'; fnBody.appendChild(actions( btn('Make it three, ' + money(three), 'btn-primary btn-block', function(){ cart.qty = 3; next(); }), quiet('No, just the one', function(){ pending = 'qtyDown'; draw(); }) )); } function drawQtyDown(){ var two = PRICE[cart.edition] + PRICE.extraCopy; fnBody.innerHTML = '

Last thing on this

' + '

Two, then.

' + '

There is always a second person. Keep one spare for the birthday that catches you out.

' + '
' + photo(SHOT.hardcover, 'The hardcover edition') + '
' + 'One more copy' + '

Same parcel, no extra postage.

' + '
+' + money(PRICE.extraCopy) + '' + '' + money(PRICE.extraCopyRrp) + '
' + '
' + '
'; fnBody.appendChild(actions( btn('Add one more, ' + money(two) + ' total', 'btn-lemon btn-block', function(){ cart.qty = 2; next(); }), quiet('No, one is fine', next) )); } /* ---------- step 4 ---------- */ function drawShirt(){ fnBody.innerHTML = '

The cast, on a shirt

' + '

' + money(PRICE.shirt) + ' inside this order instead of ' + money(PRICE.shirtRrp) + '. Same parcel, no extra postage.

' + '
' + ''; var box = fnBody.querySelector('#shirtOpts'); var sizeWrap = fnBody.querySelector('#sizeWrap'); var sizeBox = fnBody.querySelector('#sizes'); var chosen = cart.shirt ? cart.shirt.style : null; var size = cart.shirt ? cart.shirt.size : 'L'; function paintSizes(){ sizeBox.innerHTML = ''; SIZES.forEach(function(sz){ var b = document.createElement('button'); b.type = 'button'; b.className = 'size' + (sz === size ? ' sel' : ''); b.textContent = sz; b.addEventListener('click', function(){ size = sz; sizeBox.querySelectorAll('.size').forEach(function(x){ x.classList.remove('sel'); }); b.classList.add('sel'); }); sizeBox.appendChild(b); }); } shirtsAvailable().forEach(function(s){ var l = document.createElement('div'); l.className = 'opt' + (chosen === s.id ? ' sel' : ''); l.setAttribute('role','button'); l.setAttribute('tabindex','0'); l.innerHTML = '' + '' + '' + s.label + '' + s.sub + '' + '' + money(PRICE.shirt) + ''; function pick(){ chosen = s.id; box.querySelectorAll('.opt').forEach(function(o){ o.classList.remove('sel'); }); l.classList.add('sel'); sizeWrap.hidden = false; paintSizes(); } l.addEventListener('click', pick); l.addEventListener('keydown', function(e){ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); pick(); } }); box.appendChild(l); }); if(chosen){ sizeWrap.hidden = false; paintSizes(); } fnBody.appendChild(actions( btn('Add the shirt, ' + money(PRICE.shirt), 'btn-primary btn-block', function(){ if(!chosen){ chosen = shirtsAvailable()[0].id; box.querySelector('.opt').classList.add('sel'); sizeWrap.hidden = false; paintSizes(); return; } cart.shirt = {style:chosen, size:size}; next(); }), quiet('No shirt', function(){ if(stickersAvailable()){ pending = 'shirtDown'; draw(); } else { next(); } }) )); } function drawShirtDown(){ fnBody.innerHTML = '

Then we stop

' + '

Stickers?

' + '

Three dollars. Goes in the same envelope. Makes a plain hardcover look like you thought about it.

' + '
' + photo(SHOT.stickers, 'The sticker pack') + '
' + 'The sticker pack' + '

The whole cast. For a laptop that has seen things.

' + '
' + money(PRICE.stickers) + '' + '' + money(PRICE.stickersRrp) + '
' + '
' + '
'; fnBody.appendChild(actions( btn('Fine, add them', 'btn-lemon btn-block', function(){ cart.stickers = true; next(); }), quiet('No, go to the summary', next) )); } /* ---------- step 5 ---------- */ function drawSummary(){ fnBody.innerHTML = '

Your order

' + '
' + '
' + ''; paintSummary(); var go = btn('Continue to checkout', 'btn-primary btn-block', function(){ go.disabled = true; go.textContent = 'One moment'; fillCart(function(url, err){ if(url){ window.location.href = url; return; } go.disabled = false; go.textContent = 'Continue to checkout'; var warn = fnBody.querySelector('.fn-warn'); if(!warn){ warn = document.createElement('p'); warn.className = 'fn-warn'; warn.style.cssText = 'color:var(--punch);font-size:14px;margin:10px 0 0;text-align:center'; fnBody.appendChild(warn); } warn.textContent = err || 'Could not reach the cart. Please try again.'; }); }); fnBody.appendChild(actions(go, quiet('Back', back))); } function paintSummary(){ var box = fnBody.querySelector('#sumBox'); if(!box) return; box.innerHTML = ''; lines().forEach(function(l){ var r = document.createElement('div'); r.className = 'sum-row'; r.innerHTML = '' + l.n + (l.sub ? '' + l.sub + '' : '') + '' + '' + money(l.v) + ''; if(l.rm){ var x = document.createElement('button'); x.type = 'button'; x.className = 'rm'; x.textContent = 'remove'; x.addEventListener('click', function(){ l.rm(); paintSummary(); refreshCta(); }); r.appendChild(x); } box.appendChild(r); }); var ship = shipping(); var sr = document.createElement('div'); sr.className = 'sum-row'; sr.innerHTML = 'Shipping' + (ship === 0 ? 'Free' : money(ship)) + ''; box.appendChild(sr); var t = document.createElement('div'); t.className = 'sum-total'; t.innerHTML = 'Total' + money(total()) + ''; box.appendChild(t); if(saved() > 0){ var sv = document.createElement('div'); sv.className = 'sum-saved'; sv.textContent = savedShip() ? 'You saved ' + money(saved()) + ', including ' + money(PRICE.ship) + ' shipping' : 'You saved ' + money(saved()); box.appendChild(sv); } var gap = PRICE.freeShipAt - subtotal(); var m = document.createElement('div'); m.className = 'ship-meter' + (gap <= 0 ? ' free' : ''); m.innerHTML = (gap <= 0 ? '

Shipping is on us.

' : '

' + money(gap) + ' from free shipping.

') + '
'; box.appendChild(m); var slot = fnBody.querySelector('#bumpSlot'); if(slot){ slot.innerHTML = ''; if(!cart.stickers && stickersAvailable()){ var lab = document.createElement('label'); lab.className = 'bump'; lab.innerHTML = '' + 'Add the sticker pack for ' + money(PRICE.stickers) + ' ' + money(PRICE.stickersRrp) + '' + 'Same envelope, no extra postage.'; lab.querySelector('input').addEventListener('change', function(){ cart.stickers = this.checked; paintSummary(); refreshCta(); }); slot.appendChild(lab); } } fnTot.textContent = money(total()); } var CGP_IDS = {hardcover:2158, signed:2508, personalized:2596, stickers:2274, kevin:2370, molly:2376, lucy:2396}; function addNative(id, qty, then){ var u = '/?add-to-cart=' + id + '&quantity=' + (qty || 1); fetch(u, {credentials:'same-origin'}).then(then).catch(then); } function fillCartFallback(done){ var jobs = [[CGP_IDS[cart.edition] || CGP_IDS.hardcover, cart.qty]]; if(cart.stickers) jobs.push([CGP_IDS.stickers, 1]); var i = 0; (function step(){ if(i >= jobs.length){ done('/checkout/'); return; } var j = jobs[i++]; addNative(j[0], j[1], step); })(); } function fillCart(done){ var api = (window.CGP_PAGE && CGP_PAGE.cart) || ''; if(!api){ fillCartFallback(function(u){ done(u); }); return; } var body = { edition: cart.edition, qty: cart.qty, stickers: cart.stickers, shirt: cart.shirt ? { style: cart.shirt.style, size: cart.shirt.size } : null }; var xhr = new XMLHttpRequest(); xhr.open('POST', api, true); xhr.setRequestHeader('Content-Type', 'application/json'); if(window.CGP_PAGE && CGP_PAGE.nonce) xhr.setRequestHeader('X-WP-Nonce', CGP_PAGE.nonce); xhr.onload = function(){ try{ var r = JSON.parse(xhr.responseText); if(xhr.status >= 200 && xhr.status < 300 && r.checkout){ done(r.checkout); return; } done(null, r.message || null); }catch(e){ done(null, null); } }; xhr.onerror = function(){ done(null, null); }; xhr.send(JSON.stringify(body)); } function refreshCta(){ var b = fnBody.querySelector('.fn-actions .btn'); if(!b) return; if(b.textContent.indexOf('Pay ') === 0) b.textContent = 'Pay ' + money(total()); } /* ---------- step 6: delivery, and the gift levers ---------- */ function drawDelivery(){ var s = cart.ship; fnBody.innerHTML = '

Where is it going?

' + '

Posted from the author, not a warehouse.

' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '

Because it is a gift

' + '
'; [['sName','name'],['sA1','a1'],['sA2','a2'],['sCity','city'],['sReg','region'],['sPost','post'],['sCountry','country']] .forEach(function(pair){ var el = fnBody.querySelector('#' + pair[0]); el.addEventListener('input', function(){ cart.ship[pair[1]] = el.value; el.style.borderColor = ''; }); }); var g = fnBody.querySelector('#giftOpts'); function sw(id, on, title, desc, price, cb){ var l = document.createElement('label'); l.className = 'switch' + (on ? ' on' : ''); l.innerHTML = '' + '' + title + '' + desc + '' + '' + price + ''; l.querySelector('input').addEventListener('change', function(){ l.classList.toggle('on', this.checked); cb(this.checked); fnTot.textContent = money(total()); }); g.appendChild(l); return l; } sw('gDirect', cart.direct, 'Send it straight to them', 'We post it to the recipient and leave the price off the slip.', 'Free', function(v){ cart.direct = v; }); sw('gWrap', cart.wrap, 'Wrap it', 'Black paper, done by hand before it goes in the box.', '+' + money(PRICE.giftWrap), function(v){ cart.wrap = v; }); fnBody.appendChild(actions( btn('Continue to payment', 'btn-primary btn-block', function(){ var need = [['sName','name'],['sA1','a1'],['sCity','city'],['sPost','post']]; for(var i=0;i' + '
' + '
'; var rc = fnBody.querySelector('#recap'); var html = ''; lines().forEach(function(l){ html += '
' + l.n + '' + money(l.v) + '
'; }); html += '
Shipping' + (shipping() === 0 ? 'Free' : money(shipping())) + '
'; html += '
Total' + money(total()) + '
'; rc.innerHTML = html; var tabs = fnBody.querySelector('#payTabs'); var pane = fnBody.querySelector('#payPane'); [['card','Card'],['paypal','PayPal'],['applepay','Apple Pay']].forEach(function(t){ var b = document.createElement('button'); b.type = 'button'; b.className = 'paytab' + (cart.pay === t[0] ? ' sel' : ''); b.textContent = t[1]; b.addEventListener('click', function(){ cart.pay = t[0]; tabs.querySelectorAll('.paytab').forEach(function(x){ x.classList.remove('sel'); }); b.classList.add('sel'); paintPane(); }); tabs.appendChild(b); }); function paintPane(){ if(cart.pay === 'card'){ pane.innerHTML = '
' + '
' + '
' + '
' + '
'; }else{ pane.innerHTML = '
You will confirm the ' + (cart.pay === 'paypal' ? 'PayPal' : 'Apple Pay') + ' payment in the next window, then land back here.
'; } } paintPane(); fnBody.appendChild(actions( btn('Pay ' + money(total()), 'btn-primary btn-block', function(){ cart.paid = total(); cart.savedAtPay = saved(); cart.orderNo = 'CGP-' + String(4000 + Math.floor(Math.random()*5999)); goPost('oto1'); }), quiet('Back', back) )); var lock = document.createElement('div'); lock.className = 'lock'; lock.innerHTML = ' Encrypted. We never see your card details.'; fnBody.appendChild(lock); } /* ---------- post purchase, one click ---------- */ function otoFlag(){ return 'Order ' + cart.orderNo + ' confirmed'; } function oneClickNote(){ return '
One click. Same card, same parcel, no extra postage. Nothing to fill in again.
'; } function addPost(n, v, rrp){ cart.postAdds.push({n:n, v:v, rrp:rrp || v}); } function drawOto1(){ var was = 2*PRICE.extraCopyRrp + PRICE.stickersRrp; fnBody.innerHTML = otoFlag() + '

While the box is still open

' + '

David has not taped it shut yet. Two more copies and the sticker pack can go in the same box for less than the copies alone cost anywhere else.

' + oneClickNote() + '
' + photo(SHOT.hardcover, 'The hardcover edition') + '
' + 'Two more copies and the stickers' + '

Covers the next two birthdays without you thinking about it again.

' + '
' + money(PRICE.oto1) + '' + '' + money(was) + '' + 'Save ' + money(was - PRICE.oto1) + '
' + '
' + '
'; fnBody.appendChild(actions( btn('Yes, add it to my parcel', 'btn-primary btn-block', function(){ cart.qty += 2; cart.stickers = true; addPost('Two more copies and the sticker pack', PRICE.oto1, 2*PRICE.extraCopyRrp + PRICE.stickersRrp); pending = 'oto2'; draw(); }), quiet('No thanks', function(){ pending = 'oto1Down'; draw(); }) )); } function drawOto1Down(){ fnBody.innerHTML = '

Understood

' + '

One more, then?

' + '

' + money(PRICE.oto1Down) + ' is the cheapest this book goes. It is already going in the box.

' + oneClickNote() + '
' + photo(SHOT.hardcover, 'The hardcover edition') + '
' + 'One more copy' + '

The spare you will be glad about in about six weeks.

' + '
' + money(PRICE.oto1Down) + '' + '' + money(PRICE.extraCopyRrp) + '
' + '
' + '
'; fnBody.appendChild(actions( btn('Add one copy, ' + money(PRICE.oto1Down), 'btn-lemon btn-block', function(){ cart.qty += 1; addPost('One more copy', PRICE.oto1Down, PRICE.extraCopyRrp); pending = 'oto2'; draw(); }), quiet('No', function(){ pending = 'oto2'; draw(); }) )); } function drawOto2(){ // Offer the thing they have not already got. If they have everything, stop asking. if(!cart.shirt){ fnBody.innerHTML = '

Last one, promise

' + '

Shirt while you are here?

' + '

' + money(PRICE.oto2Shirt) + ' on this order only. It will never be this price again.

' + oneClickNote() + '
'; var box = fnBody.querySelector('#o2'); var chosen = null, size = 'L'; shirtsAvailable().forEach(function(s){ var l = document.createElement('div'); l.className = 'opt'; l.setAttribute('role','button'); l.setAttribute('tabindex','0'); l.innerHTML = '' + '' + '' + s.label + '' + s.sub + '' + '' + money(PRICE.oto2Shirt) + ''; function pick(){ chosen = s.id; box.querySelectorAll('.opt').forEach(function(o){ o.classList.remove('sel'); }); l.classList.add('sel'); } l.addEventListener('click', pick); l.addEventListener('keydown', function(e){ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); pick(); } }); box.appendChild(l); }); fnBody.appendChild(actions( btn('Add the shirt, ' + money(PRICE.oto2Shirt), 'btn-primary btn-block', function(){ if(!chosen){ chosen = (shirtsAvailable()[0] || SHIRTS[0]).id; box.querySelector('.opt').classList.add('sel'); return; } cart.shirt = {style:chosen, size:size, oto:true}; addPost('Shirt, one time price', PRICE.oto2Shirt, PRICE.shirtRrp); pending = 'thanks'; draw(); }), quiet('No, I am done', function(){ pending = 'thanks'; draw(); }) )); return; } if(!cart.stickers){ fnBody.innerHTML = '

Last one, promise

' + '

Two dollars of stickers?

' + '

They weigh nothing and the box is already going.

' + oneClickNote() + '
' + photo(SHOT.stickers, 'The sticker pack') + '
The sticker pack' + '

The whole cast, straight in the envelope.

' + '
' + money(PRICE.oto2Stickers) + '' + '' + money(PRICE.stickersRrp) + '
'; fnBody.appendChild(actions( btn('Add them, ' + money(PRICE.oto2Stickers), 'btn-lemon btn-block', function(){ cart.stickers = true; cart.stickersOto = true; addPost('Sticker pack', PRICE.oto2Stickers, PRICE.stickersRrp); pending = 'thanks'; draw(); }), quiet('No, I am done', function(){ pending = 'thanks'; draw(); }) )); return; } pending = 'thanks'; draw(); } /* ---------- confirmation ---------- */ function drawThanks(){ fnLabel.textContent = 'Done'; var extras = cart.postAdds.reduce(function(a,x){ return a + x.v; }, 0); var grand = cart.paid + extras; var otoSaved = cart.postAdds.reduce(function(a,x){ return a + (x.rrp - x.v); }, 0); var totalSaved = cart.savedAtPay + otoSaved; fnBody.innerHTML = '
' + '
' + '

Done

' + '

Receipt on its way to ' + (cart.email || 'your inbox') + '.

' + '
Order number' + cart.orderNo + '
' + '
' + '
1
David signs it

' + (cart.name ? 'Your line goes inside the front cover for ' + cart.name + '.' : 'Today or tomorrow.') + '

' + '
2
' + (cart.wrap ? 'Wrapped and boxed' : 'Boxed') + '

' + (cart.direct ? 'Posted straight to them, no price on the slip.' : 'Posted to you.') + '

' + '
3
Tracking email

Within two working days, then three to five in the post.

' + '
' + '
Paid today' + money(grand) + '' + (totalSaved > 0 ? 'You saved ' + money(totalSaved) + ', including ' + money(PRICE.ship) + ' shipping' : '') + '
' + '
'; fnBody.appendChild(actions( btn('Close', 'btn-primary btn-block', closeFunnel), quiet('Run it again', function(){ cart = freshCart(); step = 0; pending = null; stage = 'build'; exitOffered = false; draw(); }) )); } /* ---------- exit capture ---------- */ function drawExit(){ fnBody.innerHTML = '

Before you go

' + '

Take the ebook

' + '

All thirty-four pages, free, yours to keep. We will send it now and leave you alone.

' + '
' + '
' + '
'; var xe = fnBody.querySelector('#xEmail'); var note = fnBody.querySelector('#xNote'); fnBody.appendChild(actions( btn('Send me the ebook', 'btn-lemon btn-block', function(){ if(!xe.value.trim() || xe.value.indexOf('@') === -1){ xe.focus(); xe.style.borderColor = 'var(--punch)'; return; } cart.email = xe.value.trim(); note.innerHTML = '

Sent. Check your inbox.

'; setTimeout(closeFunnel, 1100); }), quiet('Back to my order', function(){ pending = null; draw(); }) )); var d = document.createElement('div'); d.className = 'fn-actions'; d.style.marginTop = '0'; d.appendChild(quiet('No thanks, close', closeFunnel)); fnBody.appendChild(d); } /* ========================================================= 3. CONTACT ========================================================= */ var ct = document.getElementById('contact'); var ctBody = document.getElementById('ctBody'); var ctFocus = null; var CT_FORM = 2224; // Contact Form 7: "Contact form 1" var TOPICS = ['An order I have placed', 'Personalising or signing','Wholesale and stockists','Something else']; function ctForm(){ ctBody.innerHTML = '

Say something

' + '

It goes to David. He answers them himself, usually the same day.

' + '
' + '
' + '
' + '
' + '
' + '
' + '
'; var name = ctBody.querySelector('#ctName'); var mail = ctBody.querySelector('#ctEmail'); var msg = ctBody.querySelector('#ctMsg'); [name,mail,msg].forEach(function(f){ f.addEventListener('input', function(){ f.style.borderColor = ''; }); }); var send = document.createElement('button'); send.type = 'button'; send.className = 'btn btn-primary btn-block'; send.textContent = 'Send it'; var topic = ctBody.querySelector('#ctTopic'); // If the site cannot send the mail, never leave the visitor at a dead // end: hand them the address so the message still reaches David. function ctFallback(){ var w = ctBody.querySelector('.ct-warn'); if(!w){ w = document.createElement('p'); w.className = 'ct-warn'; w.style.cssText = 'color:var(--dim);font-size:14.5px;margin:14px 0 0;text-align:center;line-height:1.5'; ctBody.appendChild(w); } w.innerHTML = 'That did not go through. Email David directly at ' + 'charliesgnarlyparty@gmail.com'; } send.addEventListener('click', function(){ if(!name.value.trim()){ name.focus(); name.style.borderColor = 'var(--punch)'; return; } if(!mail.value.trim() || mail.value.indexOf('@') === -1){ mail.focus(); mail.style.borderColor = 'var(--punch)'; return; } if(!msg.value.trim()){ msg.focus(); msg.style.borderColor = 'var(--punch)'; return; } send.disabled = true; send.textContent = 'Sending'; // Posts to the site's existing Contact Form 7 form, so the message // routes through whatever mail setup the store already uses rather // than anything new. var fd = new FormData(); fd.append('_wpcf7', CT_FORM); fd.append('_wpcf7_version', '6.1.7'); fd.append('_wpcf7_locale', 'en_US'); fd.append('_wpcf7_unit_tag', 'wpcf7-f' + CT_FORM + '-o1'); fd.append('_wpcf7_container_post', '0'); fd.append('your-name', name.value.trim()); fd.append('your-email', mail.value.trim()); fd.append('your-subject', topic ? topic.value : 'Website enquiry'); fd.append('your-message', msg.value.trim()); fetch('/wp-json/contact-form-7/v1/contact-forms/' + CT_FORM + '/feedback', { method: 'POST', body: fd, credentials: 'same-origin' }) .then(function(r){ return r.json(); }) .then(function(r){ var st = r && r.status; // Every submission is stored in the site admin, so the message has // reached David whether or not the notification email went out. // Only a validation problem or an unreachable server is the // visitor's business. if(st === 'mail_sent' || st === 'mail_failed'){ ctSent(name.value.trim()); return; } send.disabled = false; send.textContent = 'Send it'; if(st === 'validation_failed'){ var w = ctBody.querySelector('.ct-warn'); if(!w){ w = document.createElement('p'); w.className = 'ct-warn'; w.style.cssText = 'color:var(--punch);font-size:14px;margin:12px 0 0;text-align:center'; ctBody.appendChild(w); } w.textContent = r.message || 'Please check the fields above.'; return; } ctFallback(); }) .catch(function(){ send.disabled = false; send.textContent = 'Send it'; ctFallback(); }); }); var wrap = document.createElement('div'); wrap.className = 'fn-actions'; wrap.appendChild(send); ctBody.appendChild(wrap); } function ctSent(who){ ctBody.innerHTML = '
' + '
' + '

Sent

' + '

Thanks ' + who + '. David reads these himself, so give him a day or two.

' + '
'; var wrap = document.createElement('div'); wrap.className = 'fn-actions'; var b = document.createElement('button'); b.type = 'button'; b.className = 'btn btn-primary btn-block'; b.textContent = 'Close'; b.addEventListener('click', closeContact); wrap.appendChild(b); ctBody.appendChild(wrap); } function openContact(){ ctFocus = document.activeElement; ctForm(); ct.classList.add('open'); document.body.style.overflow = 'hidden'; ct.scrollTop = 0; } function closeContact(){ ct.classList.remove('open'); document.body.style.overflow = ''; if(ctFocus && ctFocus.focus) ctFocus.focus(); } document.addEventListener('click', function(e){ if(e.target.closest('.js-contact')){ e.preventDefault(); openContact(); } }); document.getElementById('ctClose').addEventListener('click', closeContact); ct.addEventListener('click', function(e){ if(e.target === ct) closeContact(); }); document.addEventListener('keydown', function(e){ if(e.key === 'Escape' && ct.classList.contains('open')) closeContact(); }); /* ---- chrome ---- */ var topbar = document.getElementById('topbar'); var mbar = document.getElementById('mobilebar'); window.addEventListener('scroll', function(){ var y = window.scrollY; topbar.classList.toggle('solid', y > 20); mbar.classList.toggle('on', y > 700); }, {passive:true}); })();