- Add product loaders (product-loader.js, products-list-loader.js) to fetch data from Shopify - Extend Shopify API client with getProductByHandle() and getAllProducts() methods - Integrate Shopify metafields for multilingual support (custom.title_en, custom.description_en) - Refactor product.php and home.php templates to load content dynamically - Simplify product blueprint to minimal routing configuration - Create generic buy-button.php snippet with variant selection - Update footer.php with conditional script loading - Refactor _section--product.scss for better Sass structure - Add translations for loading states and product errors - Clean up old Kirby product content files Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
343 lines
7.5 KiB
JavaScript
343 lines
7.5 KiB
JavaScript
/**
|
|
* Shopify Storefront API Client
|
|
* Custom implementation using Cart API (2026-01)
|
|
*/
|
|
class ShopifyCart {
|
|
constructor(config) {
|
|
this.domain = config.domain;
|
|
this.storefrontAccessToken = config.storefrontAccessToken;
|
|
this.apiVersion = '2026-01';
|
|
this.endpoint = `https://${this.domain}/api/${this.apiVersion}/graphql.json`;
|
|
this.cartId = null;
|
|
this.cartItems = [];
|
|
|
|
// Load existing cart from localStorage
|
|
this.loadCart();
|
|
}
|
|
|
|
/**
|
|
* Make GraphQL request to Shopify Storefront API
|
|
*/
|
|
async query(query, variables = {}) {
|
|
const response = await fetch(this.endpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Shopify-Storefront-Access-Token': this.storefrontAccessToken,
|
|
},
|
|
body: JSON.stringify({ query, variables }),
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.errors) {
|
|
console.error('Shopify API Error:', result.errors);
|
|
throw new Error(result.errors[0].message);
|
|
}
|
|
|
|
return result.data;
|
|
}
|
|
|
|
/**
|
|
* Get product information by ID
|
|
*/
|
|
async getProduct(productId) {
|
|
const query = `
|
|
query getProduct($id: ID!) {
|
|
product(id: $id) {
|
|
id
|
|
title
|
|
description
|
|
availableForSale
|
|
variants(first: 10) {
|
|
edges {
|
|
node {
|
|
id
|
|
title
|
|
price {
|
|
amount
|
|
currencyCode
|
|
}
|
|
availableForSale
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const data = await this.query(query, {
|
|
id: `gid://shopify/Product/${productId}`
|
|
});
|
|
|
|
return data.product;
|
|
}
|
|
|
|
/**
|
|
* Get product by handle
|
|
* @param {string} handle - Product handle (slug)
|
|
*/
|
|
async getProductByHandle(handle) {
|
|
const query = `
|
|
query getProductByHandle($handle: String!) {
|
|
product(handle: $handle) {
|
|
id
|
|
handle
|
|
title
|
|
description
|
|
descriptionHtml
|
|
availableForSale
|
|
tags
|
|
titleEn: metafield(namespace: "custom", key: "title_en") {
|
|
value
|
|
}
|
|
descriptionEn: metafield(namespace: "custom", key: "description_en") {
|
|
value
|
|
}
|
|
priceRange {
|
|
minVariantPrice {
|
|
amount
|
|
currencyCode
|
|
}
|
|
}
|
|
images(first: 10) {
|
|
edges {
|
|
node {
|
|
id
|
|
url
|
|
altText
|
|
width
|
|
height
|
|
}
|
|
}
|
|
}
|
|
variants(first: 20) {
|
|
edges {
|
|
node {
|
|
id
|
|
title
|
|
sku
|
|
availableForSale
|
|
price {
|
|
amount
|
|
currencyCode
|
|
}
|
|
selectedOptions {
|
|
name
|
|
value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const data = await this.query(query, { handle });
|
|
return data.product || null;
|
|
}
|
|
|
|
/**
|
|
* Get all products for listing page
|
|
* @param {number} first - Number of products to fetch
|
|
*/
|
|
async getAllProducts(first = 20) {
|
|
const query = `
|
|
query getAllProducts($first: Int!) {
|
|
products(first: $first, sortKey: TITLE) {
|
|
edges {
|
|
node {
|
|
id
|
|
handle
|
|
title
|
|
description
|
|
availableForSale
|
|
titleEn: metafield(namespace: "custom", key: "title_en") {
|
|
value
|
|
}
|
|
priceRange {
|
|
minVariantPrice {
|
|
amount
|
|
currencyCode
|
|
}
|
|
}
|
|
images(first: 1) {
|
|
edges {
|
|
node {
|
|
id
|
|
url
|
|
altText
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const data = await this.query(query, { first });
|
|
return data.products;
|
|
}
|
|
|
|
/**
|
|
* Create a new cart
|
|
*/
|
|
async createCart(lines = []) {
|
|
const query = `
|
|
mutation cartCreate($input: CartInput!) {
|
|
cartCreate(input: $input) {
|
|
cart {
|
|
id
|
|
checkoutUrl
|
|
lines(first: 10) {
|
|
edges {
|
|
node {
|
|
id
|
|
quantity
|
|
merchandise {
|
|
... on ProductVariant {
|
|
id
|
|
title
|
|
price {
|
|
amount
|
|
currencyCode
|
|
}
|
|
product {
|
|
title
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
userErrors {
|
|
field
|
|
message
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const data = await this.query(query, {
|
|
input: { lines }
|
|
});
|
|
|
|
if (data.cartCreate.userErrors.length > 0) {
|
|
throw new Error(data.cartCreate.userErrors[0].message);
|
|
}
|
|
|
|
this.cartId = data.cartCreate.cart.id;
|
|
this.saveCart();
|
|
|
|
return data.cartCreate.cart;
|
|
}
|
|
|
|
/**
|
|
* Add item to cart
|
|
*/
|
|
async addToCart(variantId, quantity = 1) {
|
|
const lines = [{
|
|
merchandiseId: `gid://shopify/ProductVariant/${variantId}`,
|
|
quantity: quantity
|
|
}];
|
|
|
|
let cart;
|
|
|
|
if (this.cartId) {
|
|
// Add to existing cart
|
|
const query = `
|
|
mutation cartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {
|
|
cartLinesAdd(cartId: $cartId, lines: $lines) {
|
|
cart {
|
|
id
|
|
checkoutUrl
|
|
lines(first: 10) {
|
|
edges {
|
|
node {
|
|
id
|
|
quantity
|
|
merchandise {
|
|
... on ProductVariant {
|
|
id
|
|
title
|
|
price {
|
|
amount
|
|
currencyCode
|
|
}
|
|
product {
|
|
title
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
userErrors {
|
|
field
|
|
message
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const data = await this.query(query, {
|
|
cartId: this.cartId,
|
|
lines
|
|
});
|
|
|
|
if (data.cartLinesAdd.userErrors.length > 0) {
|
|
throw new Error(data.cartLinesAdd.userErrors[0].message);
|
|
}
|
|
|
|
cart = data.cartLinesAdd.cart;
|
|
} else {
|
|
// Create new cart
|
|
cart = await this.createCart(lines);
|
|
}
|
|
|
|
this.cartItems = cart.lines.edges;
|
|
return cart;
|
|
}
|
|
|
|
/**
|
|
* Get checkout URL to redirect user
|
|
*/
|
|
getCheckoutUrl(cart) {
|
|
return cart?.checkoutUrl || null;
|
|
}
|
|
|
|
/**
|
|
* Save cart ID to localStorage
|
|
*/
|
|
saveCart() {
|
|
if (this.cartId) {
|
|
localStorage.setItem('shopify_cart_id', this.cartId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load cart ID from localStorage
|
|
*/
|
|
loadCart() {
|
|
this.cartId = localStorage.getItem('shopify_cart_id');
|
|
}
|
|
|
|
/**
|
|
* Clear cart
|
|
*/
|
|
clearCart() {
|
|
this.cartId = null;
|
|
this.cartItems = [];
|
|
localStorage.removeItem('shopify_cart_id');
|
|
}
|
|
}
|
|
|
|
// Export for use in other scripts
|
|
window.ShopifyCart = ShopifyCart;
|