Migrate product data from Kirby to Shopify Storefront API

- 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>
This commit is contained in:
isUnknown 2026-01-16 12:03:20 +01:00
parent 957cf79e45
commit ad699f0365
22 changed files with 649 additions and 579 deletions

View file

@ -1,12 +1,3 @@
.section__product,
.store__nav{
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
.section__product,
.store__nav {
max-width: 1200px;
@ -14,10 +5,6 @@
margin-right: auto;
}
.store__nav {
padding-top: calc(var(--spacing) * 1);
padding-bottom: calc(var(--spacing) * 0.5);
@ -27,174 +14,35 @@
a {
text-decoration: none;
&::before {
content: "";
}
&:hover {
text-decoration: underline;
}
}
a::before {
content: "";
}
}
.section__product .details {
// margin-bottom: calc(var(--spacing) * 2);
ul{
margin-left: 2ch;
li{
padding-bottom: 0.2em;
@media #{$small} {
a {
padding-top: 0;
font-size: var(--fs-small);
}
}
}
.section__product {
.details {
ul {
margin-left: 2ch;
.product-options__list {
list-style: none;
display: flex;
gap: 2ch;
li {
position: relative;
input[type="radio"] {
position: fixed;
opacity: 0;
pointer-events: none;
}
label {
font-family: var(--title);
font-size: var(--fs-normal);
height: 4ch;
width: 4ch;
border-radius: 50%;
border: var(--border);
border-color: transparent;
display: flex;
align-items: center;
justify-content: center;
padding-top: 0px;
cursor: pointer;
}
input[type="radio"]:checked + label {
border-color: var(--color-txt);
}
input[type="radio"]:not(:checked) + label:hover {
border-color: var(--grey-600);
background-color: var(--grey-800);
}
}
}
.product-gallery {
position: relative;
aspect-ratio: 4 / 3;
.swiper-slide {
width: 100%;
figure {
aspect-ratio: 4 / 3;
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: contain;
padding-bottom: 0.2em;
}
}
}
// Swiper navigation arrows
.swiper-button-prev,
.swiper-button-next {
color: var(--color-txt);
width: 20px;
height: 20px;
&:after {
font-size: 20px;
font-weight: bold;
}
&:hover {
opacity: 0.7;
}
}
// Swiper pagination dots
.swiper-pagination {
position: relative;
margin-top: calc(var(--spacing) * 0.5);
bottom: 0;
.swiper-pagination-bullet {
width: 8px;
height: 8px;
background: var(--grey-600);
opacity: 0.5;
transition: opacity 0.3s;
&:hover {
opacity: 0.7;
}
}
.swiper-pagination-bullet-active {
background: var(--color-txt);
opacity: 1;
}
}
}
.hero {
margin-bottom: calc(var(--spacing) * 1);
padding: calc(var(--spacing) * 0.5) 0;
border-top: var(--border-light);
border-bottom: var(--border-light);
.p__baseline-big {
margin: 0;
text-align: left;
}
}
.add-to-cart {
margin: 0;
border-bottom: var(--border-light);
padding: calc(var(--spacing) * 0.5) 0;
}
.product-options {
border-bottom: var(--border-light);
padding: calc(var(--spacing) * 0.25) 0;
}
@media #{$small} {
.store__nav a {
padding-top: 0;
font-size: var(--fs-small);
}
.section__product {
@media #{$small} {
display: flex;
flex-direction: column;
margin-bottom: 10vh;
@ -207,6 +55,7 @@
margin-top: calc(var(--spacing) * 0.5);
order: 1;
}
figure {
order: 2;
margin-bottom: calc(var(--spacing) * 1);
@ -226,25 +75,25 @@
order: 5;
}
.product-gallery{
.product-gallery {
width: 100vw;
position: relative;
left: calc(var(--padding-body)*-1);
left: calc(var(--padding-body) * -1);
.swiper-button-prev,
.swiper-button-next{ display: none; }
.swiper-button-next {
display: none;
}
}
}
}
@media #{$small-up} {
.section__product{
display: grid;
grid-template-columns: 1fr 1fr;
gap: calc(var(--padding-body)*2);
margin-bottom: calc(var(--spacing)*3);
@media #{$small-up} {
.product-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: calc(var(--padding-body) * 2);
margin-bottom: calc(var(--spacing) * 3);
}
.details {
margin-bottom: calc(var(--spacing) * 2);
@ -255,17 +104,140 @@
border-top: var(--border-light);
}
.col-left{
.col-left {
min-height: 100%;
padding-bottom: 40px; //dots
padding-bottom: 40px;
display: flex;
flex-direction: column;
}
}
.product-gallery .swiper-slide figure{
width: calc(100% - 60px);
}
}
.product-options {
border-bottom: var(--border-light);
padding: calc(var(--spacing) * 0.25) 0;
}
.product-options__list {
list-style: none;
display: flex;
gap: 2ch;
li {
position: relative;
input[type="radio"] {
position: fixed;
opacity: 0;
pointer-events: none;
&:checked + label {
border-color: var(--color-txt);
}
&:not(:checked) + label:hover {
border-color: var(--grey-600);
background-color: var(--grey-800);
}
}
label {
font-family: var(--title);
font-size: var(--fs-normal);
height: 4ch;
width: 4ch;
border-radius: 50%;
border: var(--border);
border-color: transparent;
display: flex;
align-items: center;
justify-content: center;
padding-top: 0px;
cursor: pointer;
}
}
}
.product-gallery {
position: relative;
aspect-ratio: 4 / 3;
.swiper-slide {
width: 100%;
figure {
aspect-ratio: 4 / 3;
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
@media #{$small-up} {
figure {
width: calc(100% - 60px);
}
}
}
.swiper-button-prev,
.swiper-button-next {
color: var(--color-txt);
width: 20px;
height: 20px;
&:after {
font-size: 20px;
font-weight: bold;
}
&:hover {
opacity: 0.7;
}
}
.swiper-pagination {
position: relative;
margin-top: calc(var(--spacing) * 0.5);
bottom: 0;
.swiper-pagination-bullet {
width: 8px;
height: 8px;
background: var(--grey-600);
opacity: 0.5;
transition: opacity 0.3s;
&:hover {
opacity: 0.7;
}
&.swiper-pagination-bullet-active {
background: var(--color-txt);
opacity: 1;
}
}
}
}
.hero {
margin-bottom: calc(var(--spacing) * 1);
padding: calc(var(--spacing) * 0.5) 0;
border-top: var(--border-light);
border-bottom: var(--border-light);
.p__baseline-big {
margin: 0;
text-align: left;
}
}
.add-to-cart {
margin: 0;
border-bottom: var(--border-light);
padding: calc(var(--spacing) * 0.5) 0;
}

View file

@ -1,109 +1,46 @@
/**
* Product Add to Cart functionality
* Handles the add to cart button interaction with Shopify
*/
(function() {
// Initialize Shopify Cart
const cart = new ShopifyCart({
domain: 'nv7cqv-bu.myshopify.com',
storefrontAccessToken: 'dec3d35a2554384d149c72927d1cfd1b'
});
// Get product ID from data attribute
const addToCartBtn = document.querySelector('[data-shopify-add-to-cart]');
if (!addToCartBtn) {
console.warn('Add to cart button not found');
return;
}
const productId = addToCartBtn.dataset.productId;
const variantId = addToCartBtn.dataset.variantId;
const stockDisplay = document.querySelector('[data-product-stock]');
// Get translated texts
const texts = {
add: addToCartBtn.dataset.textAdd || 'Add to cart',
adding: addToCartBtn.dataset.textAdding || 'Adding...',
added: addToCartBtn.dataset.textAdded || 'Added! ✓',
error: addToCartBtn.dataset.textError || 'Error - Try again',
outOfStock: addToCartBtn.dataset.textOutOfStock || 'Out of stock',
inStock: addToCartBtn.dataset.textInStock || 'In stock'
error: addToCartBtn.dataset.textError || 'Error - Try again'
};
// Load product data to check availability
async function loadProductData() {
try {
const product = await cart.getProduct(productId);
if (!product) {
console.error('Product not found');
return;
}
// Find the specific variant or use the first one
let variant;
if (variantId) {
variant = product.variants.edges.find(
edge => edge.node.id === `gid://shopify/ProductVariant/${variantId}`
)?.node;
} else {
variant = product.variants.edges[0]?.node;
}
if (!variant) {
console.error('Variant not found');
return;
}
// Update button based on availability
if (!variant.availableForSale) {
addToCartBtn.disabled = true;
addToCartBtn.textContent = texts.outOfStock;
addToCartBtn.classList.add('out-of-stock');
if (stockDisplay) {
stockDisplay.textContent = texts.outOfStock;
stockDisplay.classList.add('out-of-stock');
}
} else {
// Show in stock
if (stockDisplay) {
stockDisplay.textContent = texts.inStock;
stockDisplay.classList.add('in-stock');
}
}
// Store variant ID for later use
addToCartBtn.dataset.variantId = variant.id.replace('gid://shopify/ProductVariant/', '');
} catch (error) {
console.error('Error loading product:', error);
}
}
// Handle add to cart click
addToCartBtn.addEventListener('click', async function(e) {
e.preventDefault();
// Disable button during request
const variantId = this.dataset.variantId;
if (!variantId) {
console.error('No variant ID found');
return;
}
addToCartBtn.disabled = true;
const originalText = addToCartBtn.textContent;
addToCartBtn.textContent = texts.adding;
try {
const variantId = this.dataset.variantId;
const cartResult = await cart.addToCart(variantId, 1);
// Show success feedback
addToCartBtn.textContent = texts.added;
addToCartBtn.classList.add('success');
// Dispatch event to open cart drawer
document.dispatchEvent(new CustomEvent('cart:updated', {
detail: { cart: cartResult }
}));
// Reset button after short delay
setTimeout(() => {
addToCartBtn.disabled = false;
addToCartBtn.textContent = originalText;
@ -113,11 +50,9 @@
} catch (error) {
console.error('Error adding to cart:', error);
// Show error feedback
addToCartBtn.textContent = texts.error;
addToCartBtn.classList.add('error');
// Re-enable button after delay
setTimeout(() => {
addToCartBtn.disabled = false;
addToCartBtn.textContent = originalText;
@ -125,7 +60,4 @@
}, 2000);
}
});
// Load product data on page load
loadProductData();
})();

188
assets/js/product-loader.js Normal file
View file

@ -0,0 +1,188 @@
(async function () {
const container = document.querySelector("[data-product-loader]");
if (!container) return;
const handle = container.dataset.shopifyHandle;
const language = container.dataset.language || 'fr';
const isEnglish = language === 'en';
const loadingState = container.querySelector(".product-loading");
const contentState = container.querySelector(".product-content");
const errorState = container.querySelector(".product-error");
try {
const cart = new ShopifyCart({
domain: "nv7cqv-bu.myshopify.com",
storefrontAccessToken: "dec3d35a2554384d149c72927d1cfd1b",
});
const product = await cart.getProductByHandle(handle);
if (!product) {
throw new Error("Product not found");
}
renderProduct(product, isEnglish);
loadingState.style.display = "none";
contentState.removeAttribute("style");
setTimeout(() => {
if (typeof Swiper !== "undefined" && product.images.edges.length > 0) {
new Swiper(".product-gallery", {
loop: product.images.edges.length > 1,
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
pagination: {
el: ".swiper-pagination",
clickable: true,
},
keyboard: {
enabled: true,
},
});
}
}, 100);
} catch (error) {
console.error("Error loading product:", error);
loadingState.style.display = "none";
errorState.style.display = "block";
}
function renderProduct(product, isEnglish) {
renderTitle(product, isEnglish);
renderPrice(product);
renderDetails(product, isEnglish);
renderImages(product, isEnglish);
renderVariants(product);
setupAddToCart(product);
renderStock(product);
}
function renderTitle(product, isEnglish) {
const titleEl = document.querySelector("[data-product-title]");
if (titleEl) {
const title = isEnglish && product.titleEn?.value
? product.titleEn.value
: product.title;
titleEl.textContent = title;
}
}
function renderPrice(product) {
const priceEl = document.querySelector("[data-product-price]");
if (priceEl) {
const price = parseFloat(product.priceRange.minVariantPrice.amount);
priceEl.textContent = price.toFixed(2) + "€";
}
}
function renderDetails(product, isEnglish) {
const detailsEl = document.querySelector("[data-product-details]");
if (detailsEl) {
const description = isEnglish && product.descriptionEn?.value
? product.descriptionEn.value
: product.descriptionHtml || "";
detailsEl.innerHTML = description;
}
}
function renderImages(product, isEnglish) {
const imagesContainer = document.querySelector("[data-product-images]");
if (imagesContainer && product.images.edges.length > 0) {
const productTitle = isEnglish && product.titleEn?.value
? product.titleEn.value
: product.title;
imagesContainer.innerHTML = product.images.edges
.map((edge) => {
const img = edge.node;
return `
<div class="swiper-slide">
<figure>
<img src="${img.url}"
alt="${img.altText || productTitle}"
loading="lazy" />
</figure>
</div>
`;
})
.join("");
}
}
function renderVariants(product) {
if (product.variants.edges.length <= 1) return;
const variantsContainer = document.querySelector("[data-product-variants]");
const variantSelector = document.querySelector("[data-variant-selector]");
if (!variantsContainer || !variantSelector) return;
variantsContainer.style.display = "block";
variantSelector.innerHTML = product.variants.edges
.map((edge) => {
const variant = edge.node;
const variantId = variant.id.replace(
"gid://shopify/ProductVariant/",
""
);
const price = parseFloat(variant.price.amount).toFixed(2) + "€";
const availability = variant.availableForSale
? ""
: " (Rupture de stock)";
return `<option value="${variantId}" ${
!variant.availableForSale ? "disabled" : ""
}>
${variant.title} - ${price}${availability}
</option>`;
})
.join("");
variantSelector.addEventListener("change", (e) => {
const addToCartBtn = document.querySelector("[data-shopify-add-to-cart]");
if (addToCartBtn) {
addToCartBtn.dataset.variantId = e.target.value;
}
});
}
function setupAddToCart(product) {
const addToCartBtn = document.querySelector("[data-shopify-add-to-cart]");
if (!addToCartBtn) return;
const productId = product.id.replace("gid://shopify/Product/", "");
addToCartBtn.dataset.productId = productId;
const firstAvailableVariant = product.variants.edges.find(
(e) => e.node.availableForSale
);
if (firstAvailableVariant) {
const variantId = firstAvailableVariant.node.id.replace(
"gid://shopify/ProductVariant/",
""
);
addToCartBtn.dataset.variantId = variantId;
}
}
function renderStock(product) {
const stockEl = document.querySelector("[data-product-stock]");
if (!stockEl) return;
const addToCartBtn = document.querySelector("[data-shopify-add-to-cart]");
if (product.availableForSale) {
stockEl.textContent = addToCartBtn?.dataset.textInStock || "En stock";
stockEl.classList.add("in-stock");
} else {
stockEl.textContent =
addToCartBtn?.dataset.textOutOfStock || "Rupture de stock";
stockEl.classList.add("out-of-stock");
}
}
})();

View file

@ -0,0 +1,47 @@
(async function() {
const container = document.querySelector('[data-products-loader]');
if (!container) return;
const language = (container.dataset.language || 'FR').toLowerCase();
const isEnglish = language === 'en';
try {
const cart = new ShopifyCart({
domain: 'nv7cqv-bu.myshopify.com',
storefrontAccessToken: 'dec3d35a2554384d149c72927d1cfd1b'
});
const productsData = await cart.getAllProducts();
const products = productsData.edges;
const productsHtml = products.map(edge => {
const product = edge.node;
const image = product.images.edges[0]?.node;
const price = parseFloat(product.priceRange.minVariantPrice.amount).toFixed(2);
const slug = product.handle;
const productUrl = isEnglish ? `/en/${slug}` : `/${slug}`;
const productTitle = isEnglish && product.titleEn?.value
? product.titleEn.value
: product.title;
return `
<article class="store__product">
<figure>
${image ? `<img src="${image.url}" alt="${image.altText || productTitle}" loading="lazy" />` : ''}
</figure>
<p class="line-1">
<a href="${productUrl}">${productTitle}</a>
</p>
<p class="price">${price}</p>
<a href="${productUrl}" class="link-block" aria-hidden="true"></a>
</article>
`;
}).join('');
container.innerHTML = productsHtml;
} catch (error) {
console.error('Error loading products:', error);
container.innerHTML = '<p>Erreur lors du chargement des produits</p>';
}
})();

View file

@ -73,6 +73,117 @@ class ShopifyCart {
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
*/

View file

@ -0,0 +1,5 @@
Title: Éclairages : 12 entretiens et analyses sur les violences d'État
----
Uuid: gzshayl6xoefrnsz

View file

@ -0,0 +1,9 @@
Title: Éclairages : 12 entretiens et analyses sur les violences dÉtat
----
Shopifyhandle: eclairages-12-entretiens-et-analyses-sur-les-violences-d-etat
----
Uuid: gzshayl6xoefrnsz

View file

@ -1,33 +0,0 @@
Title: T-shirt Index 01
----
Price: 35
----
Description: <p>T-shirt de soutien à Index, 100% coton</p>
----
Details: <p>Organic cotton t-shirt with screen printing.<br>Print on the front: "INDEX" logo, 10 cm wide.</p><ul><li><p>100% organic cotton</p></li><li><p>Weight: 180 g/m²</p></li><li><p>Single jersey with very soft feel</p></li><li><p>Excellent durability over time</p></li><li><p>Internal neckband</p></li><li><p>Double stitching on sleeves and bottom hem</p></li></ul><p>Shipping only via Mondial Relay to France, Belgium and Switzerland.</p>
----
Options:
-
label: Size
values: XS, S, M, L, XL
----
Snipcartid: tshirt-01
----
Backgroundcolor: #ffffff
----
Template: product

View file

@ -1,56 +0,0 @@
Title: T-shirt Index 01
----
Price: 35
----
Stock: 10
----
Description: T-shirt de soutien à Index, 100% coton
----
Details: <p>T-shirt en coton organique avec impression sérigraphique.<br>Marquage sur la face avant : logo « INDEX » de 10 cm de large.</p><ul><li><p>100 % coton biologique</p></li><li><p>Grammage : 180 g/m²</p></li><li><p>Jersey simple au toucher très doux</p></li><li><p>Excellente tenue dans le temps</p></li><li><p>Bande de propreté intérieure au col</p></li><li><p>Surpiqûres doubles en bas de manches et en bas de corps</p></li></ul><p>Envoi uniquement via Mondial Relay vers la France, la Belgique et la Suisse.</p>
----
Hasoptions: true
----
Optionlabel: Taille
----
Optionvalues: XS, S, M, L, XL
----
Options:
-
label: Taille
values: XS, S, M, L, XL
-
label: Couleur
values: Rouge, Vert
----
Snipcartid: tshirt-01
----
Backgroundcolor: #ffffff
----
Template: product
----
Uuid: udrrfizhayqixfoo

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

View file

@ -1,9 +0,0 @@
Sort: 1
----
Uuid: elxkhcta8dkjhr60
----
Template: image

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

View file

@ -1,9 +0,0 @@
Sort: 2
----
Uuid: deupkqq83jvloz0r
----
Template: image

View file

@ -0,0 +1,5 @@
Title: T-shirt Index
----
Uuid: qq27mjjpethsvnwp

View file

@ -0,0 +1,9 @@
Title: T-shirt Index
----
Shopifyhandle: t-shirt-index-01
----
Uuid: qq27mjjpethsvnwp

View file

@ -1,134 +1,21 @@
title:
en: Product
fr: Produit
title: Product
icon: cart
tabs:
content:
label:
en: Content
fr: Contenu
columns:
- width: 2/3
sections:
main:
type: fields
fields:
price:
label:
en: Price (€)
fr: Prix (€)
type: number
min: 0
step: 0.01
required: true
translate: false
width: 1/4
stock:
label: Stock
type: number
min: 0
default: 0
help:
en: Edit through french version
fr: Partagé entre les versions FR et EN
translate: false
width: 1/4
space:
type: gap
width: 2/4
weight:
label:
en: Weight (g)
fr: Poids (g)
type: number
min: 0
default: 0
help:
en: Weight in grams for shipping calculation
fr: Poids en grammes pour le calcul de la livraison
translate: false
width: 1/4
length:
label:
en: Length (cm)
fr: Longueur (cm)
type: number
min: 0
default: 0
help:
en: Package length in centimeters
fr: Longueur du colis en centimètres
translate: false
width: 1/4
width:
label:
en: Width (cm)
fr: Largeur (cm)
type: number
min: 0
default: 0
help:
en: Package width in centimeters
fr: Largeur du colis en centimètres
translate: false
width: 1/4
height:
label:
en: Height (cm)
fr: Hauteur (cm)
type: number
min: 0
default: 0
help:
en: Package height in centimeters
fr: Hauteur du colis en centimètres
translate: false
width: 1/4
description:
label: Description panier
type: writer
help: Visible dans le panier seulement.
details:
label:
en: Details
fr: Détails
type: writer
hasOptions:
label:
en: Options
fr: Options
type: toggle
default: false
translate: false
width: 1/7
optionLabel:
label:
en: Option label
fr: Libellé de l'option
type: text
width: 3/7
when:
hasOptions: true
optionValues:
label:
en: Option values
fr: Valeurs de l'option
type: tags
help:
en: "Comma-separated values (e.g.: XS, S, M, L, XL)"
fr: "Valeurs séparées par des virgules (ex: XS, S, M, L, XL)"
translate: false
when:
hasOptions: true
width: 3/7
columns:
- width: 1/1
fields:
info:
type: info
text:
en: "Product data (title, description, images, price) is managed in Shopify Admin. This Kirby page only serves for routing."
fr: "Les données produit (titre, description, images, prix) sont gérées dans Shopify Admin. Cette page Kirby sert uniquement au routing."
- width: 1/3
sections:
images:
type: files
headline:
en: Product Images
fr: Images du produit
template: image
layout: cards
shopifyHandle:
label:
en: Shopify Handle
fr: Shopify Handle
type: text
help:
en: "Product handle from Shopify (e.g. tshirt-index-01). If empty, uses the page slug."
fr: "Handle du produit Shopify (ex: tshirt-index-01). Si vide, utilise le slug de la page Kirby."
placeholder: tshirt-index-01

View file

@ -27,6 +27,9 @@ return [
'addedToCart' => 'Added! ✓',
'errorAddToCart' => 'Error - Try again',
'closeCart' => 'Close cart',
'loading' => 'Loading...',
'productNotFound' => 'Product not found',
'selectVariant' => 'Select',
// Blueprints - Home
'home.title' => 'Home',

View file

@ -27,6 +27,9 @@ return [
'addedToCart' => 'Ajouté ! ✓',
'errorAddToCart' => 'Erreur - Réessayer',
'closeCart' => 'Fermer le panier',
'loading' => 'Chargement...',
'productNotFound' => 'Produit non trouvé',
'selectVariant' => 'Choisir',
// Blueprints - Home
'home.title' => 'Accueil',

View file

@ -0,0 +1,25 @@
<div class="product-purchase">
<div class="product-stock-info">
<p data-product-stock class="stock-status"></p>
</div>
<div class="product-variants" data-product-variants style="display: none;">
<label for="variant-select"><?= t('selectVariant') ?></label>
<select id="variant-select" data-variant-selector></select>
</div>
<button
class="btn-add-to-cart"
data-shopify-add-to-cart
data-product-id=""
data-variant-id=""
data-text-add="<?= t('addToCart') ?>"
data-text-adding="<?= t('addingToCart') ?>"
data-text-added="<?= t('addedToCart') ?>"
data-text-error="<?= t('errorAddToCart') ?>"
data-text-out-of-stock="<?= t('outOfStock') ?>"
data-text-in-stock="<?= t('inStock') ?>"
>
<?= t('addToCart') ?>
</button>
</div>

View file

@ -13,11 +13,16 @@
<script src="<?= url('assets/js/onload.js') ?>"></script>
<script src="<?= url('assets/js/shopify-cart.js') ?>"></script>
<script src="<?= url('assets/js/cart-drawer.js') ?>"></script>
<?php if(isset($scripts) && is_array($scripts)): ?>
<?php foreach($scripts as $script): ?>
<script src="<?= url($script) ?>"></script>
<?php endforeach ?>
<?php if ($scripts ?? null): ?>
<?php if (in_array('product', $scripts)): ?>
<script src="<?= url('assets/js/product-loader.js') ?>"></script>
<script src="<?= url('assets/js/product-add-to-cart.js') ?>"></script>
<?php endif ?>
<?php else: ?>
<script src="<?= url('assets/js/products-list-loader.js') ?>"></script>
<?php endif ?>
<script src="<?= url('assets/js/cart-drawer.js') ?>"></script>
</body>
</html>

View file

@ -1,35 +1,24 @@
<?php snippet('header', ['title' => $site->title(), 'template' => 'store']) ?>
<main>
<p class="p__baseline-big">
<?= $page->baseline()->or('Bienvenue sur la boutique de soutien à Index') ?>
</p>
<main>
<p class="p__baseline-big">
<?= $page->baseline()->or('Bienvenue sur la boutique de soutien à Index') ?>
</p>
<section id="store__container">
<?php foreach($site->children()->listed() as $product): ?>
<article class="store__product">
<figure>
<?php if($cover = $product->files()->sortBy('sort', 'asc')->first()): ?>
<?php snippet('picture', [
'file' => $cover,
'alt' => $product->title()->html(),
'preset' => 'product-card',
'size' => 25,
'lazy' => true
]) ?>
<?php endif ?>
</figure>
<p class="line-1"><a href="<?= $product->url() ?>"><?= $product->title()->html() ?></a></p>
<p class="price"><?= $product->price() ?>€</p>
<a href="<?= $product->url() ?>" class="link-block" aria-hidden="true"></a>
</article>
<?php endforeach ?>
</section>
<section id="store__container"
data-products-loader
data-language="<?= strtoupper($kirby->language()->code()) ?>">
<p class="p__baseline-big">
<?= t('supportText', 'Pour nous soutenir, vous&nbsp;pouvez&nbsp;aussi') ?>
<a href="https://soutenir.index.ngo" class="link-don"><?= t('makeDonation', 'faire un don') ?></a>
</p>
</main>
<div class="products-loading">
<p><?= t('loading') ?></p>
</div>
</section>
<p class="p__baseline-big">
<?= t('supportText') ?>
<a href="https://soutenir.index.ngo" class="link-don"><?= t('makeDonation') ?></a>
</p>
</main>
<?php snippet('footer') ?>

View file

@ -1,60 +1,47 @@
<?php snippet('header', ['title' => $page->title(), 'template' => 'shop']) ?>
<?php
$shopifyHandle = $page->shopifyHandle()->or($page->slug());
<main>
<nav class="store__nav">
<a href="<?= $site->homePage()->url() ?>"><?= t('backToShop', 'Retour à la boutique') ?></a>
</nav>
snippet('header', ['title' => $page->title(), 'template' => 'shop']);
?>
<section class="section__product">
<main>
<nav class="store__nav">
<a href="<?= $site->homePage()->url() ?>"><?= t('backToShop') ?></a>
</nav>
<section class="section__product"
data-product-loader
data-shopify-handle="<?= $shopifyHandle ?>"
data-language="<?= $kirby->language()->code() ?>">
<div class="product-loading">
<p><?= t('loading') ?></p>
</div>
<div class="product-content" style="display: none;">
<div class="col-left">
<div class="hero">
<h2 class="p__baseline-big"><?= $page->title()->html() ?></h2>
<p class="p__baseline-big"><?= $page->price() ?>€</p>
<h2 class="p__baseline-big" data-product-title></h2>
<p class="p__baseline-big" data-product-price></p>
</div>
<div class="details">
<?php if($page->details()->isNotEmpty()): ?>
<?= $page->details()->kt() ?>
<?php endif ?>
</div>
<div class="details" data-product-details></div>
<?php snippet('buy-button--t-shirt') ?>
<?php snippet('buy-button') ?>
</div>
<div class="product-gallery swiper">
<div class="swiper-wrapper">
<?php
if ($page->hasFiles()):
foreach($page->files()->sortBy('sort', 'asc') as $image):
?>
<div class="swiper-slide">
<figure>
<?php snippet('picture', [
'file' => $image,
'alt' => $page->title()->html(),
'preset' => 'product-detail',
'size' => 50,
'lazy' => false
]) ?>
</figure>
</div>
<?php
endforeach;
endif;
?>
</div>
<!-- Navigation arrows -->
<div class="swiper-wrapper" data-product-images></div>
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
<!-- Pagination dots -->
<div class="swiper-pagination"></div>
</div>
</section>
</main>
</div>
<?php snippet('footer', ['scripts' => [
'assets/js/product-add-to-cart.js',
'assets/js/product-gallery.js'
]]) ?>
<div class="product-error" style="display: none;">
<p><?= t('productNotFound') ?></p>
</div>
</section>
</main>
<?php snippet('footer', ['scripts' => ['product']]) ?>