Skip to content

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.

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.

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.

  • getState() returns null before a quiz is mounted; the guard above skips the render. Inside a page.viewed listener the quiz is always mounted.
  • A product in the grid with no entry in WEIGHTS scores 0 and sinks to the bottom; add it to the map or leave it out of the grid.
  • points is {} 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 only dry), which the || 0 above reads as zero.
  • question.answered also fires on each submit, but the points are the server’s and arrive with the page landing, so page.viewed is the event to render on.
  • Points are page-wide: two quizzes on one page share one totals map.
  • quiz.finished carries the final points too; the last page.viewed already rendered them, so no extra listener is needed.
  • Use the same dimension ids the editor shows; points never carries the dimension’s name.