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:
parent
957cf79e45
commit
ad699f0365
22 changed files with 649 additions and 579 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
25
site/snippets/buy-button.php
Normal file
25
site/snippets/buy-button.php
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 pouvez 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') ?>
|
||||
|
|
|
|||
|
|
@ -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']]) ?>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue