Categories For You
Shows collection categories personalized from the logged-in customer's past purchases. Ranks collections by purchase frequency, falls back to configured collection handles when logged out or no history, and opens the collection on tap. Hides when empty.
<div class="collection-products-wrapper">
<h2 id="collectionTitle" class="collection-title">
Recommended Products
</h2>
<div id="productsCarousel" class="products-carousel"></div>
</div>
.collection-products-wrapper {
width: 100%;
/* Max-width added to match the main card perfectly if needed */
max-width: 354px;
padding: 16px 0;
}
.collection-title {
font-size: 18px;
font-weight: 700;
color: #111827;
/* Removed the 16px side margins to push it flush left */
margin: 0 0 12px 0;
}
.products-carousel {
display: flex;
gap: 12px;
overflow-x: auto;
scroll-snap-type: x mandatory;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
/* Removed the left padding so the first card perfectly aligns with the title */
padding: 0 0 8px 0;
}
.products-carousel::-webkit-scrollbar {
display: none;
}
.products-carousel::after {
content: "";
padding-right: 4px;
}
.product-card {
flex: 0 0 180px;
scroll-snap-align: start;
background: #fff;
border: 1px solid #E5E7EB;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
}
.product-image {
width: 100%;
height: 180px;
object-fit: cover;
display: block;
}
.product-content {
padding: 10px;
}
.product-name {
font-size: 14px;
font-weight: 600;
color: #111827;
line-height: 1.4;
min-height: 40px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.product-price {
margin-top: 8px;
font-size: 14px;
font-weight: 700;
color: #111827;
}
.empty-state {
padding: 16px;
text-align: left;
color: #6B7280;
}const RECENT_ORDER_COLLECTION_QUERY = `
query getRecentOrderStatusAndProductWithCollection(
$customerAccessToken: String!
) {
customer(
customerAccessToken: $customerAccessToken
) {
orders(first: 1, reverse: true) {
edges {
node {
id
lineItems(first: 1) {
edges {
node {
variant {
product {
collections(first: 1) {
edges {
node {
id
handle
title
}
}
}
}
}
}
}
}
}
}
}
}
}
`;
const COLLECTION_PRODUCTS_QUERY = `
query getCollectionProducts(
$handle: String!
) {
collection(handle: $handle) {
id
title
products(first: 20) {
edges {
node {
id
title
handle
onlineStoreUrl
featuredImage {
url
}
priceRange {
minVariantPrice {
amount
currencyCode
}
}
}
}
}
}
}
`;
function resizeBlock() {
setTimeout(() => {
Superfans.actions.resizeBlock();
}, 100);
}
function getCustomerAccessToken() {
const customer = Superfans.variables.customer || {};
return (customer.accessToken || customer.customerAccessToken || customer.shopifyCustomerAccessToken || customer.token || null);
}
function extractCollection(response) {
return response?.data?.customer?.orders?.edges?.[0]?.node?.lineItems?.edges?.[0]?.node?.variant?.product?.collections?.edges?.[0]?.node;
}
async function fetchLatestOrderCollection() {
const customerAccessToken = getCustomerAccessToken();
if (!customerAccessToken) {
renderEmptyState();
return;
}
try {
const response = await Superfans.helpers.getStorefrontData({
query: RECENT_ORDER_COLLECTION_QUERY,
variables: {
customerAccessToken
}
});
const collection = extractCollection(response);
if (!collection?.handle) {
renderEmptyState();
return;
}
await fetchCollectionProducts(collection.handle);
} catch (error) {
console.error(error);
renderEmptyState();
}
}
async function fetchCollectionProducts(handle) {
try {
const response = await Superfans.helpers.getStorefrontData({
query: COLLECTION_PRODUCTS_QUERY,
variables: {
handle
}
});
const collection = response?.data?.collection;
if (!collection) {
renderEmptyState();
return;
}
renderProducts(collection);
} catch (error) {
renderEmptyState();
}
}
function renderProducts(collection) {
const title = document.getElementById("collectionTitle");
const carousel = document.getElementById("productsCarousel");
// title.textContent = collection.title;
title.textContent = "Recommended Products";
carousel.innerHTML = "";
collection.products.edges.forEach(
({
node
}) => {
const card = document.createElement("div");
card.className = "product-card";
card.innerHTML = `
<img
class="product-image"
src="${
node.featuredImage?.url ||
""
}"
alt="${node.title}"
/>
<div class="product-content">
<div class="product-name">
${node.title}
</div>
<div class="product-price">
${Number(
node.priceRange
.minVariantPrice.amount
).toFixed(2)}
${
node.priceRange
.minVariantPrice.currencyCode
}
</div>
</div>
`;
card.addEventListener("click", async () => {
if (!node.handle) {
return;
}
try {
await Superfans.actions.openUrl({
// path: node.onlineStoreUrl
path: `/products/${node.handle}`
});
} catch (error) {
console.error(error);
}
});
carousel.appendChild(card);
});
resizeBlock();
}
function renderEmptyState() {
const carousel = document.getElementById("productsCarousel");
carousel.innerHTML = `
<div class="empty-state">
No recommendations available.
</div>
`;
resizeBlock();
}
document.addEventListener("DOMContentLoaded",
() => {
fetchLatestOrderCollection();
});
Superfans.listeners.onLoginStatusChanged(
() => {
setTimeout(fetchLatestOrderCollection, 300);
});