Demo diagnostic — not client work
Diff: before → after
The full line-by-line code diff between the broken before/ and fixed after/ demo. See the Changed Files summary for a plain-English overview.
Demo diagnostic — not real client work. This is evidence from a self-contained sample, shown as proof of the documentation included with the Shopify bug fix.
added
removed
hunk header
diff --git a/before/index.html b/after/index.html
index 17bcbfc..4201d22 100644
--- a/before/index.html
+++ b/after/index.html
@@ -1,27 +1,28 @@
<!DOCTYPE html>
<!--
============================================================
- BEFORE (BROKEN) — Shopify-style product page + cart drawer
+ AFTER (FIXED) — Shopify-style product page + cart drawer
============================================================
- This is a DEMO that intentionally reproduces common Shopify
- theme bugs. It is NOT real client work and uses neutral,
- made-up product data.
+ Same DEMO product, with the intentional bugs repaired.
+ NOT real client work; neutral, made-up product data.
- Known (intentional) problems in this version:
- 1. Add-to-cart button does NOT open the cart drawer.
- 2. Cart count in the header does NOT visibly update.
- 3. Mobile layout breaks under 430px (CTA pushed far down).
- 4. Variant selector spacing is messy and cramped.
- 5. Product image area is too tall on mobile.
- 6. Cart feedback after adding is unclear / missing.
- 7. Avoidable layout shift from an unsized image area.
+ Fixes applied vs. the BEFORE version:
+ 1. Add-to-cart now opens the cart drawer.
+ 2. Header cart count updates visibly on every add.
+ 3. Mobile layout reworked: image constrained, CTA visible earlier.
+ 4. Variant selector spacing cleaned up (flex + wrap + gap).
+ 5. Product image uses a fixed aspect ratio (no layout shift).
+ 6. Clear cart feedback: drawer slides in, line items + subtotal,
+ remove buttons, and a brief "Added" toast.
+ 7. Added viewport meta + sticky mobile CTA bar.
See ../report/shopify-demo-diagnostic.md for the writeup.
-->
<html lang="en">
<head>
<meta charset="UTF-8" />
- <!-- BUG: no viewport meta tag, so mobile rendering is inconsistent and zoomed -->
- <title>Performance Hoodie — Shopify-style Demo (Before)</title>
+ <!-- FIX: proper viewport meta for correct mobile rendering -->
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>Performance Hoodie — Shopify-style Demo (After)</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
@@ -30,16 +31,16 @@
<nav class="nav">
<a href="#">Shop</a>
<a href="#">About</a>
- <!-- BUG: cart count element exists but JS never updates it -->
- <a href="#" class="cart-link">Cart (<span id="cart-count">0</span>)</a>
+ <!-- FIX: cart count is updated by JS on every add -->
+ <a href="#" class="cart-link" id="cart-link">Cart (<span id="cart-count">0</span>)</a>
</nav>
</header>
- <span class="demo-flag">Demo diagnostic — BEFORE (broken)</span>
+ <span class="demo-flag">Demo diagnostic — AFTER (fixed)</span>
<main class="product">
- <!-- BUG: image area has no fixed aspect ratio; it is huge on mobile
- and causes layout shift while the gradient "loads" -->
+ <!-- FIX: image area uses a fixed aspect ratio so it never blows up the
+ mobile viewport and causes no layout shift -->
<div class="product-media">
<div class="product-image"></div>
<div class="thumb-row">
@@ -57,23 +58,27 @@
Tapered fit, zip side pocket, and a drawcord hood.
</p>
- <!-- BUG: variant options are cramped and have inconsistent spacing -->
+ <!-- FIX: clean, evenly spaced variant pills (flex + wrap + gap) -->
<div class="variant-group">
<div class="variant-label">Color</div>
<div class="variant-options">
- <button class="variant is-selected">Black</button><button class="variant">Slate</button><button class="variant">Olive</button>
+ <button class="variant is-selected">Black</button>
+ <button class="variant">Slate</button>
+ <button class="variant">Olive</button>
</div>
</div>
<div class="variant-group">
<div class="variant-label">Size</div>
<div class="variant-options">
- <button class="variant">S</button><button class="variant is-selected">M</button><button class="variant">L</button><button class="variant">XL</button>
+ <button class="variant">S</button>
+ <button class="variant is-selected">M</button>
+ <button class="variant">L</button>
+ <button class="variant">XL</button>
</div>
</div>
- <!-- BUG: the add-to-cart handler updates a variable but never opens the
- drawer and never updates the visible cart count -->
- <button id="add-to-cart" class="add-to-cart">Add to cart</button>
+ <!-- FIX: this button now opens the drawer, updates the count, and shows feedback -->
+ <button id="add-to-cart" class="add-to-cart">Add to cart — $78.00</button>
<ul class="product-meta">
<li>Free returns within 30 days</li>
@@ -82,11 +87,21 @@
</div>
</main>
- <!-- The cart drawer markup exists but is never shown by the JS -->
- <div id="cart-drawer" class="cart-drawer">
+ <!-- FIX: sticky mobile CTA so the buy action is reachable without scrolling -->
+ <div class="sticky-cta">
+ <span class="sticky-price">$78.00</span>
+ <button id="sticky-add" class="sticky-add">Add to cart</button>
+ </div>
+
+ <!-- Brief confirmation toast -->
+ <div id="toast" class="toast" role="status" aria-live="polite">Added to cart</div>
+
+ <!-- Cart drawer + dimmed backdrop -->
+ <div id="cart-backdrop" class="cart-backdrop"></div>
+ <aside id="cart-drawer" class="cart-drawer" aria-hidden="true">
<div class="cart-drawer-head">
<h2>Your cart</h2>
- <button id="cart-close" class="cart-close">×</button>
+ <button id="cart-close" class="cart-close" aria-label="Close cart">×</button>
</div>
<div id="cart-items" class="cart-items"></div>
<div class="cart-foot">
@@ -95,8 +110,9 @@
<span id="cart-subtotal">$0.00</span>
</div>
<button class="checkout-btn">Checkout</button>
+ <p class="cart-note">Shipping & taxes calculated at checkout.</p>
</div>
- </div>
+ </aside>
<script src="script.js"></script>
</body>
diff --git a/before/script.js b/after/script.js
index 124100b..a7a9a21 100644
--- a/before/script.js
+++ b/after/script.js
@@ -1,19 +1,128 @@
/* ============================================================
- BEFORE (BROKEN) script — Shopify-style cart logic
+ AFTER (FIXED) script — Shopify-style cart logic
------------------------------------------------------------
- This intentionally reproduces a very common Shopify theme bug:
- the "add to cart" click runs, but the UI never reflects it.
+ The add-to-cart flow now drives the UI end to end:
+ open drawer, render line items, update count + subtotal,
+ support quantity/remove, and show a brief confirmation toast.
============================================================ */
-// Simple in-memory cart state for the demo.
var cart = [];
var PRODUCT = {
+ id: "performance-hoodie",
title: "Performance Hoodie",
price: 78.0,
};
-// Variant selection (visual only here)
+// ---- Element references ----
+var countEl = document.getElementById("cart-count");
+var cartLink = document.getElementById("cart-link");
+var drawer = document.getElementById("cart-drawer");
+var backdrop = document.getElementById("cart-backdrop");
+var itemsEl = document.getElementById("cart-items");
+var subtotalEl = document.getElementById("cart-subtotal");
+var toast = document.getElementById("toast");
+
+// ---- Helpers ----
+function money(n) {
+ return "$" + n.toFixed(2);
+}
+
+function selectedVariant() {
+ // Read the currently selected color + size labels for the line item.
+ var labels = [];
+ document.querySelectorAll(".variant-group").forEach(function (group) {
+ var sel = group.querySelector(".variant.is-selected");
+ if (sel) labels.push(sel.textContent.trim());
+ });
+ return labels.join(" / ");
+}
+
+// FIX: open / close drawer by toggling classes (no more display:none dead end)
+function openDrawer() {
+ drawer.classList.add("open");
+ backdrop.classList.add("open");
+ drawer.setAttribute("aria-hidden", "false");
+}
+function closeDrawer() {
+ drawer.classList.remove("open");
+ backdrop.classList.remove("open");
+ drawer.setAttribute("aria-hidden", "true");
+}
+
+// FIX: visible cart count + a small bump animation so the change is noticed
+function updateCount() {
+ var total = cart.reduce(function (sum, line) { return sum + line.qty; }, 0);
+ countEl.textContent = total;
+ cartLink.classList.remove("bump");
+ // force reflow so the animation can replay on each add
+ void cartLink.offsetWidth;
+ cartLink.classList.add("bump");
+}
+
+// FIX: clear, itemized cart feedback (image, title, variant, qty, price, remove)
+function renderCart() {
+ itemsEl.innerHTML = "";
+
+ if (cart.length === 0) {
+ itemsEl.innerHTML = '<p class="cart-empty">Your cart is empty.</p>';
+ subtotalEl.textContent = money(0);
+ return;
+ }
+
+ var subtotal = 0;
+ cart.forEach(function (line, index) {
+ subtotal += line.price * line.qty;
+
+ var row = document.createElement("div");
+ row.className = "cart-line";
+ row.innerHTML =
+ '<div class="cart-line-img"></div>' +
+ '<div class="cart-line-body">' +
+ '<div class="cart-line-title">' + line.title + "</div>" +
+ '<div class="cart-line-meta">' + line.variant + "</div>" +
+ '<div class="cart-line-row">' +
+ '<div class="qty">' +
+ '<button data-act="dec" data-i="' + index + '" aria-label="Decrease quantity">−</button>' +
+ "<span>" + line.qty + "</span>" +
+ '<button data-act="inc" data-i="' + index + '" aria-label="Increase quantity">+</button>' +
+ "</div>" +
+ '<span class="cart-line-price">' + money(line.price * line.qty) + "</span>" +
+ "</div>" +
+ '<button class="cart-remove" data-act="remove" data-i="' + index + '">Remove</button>' +
+ "</div>";
+ itemsEl.appendChild(row);
+ });
+
+ subtotalEl.textContent = money(subtotal);
+}
+
+function showToast(message) {
+ toast.textContent = message;
+ toast.classList.add("show");
+ clearTimeout(showToast._t);
+ showToast._t = setTimeout(function () {
+ toast.classList.remove("show");
+ }, 1600);
+}
+
+// ---- Core action: add to cart ----
+function addToCart() {
+ var variant = selectedVariant();
+ // Merge with an existing identical line, otherwise push a new one.
+ var existing = cart.find(function (l) { return l.variant === variant; });
+ if (existing) {
+ existing.qty += 1;
+ } else {
+ cart.push({ title: PRODUCT.title, variant: variant, price: PRODUCT.price, qty: 1 });
+ }
+ updateCount(); // FIX #2: count updates visibly
+ renderCart(); // FIX #6: itemized feedback
+ openDrawer(); // FIX #1: drawer actually opens
+ showToast("Added to cart");
+}
+
+// ---- Variant selection ----
document.querySelectorAll(".variant-options").forEach(function (group) {
group.querySelectorAll(".variant").forEach(function (btn) {
btn.addEventListener("click", function () {
@@ -25,19 +134,32 @@ document.querySelectorAll(".variant-options").forEach(function (group) {
});
});
-// BUG #1 + #2 + #6:
-// The handler mutates the cart array but does NONE of the UI work:
-// - it never opens the cart drawer
-// - it never updates the visible "Cart (0)" count in the header
-// - it gives no confirmation/feedback to the shopper
-// To the user, clicking "Add to cart" appears to do nothing.
-document.getElementById("add-to-cart").addEventListener("click", function () {
- cart.push({ title: PRODUCT.title, price: PRODUCT.price, qty: 1 });
- // (No drawer open. No count update. No feedback.) That's the bug.
- console.log("Item pushed to cart array, but UI was never updated.", cart);
+// ---- Wire up controls ----
+document.getElementById("add-to-cart").addEventListener("click", addToCart);
+document.getElementById("sticky-add").addEventListener("click", addToCart);
+document.getElementById("cart-close").addEventListener("click", closeDrawer);
+backdrop.addEventListener("click", closeDrawer);
+
+// Quantity +/- and remove (event delegation on the items container)
+itemsEl.addEventListener("click", function (e) {
+ var btn = e.target.closest("[data-act]");
+ if (!btn) return;
+ var i = parseInt(btn.getAttribute("data-i"), 10);
+ var act = btn.getAttribute("data-act");
+
+ if (act === "inc") cart[i].qty += 1;
+ if (act === "dec") cart[i].qty = Math.max(1, cart[i].qty - 1);
+ if (act === "remove") cart.splice(i, 1);
+
+ updateCount();
+ renderCart();
});
-// The close button is wired, but since the drawer never opens it is useless.
-document.getElementById("cart-close").addEventListener("click", function () {
- document.getElementById("cart-drawer").style.display = "none";
+// Close drawer with Escape for accessibility.
+document.addEventListener("keydown", function (e) {
+ if (e.key === "Escape") closeDrawer();
});
+
+// Initial paint.
+renderCart();
+updateCount();
diff --git a/before/styles.css b/after/styles.css
index 2c7d0f0..b91fe87 100644
--- a/before/styles.css
+++ b/after/styles.css
@@ -1,6 +1,6 @@
/* ============================================================
- BEFORE (BROKEN) styles — Shopify-style product page
- Intentionally contains realistic theme/layout problems.
+ AFTER (FIXED) styles — Shopify-style product page
+ Cleaner layout, mobile-first CTA, fixed image sizing.
============================================================ */
:root {
@@ -8,6 +8,7 @@
--muted: #6b7280;
--line: #e5e7eb;
--accent: #111827;
+ --accent-hover: #000;
}
* { box-sizing: border-box; }
@@ -30,16 +31,23 @@ body {
.brand { font-weight: 700; letter-spacing: 1px; font-size: 15px; }
.nav a { margin-left: 20px; text-decoration: none; color: var(--ink); font-size: 14px; }
.cart-link { font-weight: 600; }
+/* FIX: a tiny pulse so the cart count change is noticeable */
+.cart-link.bump { animation: bump .3s ease; }
+@keyframes bump {
+ 0% { transform: scale(1); }
+ 40% { transform: scale(1.18); }
+ 100% { transform: scale(1); }
+}
.demo-flag {
display: inline-block;
margin: 14px 28px 0;
padding: 4px 10px;
- background: #fff4d6;
- border: 1px solid #f0d999;
+ background: #e7f6ec;
+ border: 1px solid #b7e2c5;
border-radius: 4px;
font-size: 12px;
- color: #8a6d1f;
+ color: #1f7a44;
}
/* ---------- Product layout ---------- */
@@ -52,11 +60,11 @@ body {
}
.product-media { flex: 1; }
-/* BUG: no aspect-ratio / fixed height. The gradient block has no intrinsic
- size, so on mobile it stretches very tall and shifts the layout. */
+/* FIX: fixed aspect ratio reserves space up front => no layout shift,
+ and the image never dominates the mobile viewport. */
.product-image {
width: 100%;
- min-height: 520px;
+ aspect-ratio: 1 / 1;
background: linear-gradient(135deg, #cbd5e1 0%, #94a3b8 60%, #64748b 100%);
border-radius: 10px;
}
@@ -71,32 +79,37 @@ body {
.product-price { font-size: 22px; font-weight: 600; margin-bottom: 16px; }
.product-desc { color: var(--muted); line-height: 1.6; margin-bottom: 24px; }
-/* BUG: cramped, inconsistent variant spacing. Buttons jammed together,
- no wrap control, uneven label gaps. */
-.variant-group { margin-bottom: 6px; }
-.variant-label { font-size: 13px; color: var(--muted); margin-bottom: 2px; }
-.variant-options { display: block; }
+/* FIX: clean, consistent variant spacing using flex + wrap + gap */
+.variant-group { margin-bottom: 18px; }
+.variant-label { font-size: 13px; color: var(--muted); margin-bottom: 8px; }
+.variant-options { display: flex; flex-wrap: wrap; gap: 10px; }
.variant {
- padding: 6px 9px;
+ padding: 9px 16px;
border: 1px solid var(--line);
background: #fff;
- font-size: 13px;
+ border-radius: 8px;
+ font-size: 14px;
cursor: pointer;
+ transition: border-color .15s ease, background .15s ease;
}
-.variant.is-selected { border-color: var(--ink); background: #f3f4f6; }
+.variant:hover { border-color: #9ca3af; }
+.variant.is-selected { border-color: var(--ink); background: #f3f4f6; font-weight: 600; }
-/* BUG: CTA sits far below a too-tall image on mobile; not sticky, no emphasis */
+/* FIX: prominent CTA with hover feedback */
.add-to-cart {
- margin-top: 22px;
+ margin-top: 6px;
width: 100%;
- padding: 15px;
+ padding: 16px;
background: var(--accent);
color: #fff;
border: none;
- border-radius: 8px;
+ border-radius: 10px;
font-size: 15px;
+ font-weight: 600;
cursor: pointer;
+ transition: background .15s ease;
}
+.add-to-cart:hover { background: var(--accent-hover); }
.product-meta {
list-style: none;
@@ -107,28 +120,126 @@ body {
}
.product-meta li { margin-bottom: 4px; }
-/* ---------- Cart drawer (markup present, never opened by JS) ---------- */
+/* ---------- Sticky mobile CTA (hidden on desktop) ---------- */
+.sticky-cta {
+ display: none;
+ position: fixed;
+ bottom: 0; left: 0; right: 0;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 12px 16px;
+ background: #fff;
+ border-top: 1px solid var(--line);
+ box-shadow: 0 -6px 20px rgba(0,0,0,.06);
+ z-index: 30;
+}
+.sticky-price { font-weight: 700; font-size: 16px; }
+.sticky-add {
+ flex: 1;
+ max-width: 220px;
+ padding: 13px;
+ background: var(--accent);
+ color: #fff;
+ border: none;
+ border-radius: 10px;
+ font-size: 15px;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+/* ---------- Toast feedback ---------- */
+.toast {
+ position: fixed;
+ top: 16px;
+ left: 50%;
+ transform: translateX(-50%) translateY(-20px);
+ background: #111827;
+ color: #fff;
+ padding: 10px 18px;
+ border-radius: 999px;
+ font-size: 14px;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity .2s ease, transform .2s ease;
+ z-index: 50;
+}
+.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
+
+/* ---------- Cart drawer + backdrop ---------- */
+.cart-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,.35);
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity .25s ease, visibility .25s ease;
+ z-index: 40;
+}
+.cart-backdrop.open { opacity: 1; visibility: visible; }
+
.cart-drawer {
position: fixed;
top: 0; right: 0;
- width: 360px; height: 100%;
+ width: 380px;
+ max-width: 90vw;
+ height: 100%;
background: #fff;
border-left: 1px solid var(--line);
- box-shadow: -10px 0 30px rgba(0,0,0,.08);
- /* BUG: drawer is hidden and there is no open state toggled by JS */
- display: none;
+ box-shadow: -10px 0 30px rgba(0,0,0,.12);
+ /* FIX: drawer slides in via a translate + .open class toggled by JS */
+ transform: translateX(100%);
+ transition: transform .28s ease;
padding: 20px;
+ z-index: 45;
+ display: flex;
+ flex-direction: column;
}
+.cart-drawer.open { transform: translateX(0); }
+
.cart-drawer-head { display: flex; justify-content: space-between; align-items: center; }
+.cart-drawer-head h2 { font-size: 18px; margin: 0; }
.cart-close { border: none; background: none; font-size: 26px; cursor: pointer; line-height: 1; }
-.cart-items { margin-top: 16px; }
-.cart-foot { position: absolute; bottom: 20px; left: 20px; right: 20px; }
-.cart-subtotal { display: flex; justify-content: space-between; font-weight: 600; margin-bottom: 12px; }
-.checkout-btn { width: 100%; padding: 14px; background: var(--accent); color: #fff; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; }
-
-/* ---------- "Responsive" rules (intentionally inadequate) ---------- */
-/* BUG: breakpoint is 480px and only stacks the columns. It does NOT fix the
- oversized image, the CTA stays buried, and nothing handles <430px well. */
-@media (max-width: 480px) {
- .product { flex-direction: column; gap: 20px; }
+
+.cart-items { margin-top: 16px; flex: 1; overflow-y: auto; }
+.cart-empty { color: var(--muted); font-size: 14px; margin-top: 20px; }
+
+.cart-line { display: flex; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--line); }
+.cart-line-img {
+ width: 56px; height: 56px; border-radius: 8px; flex-shrink: 0;
+ background: linear-gradient(135deg, #cbd5e1, #64748b);
+}
+.cart-line-body { flex: 1; }
+.cart-line-title { font-size: 14px; font-weight: 600; }
+.cart-line-meta { font-size: 12px; color: var(--muted); margin-top: 2px; }
+.cart-line-row { display: flex; align-items: center; justify-content: space-between; margin-top: 6px; }
+.qty { display: inline-flex; align-items: center; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
+.qty button { width: 28px; height: 28px; border: none; background: #f9fafb; cursor: pointer; font-size: 15px; }
+.qty span { width: 30px; text-align: center; font-size: 14px; }
+.cart-line-price { font-size: 14px; font-weight: 600; }
+.cart-remove { border: none; background: none; color: var(--muted); font-size: 12px; cursor: pointer; text-decoration: underline; padding: 0; }
+
+.cart-foot { border-top: 1px solid var(--line); padding-top: 14px; }
+.cart-subtotal { display: flex; justify-content: space-between; font-weight: 600; margin-bottom: 12px; font-size: 16px; }
+.checkout-btn { width: 100%; padding: 14px; background: var(--accent); color: #fff; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer; }
+.checkout-btn:hover { background: var(--accent-hover); }
+.cart-note { text-align: center; color: var(--muted); font-size: 12px; margin: 10px 0 0; }
+
+/* ---------- Responsive: real mobile handling, incl. <430px ---------- */
+@media (max-width: 860px) {
+ .product { flex-direction: column; gap: 22px; margin-top: 18px; }
+ /* FIX: cap the image height on mobile so the CTA appears sooner */
+ .product-image { aspect-ratio: 4 / 3; max-height: 46vh; }
+ .product-title { font-size: 24px; }
+ /* leave room for the sticky bar */
+ body { padding-bottom: 76px; }
+ .sticky-cta { display: flex; }
+}
+
+@media (max-width: 430px) {
+ .site-header { padding: 14px 16px; }
+ .demo-flag { margin: 12px 16px 0; }
+ .product { padding: 0 16px; }
+ .product-image { aspect-ratio: 3 / 2; max-height: 40vh; }
+ .variant { padding: 8px 14px; }
}