A running match score while the shopper answers
Goal: an inline quiz sits above a product grid; as the shopper answers, every product card shows how well it matches so far (“72% match”), and the best ones rise to the top.
Uses: Storefront JS API, page.viewed, getState().points. Nothing to install; no API key. Events fire on the Plus and Enterprise plans.
What the quiz gives you
Section titled “What the quiz gives you”A points quiz scores dimensions, not products. getState().points maps each dimension id (the id set in the editor) to the shopper’s total so far, and it is fresh on every page.viewed: the server scored the submitted page before the next one landed. The step from dimensions to products is yours: a map that says how much each product belongs to each dimension.
Score the cards
Section titled “Score the cards”Each card on the page carries its product id; the map weights each product per dimension. The score is the weighted sum of the shopper’s points over the sum of all their points, so it reads as a share and grows as pages are answered.
<div class="grid"> <article data-product-id="gid://shopify/Product/8692896989320"><h3>Night cream</h3><span class="match"></span></article> <article data-product-id="gid://shopify/Product/8692896989321"><h3>Gel cleanser</h3><span class="match"></span></article></div><script> window.octaneai = window.octaneai || []; window.octaneai.push(function (octaneai) { var WEIGHTS = { 'gid://shopify/Product/8692896989320': { dry: 1, sensitive: 0.5 }, 'gid://shopify/Product/8692896989321': { oily: 1, combination: 0.5 } };
function render(points) { var total = Object.keys(points).reduce(function (sum, id) { return sum + points[id]; }, 0); var cards = Array.prototype.slice.call(document.querySelectorAll('[data-product-id]')); cards.forEach(function (card) { var weights = WEIGHTS[card.dataset.productId] || {}; var score = Object.keys(weights).reduce(function (sum, id) { return sum + (points[id] || 0) * weights[id]; }, 0); var pct = total > 0 ? Math.round(100 * score / total) : 0; card.dataset.match = String(pct); card.querySelector('.match').textContent = total > 0 ? pct + '% match' : ''; }); cards.sort(function (a, b) { return Number(b.dataset.match) - Number(a.dataset.match); }) .forEach(function (card) { card.parentNode.appendChild(card); }); }
octaneai.on('page.viewed', function () { var state = octaneai.getState(); if (state) render(state.points); }); });</script>Re-appending the cards in score order reorders the grid without rebuilding it. Until the first page is submitted every total is 0 and the cards show no score.
Errors to handle
Section titled “Errors to handle”getState()returnsnullbefore a quiz is mounted; the guard above skips the render. Inside apage.viewedlistener the quiz is always mounted.- A product in the grid with no entry in
WEIGHTSscores0and sinks to the bottom; add it to the map or leave it out of the grid.
Things to know
Section titled “Things to know”pointsis{}on a quiz without dimensions, and every card then shows nothing. A dimension with no points yet is absent from the map ({ dry: 3 }after a page that scored onlydry), which the|| 0above reads as zero.question.answeredalso fires on each submit, but the points are the server’s and arrive with the page landing, sopage.viewedis the event to render on.- Points are page-wide: two quizzes on one page share one totals map.
quiz.finishedcarries the finalpointstoo; the lastpage.viewedalready rendered them, so no extra listener is needed.- Use the same dimension ids the editor shows;
pointsnever carries the dimension’s name.
