Compare commits

..

6 commits

Author SHA1 Message Date
isUnknown
0c8cc5000c improve styles and ignore claude settings
All checks were successful
Deploy / Deploy to Production (push) Successful in 7s
2026-01-16 12:04:15 +01:00
isUnknown
ad699f0365 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>
2026-01-16 12:03:20 +01:00
isUnknown
957cf79e45 Add cart button to header with item count
Add a cart button in the header (right of language switcher) that displays the number of items in parentheses when cart is not empty. Button opens the cart drawer on click and count updates dynamically when items are added/removed.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-14 12:06:26 +01:00
isUnknown
b3940bba08 Add i18n support and cart total to Shopify integration
Implement multilingual support for shop interface and add total calculation to cart drawer:
- Add FR/EN translations for all shop-related texts (cart, checkout, stock status)
- Update templates and JavaScript to use translation system
- Add cart total calculation with formatted currency display
- Refactor cart drawer styles to SASS with improved button styling (black borders on +/-)
- Fix English product content (replace JSON with proper HTML)
- Extract cart drawer to separate snippet for better organization

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-14 12:02:55 +01:00
isUnknown
28501fec7c Implement custom Shopify cart with drawer UI
Replace Shopify Buy Button iframe with custom implementation using Storefront API 2026-01. Create interactive cart drawer with full product management capabilities (add, remove, update quantities) and seamless checkout flow.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-14 11:26:14 +01:00
isUnknown
c08662caf8 Archive and disable Snipcart integration for Shopify migration
Disable Snipcart e-commerce integration to prepare for Shopify Buy Button. All Snipcart code is preserved in assets/snipcart-archive/ with complete restoration instructions.

Changes:
- Comment out Snipcart SCSS import
- Disable Snipcart routes (validate.json, webhook)
- Comment Snipcart attributes in product template
- Remove Snipcart scripts from footer
- Create archive with snipcart.js, product-size.js, _snipcart.scss
- Add comprehensive README.md with restoration guide

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-13 17:17:40 +01:00
39 changed files with 2448 additions and 710 deletions

View file

@ -6,7 +6,8 @@
"Bash(git commit:*)",
"Bash(find:*)",
"Bash(curl:*)",
"WebFetch(domain:snipcart.com)"
"WebFetch(domain:snipcart.com)",
"Bash(grep:*)"
]
}
}

7
.gitignore vendored
View file

