Compare commits
6 commits
84aa4cac17
...
0c8cc5000c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c8cc5000c | ||
|
|
ad699f0365 | ||
|
|
957cf79e45 | ||
|
|
b3940bba08 | ||
|
|
28501fec7c | ||
|
|
c08662caf8 |
39 changed files with 2448 additions and 710 deletions
|
|
@ -6,7 +6,8 @@
|
|||
"Bash(git commit:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(curl:*)",
|
||||
"WebFetch(domain:snipcart.com)"
|
||||
"WebFetch(domain:snipcart.com)",
|
||||
"Bash(grep:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -52,3 +52,8 @@ Icon
|
|||
# Guide d'intégration (contient des informations sensibles)
|
||||
# ---------------
|
||||
GUIDE-INTEGRATION-MONDIAL-RELAY.md
|
||||
|
||||
# Claude settings
|
||||
# ---------------
|
||||
.claude
|
||||
/.claude/*
|
||||
68
assets/css/components/_shopify-buy-button.scss
Normal file
68
assets/css/components/_shopify-buy-button.scss
Normal 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;
|
||||
}
|
||||
254
assets/css/components/_shopify-cart-drawer.scss
Normal file
254
assets/css/components/_shopify-cart-drawer.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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: ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -5,19 +5,6 @@
|
|||
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);
|
||||
|
|
@ -27,174 +14,35 @@
|
|||
a {
|
||||
text-decoration: none;
|
||||
|
||||
&::before {
|
||||
content: "← ";
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
a::before {
|
||||
content: "← ";
|
||||
@media #{$small} {
|
||||
a {
|
||||
padding-top: 0;
|
||||
font-size: var(--fs-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.section__product .details {
|
||||
// margin-bottom: calc(var(--spacing) * 2);
|
||||
|
||||
.section__product {
|
||||
.details {
|
||||
ul {
|
||||
margin-left: 2ch;
|
||||
|
||||
li {
|
||||
padding-bottom: 0.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
|
@ -207,6 +55,7 @@
|
|||
margin-top: calc(var(--spacing) * 0.5);
|
||||
order: 1;
|
||||
}
|
||||
|
||||
figure {
|
||||
order: 2;
|
||||
margin-bottom: calc(var(--spacing) * 1);
|
||||
|
|
@ -232,19 +81,19 @@
|
|||
left: calc(var(--padding-body) * -1);
|
||||
|
||||
.swiper-button-prev,
|
||||
.swiper-button-next{ display: none; }
|
||||
.swiper-button-next {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$small-up} {
|
||||
|
||||
|
||||
.section__product{
|
||||
.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);
|
||||
|
|
@ -257,15 +106,138 @@
|
|||
|
||||
.col-left {
|
||||
min-height: 100%;
|
||||
padding-bottom: 40px; //dots
|
||||
|
||||
padding-bottom: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.product-gallery .swiper-slide figure{
|
||||
.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
275
assets/js/cart-drawer.js
Normal 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();
|
||||
}
|
||||
})();
|
||||
63
assets/js/product-add-to-cart.js
Normal file
63
assets/js/product-add-to-cart.js
Normal 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
188
assets/js/product-loader.js
Normal 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");
|
||||
}
|
||||
}
|
||||
})();
|
||||
47
assets/js/products-list-loader.js
Normal file
47
assets/js/products-list-loader.js
Normal 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
343
assets/js/shopify-cart.js
Normal 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;
|
||||
164
assets/snipcart-archive/README.md
Normal file
164
assets/snipcart-archive/README.md
Normal 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
|
||||
66
assets/snipcart-archive/product-size.js
Normal file
66
assets/snipcart-archive/product-size.js
Normal 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);
|
||||
|
||||
})();
|
||||
94
assets/snipcart-archive/snipcart.js
Normal file
94
assets/snipcart-archive/snipcart.js
Normal 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));
|
||||
}
|
||||
})();
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
Title: Éclairages : 12 entretiens et analyses sur les violences d'État
|
||||
|
||||
----
|
||||
|
||||
Uuid: gzshayl6xoefrnsz
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 |
|
|
@ -1,9 +0,0 @@
|
|||
Sort: 1
|
||||
|
||||
----
|
||||
|
||||
Uuid: elxkhcta8dkjhr60
|
||||
|
||||
----
|
||||
|
||||
Template: image
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 MiB |
|
|
@ -1,9 +0,0 @@
|
|||
Sort: 2
|
||||
|
||||
----
|
||||
|
||||
Uuid: deupkqq83jvloz0r
|
||||
|
||||
----
|
||||
|
||||
Template: image
|
||||
5
content/2_t-shirt-index/product.en.txt
Normal file
5
content/2_t-shirt-index/product.en.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Title: T-shirt Index
|
||||
|
||||
----
|
||||
|
||||
Uuid: qq27mjjpethsvnwp
|
||||
9
content/2_t-shirt-index/product.fr.txt
Normal file
9
content/2_t-shirt-index/product.fr.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
Title: T-shirt Index
|
||||
|
||||
----
|
||||
|
||||
Shopifyhandle: t-shirt-index-01
|
||||
|
||||
----
|
||||
|
||||
Uuid: qq27mjjpethsvnwp
|
||||
|
|
@ -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
|
||||
- width: 1/1
|
||||
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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
]
|
||||
*/
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -13,7 +13,23 @@ return [
|
|||
'backToShop' => 'Back to shop',
|
||||
'supportText' => 'To support us, you can 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',
|
||||
|
|
|
|||
|
|
@ -13,7 +13,23 @@ return [
|
|||
'backToShop' => 'Retour à la boutique',
|
||||
'supportText' => 'Pour nous soutenir, vous pouvez 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',
|
||||
|
|
|
|||
20
site/snippets/buy-button--t-shirt.php
Normal file
20
site/snippets/buy-button--t-shirt.php
Normal 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>
|
||||
25
site/snippets/buy-button.php
Normal file
25
site/snippets/buy-button.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<div class="product-purchase">
|
||||
<div class="product-stock-info">
|
||||
<p data-product-stock class="stock-status"></p>
|
||||
</div>
|
||||
|
||||
<div class="product-variants" data-product-variants style="display: none;">
|
||||
<label for="variant-select"><?= t('selectVariant') ?></label>
|
||||
<select id="variant-select" data-variant-selector></select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn-add-to-cart"
|
||||
data-shopify-add-to-cart
|
||||
data-product-id=""
|
||||
data-variant-id=""
|
||||
data-text-add="<?= t('addToCart') ?>"
|
||||
data-text-adding="<?= t('addingToCart') ?>"
|
||||
data-text-added="<?= t('addedToCart') ?>"
|
||||
data-text-error="<?= t('errorAddToCart') ?>"
|
||||
data-text-out-of-stock="<?= t('outOfStock') ?>"
|
||||
data-text-in-stock="<?= t('inStock') ?>"
|
||||
>
|
||||
<?= t('addToCart') ?>
|
||||
</button>
|
||||
</div>
|
||||
32
site/snippets/cart-drawer.php
Normal file
32
site/snippets/cart-drawer.php
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -5,30 +5,19 @@
|
|||
<?= $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 id="store__container"
|
||||
data-products-loader
|
||||
data-language="<?= strtoupper($kirby->language()->code()) ?>">
|
||||
|
||||
<div class="products-loading">
|
||||
<p><?= t('loading') ?></p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<p class="p__baseline-big">
|
||||
<?= t('supportText', 'Pour nous soutenir, vous pouvez aussi') ?>
|
||||
<a href="https://soutenir.index.ngo" class="link-don"><?= t('makeDonation', 'faire un don') ?></a>
|
||||
<?= t('supportText') ?>
|
||||
<a href="https://soutenir.index.ngo" class="link-don"><?= t('makeDonation') ?></a>
|
||||
</p>
|
||||
</main>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,114 +1,47 @@
|
|||
<?php snippet('header', ['title' => $page->title(), 'template' => 'shop']) ?>
|
||||
<?php
|
||||
$shopifyHandle = $page->shopifyHandle()->or($page->slug());
|
||||
|
||||
snippet('header', ['title' => $page->title(), 'template' => 'shop']);
|
||||
?>
|
||||
|
||||
<main>
|
||||
<nav class="store__nav">
|
||||
<a href="<?= $site->homePage()->url() ?>"><?= t('backToShop', 'Retour à la boutique') ?></a>
|
||||
<a href="<?= $site->homePage()->url() ?>"><?= t('backToShop') ?></a>
|
||||
</nav>
|
||||
|
||||
<section class="section__product">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="product-error" style="display: none;">
|
||||
<p><?= t('productNotFound') ?></p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<?php snippet('footer', ['scripts' => ['assets/js/product-size.js', 'assets/js/snipcart.js', 'assets/js/product-gallery.js']]) ?>
|
||||
<?php snippet('footer', ['scripts' => ['product']]) ?>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue