Compare commits

..

No commits in common. "0c8cc5000ca415df2112f6e9dfcaed62582a610e" and "84aa4cac17679637ea3b0eb536ac62ce3c1fa04e" have entirely different histories.

39 changed files with 707 additions and 2445 deletions

View file

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

7
.gitignore vendored
View file

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

View file

@ -1,68 +0,0 @@
.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

@ -1,254 +0,0 @@
/* 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,20 +43,14 @@
}
}
.header-left {
.header-left,
.header-right {
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;
@ -81,39 +75,4 @@
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,18 +524,13 @@ main {
#site-header.is-shrinked .site-title {
width: 80px !important;
}
#site-header .header-left {
#site-header .header-left,
#site-header .header-right {
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;
@ -558,35 +553,6 @@ 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;
@ -1053,6 +1019,13 @@ 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);
@ -1062,17 +1035,11 @@ body.is-fullscreen {
.store__nav a {
text-decoration: none;
}
.store__nav a::before {
content: "← ";
}
.store__nav a:hover {
text-decoration: underline;
}
@media screen and (max-width: 720px) {
.store__nav a {
padding-top: 0;
font-size: var(--fs-small);
}
.store__nav a::before {
content: "← ";
}
.section__product .details ul {
@ -1081,7 +1048,122 @@ 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;
@ -1120,7 +1202,7 @@ body.is-fullscreen {
}
}
@media screen and (min-width: 720px) {
.section__product .product-content {
.section__product {
display: grid;
grid-template-columns: 1fr 1fr;
gap: calc(var(--padding-body) * 2);
@ -1139,123 +1221,10 @@ 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;
@ -1289,296 +1258,8 @@ body.is-fullscreen {
margin-top: calc(var(--spacing) * 4);
}
.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;
.snipcart-modal__container {
z-index: 1000;
}
[data-template=subscription-newsletter] main {

File diff suppressed because one or more lines are too long

View file

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

View file

@ -1,3 +1,12 @@
.section__product,
.store__nav{
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
.section__product,
.store__nav {
max-width: 1200px;
@ -5,6 +14,10 @@
margin-right: auto;
}
.store__nav {
padding-top: calc(var(--spacing) * 1);
padding-bottom: calc(var(--spacing) * 0.5);
@ -14,35 +27,174 @@
a {
text-decoration: none;
&::before {
content: "";
}
&:hover {
text-decoration: underline;
}
}
@media #{$small} {
a {
padding-top: 0;
font-size: var(--fs-small);
a::before {
content: "";
}
}
.section__product .details {
// margin-bottom: calc(var(--spacing) * 2);
ul{
margin-left: 2ch;
li{
padding-bottom: 0.2em;
}
}
}
.section__product {
.details {
ul {
margin-left: 2ch;
.product-options__list {
list-style: none;
display: flex;
gap: 2ch;
li {
padding-bottom: 0.2em;
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;
}
}
}
@media #{$small} {
// 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 {
display: flex;
flex-direction: column;
margin-bottom: 10vh;
@ -55,7 +207,6 @@
margin-top: calc(var(--spacing) * 0.5);
order: 1;
}
figure {
order: 2;
margin-bottom: calc(var(--spacing) * 1);
@ -75,25 +226,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} {
.product-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: calc(var(--padding-body) * 2);
margin-bottom: calc(var(--spacing) * 3);
}
@media #{$small-up} {
.section__product{
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);
@ -104,140 +255,17 @@
border-top: var(--border-light);
}
.col-left {
.col-left{
min-height: 100%;
padding-bottom: 40px;
padding-bottom: 40px; //dots
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;
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;
}
}
.product-gallery .swiper-slide figure{
width: calc(100% - 60px);
}
}
.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,275 +0,0 @@
/**
* 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

@ -1,63 +0,0 @@
(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);
}
});
})();

View file

@ -1,188 +0,0 @@
(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

@ -1,47 +0,0 @@
(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

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

@ -1,164 +0,0 @@
# 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

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

@ -1,94 +0,0 @@
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

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

View file

@ -1,9 +0,0 @@
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

@ -0,0 +1,52 @@
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

@ -0,0 +1,56 @@
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.

After

Width:  |  Height:  |  Size: 262 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View file

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

View file

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

View file

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

View file

@ -1,21 +1,134 @@
title: Product
title:
en: Product
fr: Produit
icon: cart
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."
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
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
- width: 1/3
sections:
images:
type: files
headline:
en: Product Images
fr: Images du produit
template: image
layout: cards

View file

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

View file

@ -13,23 +13,7 @@ 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,23 +13,7 @@ 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

@ -1,20 +0,0 @@
<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

@ -1,25 +0,0 @@
<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

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

View file

@ -12,7 +12,6 @@
<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">
@ -47,9 +46,5 @@
</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,24 +1,35 @@
<?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"
data-products-loader
data-language="<?= strtoupper($kirby->language()->code()) ?>">
<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>
<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>
<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>
<?php snippet('footer') ?>

View file

@ -1,47 +1,114 @@
<?php
$shopifyHandle = $page->shopifyHandle()->or($page->slug());
<?php snippet('header', ['title' => $page->title(), 'template' => 'shop']) ?>
snippet('header', ['title' => $page->title(), 'template' => 'shop']);
?>
<main>
<nav class="store__nav">
<a href="<?= $site->homePage()->url() ?>"><?= t('backToShop', 'Retour à la boutique') ?></a>
</nav>
<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;">
<section class="section__product">
<div class="col-left">
<div class="hero">
<h2 class="p__baseline-big" data-product-title></h2>
<p class="p__baseline-big" data-product-price></p>
<h2 class="p__baseline-big"><?= $page->title()->html() ?></h2>
<p class="p__baseline-big"><?= $page->price() ?>€</p>
</div>
<div class="details" data-product-details></div>
<div class="details">
<?php if($page->details()->isNotEmpty()): ?>
<?= $page->details()->kt() ?>
<?php endif ?>
</div>
<?php snippet('buy-button') ?>
<?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>
</div>
<div class="product-gallery swiper">
<div class="swiper-wrapper" data-product-images></div>
<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-button-prev"></div>
<div class="swiper-button-next"></div>
<!-- Pagination dots -->
<div class="swiper-pagination"></div>
</div>
</div>
</section>
</main>
<div class="product-error" style="display: none;">
<p><?= t('productNotFound') ?></p>
</div>
</section>
</main>
<?php snippet('footer', ['scripts' => ['product']]) ?>
<?php snippet('footer', ['scripts' => ['assets/js/product-size.js', 'assets/js/snipcart.js', 'assets/js/product-gallery.js']]) ?>