@ -51,4 +51,9 @@ Icon
# Guide d'intégration (contient des informations sensibles)
# ---------------
GUIDE-INTEGRATION-MONDIAL-RELAY.md
GUIDE-INTEGRATION-MONDIAL-RELAY.md
# Claude settings
# ---------------
.claude
/.claude/*

View file

@ -0,0 +1,68 @@
.product-purchase {
margin-top: 2rem;
}
.product-stock-info {
margin-bottom: 1rem;
}
.stock-status {
font-size: 0.9rem;
font-weight: 600;
margin: 0;
}
.stock-status.in-stock {
color: #00cc00;
}
.stock-status.low-stock {
color: #ff9900;
}
.stock-status.out-of-stock {
color: #ff3333;
}
.btn-add-to-cart {
font-family: "Open Sans", sans-serif;
font-weight: bold;
font-size: 1rem;
color: #000000;
background-color: #00ff00;
border: none;
border-radius: 40px;
padding: 12px 34px;
cursor: pointer;
transition: background-color 0.3s ease;
width: 100%;
max-width: 300px;
}
.btn-add-to-cart:hover:not(:disabled) {
background-color: #00e600;
}
.btn-add-to-cart:focus {
outline: 2px solid #00e600;
outline-offset: 2px;
}
.btn-add-to-cart:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-add-to-cart.success {
background-color: #00cc00;
}
.btn-add-to-cart.error {
background-color: #ff3333;
color: #ffffff;
}
.btn-add-to-cart.out-of-stock {
background-color: #cccccc;
color: #666666;
}

View file

@ -0,0 +1,254 @@
/* Cart Drawer Styles */
.cart-drawer {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
color: #000;
&.is-open {
pointer-events: auto;
opacity: 1;
.cart-drawer__panel {
transform: translateX(0);
}
}
&__overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
cursor: pointer;
}
&__panel {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 100%;
max-width: 420px;
background-color: #ffffff;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform 0.3s ease;
@media (max-width: 768px) {
max-width: 100%;
}
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.5rem;
border-bottom: 1px solid #e0e0e0;
h3 {
margin: 0;
font-size: 1.5rem;
font-weight: bold;
}
}
&__close {
background: none;
border: none;
cursor: pointer;
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.2s;
&:hover {
opacity: 0.7;
}
}
&__content {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
&.is-loading {
opacity: 0.5;
pointer-events: none;
}
}
&__empty {
text-align: center;
padding: 3rem 1rem;
color: #666;
&.hidden {
display: none;
}
}
&__items {
display: flex;
flex-direction: column;
gap: 1rem;
&.hidden {
display: none;
}
}
&__footer {
border-top: 1px solid #e0e0e0;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
&__total {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1.125rem;
font-weight: bold;
&-label {
color: #000;
}
&-amount {
color: #000;
font-size: 1.25rem;
}
}
&__checkout-btn {
width: 100%;
font-family: "Open Sans", sans-serif;
font-weight: bold;
font-size: 1rem;
color: #000000;
background-color: #00ff00;
border: none;
border-radius: 40px;
padding: 14px 34px;
cursor: pointer;
transition: background-color 0.3s ease;
&:hover:not(:disabled) {
background-color: #00e600;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
}
// Cart Item
.cart-item {
display: flex;
gap: 1rem;
padding: 1rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
&__image {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 4px;
flex-shrink: 0;
}
&__details {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
&__title {
font-weight: 600;
margin: 0;
font-size: 1rem;
}
&__variant {
font-size: 0.875rem;
color: #666;
margin: 0;
}
&__price {
font-weight: bold;
color: #000;
}
&__quantity {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: auto;
}
&__qty-btn {
width: 28px;
height: 28px;
border: 1px solid #000;
background: #fff;
color: #000;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
font-weight: bold;
transition: all 0.2s;
&:hover:not(:disabled) {
background-color: #000;
color: #fff;
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
&__qty-value {
min-width: 30px;
text-align: center;
font-weight: 600;
}
&__remove {
background: none;
border: none;
color: #ff3333;
cursor: pointer;
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
text-decoration: underline;
align-self: flex-start;
&:hover {
color: #cc0000;
}
}
}

View file

@ -43,14 +43,20 @@
}
}
.header-left,
.header-right {
.header-left {
width: 90px;
display: flex;
align-items: center;
justify-content: flex-end;
}
.header-right {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 1rem;
}
.header-center {
display: flex;
flex-direction: column;
@ -75,4 +81,39 @@
color: var(--color-txt);
}
}
.header-cart-btn {
background: none;
border: none;
cursor: pointer;
font-size: 0.875rem;
text-transform: uppercase;
color: var(--color-txt);
padding: 0;
line-height: 1;
display: flex;
align-items: center;
gap: 0.25rem;
transition: opacity 0.2s;
&:hover {
opacity: 0.7;
}
}
.header-cart-count {
font-weight: normal;
&:empty {
display: none;
}
&:not(:empty)::before {
content: '(';
}
&:not(:empty)::after {
content: ')';
}
}
}

View file

@ -524,13 +524,18 @@ main {
#site-header.is-shrinked .site-title {
width: 80px !important;
}
#site-header .header-left,
#site-header .header-right {
#site-header .header-left {
width: 90px;
display: flex;
align-items: center;
justify-content: flex-end;
}
#site-header .header-right {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 1rem;
}
#site-header .header-center {
display: flex;
flex-direction: column;
@ -553,6 +558,35 @@ main {
#site-header #toggle-lang li.is-selected {
color: var(--color-txt);
}
#site-header .header-cart-btn {
background: none;
border: none;
cursor: pointer;
font-size: 0.875rem;
text-transform: uppercase;
color: var(--color-txt);
padding: 0;
line-height: 1;
display: flex;
align-items: center;
gap: 0.25rem;
transition: opacity 0.2s;
}
#site-header .header-cart-btn:hover {
opacity: 0.7;
}
#site-header .header-cart-count {
font-weight: normal;
}
#site-header .header-cart-count:empty {
display: none;
}
#site-header .header-cart-count:not(:empty)::before {
content: "(";
}
#site-header .header-cart-count:not(:empty)::after {
content: ")";
}
#site-footer {
background-color: black;
@ -1019,13 +1053,6 @@ body.is-fullscreen {
margin-right: auto;
}
.section__product,
.store__nav {
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
.store__nav {
padding-top: calc(var(--spacing) * 1);
padding-bottom: calc(var(--spacing) * 0.5);
@ -1035,11 +1062,17 @@ body.is-fullscreen {
.store__nav a {
text-decoration: none;
}
.store__nav a::before {
content: "← ";
}
.store__nav a:hover {
text-decoration: underline;
}
.store__nav a::before {
content: "← ";
@media screen and (max-width: 720px) {
.store__nav a {
padding-top: 0;
font-size: var(--fs-small);
}
}
.section__product .details ul {
@ -1048,122 +1081,7 @@ body.is-fullscreen {
.section__product .details ul li {
padding-bottom: 0.2em;
}
.product-options__list {
list-style: none;
display: flex;
gap: 2ch;
}
.product-options__list li {
position: relative;
}
.product-options__list li input[type=radio] {
position: fixed;
opacity: 0;
pointer-events: none;
}
.product-options__list li 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-options__list li input[type=radio]:checked + label {
border-color: var(--color-txt);
}
.product-options__list li 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;
}
.product-gallery .swiper-slide {
width: 100%;
}
.product-gallery .swiper-slide figure {
aspect-ratio: 4/3;
width: 100%;
height: 100%;
}
.product-gallery .swiper-slide figure img {
width: 100%;
height: 100%;
-o-object-fit: contain;
object-fit: contain;
}
.product-gallery .swiper-button-prev,
.product-gallery .swiper-button-next {
color: var(--color-txt);
width: 20px;
height: 20px;
}
.product-gallery .swiper-button-prev:after,
.product-gallery .swiper-button-next:after {
font-size: 20px;
font-weight: bold;
}
.product-gallery .swiper-button-prev:hover,
.product-gallery .swiper-button-next:hover {
opacity: 0.7;
}
.product-gallery .swiper-pagination {
position: relative;
margin-top: calc(var(--spacing) * 0.5);
bottom: 0;
}
.product-gallery .swiper-pagination .swiper-pagination-bullet {
width: 8px;
height: 8px;
background: var(--grey-600);
opacity: 0.5;
transition: opacity 0.3s;
}
.product-gallery .swiper-pagination .swiper-pagination-bullet:hover {
opacity: 0.7;
}
.product-gallery .swiper-pagination .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);
}
.hero .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 screen and (max-width: 720px) {
.store__nav a {
padding-top: 0;
font-size: var(--fs-small);
}
.section__product {
display: flex;
flex-direction: column;
@ -1202,7 +1120,7 @@ body.is-fullscreen {
}
}
@media screen and (min-width: 720px) {
.section__product {
.section__product .product-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: calc(var(--padding-body) * 2);
@ -1221,10 +1139,123 @@ body.is-fullscreen {
display: flex;
flex-direction: column;
}
}
.product-options {
border-bottom: var(--border-light);
padding: calc(var(--spacing) * 0.25) 0;
}
.product-options__list {
list-style: none;
display: flex;
gap: 2ch;
}
.product-options__list li {
position: relative;
}
.product-options__list li input[type=radio] {
position: fixed;
opacity: 0;
pointer-events: none;
}
.product-options__list li input[type=radio]:checked + label {
border-color: var(--color-txt);
}
.product-options__list li input[type=radio]:not(:checked) + label:hover {
border-color: var(--grey-600);
background-color: var(--grey-800);
}
.product-options__list li 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;
}
.product-gallery .swiper-slide {
width: 100%;
}
.product-gallery .swiper-slide figure {
aspect-ratio: 4/3;
width: 100%;
height: 100%;
}
.product-gallery .swiper-slide figure img {
width: 100%;
height: 100%;
-o-object-fit: contain;
object-fit: contain;
}
@media screen and (min-width: 720px) {
.product-gallery .swiper-slide figure {
width: calc(100% - 60px);
}
}
.product-gallery .swiper-button-prev,
.product-gallery .swiper-button-next {
color: var(--color-txt);
width: 20px;
height: 20px;
}
.product-gallery .swiper-button-prev:after,
.product-gallery .swiper-button-next:after {
font-size: 20px;
font-weight: bold;
}
.product-gallery .swiper-button-prev:hover,
.product-gallery .swiper-button-next:hover {
opacity: 0.7;
}
.product-gallery .swiper-pagination {
position: relative;
margin-top: calc(var(--spacing) * 0.5);
bottom: 0;
}
.product-gallery .swiper-pagination .swiper-pagination-bullet {
width: 8px;
height: 8px;
background: var(--grey-600);
opacity: 0.5;
transition: opacity 0.3s;
}
.product-gallery .swiper-pagination .swiper-pagination-bullet:hover {
opacity: 0.7;
}
.product-gallery .swiper-pagination .swiper-pagination-bullet.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);
}
.hero .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;
}
[data-template=thanks] .thanks-page {
min-height: 60vh;
display: flex;
@ -1258,8 +1289,296 @@ body.is-fullscreen {
margin-top: calc(var(--spacing) * 4);
}
.snipcart-modal__container {
z-index: 1000;
.product-purchase {
margin-top: 2rem;
}
.product-stock-info {
margin-bottom: 1rem;
}
.stock-status {
font-size: 0.9rem;
font-weight: 600;
margin: 0;
}
.stock-status.in-stock {
color: #00cc00;
}
.stock-status.low-stock {
color: #ff9900;
}
.stock-status.out-of-stock {
color: #ff3333;
}
.btn-add-to-cart {
font-family: "Open Sans", sans-serif;
font-weight: bold;
font-size: 1rem;
color: #000000;
background-color: #00ff00;
border: none;
border-radius: 40px;
padding: 12px 34px;
cursor: pointer;
transition: background-color 0.3s ease;
width: 100%;
max-width: 300px;
}
.btn-add-to-cart:hover:not(:disabled) {
background-color: #00e600;
}
.btn-add-to-cart:focus {
outline: 2px solid #00e600;
outline-offset: 2px;
}
.btn-add-to-cart:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-add-to-cart.success {
background-color: #00cc00;
}
.btn-add-to-cart.error {
background-color: #ff3333;
color: #ffffff;
}
.btn-add-to-cart.out-of-stock {
background-color: #cccccc;
color: #666666;
}
/* Cart Drawer Styles */
.cart-drawer {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
color: #000;
}
.cart-drawer.is-open {
pointer-events: auto;
opacity: 1;
}
.cart-drawer.is-open .cart-drawer__panel {
transform: translateX(0);
}
.cart-drawer__overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
cursor: pointer;
}
.cart-drawer__panel {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 100%;
max-width: 420px;
background-color: #ffffff;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform 0.3s ease;
}
@media (max-width: 768px) {
.cart-drawer__panel {
max-width: 100%;
}
}
.cart-drawer__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.5rem;
border-bottom: 1px solid #e0e0e0;
}
.cart-drawer__header h3 {
margin: 0;
font-size: 1.5rem;
font-weight: bold;
}
.cart-drawer__close {
background: none;
border: none;
cursor: pointer;
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.2s;
}
.cart-drawer__close:hover {
opacity: 0.7;
}
.cart-drawer__content {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
}
.cart-drawer__content.is-loading {
opacity: 0.5;
pointer-events: none;
}
.cart-drawer__empty {
text-align: center;
padding: 3rem 1rem;
color: #666;
}
.cart-drawer__empty.hidden {
display: none;
}
.cart-drawer__items {
display: flex;
flex-direction: column;
gap: 1rem;
}
.cart-drawer__items.hidden {
display: none;
}
.cart-drawer__footer {
border-top: 1px solid #e0e0e0;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.cart-drawer__total {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1.125rem;
font-weight: bold;
}
.cart-drawer__total-label {
color: #000;
}
.cart-drawer__total-amount {
color: #000;
font-size: 1.25rem;
}
.cart-drawer__checkout-btn {
width: 100%;
font-family: "Open Sans", sans-serif;
font-weight: bold;
font-size: 1rem;
color: #000000;
background-color: #00ff00;
border: none;
border-radius: 40px;
padding: 14px 34px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.cart-drawer__checkout-btn:hover:not(:disabled) {
background-color: #00e600;
}
.cart-drawer__checkout-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.cart-item {
display: flex;
gap: 1rem;
padding: 1rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.cart-item__image {
width: 80px;
height: 80px;
-o-object-fit: cover;
object-fit: cover;
border-radius: 4px;
flex-shrink: 0;
}
.cart-item__details {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.cart-item__title {
font-weight: 600;
margin: 0;
font-size: 1rem;
}
.cart-item__variant {
font-size: 0.875rem;
color: #666;
margin: 0;
}
.cart-item__price {
font-weight: bold;
color: #000;
}
.cart-item__quantity {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: auto;
}
.cart-item__qty-btn {
width: 28px;
height: 28px;
border: 1px solid #000;
background: #fff;
color: #000;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
font-weight: bold;
transition: all 0.2s;
}
.cart-item__qty-btn:hover:not(:disabled) {
background-color: #000;
color: #fff;
}
.cart-item__qty-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.cart-item__qty-value {
min-width: 30px;
text-align: center;
font-weight: 600;
}
.cart-item__remove {
background: none;
border: none;
color: #ff3333;
cursor: pointer;
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
text-decoration: underline;
align-self: flex-start;
}
.cart-item__remove:hover {
color: #cc0000;
}
[data-template=subscription-newsletter] main {

File diff suppressed because one or more lines are too long

View file

@ -22,6 +22,7 @@
@import "template/shop/layout";
@import "template/shop/section--product";
@import "template/shop/thanks";
@import "template/shop/snipcart";
@import "components/shopify-buy-button.scss";
@import "components/shopify-cart-drawer.scss";
@import "template/subscription-newsletter/layout";

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;
}

275
assets/js/cart-drawer.js Normal file
View file

@ -0,0 +1,275 @@
/**
* Cart Drawer Component
* Manages the cart sidebar with add/remove/update functionality
*/
(function() {
const drawer = document.getElementById('cart-drawer');
const emptyState = document.querySelector('[data-cart-empty]');
const itemsContainer = document.querySelector('[data-cart-items]');
const checkoutBtn = document.querySelector('[data-cart-checkout]');
const closeButtons = document.querySelectorAll('[data-cart-close]');
const totalDisplay = document.querySelector('[data-cart-total]');
const headerCartBtn = document.querySelector('[data-cart-open]');
const headerCartCount = document.querySelector('[data-cart-count]');
// Get translated text
const removeText = drawer.dataset.textRemove || 'Remove';
let currentCart = null;
let cartInstance = null;
// Wait for ShopifyCart to be available
function initCartDrawer() {
if (typeof ShopifyCart === 'undefined') {
setTimeout(initCartDrawer, 100);
return;
}
cartInstance = new ShopifyCart({
domain: 'nv7cqv-bu.myshopify.com',
storefrontAccessToken: 'dec3d35a2554384d149c72927d1cfd1b'
});
// Initialize event listeners
setupEventListeners();
}
function setupEventListeners() {
// Close drawer
closeButtons.forEach(btn => {
btn.addEventListener('click', closeDrawer);
});
// Open drawer from header button
if (headerCartBtn) {
headerCartBtn.addEventListener('click', openDrawer);
}
// Checkout button
checkoutBtn.addEventListener('click', () => {
if (currentCart?.checkoutUrl) {
window.location.href = currentCart.checkoutUrl;
}
});
// Listen for custom cart update events
document.addEventListener('cart:updated', (e) => {
currentCart = e.detail.cart;
renderCart();
openDrawer();
});
}
function openDrawer() {
drawer.classList.add('is-open');
document.body.style.overflow = 'hidden';
}
function closeDrawer() {
drawer.classList.remove('is-open');
document.body.style.overflow = '';
}
function calculateTotal() {
if (!currentCart || !currentCart.lines) return 0;
return currentCart.lines.edges.reduce((total, edge) => {
const item = edge.node;
const price = parseFloat(item.merchandise.price.amount);
const quantity = item.quantity;
return total + (price * quantity);
}, 0);
}
function formatPrice(amount, currency = 'EUR') {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: currency
}).format(amount);
}
function updateCartCount() {
if (!currentCart || !currentCart.lines || currentCart.lines.edges.length === 0) {
if (headerCartCount) {
headerCartCount.textContent = '';
}
return;
}
// Calculate total quantity
const totalQty = currentCart.lines.edges.reduce((sum, edge) => {
return sum + edge.node.quantity;
}, 0);
if (headerCartCount) {
headerCartCount.textContent = totalQty > 0 ? totalQty : '';
}
}
function renderCart() {
if (!currentCart || !currentCart.lines || currentCart.lines.edges.length === 0) {
emptyState.classList.remove('hidden');
itemsContainer.classList.add('hidden');
checkoutBtn.disabled = true;
if (totalDisplay) {
totalDisplay.textContent = '0,00 €';
}
updateCartCount();
return;
}
emptyState.classList.add('hidden');
itemsContainer.classList.remove('hidden');
checkoutBtn.disabled = false;
// Calculate and display total
const total = calculateTotal();
const currency = currentCart.lines.edges[0]?.node.merchandise.price.currencyCode || 'EUR';
if (totalDisplay) {
totalDisplay.textContent = formatPrice(total, currency);
}
// Update header cart count
updateCartCount();
// Render cart items
itemsContainer.innerHTML = currentCart.lines.edges.map(edge => {
const item = edge.node;
const merchandise = item.merchandise;
return `
<div class="cart-item" data-line-id="${item.id}">
<div class="cart-item__details">
<h4 class="cart-item__title">${merchandise.product.title}</h4>
${merchandise.title !== 'Default Title' ? `<p class="cart-item__variant">${merchandise.title}</p>` : ''}
<p class="cart-item__price">${formatPrice(parseFloat(merchandise.price.amount), merchandise.price.currencyCode)}</p>
<div class="cart-item__quantity">
<button class="cart-item__qty-btn" data-action="decrease" data-line-id="${item.id}"></button>
<span class="cart-item__qty-value">${item.quantity}</span>
<button class="cart-item__qty-btn" data-action="increase" data-line-id="${item.id}">+</button>
</div>
<button class="cart-item__remove" data-action="remove" data-line-id="${item.id}">
${removeText}
</button>
</div>
</div>
`;
}).join('');
// Attach event listeners to quantity buttons
attachQuantityListeners();
}
function attachQuantityListeners() {
const buttons = itemsContainer.querySelectorAll('[data-action]');
buttons.forEach(btn => {
btn.addEventListener('click', async (e) => {
const action = e.target.dataset.action;
const lineId = e.target.dataset.lineId;
await handleQuantityChange(action, lineId);
});
});
}
async function handleQuantityChange(action, lineId) {
if (!cartInstance || !currentCart) return;
// Find the line item
const line = currentCart.lines.edges.find(edge => edge.node.id === lineId);
if (!line) return;
const currentQty = line.node.quantity;
let newQty = currentQty;
if (action === 'increase') {
newQty = currentQty + 1;
} else if (action === 'decrease') {
newQty = Math.max(0, currentQty - 1);
} else if (action === 'remove') {
newQty = 0;
}
// Update cart via API
try {
itemsContainer.classList.add('is-loading');
const query = `
mutation cartLinesUpdate($cartId: ID!, $lines: [CartLineUpdateInput!]!) {
cartLinesUpdate(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 cartInstance.query(query, {
cartId: currentCart.id,
lines: [{
id: lineId,
quantity: newQty
}]
});
if (data.cartLinesUpdate.userErrors.length > 0) {
throw new Error(data.cartLinesUpdate.userErrors[0].message);
}
currentCart = data.cartLinesUpdate.cart;
renderCart();
} catch (error) {
console.error('Error updating cart:', error);
alert('Erreur lors de la mise à jour du panier');
} finally {
itemsContainer.classList.remove('is-loading');
}
}
// Public API
window.CartDrawer = {
open: openDrawer,
close: closeDrawer,
updateCart: (cart) => {
currentCart = cart;
renderCart();
}
};
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCartDrawer);
} else {
initCartDrawer();
}
})();

View file

@ -0,0 +1,63 @@
(function() {
const cart = new ShopifyCart({
domain: 'nv7cqv-bu.myshopify.com',
storefrontAccessToken: 'dec3d35a2554384d149c72927d1cfd1b'
});
const addToCartBtn = document.querySelector('[data-shopify-add-to-cart]');
if (!addToCartBtn) {
return;
}
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'
};
addToCartBtn.addEventListener('click', async function(e) {
e.preventDefault();
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 cartResult = await cart.addToCart(variantId, 1);
addToCartBtn.textContent = texts.added;
addToCartBtn.classList.add('success');
document.dispatchEvent(new CustomEvent('cart:updated', {
detail: { cart: cartResult }
}));
setTimeout(() => {
addToCartBtn.disabled = false;
addToCartBtn.textContent = originalText;
addToCartBtn.classList.remove('success');
}, 1500);
} catch (error) {
console.error('Error adding to cart:', error);
addToCartBtn.textContent = texts.error;
addToCartBtn.classList.add('error');
setTimeout(() => {
addToCartBtn.disabled = false;
addToCartBtn.textContent = originalText;
addToCartBtn.classList.remove('error');
}, 2000);
}
});
})();

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>';
}
})();

343
assets/js/shopify-cart.js Normal file
View file

@ -0,0 +1,343 @@
/**
* 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;

View file

@ -0,0 +1,164 @@
# Archive Snipcart
Cette archive contient tous les fichiers et le code nécessaires pour réactiver l'intégration Snipcart si besoin.
## Date d'archivage
13 janvier 2026
## Raison de l'archivage
Remplacement par l'intégration Shopify Buy Button
---
## Fichiers archivés
### Fichiers JavaScript
- `snipcart.js` - Loader Snipcart avec configuration de la clé API
- `product-size.js` - Gestion des options produit (tailles, etc.) pour Snipcart
### Fichiers SCSS
- `_snipcart.scss` - Styles pour le modal Snipcart
---
## Comment restaurer l'intégration Snipcart
### 1. Restaurer les fichiers JavaScript
#### Fichier: `assets/js/snipcart.js`
Copier le fichier depuis l'archive:
```bash
cp assets/snipcart-archive/snipcart.js assets/js/
```
#### Fichier: `assets/js/product-size.js`
Copier le fichier depuis l'archive:
```bash
cp assets/snipcart-archive/product-size.js assets/js/
```
### 2. Restaurer les styles SCSS
#### Fichier: `assets/css/template/shop/_snipcart.scss`
Copier le fichier depuis l'archive:
```bash
cp assets/snipcart-archive/_snipcart.scss assets/css/template/shop/
```
Puis décommenter l'import dans `assets/css/style.scss`:
```scss
// Décommenter cette ligne:
@import 'template/shop/snipcart';
```
### 3. Restaurer le template produit
#### Fichier: `site/templates/product.php`
1. **Restaurer le bouton "Ajouter au panier" avec les attributs Snipcart** (ligne ~40-78):
- Décommenter tous les attributs `data-item-*` du bouton
- Ajouter la classe `snipcart-add-item` au bouton
2. **Restaurer les scripts dans le footer** (ligne 114):
```php
<?php snippet('footer', ['scripts' => ['assets/js/product-size.js', 'assets/js/snipcart.js', 'assets/js/product-gallery.js']]) ?>
```
### 4. Restaurer les routes et webhooks
#### Fichier: `site/config/config.php`
Décommenter les routes Snipcart (lignes 39-147):
1. **Route de validation produit** (`validate.json`):
- Permet à Snipcart de valider les prix et stock
- Route: `(:any)/validate.json`
2. **Webhook Snipcart**:
- Gère les événements de commande (décrémente le stock)
- Route: `snipcart-webhook`
### 5. Configuration Snipcart
#### Clé API publique
La clé API publique Snipcart est dans `snipcart.js`:
```javascript
publicApiKey: 'NGU4ODQ3MjAtY2MzMC00MWEyLWI2YTMtNjBmNGYzMTBlOTZkNjM4OTY1NDY4OTE5MTQyMTI3'
```
#### Configuration du webhook
Pour que le webhook fonctionne, il faut:
1. Configurer l'URL du webhook dans le dashboard Snipcart
2. URL: `https://votre-domaine.com/snipcart-webhook`
3. Événements à écouter: `order.completed`
### 6. Vérifications après restauration
- [ ] Les fichiers JS sont présents dans `assets/js/`
- [ ] Le fichier SCSS est présent et importé
- [ ] Les boutons "Ajouter au panier" ont la classe `snipcart-add-item`
- [ ] Les attributs `data-item-*` sont présents sur les boutons
- [ ] Les routes sont décommentées dans `config.php`
- [ ] Le CSS de Snipcart est compilé
- [ ] Le webhook est configuré dans le dashboard Snipcart
- [ ] Les traductions sont restaurées dans `site/languages/en.php` et `fr.php`
---
## Fonctionnalités Snipcart implémentées
### Gestion des produits
- Affichage du prix
- Options de produit (tailles, couleurs, etc.)
- Validation des options obligatoires
- Images produit
### Gestion du panier
- Ajout au panier
- Gestion du stock
- Calcul des frais de port (basé sur poids/dimensions)
- Validation des prix côté serveur
### Gestion des commandes
- Webhook pour décrémenter le stock automatiquement
- Redirection vers page de remerciement après paiement
- Token de commande dans l'URL
### Multi-langue
- Support FR/EN
- Redirection post-paiement avec détection de la langue
---
## Dépendances externes
### CDN Snipcart
Snipcart est chargé depuis le CDN officiel:
- Version: 3.0
- JS: `https://cdn.snipcart.com/themes/v3.0/default/snipcart.js`
- CSS: `https://cdn.snipcart.com/themes/v3.0/default/snipcart.css`
### Stratégie de chargement
- `loadStrategy: 'on-user-interaction'`
- Chargement différé pour optimiser les performances
- Timeout: 2750ms
---
## Notes importantes
1. **Sécurité**: Le webhook devrait valider la signature Snipcart en production (voir commentaire dans `config.php`)
2. **Stock**: Le système décrémente automatiquement le stock via le webhook `order.completed`
3. **Validation**: Chaque produit expose une route `validate.json` pour que Snipcart puisse vérifier les prix
4. **Multi-langue**: La redirection post-paiement détecte automatiquement la langue depuis l'URL
---
## Support
Pour toute question sur Snipcart:
- Documentation: https://docs.snipcart.com/
- Support: https://snipcart.com/support

View file

@ -0,0 +1,66 @@
/**
* Gestion de la sélection des options produit
* Met à jour les attributs Snipcart et gère les classes CSS
*/
(function() {
'use strict';
/**
* Initialise la gestion des options
*/
function initOptionSelector() {
const optionsContainer = document.querySelector('.product-options');
const addToCartButton = document.querySelector('.snipcart-add-item');
if (!addToCartButton) {
return;
}
// Si pas d'options, le bouton est déjà actif
if (!optionsContainer) {
return;
}
const radios = optionsContainer.querySelectorAll('input[type="radio"]');
// Réinitialiser toutes les options (important pour le cache navigateur)
radios.forEach(radio => {
radio.checked = false;
});
// Retirer la classe is-selected de tous les li
const allLi = optionsContainer.querySelectorAll('li');
allLi.forEach(li => li.classList.remove('is-selected'));
// S'assurer que le bouton est désactivé au départ
addToCartButton.setAttribute('disabled', 'disabled');
// Écouter les changements de sélection
radios.forEach(radio => {
radio.addEventListener('change', function() {
// Mettre à jour l'attribut Snipcart
addToCartButton.setAttribute('data-item-custom1-value', this.value);
// Activer le bouton
addToCartButton.removeAttribute('disabled');
// Changer le texte du bouton
const buttonText = addToCartButton.querySelector('.txt');
if (buttonText) {
buttonText.textContent = buttonText.getAttribute('data-default-text') || 'Ajouter au panier';
}
// Gérer la classe is-selected sur les li parents
allLi.forEach(li => li.classList.remove('is-selected'));
this.closest('li').classList.add('is-selected');
});
});
}
/**
* Initialisation au chargement de la page
*/
document.addEventListener('DOMContentLoaded', initOptionSelector);
})();

View file

@ -0,0 +1,94 @@
window.SnipcartSettings = {
publicApiKey:
'NGU4ODQ3MjAtY2MzMC00MWEyLWI2YTMtNjBmNGYzMTBlOTZkNjM4OTY1NDY4OTE5MTQyMTI3',
loadStrategy: 'on-user-interaction',
};
// Redirection après paiement réussi
document.addEventListener('snipcart.ready', function() {
Snipcart.events.on('cart.confirmed', function(cartState) {
// Détecter la langue actuelle depuis l'URL
const currentPath = window.location.pathname;
const langMatch = currentPath.match(/^\/([a-z]{2})(\/|$)/);
const langPrefix = langMatch ? '/' + langMatch[1] : '';
window.location.href = langPrefix + '/thanks?order=' + cartState.token;
});
});
(() => {
var c, d;
(d = (c = window.SnipcartSettings).version) != null || (c.version = '3.0');
var s, S;
(S = (s = window.SnipcartSettings).timeoutDuration) != null ||
(s.timeoutDuration = 2750);
var l, p;
(p = (l = window.SnipcartSettings).domain) != null ||
(l.domain = 'cdn.snipcart.com');
var w, u;
(u = (w = window.SnipcartSettings).protocol) != null ||
(w.protocol = 'https');
var f =
window.SnipcartSettings.version.includes('v3.0.0-ci') ||
(window.SnipcartSettings.version != '3.0' &&
window.SnipcartSettings.version.localeCompare('3.4.0', void 0, {
numeric: !0,
sensitivity: 'base',
}) === -1),
m = ['focus', 'mouseover', 'touchmove', 'scroll', 'keydown'];
window.LoadSnipcart = o;
document.readyState === 'loading'
? document.addEventListener('DOMContentLoaded', r)
: r();
function r() {
window.SnipcartSettings.loadStrategy
? window.SnipcartSettings.loadStrategy === 'on-user-interaction' &&
(m.forEach((t) => document.addEventListener(t, o)),
setTimeout(o, window.SnipcartSettings.timeoutDuration))
: o();
}
var a = !1;
function o() {
if (a) return;
a = !0;
let t = document.getElementsByTagName('head')[0],
e = document.querySelector('#snipcart'),
i = document.querySelector(
`src[src^="${window.SnipcartSettings.protocol}://${window.SnipcartSettings.domain}"][src$="snipcart.js"]`
),
n = document.querySelector(
`link[href^="${window.SnipcartSettings.protocol}://${window.SnipcartSettings.domain}"][href$="snipcart.css"]`
);
e ||
((e = document.createElement('div')),
(e.id = 'snipcart'),
e.setAttribute('hidden', 'true'),
document.body.appendChild(e)),
v(e),
i ||
((i = document.createElement('script')),
(i.src = `${window.SnipcartSettings.protocol}://${window.SnipcartSettings.domain}/themes/v${window.SnipcartSettings.version}/default/snipcart.js`),
(i.async = !0),
t.appendChild(i)),
n ||
((n = document.createElement('link')),
(n.rel = 'stylesheet'),
(n.type = 'text/css'),
(n.href = `${window.SnipcartSettings.protocol}://${window.SnipcartSettings.domain}/themes/v${window.SnipcartSettings.version}/default/snipcart.css`),
t.prepend(n)),
m.forEach((g) => document.removeEventListener(g, o));
}
function v(t) {
!f ||
((t.dataset.apiKey = window.SnipcartSettings.publicApiKey),
window.SnipcartSettings.addProductBehavior &&
(t.dataset.configAddProductBehavior =
window.SnipcartSettings.addProductBehavior),
window.SnipcartSettings.modalStyle &&
(t.dataset.configModalStyle = window.SnipcartSettings.modalStyle),
window.SnipcartSettings.currency &&
(t.dataset.currency = window.SnipcartSettings.currency),
window.SnipcartSettings.templatesUrl &&
(t.dataset.templatesUrl = window.SnipcartSettings.templatesUrl));
}
})();

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,52 +0,0 @@
Title: T-shirt Index 01
----
Price: 35
----
Description: <p>T-shirt de soutien à Index, 100% coton</p>
----
Details:
[
{
"content": {
"text": "<p>100% cotton</p>"
},
"id": "detail1",
"isHidden": false,
"type": "text"
},
{
"content": {
"text": "<p>Lorem ipsum dolor sit amet</p>"
},
"id": "detail2",
"isHidden": false,
"type": "text"
}
]
----
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

@ -37,6 +37,8 @@ return [
],
'routes' => [
// SNIPCART ROUTES - Désactivées, voir assets/snipcart-archive/README.md pour restauration
/*
[
'pattern' => '(:any)/validate.json',
'method' => 'GET',
@ -144,5 +146,6 @@ return [
return Response::json(['status' => 'success'], 200);
}
]
*/
]
];

View file

@ -13,7 +13,23 @@ return [
'backToShop' => 'Back to shop',
'supportText' => 'To support us, you&nbsp;can&nbsp;also',
'makeDonation' => 'make a donation',
// Shop / Cart
'addToCart' => 'Add to cart',
'cart' => 'Cart',
'cartEmpty' => 'Your cart is empty',
'total' => 'Total',
'checkout' => 'Checkout',
'remove' => 'Remove',
'inStock' => 'In stock',
'outOfStock' => 'Out of stock',
'addingToCart' => 'Adding...',
'addedToCart' => 'Added! ✓',
'errorAddToCart' => 'Error - Try again',
'closeCart' => 'Close cart',
'loading' => 'Loading...',
'productNotFound' => 'Product not found',
'selectVariant' => 'Select',
// Blueprints - Home
'home.title' => 'Home',

View file

@ -13,7 +13,23 @@ return [
'backToShop' => 'Retour à la boutique',
'supportText' => 'Pour nous soutenir, vous&nbsp;pouvez&nbsp;aussi',
'makeDonation' => 'faire un don',
// Shop / Cart
'addToCart' => 'Ajouter au panier',
'cart' => 'Panier',
'cartEmpty' => 'Votre panier est vide',
'total' => 'Total',
'checkout' => 'Passer commande',
'remove' => 'Retirer',
'inStock' => 'En stock',
'outOfStock' => 'Rupture de stock',
'addingToCart' => 'Ajout en cours...',
'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,20 @@
<div class="product-purchase">
<div class="product-stock-info">
<p data-product-stock class="stock-status"></p>
</div>
<button
class="btn-add-to-cart"
data-shopify-add-to-cart
data-product-id="15689076179317"
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

@ -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

@ -0,0 +1,32 @@
<div id="cart-drawer" class="cart-drawer" data-text-remove="<?= t('remove') ?>">
<div class="cart-drawer__overlay" data-cart-close></div>
<div class="cart-drawer__panel">
<div class="cart-drawer__header">
<h3><?= t('cart') ?></h3>
<button class="cart-drawer__close" data-cart-close aria-label="<?= t('closeCart') ?>">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="cart-drawer__content">
<div class="cart-drawer__empty" data-cart-empty>
<p><?= t('cartEmpty') ?></p>
</div>
<div class="cart-drawer__items" data-cart-items></div>
</div>
<div class="cart-drawer__footer">
<div class="cart-drawer__total">
<span class="cart-drawer__total-label"><?= t('total') ?></span>
<span class="cart-drawer__total-amount" data-cart-total>0,00 </span>
</div>
<button class="cart-drawer__checkout-btn" data-cart-checkout>
<?= t('checkout') ?>
</button>
</div>
</div>
</div>

View file

@ -1,3 +1,5 @@
<?php snippet('cart-drawer') ?>
<footer id="site-footer">
<div class="site-footer__container">
<div class="footer__mentions">
@ -10,10 +12,17 @@
</footer>
<script src="<?= url('assets/js/onload.js') ?>"></script>
<?php if(isset($scripts) && is_array($scripts)): ?>
<?php foreach($scripts as $script): ?>
<script src="<?= url($script) ?>"></script>
<?php endforeach ?>
<script src="<?= url('assets/js/shopify-cart.js') ?>"></script>
<?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

@ -12,6 +12,7 @@
<link rel="stylesheet" type="text/css" href="<?= url('assets/fonts/stylesheet.css') ?>?version-cache-prevent<?= rand(0, 1000)?>" />
<link rel="stylesheet" type="text/css" href="<?= url('assets/css/style.css') ?>" />
<link rel="stylesheet" type="text/css" href="<?= url('assets/css/cart-drawer.css') ?>" />
</head>
<body data-template="<?= $page->template() ?>">
<header id="site-header">
@ -46,5 +47,9 @@
</li>
<?php endforeach ?>
</ul>
<button class="header-cart-btn" data-cart-open aria-label="<?= t('cart') ?>">
<?= t('cart') ?> <span class="header-cart-count" data-cart-count></span>
</button>
</div>
</header>

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,114 +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 if($page->hasOptions()->toBool() && $page->optionValues()->isNotEmpty()): ?>
<div class="product-options">
<ul class="product-options__list">
<?php
$values = $page->optionValues()->split(',');
$optionSlug = $page->optionLabel()->slug();
foreach($values as $index => $value):
$value = trim($value);
$uniqueId = $optionSlug . '-' . Str::slug(strtolower($value));
?>
<li>
<input type="radio" id="<?= $uniqueId ?>" name="<?= $optionSlug ?>" value="<?= $value ?>" />
<label for="<?= $uniqueId ?>"><?= $value ?></label>
</li>
<?php endforeach ?>
</ul>
</div>
<?php endif ?>
<div class="add-to-cart">
<button
class="btn__default snipcart-add-item"
data-item-id="<?= $page->slug() ?>"
data-item-price="<?= $page->price() ?>"
data-item-description="<?= $page->description()->excerpt(100) ?>"
data-item-image="<?= $page->images()->first() ? $page->images()->first()->url() : '' ?>"
data-item-name="<?= $page->title()->html() ?>"
data-item-shippable="true"
data-item-weight="<?= $page->weight()->or(0) ?>"
data-item-length="<?= $page->length()->or(0) ?>"
data-item-width="<?= $page->width()->or(0) ?>"
data-item-height="<?= $page->height()->or(0) ?>"
<?php
if($page->hasOptions()->toBool() && $page->optionValues()->isNotEmpty()):
$values = $page->optionValues()->split(',');
$trimmedValues = array_map('trim', $values);
$snipcartOptions = implode('|', $trimmedValues);
?>
data-item-custom1-name="<?= $page->optionLabel()->html() ?>"
data-item-custom1-options="<?= $snipcartOptions ?>"
data-item-custom1-required="true"
disabled
<?php endif; ?>
>
<span class="icon">
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m14.523 18.787s4.501-4.505 6.255-6.26c.146-.146.219-.338.219-.53s-.073-.383-.219-.530c-1.753-1.754-6.255-6.258-6.255-6.258-.144-.145-.334-.217-.524-.217-.193 0-.385.074-.532.221-.293.292-.295.766-.004 1.056l4.978 4.978h-14.692c-.414 0-.75.336-.75.75s.336.75.75.75h14.692l-4.979 4.979c-.289.289-.286.762.006 1.054.148.148.341.222.533.222.19 0 .378-.072.522-.215z" fill-rule="nonzero" />
</svg>
</span>
<div class="txt" data-default-text="<?= t('addToCart', 'Ajouter au panier') ?>">
<?php if($page->hasOptions()->toBool() && $page->optionValues()->isNotEmpty()): ?>
<?= t('chooseOption', 'Choisissez une option') ?>
<?php else: ?>
<?= t('addToCart', 'Ajouter au panier') ?>
<?php endif ?>
</div>
</button>
</div>
<?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-size.js', 'assets/js/snipcart.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']]) ?>