Compare commits
26 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b78a6f822a | ||
|
|
f829024aae | ||
|
|
6d68ea0145 | ||
|
|
4b7062e0bd | ||
|
|
7575e5adbc | ||
|
|
9b4bd4b731 | ||
|
|
aa873e117f | ||
| 9396ae4e02 | |||
|
|
f3f302513e | ||
|
|
9eb8d08bcc | ||
|
|
f69d990349 | ||
|
|
f4ecdcf947 | ||
|
|
4489e705b8 | ||
|
|
ade0ed1a67 | ||
|
|
4987c4830f | ||
|
|
0c8cc5000c | ||
|
|
ad699f0365 | ||
|
|
957cf79e45 | ||
|
|
b3940bba08 | ||
|
|
28501fec7c | ||
|
|
c08662caf8 | ||
|
|
84aa4cac17 | ||
|
|
6c8cdf21d2 | ||
|
|
b59d841d39 | ||
|
|
3ba37020ff | ||
|
|
010c4f6c20 |
63 changed files with 3705 additions and 897 deletions
|
|
@ -1,7 +1,17 @@
|
||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"WebSearch"
|
"WebSearch",
|
||||||
|
"Bash(git add:*)",
|
||||||
|
"Bash(git commit:*)",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(curl:*)",
|
||||||
|
"WebFetch(domain:snipcart.com)",
|
||||||
|
"Bash(grep:*)",
|
||||||
|
"Bash(npm run build:*)",
|
||||||
|
"Bash(php test-shopify.php:*)",
|
||||||
|
"WebFetch(domain:getkirby.com)",
|
||||||
|
"WebFetch(domain:forum.getkirby.com)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ jobs:
|
||||||
set ftp:ssl-allow no
|
set ftp:ssl-allow no
|
||||||
open -u $USERNAME,$PASSWORD $PRODUCTION_HOST
|
open -u $USERNAME,$PASSWORD $PRODUCTION_HOST
|
||||||
mirror --reverse --verbose --ignore-time --parallel=10 -x local/ assets assets
|
mirror --reverse --verbose --ignore-time --parallel=10 -x local/ assets assets
|
||||||
mirror --reverse --verbose --ignore-time --parallel=10 -x accounts/ -x cache/ -x sessions/ -x header.php site site
|
mirror --reverse --verbose --ignore-time --parallel=10 -x accounts/ -x cache/ -x sessions/ site site
|
||||||
mirror --reverse --verbose --ignore-time --parallel=10 kirby kirby
|
mirror --reverse --verbose --ignore-time --parallel=10 kirby kirby
|
||||||
mirror --reverse --verbose --ignore-time --parallel=10 vendor vendor
|
mirror --reverse --verbose --ignore-time --parallel=10 vendor vendor
|
||||||
quit
|
quit
|
||||||
|
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -52,3 +52,8 @@ Icon
|
||||||
# Guide d'intégration (contient des informations sensibles)
|
# Guide d'intégration (contient des informations sensibles)
|
||||||
# ---------------
|
# ---------------
|
||||||
GUIDE-INTEGRATION-MONDIAL-RELAY.md
|
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;
|
||||||
|
}
|
||||||
258
assets/css/components/_shopify-cart-drawer.scss
Normal file
258
assets/css/components/_shopify-cart-drawer.scss
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
stroke: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[data-template="subscription-newsletter"],
|
[data-template="subscription-newsletter"],
|
||||||
[data-template="thanks"],
|
[data-template="thanks"],
|
||||||
[data-template="support"],
|
[data-template="support"],
|
||||||
[data-template="store"] {
|
[data-template="home"] {
|
||||||
.p__baseline-big {
|
.p__baseline-big {
|
||||||
font-family: var(--title);
|
font-family: var(--title);
|
||||||
font-size: var(--fs-big);
|
font-size: var(--fs-big);
|
||||||
|
|
|
||||||
|
|
@ -43,14 +43,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-left,
|
.header-left {
|
||||||
.header-right {
|
|
||||||
width: 90px;
|
width: 90px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.header-center {
|
.header-center {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -75,4 +81,40 @@
|
||||||
color: var(--color-txt);
|
color: var(--color-txt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-cart-btn {
|
||||||
|
font-family: var(--font);
|
||||||
|
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: ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -402,7 +402,7 @@ main {
|
||||||
[data-template=subscription-newsletter] .p__baseline-big,
|
[data-template=subscription-newsletter] .p__baseline-big,
|
||||||
[data-template=thanks] .p__baseline-big,
|
[data-template=thanks] .p__baseline-big,
|
||||||
[data-template=support] .p__baseline-big,
|
[data-template=support] .p__baseline-big,
|
||||||
[data-template=store] .p__baseline-big {
|
[data-template=home] .p__baseline-big {
|
||||||
font-family: var(--title);
|
font-family: var(--title);
|
||||||
font-size: var(--fs-big);
|
font-size: var(--fs-big);
|
||||||
font-weight: var(--fw-bold);
|
font-weight: var(--fw-bold);
|
||||||
|
|
@ -413,14 +413,14 @@ main {
|
||||||
[data-template=subscription-newsletter] .p__baseline-big strong,
|
[data-template=subscription-newsletter] .p__baseline-big strong,
|
||||||
[data-template=thanks] .p__baseline-big strong,
|
[data-template=thanks] .p__baseline-big strong,
|
||||||
[data-template=support] .p__baseline-big strong,
|
[data-template=support] .p__baseline-big strong,
|
||||||
[data-template=store] .p__baseline-big strong {
|
[data-template=home] .p__baseline-big strong {
|
||||||
font-weight: var(--fw-bolf);
|
font-weight: var(--fw-bolf);
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
}
|
}
|
||||||
[data-template=subscription-newsletter] .p__baseline-big .link-don,
|
[data-template=subscription-newsletter] .p__baseline-big .link-don,
|
||||||
[data-template=thanks] .p__baseline-big .link-don,
|
[data-template=thanks] .p__baseline-big .link-don,
|
||||||
[data-template=support] .p__baseline-big .link-don,
|
[data-template=support] .p__baseline-big .link-don,
|
||||||
[data-template=store] .p__baseline-big .link-don {
|
[data-template=home] .p__baseline-big .link-don {
|
||||||
display: block;
|
display: block;
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
@ -428,7 +428,7 @@ main {
|
||||||
[data-template=subscription-newsletter] .p__baseline-big .link-don:hover,
|
[data-template=subscription-newsletter] .p__baseline-big .link-don:hover,
|
||||||
[data-template=thanks] .p__baseline-big .link-don:hover,
|
[data-template=thanks] .p__baseline-big .link-don:hover,
|
||||||
[data-template=support] .p__baseline-big .link-don:hover,
|
[data-template=support] .p__baseline-big .link-don:hover,
|
||||||
[data-template=store] .p__baseline-big .link-don:hover {
|
[data-template=home] .p__baseline-big .link-don:hover {
|
||||||
-webkit-text-decoration: underline 2px;
|
-webkit-text-decoration: underline 2px;
|
||||||
text-decoration: underline 2px;
|
text-decoration: underline 2px;
|
||||||
text-underline-offset: 4px;
|
text-underline-offset: 4px;
|
||||||
|
|
@ -436,7 +436,7 @@ main {
|
||||||
[data-template=subscription-newsletter] .p__baseline,
|
[data-template=subscription-newsletter] .p__baseline,
|
||||||
[data-template=thanks] .p__baseline,
|
[data-template=thanks] .p__baseline,
|
||||||
[data-template=support] .p__baseline,
|
[data-template=support] .p__baseline,
|
||||||
[data-template=store] .p__baseline {
|
[data-template=home] .p__baseline {
|
||||||
font-size: var(--fs-medium);
|
font-size: var(--fs-medium);
|
||||||
font-weight: var(--fw-medium);
|
font-weight: var(--fw-medium);
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
|
|
@ -447,7 +447,7 @@ main {
|
||||||
[data-template=subscription-newsletter] .p__baseline,
|
[data-template=subscription-newsletter] .p__baseline,
|
||||||
[data-template=thanks] .p__baseline,
|
[data-template=thanks] .p__baseline,
|
||||||
[data-template=support] .p__baseline,
|
[data-template=support] .p__baseline,
|
||||||
[data-template=store] .p__baseline {
|
[data-template=home] .p__baseline {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: var(--spacing) 0;
|
margin: var(--spacing) 0;
|
||||||
}
|
}
|
||||||
|
|
@ -455,7 +455,7 @@ main {
|
||||||
[data-template=subscription-newsletter] .p__details,
|
[data-template=subscription-newsletter] .p__details,
|
||||||
[data-template=thanks] .p__details,
|
[data-template=thanks] .p__details,
|
||||||
[data-template=support] .p__details,
|
[data-template=support] .p__details,
|
||||||
[data-template=store] .p__details {
|
[data-template=home] .p__details {
|
||||||
font-size: var(--fs-small);
|
font-size: var(--fs-small);
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
color: var(--grey-400);
|
color: var(--grey-400);
|
||||||
|
|
@ -463,7 +463,7 @@ main {
|
||||||
[data-template=subscription-newsletter] .section__heading,
|
[data-template=subscription-newsletter] .section__heading,
|
||||||
[data-template=thanks] .section__heading,
|
[data-template=thanks] .section__heading,
|
||||||
[data-template=support] .section__heading,
|
[data-template=support] .section__heading,
|
||||||
[data-template=store] .section__heading {
|
[data-template=home] .section__heading {
|
||||||
font-size: var(--fs-normal);
|
font-size: var(--fs-normal);
|
||||||
font-weight: var(--fw-medium);
|
font-weight: var(--fw-medium);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
@ -477,8 +477,8 @@ main {
|
||||||
[data-template=thanks] ol,
|
[data-template=thanks] ol,
|
||||||
[data-template=support] ul,
|
[data-template=support] ul,
|
||||||
[data-template=support] ol,
|
[data-template=support] ol,
|
||||||
[data-template=store] ul,
|
[data-template=home] ul,
|
||||||
[data-template=store] ol {
|
[data-template=home] ol {
|
||||||
margin-left: 3ch;
|
margin-left: 3ch;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
@ -524,13 +524,18 @@ main {
|
||||||
#site-header.is-shrinked .site-title {
|
#site-header.is-shrinked .site-title {
|
||||||
width: 80px !important;
|
width: 80px !important;
|
||||||
}
|
}
|
||||||
#site-header .header-left,
|
#site-header .header-left {
|
||||||
#site-header .header-right {
|
|
||||||
width: 90px;
|
width: 90px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
#site-header .header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
#site-header .header-center {
|
#site-header .header-center {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -553,6 +558,36 @@ main {
|
||||||
#site-header #toggle-lang li.is-selected {
|
#site-header #toggle-lang li.is-selected {
|
||||||
color: var(--color-txt);
|
color: var(--color-txt);
|
||||||
}
|
}
|
||||||
|
#site-header .header-cart-btn {
|
||||||
|
font-family: var(--font);
|
||||||
|
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 {
|
#site-footer {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
|
|
@ -940,36 +975,36 @@ body.is-fullscreen {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-template=store] .p__baseline-big {
|
[data-template=home] .p__baseline-big {
|
||||||
margin-top: calc(var(--spacing) * 2);
|
margin-top: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
[data-template=store] #store__container {
|
[data-template=home] #store__container {
|
||||||
margin-top: calc(var(--spacing) * 2);
|
margin-top: calc(var(--spacing) * 2);
|
||||||
margin-bottom: calc(var(--spacing) * 4);
|
margin-bottom: calc(var(--spacing) * 4);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
}
|
}
|
||||||
[data-template=store] #store__container .store__product {
|
[data-template=home] #store__container .store__product {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
[data-template=store] #store__container .store__product figure {
|
[data-template=home] #store__container .store__product figure {
|
||||||
aspect-ratio: 4/3;
|
aspect-ratio: 4/3;
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
background-color: var(--data-bg);
|
background-color: var(--data-bg);
|
||||||
margin-bottom: calc(var(--spacing) * 0.5);
|
margin-bottom: calc(var(--spacing) * 0.5);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
[data-template=store] #store__container .store__product img {
|
[data-template=home] #store__container .store__product img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
-o-object-fit: contain;
|
-o-object-fit: contain;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
transition: var(--curve) 0.5s;
|
transition: var(--curve) 0.5s;
|
||||||
}
|
}
|
||||||
[data-template=store] #store__container .store__product a {
|
[data-template=home] #store__container .store__product a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
[data-template=store] #store__container .store__product .link-block {
|
[data-template=home] #store__container .store__product .link-block {
|
||||||
display: block;
|
display: block;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -978,23 +1013,23 @@ body.is-fullscreen {
|
||||||
left: 0;
|
left: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
[data-template=store] #store__container .store__product:hover figure {
|
[data-template=home] #store__container .store__product:hover figure {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
[data-template=store] #store__container .store__product:hover img {
|
[data-template=home] #store__container .store__product:hover img {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
[data-template=store] #store__container .store__product:hover .line-1 {
|
[data-template=home] #store__container .store__product:hover .line-1 {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 720px) {
|
@media screen and (max-width: 720px) {
|
||||||
[data-template=store] #store__container .store__product {
|
[data-template=home] #store__container .store__product {
|
||||||
margin-top: calc(var(--spacing) * 1.5);
|
margin-top: calc(var(--spacing) * 1.5);
|
||||||
margin-bottom: calc(var(--spacing) * 0.5);
|
margin-bottom: calc(var(--spacing) * 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media screen and (min-width: 720px) {
|
@media screen and (min-width: 720px) {
|
||||||
[data-template=store] #store__container {
|
[data-template=home] #store__container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(6, 1fr);
|
grid-template-columns: repeat(6, 1fr);
|
||||||
-moz-column-gap: calc(var(--padding-body) * 0.75);
|
-moz-column-gap: calc(var(--padding-body) * 0.75);
|
||||||
|
|
@ -1003,11 +1038,11 @@ body.is-fullscreen {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
[data-template=store] #store__container .store__product {
|
[data-template=home] #store__container .store__product {
|
||||||
grid-column: span 2;
|
grid-column: span 2;
|
||||||
}
|
}
|
||||||
[data-template=store] #store__container .store__product:nth-of-type(1),
|
[data-template=home] #store__container .store__product:nth-of-type(1),
|
||||||
[data-template=store] #store__container .store__product:nth-of-type(2) {
|
[data-template=home] #store__container .store__product:nth-of-type(2) {
|
||||||
grid-column: span 3;
|
grid-column: span 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1019,11 +1054,8 @@ body.is-fullscreen {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section__product,
|
.product-content {
|
||||||
.store__nav {
|
display: contents;
|
||||||
max-width: 1200px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.store__nav {
|
.store__nav {
|
||||||
|
|
@ -1035,11 +1067,17 @@ body.is-fullscreen {
|
||||||
.store__nav a {
|
.store__nav a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
.store__nav a::before {
|
||||||
|
content: "← ";
|
||||||
|
}
|
||||||
.store__nav a:hover {
|
.store__nav a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
.store__nav a::before {
|
@media screen and (max-width: 720px) {
|
||||||
content: "← ";
|
.store__nav a {
|
||||||
|
padding-top: 0;
|
||||||
|
font-size: var(--fs-small);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section__product .details ul {
|
.section__product .details ul {
|
||||||
|
|
@ -1048,122 +1086,7 @@ body.is-fullscreen {
|
||||||
.section__product .details ul li {
|
.section__product .details ul li {
|
||||||
padding-bottom: 0.2em;
|
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) {
|
@media screen and (max-width: 720px) {
|
||||||
.store__nav a {
|
|
||||||
padding-top: 0;
|
|
||||||
font-size: var(--fs-small);
|
|
||||||
}
|
|
||||||
.section__product {
|
.section__product {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -1202,7 +1125,7 @@ body.is-fullscreen {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media screen and (min-width: 720px) {
|
@media screen and (min-width: 720px) {
|
||||||
.section__product {
|
.section__product .product-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: calc(var(--padding-body) * 2);
|
gap: calc(var(--padding-body) * 2);
|
||||||
|
|
@ -1221,10 +1144,123 @@ body.is-fullscreen {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.product-gallery .swiper-slide figure {
|
||||||
width: calc(100% - 60px);
|
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 {
|
[data-template=thanks] .thanks-page {
|
||||||
min-height: 60vh;
|
min-height: 60vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -1232,32 +1268,325 @@ body.is-fullscreen {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: calc(var(--spacing) * 4) var(--spacing);
|
padding: calc(var(--spacing) * 4) var(--spacing);
|
||||||
}
|
}
|
||||||
[data-template=thanks] .thanks-content {
|
[data-template=thanks] .thanks-page .thanks-content {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
[data-template=thanks] .thanks-content h1 {
|
[data-template=thanks] .thanks-page .thanks-content h1 {
|
||||||
font-size: var(--fs-x-big);
|
|
||||||
margin-bottom: calc(var(--spacing) * 2);
|
margin-bottom: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
[data-template=thanks] .thanks-content .thanks-message {
|
[data-template=thanks] .thanks-page .thanks-content .thanks-message {
|
||||||
font-size: var(--fs-big);
|
font-size: var(--fs-medium);
|
||||||
margin-bottom: calc(var(--spacing) * 3);
|
line-height: 1.1;
|
||||||
line-height: 1.6;
|
|
||||||
}
|
}
|
||||||
[data-template=thanks] .thanks-content .thanks-message p {
|
[data-template=thanks] .thanks-page .thanks-content .thanks-message p {
|
||||||
margin-bottom: var(--spacing);
|
margin-bottom: var(--spacing);
|
||||||
}
|
}
|
||||||
[data-template=thanks] .thanks-content .thanks-actions {
|
[data-template=thanks] .thanks-page .thanks-content .thanks-actions {
|
||||||
margin-top: calc(var(--spacing) * 3);
|
width: -moz-max-content;
|
||||||
|
width: max-content;
|
||||||
}
|
}
|
||||||
[data-template=thanks] #site-footer {
|
[data-template=thanks] #site-footer {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
margin-top: calc(var(--spacing) * 4);
|
margin-top: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.snipcart-modal__container {
|
.product-purchase {
|
||||||
z-index: 1000;
|
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__close svg {
|
||||||
|
stroke: #000;
|
||||||
|
}
|
||||||
|
.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 {
|
[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/layout";
|
||||||
@import "template/shop/section--product";
|
@import "template/shop/section--product";
|
||||||
@import "template/shop/thanks";
|
@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";
|
@import "template/subscription-newsletter/layout";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
[data-template="store"] {
|
[data-template="home"] {
|
||||||
|
|
||||||
|
|
||||||
.p__baseline-big {
|
.p__baseline-big {
|
||||||
margin-top: calc(var(--spacing) * 2);
|
margin-top: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,10 @@
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-content {
|
||||||
|
display: contents;
|
||||||
.section__product,
|
|
||||||
.store__nav {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.store__nav {
|
.store__nav {
|
||||||
padding-top: calc(var(--spacing) * 1);
|
padding-top: calc(var(--spacing) * 1);
|
||||||
padding-bottom: calc(var(--spacing) * 0.5);
|
padding-bottom: calc(var(--spacing) * 0.5);
|
||||||
|
|
@ -27,174 +18,35 @@
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "← ";
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a::before {
|
@media #{$small} {
|
||||||
content: "← ";
|
a {
|
||||||
|
padding-top: 0;
|
||||||
|
font-size: var(--fs-small);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section__product {
|
||||||
|
.details {
|
||||||
.section__product .details {
|
|
||||||
// margin-bottom: calc(var(--spacing) * 2);
|
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
margin-left: 2ch;
|
margin-left: 2ch;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
padding-bottom: 0.2em;
|
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} {
|
@media #{$small} {
|
||||||
.store__nav a {
|
|
||||||
padding-top: 0;
|
|
||||||
font-size: var(--fs-small);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section__product {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-bottom: 10vh;
|
margin-bottom: 10vh;
|
||||||
|
|
@ -207,6 +59,7 @@
|
||||||
margin-top: calc(var(--spacing) * 0.5);
|
margin-top: calc(var(--spacing) * 0.5);
|
||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
figure {
|
figure {
|
||||||
order: 2;
|
order: 2;
|
||||||
margin-bottom: calc(var(--spacing) * 1);
|
margin-bottom: calc(var(--spacing) * 1);
|
||||||
|
|
@ -232,19 +85,19 @@
|
||||||
left: calc(var(--padding-body) * -1);
|
left: calc(var(--padding-body) * -1);
|
||||||
|
|
||||||
.swiper-button-prev,
|
.swiper-button-prev,
|
||||||
.swiper-button-next{ display: none; }
|
.swiper-button-next {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media #{$small-up} {
|
@media #{$small-up} {
|
||||||
|
.product-content {
|
||||||
|
|
||||||
.section__product{
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: calc(var(--padding-body) * 2);
|
gap: calc(var(--padding-body) * 2);
|
||||||
margin-bottom: calc(var(--spacing) * 3);
|
margin-bottom: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
|
|
||||||
.details {
|
.details {
|
||||||
margin-bottom: calc(var(--spacing) * 2);
|
margin-bottom: calc(var(--spacing) * 2);
|
||||||
|
|
@ -257,15 +110,138 @@
|
||||||
|
|
||||||
.col-left {
|
.col-left {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
padding-bottom: 40px; //dots
|
padding-bottom: 40px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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);
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,21 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: calc(var(--spacing) * 4) var(--spacing);
|
padding: calc(var(--spacing) * 4) var(--spacing);
|
||||||
}
|
|
||||||
|
|
||||||
.thanks-content {
|
.thanks-content {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: var(--fs-x-big);
|
|
||||||
margin-bottom: calc(var(--spacing) * 2);
|
margin-bottom: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thanks-message {
|
.thanks-message {
|
||||||
font-size: var(--fs-big);
|
font-size: var(--fs-medium);
|
||||||
margin-bottom: calc(var(--spacing) * 3);
|
line-height: 1.1;
|
||||||
line-height: 1.6;
|
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: var(--spacing);
|
margin-bottom: var(--spacing);
|
||||||
|
|
@ -27,7 +27,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.thanks-actions {
|
.thanks-actions {
|
||||||
margin-top: calc(var(--spacing) * 3);
|
width: max-content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
290
assets/js/cart-drawer.js
Normal file
290
assets/js/cart-drawer.js
Normal file
|
|
@ -0,0 +1,290 @@
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
|
||||||
|
// Load initial cart state
|
||||||
|
loadCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCart() {
|
||||||
|
if (!cartInstance) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cart = await cartInstance.getCart();
|
||||||
|
currentCart = cart;
|
||||||
|
renderCart();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading cart:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
70
assets/js/product-add-to-cart.js
Normal file
70
assets/js/product-add-to-cart.js
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
(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 buttonTextDiv = addToCartBtn.querySelector('.txt[data-button-text]');
|
||||||
|
|
||||||
|
if (!buttonTextDiv) {
|
||||||
|
console.error('Button text div not found');
|
||||||
|
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 = buttonTextDiv.textContent;
|
||||||
|
buttonTextDiv.textContent = texts.adding;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cartResult = await cart.addToCart(variantId, 1);
|
||||||
|
|
||||||
|
buttonTextDiv.textContent = texts.added;
|
||||||
|
addToCartBtn.classList.add('success');
|
||||||
|
|
||||||
|
document.dispatchEvent(new CustomEvent('cart:updated', {
|
||||||
|
detail: { cart: cartResult }
|
||||||
|
}));
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
addToCartBtn.disabled = false;
|
||||||
|
buttonTextDiv.textContent = originalText;
|
||||||
|
addToCartBtn.classList.remove('success');
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding to cart:', error);
|
||||||
|
|
||||||
|
buttonTextDiv.textContent = texts.error;
|
||||||
|
addToCartBtn.classList.add('error');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
addToCartBtn.disabled = false;
|
||||||
|
buttonTextDiv.textContent = originalText;
|
||||||
|
addToCartBtn.classList.remove('error');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
306
assets/js/product-loader.js
Normal file
306
assets/js/product-loader.js
Normal file
|
|
@ -0,0 +1,306 @@
|
||||||
|
(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);
|
||||||
|
updateMetaTags(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);
|
||||||
|
renderOptions(product);
|
||||||
|
setupAddToCart(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.replaceAll("\n", "<br>")
|
||||||
|
: 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 renderOptions(product) {
|
||||||
|
if (product.variants.edges.length <= 1) return;
|
||||||
|
|
||||||
|
const firstVariant = product.variants.edges[0].node;
|
||||||
|
if (
|
||||||
|
!firstVariant.selectedOptions ||
|
||||||
|
firstVariant.selectedOptions.length === 0
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const mainOption = firstVariant.selectedOptions[0];
|
||||||
|
const optionValues = new Set();
|
||||||
|
|
||||||
|
product.variants.edges.forEach((edge) => {
|
||||||
|
const variant = edge.node;
|
||||||
|
if (variant.selectedOptions && variant.selectedOptions[0]) {
|
||||||
|
optionValues.add(variant.selectedOptions[0].value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (optionValues.size <= 1) return;
|
||||||
|
|
||||||
|
const optionsContainer = document.querySelector("[data-product-options]");
|
||||||
|
const optionsList = document.querySelector("[data-product-options-list]");
|
||||||
|
|
||||||
|
if (!optionsContainer || !optionsList) return;
|
||||||
|
|
||||||
|
const optionName = mainOption.name;
|
||||||
|
const optionSlug = optionName.toLowerCase().replace(/\s+/g, "-");
|
||||||
|
|
||||||
|
optionsList.innerHTML = Array.from(optionValues)
|
||||||
|
.map((value) => {
|
||||||
|
const uniqueId = `${optionSlug}-${value
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, "-")}`;
|
||||||
|
const variant = product.variants.edges.find(
|
||||||
|
(e) =>
|
||||||
|
e.node.selectedOptions && e.node.selectedOptions[0]?.value === value
|
||||||
|
)?.node;
|
||||||
|
const isAvailable = variant?.availableForSale || false;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<li>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="${uniqueId}"
|
||||||
|
name="${optionSlug}"
|
||||||
|
value="${value}"
|
||||||
|
data-variant-id="${
|
||||||
|
variant
|
||||||
|
? variant.id.replace("gid://shopify/ProductVariant/", "")
|
||||||
|
: ""
|
||||||
|
}"
|
||||||
|
${!isAvailable ? "disabled" : ""}
|
||||||
|
/>
|
||||||
|
<label for="${uniqueId}">${value}</label>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
optionsContainer.style.display = "block";
|
||||||
|
|
||||||
|
const radios = optionsList.querySelectorAll('input[type="radio"]');
|
||||||
|
const addToCartBtn = document.querySelector("[data-shopify-add-to-cart]");
|
||||||
|
const buttonText = addToCartBtn?.querySelector("[data-button-text]");
|
||||||
|
|
||||||
|
radios.forEach((radio) => {
|
||||||
|
radio.addEventListener("change", function () {
|
||||||
|
const variantId = this.dataset.variantId;
|
||||||
|
|
||||||
|
if (addToCartBtn) {
|
||||||
|
addToCartBtn.dataset.variantId = variantId;
|
||||||
|
addToCartBtn.removeAttribute("disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttonText) {
|
||||||
|
buttonText.textContent =
|
||||||
|
addToCartBtn.dataset.defaultText || "Ajouter au panier";
|
||||||
|
}
|
||||||
|
|
||||||
|
const allLi = optionsList.querySelectorAll("li");
|
||||||
|
allLi.forEach((li) => li.classList.remove("is-selected"));
|
||||||
|
this.closest("li").classList.add("is-selected");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 hasMultipleVariants = product.variants.edges.length > 1;
|
||||||
|
const firstVariant = product.variants.edges[0]?.node;
|
||||||
|
const hasOptions =
|
||||||
|
firstVariant?.selectedOptions && firstVariant.selectedOptions.length > 0;
|
||||||
|
|
||||||
|
const uniqueOptions = new Set();
|
||||||
|
product.variants.edges.forEach((edge) => {
|
||||||
|
if (edge.node.selectedOptions && edge.node.selectedOptions[0]) {
|
||||||
|
uniqueOptions.add(edge.node.selectedOptions[0].value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const hasMultipleOptions = uniqueOptions.size > 1;
|
||||||
|
|
||||||
|
if (hasMultipleVariants && hasOptions && hasMultipleOptions) {
|
||||||
|
addToCartBtn.setAttribute("disabled", "disabled");
|
||||||
|
const buttonText = addToCartBtn.querySelector("[data-button-text]");
|
||||||
|
if (buttonText) {
|
||||||
|
buttonText.textContent =
|
||||||
|
addToCartBtn.dataset.textChooseOption || "Choisissez une option";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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 updateMetaTags(product, isEnglish) {
|
||||||
|
// Update title and description
|
||||||
|
const title =
|
||||||
|
isEnglish && product.titleEn?.value
|
||||||
|
? product.titleEn.value
|
||||||
|
: product.title;
|
||||||
|
const description =
|
||||||
|
isEnglish && product.descriptionEn?.value
|
||||||
|
? product.descriptionEn.value
|
||||||
|
: product.description;
|
||||||
|
|
||||||
|
// Update Open Graph title
|
||||||
|
const ogTitle = document.getElementById("og-title");
|
||||||
|
if (ogTitle) {
|
||||||
|
ogTitle.setAttribute("content", title);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Open Graph description
|
||||||
|
const ogDescription = document.getElementById("og-description");
|
||||||
|
if (ogDescription && description) {
|
||||||
|
const excerpt = description.substring(0, 160);
|
||||||
|
ogDescription.setAttribute("content", excerpt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Open Graph image
|
||||||
|
const ogImage = document.getElementById("og-image");
|
||||||
|
if (ogImage && product.images.edges.length > 0) {
|
||||||
|
ogImage.setAttribute("content", product.images.edges[0].node.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update product price
|
||||||
|
const ogPrice = document.getElementById("og-price");
|
||||||
|
if (ogPrice) {
|
||||||
|
const price = parseFloat(
|
||||||
|
product.priceRange.minVariantPrice.amount
|
||||||
|
).toFixed(2);
|
||||||
|
ogPrice.setAttribute("content", price);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update availability
|
||||||
|
const ogAvailability = document.getElementById("og-availability");
|
||||||
|
if (ogAvailability) {
|
||||||
|
const availability = product.availableForSale
|
||||||
|
? "in stock"
|
||||||
|
: "out of stock";
|
||||||
|
ogAvailability.setAttribute("content", availability);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update page title
|
||||||
|
document.title = `${title} | Index.ngo`;
|
||||||
|
|
||||||
|
// Update meta description
|
||||||
|
let metaDescription = document.querySelector('meta[name="description"]');
|
||||||
|
if (metaDescription && description) {
|
||||||
|
const excerpt = description.substring(0, 160);
|
||||||
|
metaDescription.setAttribute("content", excerpt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
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>';
|
||||||
|
}
|
||||||
|
})();
|
||||||
395
assets/js/shopify-cart.js
Normal file
395
assets/js/shopify-cart.js
Normal file
|
|
@ -0,0 +1,395 @@
|
||||||
|
/**
|
||||||
|
* 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 existing cart by ID
|
||||||
|
*/
|
||||||
|
async getCart() {
|
||||||
|
if (!this.cartId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
query getCart($cartId: ID!) {
|
||||||
|
cart(id: $cartId) {
|
||||||
|
id
|
||||||
|
checkoutUrl
|
||||||
|
lines(first: 10) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
quantity
|
||||||
|
merchandise {
|
||||||
|
... on ProductVariant {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
price {
|
||||||
|
amount
|
||||||
|
currencyCode
|
||||||
|
}
|
||||||
|
product {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.query(query, {
|
||||||
|
cartId: this.cartId
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.cart;
|
||||||
|
} catch (error) {
|
||||||
|
// Cart might be expired or invalid
|
||||||
|
console.error('Error fetching cart:', error);
|
||||||
|
this.clearCart();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
@ -6,13 +6,13 @@ window.SnipcartSettings = {
|
||||||
|
|
||||||
// Redirection après paiement réussi
|
// Redirection après paiement réussi
|
||||||
document.addEventListener('snipcart.ready', function() {
|
document.addEventListener('snipcart.ready', function() {
|
||||||
Snipcart.execute('bind', 'order.completed', function(order) {
|
Snipcart.events.on('cart.confirmed', function(cartState) {
|
||||||
// Détecter la langue actuelle depuis l'URL
|
// Détecter la langue actuelle depuis l'URL
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname;
|
||||||
const langMatch = currentPath.match(/^\/([a-z]{2})(\/|$)/);
|
const langMatch = currentPath.match(/^\/([a-z]{2})(\/|$)/);
|
||||||
const langPrefix = langMatch ? '/' + langMatch[1] : '';
|
const langPrefix = langMatch ? '/' + langMatch[1] : '';
|
||||||
|
|
||||||
window.location.href = langPrefix + '/thanks?order=' + order.token;
|
window.location.href = langPrefix + '/thanks?order=' + cartState.token;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
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));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -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: 2.4 MiB |
|
|
@ -1,9 +0,0 @@
|
||||||
Sort: 1
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
Uuid: deupkqq83jvloz0r
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
Template: image
|
|
||||||
5
content/error/error.en.txt
Normal file
5
content/error/error.en.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
Title: Error
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
Template: default
|
||||||
|
|
@ -1 +1,5 @@
|
||||||
|
Title: Erreur
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
Uuid: kcrqkszqasludg2h
|
Uuid: kcrqkszqasludg2h
|
||||||
13
content/thanks/thanks.en.txt
Normal file
13
content/thanks/thanks.en.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
Title: Thank you for your order
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
Text:
|
||||||
|
|
||||||
|
Your order has been confirmed! You will receive a confirmation email shortly.
|
||||||
|
|
||||||
|
Thank you for your purchase.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
Template: thanks
|
||||||
|
|
@ -7,3 +7,7 @@ Text:
|
||||||
Votre commande a été confirmée ! Vous allez recevoir un email de confirmation sous peu.
|
Votre commande a été confirmée ! Vous allez recevoir un email de confirmation sous peu.
|
||||||
|
|
||||||
Merci pour votre achat.
|
Merci pour votre achat.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
Template: thanks
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1 @@
|
||||||
Title: Thank you for your order
|
Title: Thanks
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
Text:
|
|
||||||
|
|
||||||
Your order has been confirmed! You will receive a confirmation email shortly.
|
|
||||||
|
|
||||||
Thank you for your purchase.
|
|
||||||
73
create-og-image.html
Normal file
73
create-og-image.html
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Generate OG Image</title>
|
||||||
|
<style>
|
||||||
|
#canvas-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
#preview {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
#preview img {
|
||||||
|
max-width: 600px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Générer l'image Open Graph</h1>
|
||||||
|
<p>Cette page génère une image 1200x630px avec le logo Index pour les partages sociaux.</p>
|
||||||
|
<button onclick="generateImage()">Générer l'image</button>
|
||||||
|
<div id="canvas-container">
|
||||||
|
<canvas id="canvas" width="1200" height="630"></canvas>
|
||||||
|
</div>
|
||||||
|
<div id="preview"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function generateImage() {
|
||||||
|
const canvas = document.getElementById('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Fond blanc
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.fillRect(0, 0, 1200, 630);
|
||||||
|
|
||||||
|
// Charger et dessiner le logo SVG
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = function() {
|
||||||
|
// Centrer le logo
|
||||||
|
const logoWidth = 400;
|
||||||
|
const logoHeight = (this.height / this.width) * logoWidth;
|
||||||
|
const x = (1200 - logoWidth) / 2;
|
||||||
|
const y = (630 - logoHeight) / 2 - 50;
|
||||||
|
|
||||||
|
ctx.drawImage(img, x, y, logoWidth, logoHeight);
|
||||||
|
|
||||||
|
// Ajouter le texte en dessous
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.font = '24px sans-serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText('Boutique de Index, ONG d\'investigation indépendante', 600, 500);
|
||||||
|
|
||||||
|
// Convertir en PNG et afficher
|
||||||
|
canvas.toBlob(function(blob) {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const preview = document.getElementById('preview');
|
||||||
|
preview.innerHTML = `
|
||||||
|
<h2>Aperçu</h2>
|
||||||
|
<img src="${url}" alt="OG Image Preview">
|
||||||
|
<p><a href="${url}" download="og-logo.png">Télécharger og-logo.png</a></p>
|
||||||
|
<p>Placez ce fichier dans <code>/assets/og-logo.png</code></p>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
img.src = 'assets/index-logo.svg';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,134 +1,21 @@
|
||||||
title:
|
title: Product
|
||||||
en: Product
|
|
||||||
fr: Produit
|
|
||||||
icon: cart
|
icon: cart
|
||||||
|
|
||||||
tabs:
|
|
||||||
content:
|
|
||||||
label:
|
|
||||||
en: Content
|
|
||||||
fr: Contenu
|
|
||||||
columns:
|
columns:
|
||||||
- width: 2/3
|
- width: 1/1
|
||||||
sections:
|
|
||||||
main:
|
|
||||||
type: fields
|
|
||||||
fields:
|
fields:
|
||||||
price:
|
info:
|
||||||
label:
|
type: info
|
||||||
en: Price (€)
|
text:
|
||||||
fr: Prix (€)
|
en: "Product data (title, description, images, price) is managed in Shopify Admin. This Kirby page only serves for routing."
|
||||||
type: number
|
fr: "Les données produit (titre, description, images, prix) sont gérées dans Shopify Admin. Cette page Kirby sert uniquement au routing."
|
||||||
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
|
|
||||||
|
|
||||||
- width: 1/3
|
shopifyHandle:
|
||||||
sections:
|
label:
|
||||||
images:
|
en: Shopify Handle
|
||||||
type: files
|
fr: Shopify Handle
|
||||||
headline:
|
type: text
|
||||||
en: Product Images
|
help:
|
||||||
fr: Images du produit
|
en: "Product handle from Shopify (e.g. tshirt-index-01). If empty, uses the page slug."
|
||||||
template: image
|
fr: "Handle du produit Shopify (ex: tshirt-index-01). Si vide, utilise le slug de la page Kirby."
|
||||||
layout: cards
|
placeholder: tshirt-index-01
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
title: Site
|
title: Site
|
||||||
|
|
||||||
sections:
|
sections:
|
||||||
pages:
|
shopify:
|
||||||
type: pages
|
type: fields
|
||||||
headline:
|
fields:
|
||||||
en: Products
|
shopifyRefreshButton:
|
||||||
fr: Produits
|
type: shopify-refresh
|
||||||
template: product
|
label:
|
||||||
sortBy: title asc
|
en: Shopify Products
|
||||||
info: "{{ page.stock }} en stock"
|
fr: Produits Shopify
|
||||||
layout: cardlets
|
|
||||||
image:
|
|
||||||
query: page.files.first
|
|
||||||
cover: true
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,142 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/shopify.php';
|
||||||
|
|
||||||
|
use Kirby\Cms\Page;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'debug' => true,
|
'debug' => true,
|
||||||
|
|
||||||
'languages' => true,
|
'languages' => true,
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
'shopify' => true
|
||||||
|
],
|
||||||
|
|
||||||
|
'routes' => [
|
||||||
|
// Sitemap
|
||||||
|
[
|
||||||
|
'pattern' => 'sitemap.xml',
|
||||||
|
'action' => function() {
|
||||||
|
$sitemap = page('home');
|
||||||
|
return new Kirby\Cms\Response(
|
||||||
|
snippet('sitemap', ['page' => $sitemap], true),
|
||||||
|
'application/xml'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// English homepage
|
||||||
|
[
|
||||||
|
'pattern' => 'en',
|
||||||
|
'action' => function() {
|
||||||
|
$home = page('home');
|
||||||
|
if ($home) {
|
||||||
|
site()->visit($home, 'en');
|
||||||
|
return $home;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// English thanks page
|
||||||
|
[
|
||||||
|
'pattern' => 'en/thanks',
|
||||||
|
'action' => function() {
|
||||||
|
$thanks = page('thanks');
|
||||||
|
if ($thanks) {
|
||||||
|
site()->visit($thanks, 'en');
|
||||||
|
return $thanks;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// English error page
|
||||||
|
[
|
||||||
|
'pattern' => 'en/error',
|
||||||
|
'action' => function() {
|
||||||
|
$error = page('error');
|
||||||
|
if ($error) {
|
||||||
|
site()->visit($error, 'en');
|
||||||
|
return $error;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// French thanks page
|
||||||
|
[
|
||||||
|
'pattern' => 'thanks',
|
||||||
|
'action' => function() {
|
||||||
|
return page('thanks');
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// French error page
|
||||||
|
[
|
||||||
|
'pattern' => 'error',
|
||||||
|
'action' => function() {
|
||||||
|
return page('error');
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// French products (default)
|
||||||
|
[
|
||||||
|
'pattern' => '(:any)',
|
||||||
|
'action' => function($slug) {
|
||||||
|
if (in_array($slug, ['home', 'error', 'thanks'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$products = getShopifyProducts();
|
||||||
|
|
||||||
|
foreach ($products as $product) {
|
||||||
|
if ($product['handle'] === $slug) {
|
||||||
|
$page = Page::factory([
|
||||||
|
'slug' => $product['handle'],
|
||||||
|
'template' => 'product',
|
||||||
|
'content' => [
|
||||||
|
'title' => $product['title'],
|
||||||
|
'shopifyHandle' => $product['handle'],
|
||||||
|
'uuid' => $product['id']
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
site()->visit($page, 'fr');
|
||||||
|
return $page;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// English products
|
||||||
|
[
|
||||||
|
'pattern' => 'en/(:any)',
|
||||||
|
'action' => function($slug) {
|
||||||
|
if (in_array($slug, ['home', 'error', 'thanks'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$products = getShopifyProducts();
|
||||||
|
|
||||||
|
foreach ($products as $product) {
|
||||||
|
if ($product['handle'] === $slug) {
|
||||||
|
$page = Page::factory([
|
||||||
|
'slug' => $product['handle'],
|
||||||
|
'template' => 'product',
|
||||||
|
'content' => [
|
||||||
|
'title' => $product['title'],
|
||||||
|
'shopifyHandle' => $product['handle'],
|
||||||
|
'uuid' => $product['id']
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
site()->visit($page, 'en');
|
||||||
|
return $page;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
|
||||||
'thumbs' => [
|
'thumbs' => [
|
||||||
'quality' => 85,
|
'quality' => 85,
|
||||||
'format' => 'webp',
|
'format' => 'webp',
|
||||||
|
|
@ -35,114 +167,4 @@ return [
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'routes' => [
|
|
||||||
[
|
|
||||||
'pattern' => '(:any)/validate.json',
|
|
||||||
'method' => 'GET',
|
|
||||||
'action' => function ($slug) {
|
|
||||||
$page = page($slug);
|
|
||||||
|
|
||||||
if (!$page || $page->intendedTemplate() !== 'product') {
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
http_response_code(404);
|
|
||||||
echo json_encode(['error' => 'Product not found']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer le stock actuel
|
|
||||||
$stock = (int) $page->stock()->value();
|
|
||||||
|
|
||||||
// Préparer la réponse JSON pour Snipcart
|
|
||||||
$response = [
|
|
||||||
'id' => $page->slug(),
|
|
||||||
'price' => (float) $page->price()->value(),
|
|
||||||
'url' => $page->url() . '/validate.json',
|
|
||||||
'name' => $page->title()->value(),
|
|
||||||
'description' => $page->description()->value(),
|
|
||||||
'image' => $page->images()->first() ? $page->images()->first()->url() : '',
|
|
||||||
'inventory' => $stock,
|
|
||||||
'stock' => $stock
|
|
||||||
];
|
|
||||||
|
|
||||||
// Ajouter les options si disponibles
|
|
||||||
if ($page->hasOptions()->toBool() && $page->optionValues()->isNotEmpty()) {
|
|
||||||
$values = $page->optionValues()->split(',');
|
|
||||||
$trimmedValues = array_map('trim', $values);
|
|
||||||
$snipcartOptions = implode('|', $trimmedValues);
|
|
||||||
|
|
||||||
$response['customFields'] = [
|
|
||||||
[
|
|
||||||
'name' => $page->optionLabel()->value(),
|
|
||||||
'options' => $snipcartOptions,
|
|
||||||
'required' => true
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
echo json_encode($response);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'pattern' => 'snipcart-webhook',
|
|
||||||
'method' => 'POST',
|
|
||||||
'action' => function () {
|
|
||||||
// Webhook handler pour Snipcart
|
|
||||||
// Vérifie la signature et décrémente le stock
|
|
||||||
|
|
||||||
$requestBody = file_get_contents('php://input');
|
|
||||||
$event = json_decode($requestBody, true);
|
|
||||||
|
|
||||||
// Vérifier la signature Snipcart (à implémenter avec la clé secrète)
|
|
||||||
// $signature = $_SERVER['HTTP_X_SNIPCART_REQUESTTOKEN'] ?? '';
|
|
||||||
|
|
||||||
if (!$event || !isset($event['eventName'])) {
|
|
||||||
return Response::json(['error' => 'Invalid request'], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gérer l'événement order.completed
|
|
||||||
if ($event['eventName'] === 'order.completed') {
|
|
||||||
$order = $event['content'] ?? null;
|
|
||||||
|
|
||||||
if ($order && isset($order['items'])) {
|
|
||||||
// Impersonate pour avoir les permissions d'écriture
|
|
||||||
kirby()->impersonate('kirby');
|
|
||||||
|
|
||||||
foreach ($order['items'] as $item) {
|
|
||||||
$productId = $item['id'] ?? null;
|
|
||||||
$quantity = $item['quantity'] ?? 0;
|
|
||||||
|
|
||||||
if ($productId && $quantity > 0) {
|
|
||||||
// Trouver le produit par son slug
|
|
||||||
$products = site()->index()->filterBy('intendedTemplate', 'product');
|
|
||||||
|
|
||||||
foreach ($products as $product) {
|
|
||||||
if ($product->slug() === $productId) {
|
|
||||||
// Décrémenter le stock
|
|
||||||
$currentStock = (int) $product->stock()->value();
|
|
||||||
$newStock = max(0, $currentStock - $quantity);
|
|
||||||
|
|
||||||
// Mettre à jour le stock
|
|
||||||
try {
|
|
||||||
$product->update([
|
|
||||||
'stock' => $newStock
|
|
||||||
]);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
// Log l'erreur mais continue le traitement
|
|
||||||
error_log('Webhook stock update error: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response::json(['status' => 'success'], 200);
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
82
site/config/shopify.php
Normal file
82
site/config/shopify.php
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les produits Shopify avec cache (TTL 1h)
|
||||||
|
*/
|
||||||
|
function getShopifyProducts(): array
|
||||||
|
{
|
||||||
|
$cache = kirby()->cache('shopify');
|
||||||
|
$products = $cache->get('products');
|
||||||
|
|
||||||
|
if ($products === null) {
|
||||||
|
$products = fetchShopifyProducts();
|
||||||
|
$cache->set('products', $products, 60); // Cache 60 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
return $products;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appel direct à l'API Shopify Storefront
|
||||||
|
*/
|
||||||
|
function fetchShopifyProducts(): array
|
||||||
|
{
|
||||||
|
$domain = 'nv7cqv-bu.myshopify.com';
|
||||||
|
$token = 'dec3d35a2554384d149c72927d1cfd1b';
|
||||||
|
$apiVersion = '2026-01';
|
||||||
|
$endpoint = "https://{$domain}/api/{$apiVersion}/graphql.json";
|
||||||
|
|
||||||
|
$query = '
|
||||||
|
query getAllProducts {
|
||||||
|
products(first: 250, sortKey: TITLE) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
handle
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
';
|
||||||
|
|
||||||
|
$ch = curl_init($endpoint);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Nécessaire pour dev local sur Windows
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'X-Shopify-Storefront-Access-Token: ' . $token
|
||||||
|
]);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
||||||
|
'query' => $query
|
||||||
|
]));
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($httpCode !== 200) {
|
||||||
|
error_log("Shopify API error: HTTP {$httpCode}");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
|
||||||
|
if (isset($data['errors'])) {
|
||||||
|
error_log("Shopify API GraphQL errors: " . json_encode($data['errors']));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$products = [];
|
||||||
|
foreach ($data['data']['products']['edges'] as $edge) {
|
||||||
|
$node = $edge['node'];
|
||||||
|
$products[] = [
|
||||||
|
'id' => $node['id'],
|
||||||
|
'handle' => $node['handle'],
|
||||||
|
'title' => $node['title']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $products;
|
||||||
|
}
|
||||||
11
site/controllers/product.php
Normal file
11
site/controllers/product.php
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return function ($page, $kirby) {
|
||||||
|
$shopifyHandle = $page->shopifyHandle()->or($page->slug())->value();
|
||||||
|
$language = $kirby->language()->code();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'shopifyHandle' => $shopifyHandle,
|
||||||
|
'language' => $language
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
@ -13,7 +13,24 @@ return [
|
||||||
'backToShop' => 'Back to shop',
|
'backToShop' => 'Back to shop',
|
||||||
'supportText' => 'To support us, you can also',
|
'supportText' => 'To support us, you can also',
|
||||||
'makeDonation' => 'make a donation',
|
'makeDonation' => 'make a donation',
|
||||||
|
|
||||||
|
// Shop / Cart
|
||||||
'addToCart' => 'Add to 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',
|
||||||
|
'chooseOption' => 'Choose an option',
|
||||||
|
|
||||||
// Blueprints - Home
|
// Blueprints - Home
|
||||||
'home.title' => 'Home',
|
'home.title' => 'Home',
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,24 @@ return [
|
||||||
'backToShop' => 'Retour à la boutique',
|
'backToShop' => 'Retour à la boutique',
|
||||||
'supportText' => 'Pour nous soutenir, vous pouvez aussi',
|
'supportText' => 'Pour nous soutenir, vous pouvez aussi',
|
||||||
'makeDonation' => 'faire un don',
|
'makeDonation' => 'faire un don',
|
||||||
|
|
||||||
|
// Shop / Cart
|
||||||
'addToCart' => 'Ajouter au panier',
|
'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',
|
||||||
|
'chooseOption' => 'Choisissez une option',
|
||||||
|
|
||||||
// Blueprints - Home
|
// Blueprints - Home
|
||||||
'home.title' => 'Accueil',
|
'home.title' => 'Accueil',
|
||||||
|
|
|
||||||
20
site/plugins/shopify-refresh-button/.editorconfig
Normal file
20
site/plugins/shopify-refresh-button/.editorconfig
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# This file is for unifying the coding style for different editors and IDEs
|
||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.php]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.md,*.txt]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
insert_final_newline = false
|
||||||
|
|
||||||
|
[composer.json]
|
||||||
|
indent_size = 4
|
||||||
11
site/plugins/shopify-refresh-button/.gitattributes
vendored
Normal file
11
site/plugins/shopify-refresh-button/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Note: You need to uncomment the lines you want to use; the other lines can be deleted
|
||||||
|
|
||||||
|
# Git
|
||||||
|
# .gitattributes export-ignore
|
||||||
|
# .gitignore export-ignore
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
# /.coveralls.yml export-ignore
|
||||||
|
# /.travis.yml export-ignore
|
||||||
|
# /phpunit.xml.dist export-ignore
|
||||||
|
# /tests/ export-ignore
|
||||||
14
site/plugins/shopify-refresh-button/.gitignore
vendored
Normal file
14
site/plugins/shopify-refresh-button/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# npm modules
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Parcel cache folder
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Composer files
|
||||||
|
/vendor
|
||||||
|
|
||||||
|
# kirbyup temp development entry
|
||||||
|
/index.dev.mjs
|
||||||
21
site/plugins/shopify-refresh-button/LICENSE.md
Executable file
21
site/plugins/shopify-refresh-button/LICENSE.md
Executable file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) <Year> <Your Name>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
117
site/plugins/shopify-refresh-button/README.md
Executable file
117
site/plugins/shopify-refresh-button/README.md
Executable file
|
|
@ -0,0 +1,117 @@
|
||||||
|
# Kirby Pluginkit: Example plugin for Kirby
|
||||||
|
|
||||||
|
> Variant "Panel plugin setup"
|
||||||
|
|
||||||
|
This is a boilerplate for a Kirby Panel plugin that can be installed via all three [supported installation methods](https://getkirby.com/docs/guide/plugins/plugin-setup-basic#the-three-plugin-installation-methods).
|
||||||
|
|
||||||
|
You can find a list of Pluginkit variants on the [`master` branch](https://github.com/getkirby/pluginkit/tree/master).
|
||||||
|
|
||||||
|
****
|
||||||
|
|
||||||
|
## How to use the Pluginkit
|
||||||
|
|
||||||
|
1. Fork this repository
|
||||||
|
2. Change the plugin name and description in the `composer.json`
|
||||||
|
3. Change the plugin name in the `index.php` and `src/index.js`
|
||||||
|
4. Change the license if you don't want to publish under MIT
|
||||||
|
5. Add your plugin code to the `index.php` and `src/index.js`
|
||||||
|
6. Update this `README` with instructions for your plugin
|
||||||
|
|
||||||
|
### Install the development and build setup
|
||||||
|
|
||||||
|
We use [kirbyup](https://github.com/johannschopplich/kirbyup) for the development and build setup.
|
||||||
|
|
||||||
|
You can start developing directly. kirbyup will be fetched remotely with your first `npm run` command, which may take a short amount of time.
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
You can start the dev process with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This will automatically update the `index.js` and `index.css` of your plugin as soon as you make changes.
|
||||||
|
Reload the Panel to see your code changes reflected.
|
||||||
|
|
||||||
|
With kirbyup 2.0.0+ and Kirby 3.7.4+ you can alternatively use hot module reloading (HMR):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start a development server that updates the page as soon as you make changes. Some updates are instant, like CSS or Vue template changes, others require a reload of the page, which happens automatically.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The live reload functionality requires top level await, [which is only supported in modern browsers](https://caniuse.com/mdn-javascript_operators_await_top_level). If you're developing in older browsers, use `npm run dev` and reload the page manually to see changes.
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
As soon as you are happy with your plugin, you should build the final version with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will automatically create a minified and optimized version of your `index.js` and `index.css`
|
||||||
|
which you can ship with your plugin.
|
||||||
|
|
||||||
|
We have a tutorial on how to build your own plugin based on the Pluginkit [in the Kirby documentation](https://getkirby.com/docs/guide/plugins/plugin-setup-basic).
|
||||||
|
|
||||||
|
### Build reproducibility
|
||||||
|
|
||||||
|
While kirbyup will stay backwards compatible, exact build reproducibility may be of importance to you. If so, we recommend to target a specific package version, rather than using npx:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "kirbyup src/index.js --watch",
|
||||||
|
"build": "kirbyup src/index.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"kirbyup": "^3.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
What follows is an example README for your plugin.
|
||||||
|
|
||||||
|
****
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Download
|
||||||
|
|
||||||
|
Download and copy this repository to `/site/plugins/{{ plugin-name }}`.
|
||||||
|
|
||||||
|
### Git submodule
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule add https://github.com/{{ your-name }}/{{ plugin-name }}.git site/plugins/{{ plugin-name }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Composer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require {{ your-name }}/{{ plugin-name }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
*Additional instructions on how to configure the plugin (e.g. blueprint setup, config options, etc.)*
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
*Document the options and APIs that this plugin offers*
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
*Add instructions on how to help working on the plugin (e.g. npm setup, Composer dev dependencies, etc.)*
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- [Your Name](https://github.com/ghost)
|
||||||
18
site/plugins/shopify-refresh-button/SECURITY.md
Normal file
18
site/plugins/shopify-refresh-button/SECURITY.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
*Use this section to tell people about which versions of your project are currently being supported with security updates.*
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 5.1.x | :white_check_mark: |
|
||||||
|
| 5.0.x | :x: |
|
||||||
|
| 4.0.x | :white_check_mark: |
|
||||||
|
| < 4.0 | :x: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
*Use this section to tell people how to report a vulnerability.*
|
||||||
|
|
||||||
|
*Tell them where to go, how often they can expect to get an update on a reported vulnerability, what to expect if the vulnerability is accepted or declined, etc.*
|
||||||
21
site/plugins/shopify-refresh-button/composer.json
Executable file
21
site/plugins/shopify-refresh-button/composer.json
Executable file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "getkirby/pluginkit",
|
||||||
|
"description": "Kirby Example Plugin",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "kirby-plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Your Name",
|
||||||
|
"email": "you@example.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"getkirby/composer-installer": "^1.1"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"getkirby/composer-installer": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
site/plugins/shopify-refresh-button/composer.lock
generated
Normal file
66
site/plugins/shopify-refresh-button/composer.lock
generated
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
{
|
||||||
|
"_readme": [
|
||||||
|
"This file locks the dependencies of your project to a known state",
|
||||||
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
|
"This file is @generated automatically"
|
||||||
|
],
|
||||||
|
"content-hash": "37a8e61308b9b6f49cb9835f477f0c64",
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "getkirby/composer-installer",
|
||||||
|
"version": "1.2.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/getkirby/composer-installer.git",
|
||||||
|
"reference": "c98ece30bfba45be7ce457e1102d1b169d922f3d"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/getkirby/composer-installer/zipball/c98ece30bfba45be7ce457e1102d1b169d922f3d",
|
||||||
|
"reference": "c98ece30bfba45be7ce457e1102d1b169d922f3d",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"composer-plugin-api": "^1.0 || ^2.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"composer/composer": "^1.8 || ^2.0"
|
||||||
|
},
|
||||||
|
"type": "composer-plugin",
|
||||||
|
"extra": {
|
||||||
|
"class": "Kirby\\ComposerInstaller\\Plugin"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Kirby\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "Kirby's custom Composer installer for the Kirby CMS and for Kirby plugins",
|
||||||
|
"homepage": "https://getkirby.com",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/getkirby/composer-installer/issues",
|
||||||
|
"source": "https://github.com/getkirby/composer-installer/tree/1.2.1"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://getkirby.com/buy",
|
||||||
|
"type": "custom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2020-12-28T12:54:39+00:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"packages-dev": [],
|
||||||
|
"aliases": [],
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"stability-flags": [],
|
||||||
|
"prefer-stable": false,
|
||||||
|
"prefer-lowest": false,
|
||||||
|
"platform": [],
|
||||||
|
"platform-dev": [],
|
||||||
|
"plugin-api-version": "2.6.0"
|
||||||
|
}
|
||||||
2
site/plugins/shopify-refresh-button/index.js
Normal file
2
site/plugins/shopify-refresh-button/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
(function(){"use strict";function l(n,r,t,e,o,a,s,f){var i=typeof n=="function"?n.options:n;return r&&(i.render=r,i.staticRenderFns=t,i._compiled=!0),{exports:n,options:i}}const p={__name:"ShopifyRefreshButton",props:{products:{type:Array,default:()=>[]}},setup(n){const r=n,t=Vue.ref("Synchroniser depuis Shopify"),e=Vue.ref("refresh"),o=Vue.ref("aqua-icon"),a=Vue.ref(!1),s=Vue.ref([]);Vue.onMounted(()=>{s.value=r.products||[]});const f=Vue.computed(()=>{const u=s.value.length;return`${u} produit${u>1?"s":""} Shopify en cache`}),i=Vue.computed(()=>s.value.length===0?"Aucun produit trouvé. Cliquez sur 'Rafraîchir Shopify' pour récupérer les produits.":s.value.map(u=>`• ${u.title}<br/>`).join(`
|
||||||
|
`));async function y(){a.value=!0,e.value="loader",o.value="orange-icon",t.value="En cours…";try{const c=await(await fetch("/shopify/refresh-cache.json",{method:"POST",headers:{"Content-Type":"application/json"}})).json();if(c.status==="error")throw new Error(c.message);s.value=c.products||[],t.value=`Terminé - ${c.count} produit${c.count>1?"s":""}`,e.value="check",o.value="green-icon",setTimeout(()=>{t.value="Synchroniser depuis Shopify",e.value="refresh",o.value="aqua-icon",a.value=!1},3e3)}catch(u){console.error(u),t.value="Erreur",e.value="alert",o.value="red-icon",setTimeout(()=>{t.value="Synchroniser depuis Shopify",e.value="refresh",o.value="aqua-icon",a.value=!1},3e3)}}return{__sfc:!0,props:r,text:t,icon:e,theme:o,isProcessing:a,currentProducts:s,infoLabel:f,infoText:i,refreshCache:y}}};var h=function(){var r=this,t=r._self._c,e=r._self._setupProxy;return t("div",[t("k-info-field",{attrs:{label:e.infoLabel,text:e.infoText,theme:"info"}}),t("k-button",{staticStyle:{"margin-top":"1rem"},attrs:{theme:e.theme,variant:"dimmed",icon:e.icon,title:"Synchroniser le cache des produits Shopify",disabled:e.isProcessing},on:{click:function(o){return e.refreshCache()}}},[r._v(" "+r._s(e.text)+" ")])],1)},d=[],v=l(p,h,d);const m=v.exports;window.panel.plugin("index/shopify-refresh-button",{fields:{"shopify-refresh":m}})})();
|
||||||
46
site/plugins/shopify-refresh-button/index.php
Executable file
46
site/plugins/shopify-refresh-button/index.php
Executable file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
Kirby::plugin('index/shopify-refresh-button', [
|
||||||
|
'fields' => [
|
||||||
|
'shopify-refresh' => [
|
||||||
|
'props' => [
|
||||||
|
'products' => function() {
|
||||||
|
return getShopifyProducts();
|
||||||
|
}
|
||||||
|
],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'routes' => [
|
||||||
|
[
|
||||||
|
'pattern' => 'shopify/refresh-cache.json',
|
||||||
|
'method' => 'POST',
|
||||||
|
'action' => function() {
|
||||||
|
if (!kirby()->user()) {
|
||||||
|
return [
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Unauthorized'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
kirby()->cache('shopify')->flush();
|
||||||
|
|
||||||
|
$products = fetchShopifyProducts();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Cache Shopify rafraîchi avec succès',
|
||||||
|
'count' => count($products),
|
||||||
|
'products' => $products
|
||||||
|
];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return [
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreur lors du rafraîchissement du cache',
|
||||||
|
'details' => $e->getMessage()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
7
site/plugins/shopify-refresh-button/package.json
Normal file
7
site/plugins/shopify-refresh-button/package.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "npx -y kirbyup src/index.js --watch",
|
||||||
|
"serve": "npx -y kirbyup serve src/index.js",
|
||||||
|
"build": "npx -y kirbyup src/index.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<k-info-field :label="infoLabel" :text="infoText" theme="info" />
|
||||||
|
|
||||||
|
<k-button
|
||||||
|
style="margin-top: 1rem"
|
||||||
|
:theme="theme"
|
||||||
|
variant="dimmed"
|
||||||
|
:icon="icon"
|
||||||
|
title="Synchroniser le cache des produits Shopify"
|
||||||
|
@click="refreshCache()"
|
||||||
|
:disabled="isProcessing"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</k-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
products: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = ref("Synchroniser depuis Shopify");
|
||||||
|
const icon = ref("refresh");
|
||||||
|
const theme = ref("aqua-icon");
|
||||||
|
const isProcessing = ref(false);
|
||||||
|
const currentProducts = ref([]);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
currentProducts.value = props.products || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const infoLabel = computed(() => {
|
||||||
|
const count = currentProducts.value.length;
|
||||||
|
return `${count} produit${count > 1 ? "s" : ""} Shopify en cache`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const infoText = computed(() => {
|
||||||
|
if (currentProducts.value.length === 0) {
|
||||||
|
return "Aucun produit trouvé. Cliquez sur 'Rafraîchir Shopify' pour récupérer les produits.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentProducts.value.map((p) => `• ${p.title}<br/>`).join("\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
async function refreshCache() {
|
||||||
|
isProcessing.value = true;
|
||||||
|
icon.value = "loader";
|
||||||
|
theme.value = "orange-icon";
|
||||||
|
text.value = "En cours…";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/shopify/refresh-cache.json", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.status === "error") {
|
||||||
|
throw new Error(json.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentProducts.value = json.products || [];
|
||||||
|
text.value = `Terminé - ${json.count} produit${json.count > 1 ? "s" : ""}`;
|
||||||
|
icon.value = "check";
|
||||||
|
theme.value = "green-icon";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
text.value = "Synchroniser depuis Shopify";
|
||||||
|
icon.value = "refresh";
|
||||||
|
theme.value = "aqua-icon";
|
||||||
|
isProcessing.value = false;
|
||||||
|
}, 3000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
text.value = "Erreur";
|
||||||
|
icon.value = "alert";
|
||||||
|
theme.value = "red-icon";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
text.value = "Synchroniser depuis Shopify";
|
||||||
|
icon.value = "refresh";
|
||||||
|
theme.value = "aqua-icon";
|
||||||
|
isProcessing.value = false;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
7
site/plugins/shopify-refresh-button/src/index.js
Executable file
7
site/plugins/shopify-refresh-button/src/index.js
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import ShopifyRefreshButton from "./components/ShopifyRefreshButton.vue";
|
||||||
|
|
||||||
|
window.panel.plugin("index/shopify-refresh-button", {
|
||||||
|
fields: {
|
||||||
|
"shopify-refresh": ShopifyRefreshButton
|
||||||
|
}
|
||||||
|
});
|
||||||
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>
|
||||||
20
site/snippets/buy-button.php
Normal file
20
site/snippets/buy-button.php
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<div class="add-to-cart">
|
||||||
|
<button
|
||||||
|
class="btn__default"
|
||||||
|
data-shopify-add-to-cart
|
||||||
|
data-product-id=""
|
||||||
|
data-variant-id=""
|
||||||
|
data-default-text="<?= t('addToCart') ?>"
|
||||||
|
data-text-choose-option="<?= t('chooseOption') ?>"
|
||||||
|
data-text-adding="<?= t('addingToCart') ?>"
|
||||||
|
data-text-added="<?= t('addedToCart') ?>"
|
||||||
|
data-text-error="<?= t('errorAddToCart') ?>"
|
||||||
|
>
|
||||||
|
<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-.53c-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-button-text><?= t('addToCart') ?></div>
|
||||||
|
</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">
|
<footer id="site-footer">
|
||||||
<div class="site-footer__container">
|
<div class="site-footer__container">
|
||||||
<div class="footer__mentions">
|
<div class="footer__mentions">
|
||||||
|
|
@ -10,10 +12,17 @@
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="<?= url('assets/js/onload.js') ?>"></script>
|
<script src="<?= url('assets/js/onload.js') ?>"></script>
|
||||||
<?php if(isset($scripts) && is_array($scripts)): ?>
|
<script src="<?= url('assets/js/shopify-cart.js') ?>"></script>
|
||||||
<?php foreach($scripts as $script): ?>
|
|
||||||
<script src="<?= url($script) ?>"></script>
|
<?php if ($scripts ?? null): ?>
|
||||||
<?php endforeach ?>
|
<?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 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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,16 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="<?= $kirby->language()->code() ?>">
|
<html lang="<?= $kirby->language()->code() ?>">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<?php snippet('seo') ?>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="robots" content="noindex, nofollow">
|
|
||||||
<title><?= $title ?? $page->title() ?> | Index.ngo</title>
|
|
||||||
<link rel="icon" type="image/png" href="<?= url('assets/favicon.png') ?>"/>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.css" />
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.css" />
|
||||||
<script src="https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.js"></script>
|
||||||
|
|
||||||
<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/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/style.css') ?>" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="<?= url('assets/css/cart-drawer.css') ?>" />
|
||||||
</head>
|
</head>
|
||||||
<body data-template="<?= $template ?? 'default' ?>">
|
<body data-template="<?= $page->template() ?>">
|
||||||
<header id="site-header">
|
<header id="site-header">
|
||||||
<div class="header-left"></div>
|
<div class="header-left"></div>
|
||||||
|
|
||||||
|
|
@ -46,5 +43,9 @@
|
||||||
</li>
|
</li>
|
||||||
<?php endforeach ?>
|
<?php endforeach ?>
|
||||||
</ul>
|
</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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
79
site/snippets/seo.php
Normal file
79
site/snippets/seo.php
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* SEO meta tags
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Language
|
||||||
|
$lang = $kirby->language()->code();
|
||||||
|
|
||||||
|
// Basic meta
|
||||||
|
$title = $page->customTitle()->or($page->title())->value();
|
||||||
|
$siteName = 'Index.ngo';
|
||||||
|
$fullTitle = $title . ' | ' . $siteName;
|
||||||
|
|
||||||
|
// Default descriptions by language
|
||||||
|
$defaultDescriptionFr = 'Boutique de Index, ONG d\'investigation indépendante';
|
||||||
|
$defaultDescriptionEn = 'Index shop, independent investigative NGO';
|
||||||
|
$defaultDescription = $lang == 'en' ? $defaultDescriptionEn : $defaultDescriptionFr;
|
||||||
|
|
||||||
|
$description = $page->metaDescription()->or($page->description())->value();
|
||||||
|
if ($description) {
|
||||||
|
$description = excerpt($description, 160);
|
||||||
|
} else {
|
||||||
|
$description = $defaultDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = $page->url();
|
||||||
|
// Use product image if available, otherwise use default OG image
|
||||||
|
// TODO: Create assets/og-logo.png (1200x630px) with Index logo + description
|
||||||
|
$image = $page->image() ? $page->image()->url() : url('assets/favicon.png');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Basic Meta Tags -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?= $fullTitle ?></title>
|
||||||
|
<?php if ($description): ?>
|
||||||
|
<meta name="description" content="<?= $description ?>">
|
||||||
|
<?php endif ?>
|
||||||
|
|
||||||
|
<!-- Canonical & Alternate Languages -->
|
||||||
|
<link rel="canonical" href="<?= $url ?>">
|
||||||
|
<?php foreach($kirby->languages() as $language): ?>
|
||||||
|
<link rel="alternate" hreflang="<?= $language->code() ?>" href="<?= $page->url($language->code()) ?>">
|
||||||
|
<?php endforeach ?>
|
||||||
|
<link rel="alternate" hreflang="x-default" href="<?= $page->url('fr') ?>">
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:type" content="<?= $page->template() == 'product' ? 'product' : 'website' ?>">
|
||||||
|
<meta property="og:site_name" content="<?= $siteName ?>">
|
||||||
|
<meta property="og:title" content="<?= $title ?>" id="og-title">
|
||||||
|
<meta property="og:description" content="<?= $description ?>" id="og-description">
|
||||||
|
<meta property="og:url" content="<?= $url ?>">
|
||||||
|
<meta property="og:image" content="<?= $image ?>" id="og-image">
|
||||||
|
<meta property="og:image:width" content="1200">
|
||||||
|
<meta property="og:image:height" content="630">
|
||||||
|
<meta property="og:locale" content="<?= $lang == 'fr' ? 'fr_FR' : 'en_US' ?>">
|
||||||
|
<?php foreach($kirby->languages() as $language): ?>
|
||||||
|
<?php if ($language->code() != $lang): ?>
|
||||||
|
<meta property="og:locale:alternate" content="<?= $language->code() == 'fr' ? 'fr_FR' : 'en_US' ?>">
|
||||||
|
<?php endif ?>
|
||||||
|
<?php endforeach ?>
|
||||||
|
<?php if ($page->template() == 'product'): ?>
|
||||||
|
<!-- Product-specific OG tags (will be updated by JS) -->
|
||||||
|
<meta property="product:price:amount" content="0" id="og-price">
|
||||||
|
<meta property="product:price:currency" content="EUR">
|
||||||
|
<meta property="product:availability" content="in stock" id="og-availability">
|
||||||
|
<meta property="product:brand" content="Index.ngo">
|
||||||
|
<?php endif ?>
|
||||||
|
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="<?= $title ?>">
|
||||||
|
<?php if ($description): ?>
|
||||||
|
<meta name="twitter:description" content="<?= $description ?>">
|
||||||
|
<?php endif ?>
|
||||||
|
<meta name="twitter:image" content="<?= $image ?>">
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" type="image/png" href="<?= url('assets/favicon.png') ?>">
|
||||||
54
site/snippets/sitemap.php
Normal file
54
site/snippets/sitemap.php
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?= '<?xml version="1.0" encoding="UTF-8"?>' ?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||||
|
xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||||
|
<?php
|
||||||
|
$site = site();
|
||||||
|
$kirby = kirby();
|
||||||
|
|
||||||
|
// Static pages
|
||||||
|
$pages = $site->index()->filterBy('template', 'in', ['home', 'error', 'thanks']);
|
||||||
|
|
||||||
|
foreach ($pages as $p) {
|
||||||
|
foreach ($kirby->languages() as $lang) {
|
||||||
|
$url = $p->url($lang->code());
|
||||||
|
?>
|
||||||
|
<url>
|
||||||
|
<loc><?= $url ?></loc>
|
||||||
|
<lastmod><?= $p->modified('c', 'date') ?></lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority><?= $p->isHomePage() ? '1.0' : '0.8' ?></priority>
|
||||||
|
<?php foreach ($kirby->languages() as $altLang): ?>
|
||||||
|
<xhtml:link rel="alternate" hreflang="<?= $altLang->code() ?>" href="<?= $p->url($altLang->code()) ?>" />
|
||||||
|
<?php endforeach ?>
|
||||||
|
</url>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Virtual product pages from Shopify
|
||||||
|
$products = getShopifyProducts();
|
||||||
|
foreach ($products as $product) {
|
||||||
|
foreach ($kirby->languages() as $lang) {
|
||||||
|
$url = $lang->code() == 'fr'
|
||||||
|
? $site->url() . '/' . $product['handle']
|
||||||
|
: $site->url() . '/en/' . $product['handle'];
|
||||||
|
?>
|
||||||
|
<url>
|
||||||
|
<loc><?= $url ?></loc>
|
||||||
|
<lastmod><?= date('c') ?></lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.9</priority>
|
||||||
|
<?php foreach ($kirby->languages() as $altLang): ?>
|
||||||
|
<?php
|
||||||
|
$altUrl = $altLang->code() == 'fr'
|
||||||
|
? $site->url() . '/' . $product['handle']
|
||||||
|
: $site->url() . '/en/' . $product['handle'];
|
||||||
|
?>
|
||||||
|
<xhtml:link rel="alternate" hreflang="<?= $altLang->code() ?>" href="<?= $altUrl ?>" />
|
||||||
|
<?php endforeach ?>
|
||||||
|
</url>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</urlset>
|
||||||
94
site/snippets/structured-data-product.php
Normal file
94
site/snippets/structured-data-product.php
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Structured Data (JSON-LD) for Product pages
|
||||||
|
* This will be populated by JavaScript after Shopify data is loaded
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
<script type="application/ld+json" id="product-schema">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org/",
|
||||||
|
"@type": "Product",
|
||||||
|
"name": "<?= $page->title() ?>",
|
||||||
|
"description": "",
|
||||||
|
"image": [],
|
||||||
|
"offers": {
|
||||||
|
"@type": "Offer",
|
||||||
|
"url": "<?= $page->url() ?>",
|
||||||
|
"priceCurrency": "EUR",
|
||||||
|
"price": "0",
|
||||||
|
"availability": "https://schema.org/InStock",
|
||||||
|
"seller": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "Index.ngo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"brand": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "Index.ngo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
// Update structured data when product loads
|
||||||
|
(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';
|
||||||
|
|
||||||
|
function initStructuredData() {
|
||||||
|
if (typeof ShopifyCart === 'undefined') {
|
||||||
|
setTimeout(initStructuredData, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cart = new ShopifyCart({
|
||||||
|
domain: 'nv7cqv-bu.myshopify.com',
|
||||||
|
storefrontAccessToken: 'dec3d35a2554384d149c72927d1cfd1b'
|
||||||
|
});
|
||||||
|
|
||||||
|
cart.getProductByHandle(handle).then(product => {
|
||||||
|
if (!product) return;
|
||||||
|
|
||||||
|
const title = isEnglish && product.titleEn?.value ? product.titleEn.value : product.title;
|
||||||
|
const description = isEnglish && product.descriptionEn?.value ? product.descriptionEn.value : product.description;
|
||||||
|
const price = parseFloat(product.priceRange.minVariantPrice.amount);
|
||||||
|
const images = product.images.edges.map(edge => edge.node.url);
|
||||||
|
const availability = product.availableForSale ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
"@context": "https://schema.org/",
|
||||||
|
"@type": "Product",
|
||||||
|
"name": title,
|
||||||
|
"description": description,
|
||||||
|
"image": images,
|
||||||
|
"offers": {
|
||||||
|
"@type": "Offer",
|
||||||
|
"url": window.location.href,
|
||||||
|
"priceCurrency": "EUR",
|
||||||
|
"price": price.toFixed(2),
|
||||||
|
"availability": availability,
|
||||||
|
"seller": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "Index.ngo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"brand": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "Index.ngo"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const schemaScript = document.getElementById('product-schema');
|
||||||
|
if (schemaScript) {
|
||||||
|
schemaScript.textContent = JSON.stringify(schema, null, 2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize when ShopifyCart is available
|
||||||
|
initStructuredData();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
@ -5,30 +5,19 @@
|
||||||
<?= $page->baseline()->or('Bienvenue sur la boutique de soutien à Index') ?>
|
<?= $page->baseline()->or('Bienvenue sur la boutique de soutien à Index') ?>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<section id="store__container">
|
<section id="store__container"
|
||||||
<?php foreach($site->children()->listed() as $product): ?>
|
data-products-loader
|
||||||
<article class="store__product">
|
data-language="<?= strtoupper($kirby->language()->code()) ?>">
|
||||||
<figure>
|
|
||||||
<?php if($cover = $product->files()->sort()->first()): ?>
|
<div class="products-loading">
|
||||||
<?php snippet('picture', [
|
<p><?= t('loading') ?></p>
|
||||||
'file' => $cover,
|
</div>
|
||||||
'alt' => $product->title()->html(),
|
|
||||||
'preset' => 'product-card',
|
|
||||||
'size' => 25,
|
|
||||||
'lazy' => true
|
|
||||||
]) ?>
|
|
||||||
<?php endif ?>
|
|
||||||
</figure>
|
|
||||||
<p class="line-1"><a href="<?= $product->url() ?>"><?= $product->title()->html() ?></a></p>
|
|
||||||
<p class="price"><?= $product->price() ?>€</p>
|
|
||||||
<a href="<?= $product->url() ?>" class="link-block" aria-hidden="true"></a>
|
|
||||||
</article>
|
|
||||||
<?php endforeach ?>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<p class="p__baseline-big">
|
<p class="p__baseline-big">
|
||||||
<?= t('supportText', 'Pour nous soutenir, vous pouvez aussi') ?>
|
<?= t('supportText') ?>
|
||||||
<a href="https://soutenir.index.ngo" class="link-don"><?= t('makeDonation', 'faire un don') ?></a>
|
<a href="https://soutenir.index.ngo" class="link-don"><?= t('makeDonation') ?></a>
|
||||||
</p>
|
</p>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,115 +1,52 @@
|
||||||
<?php snippet('header', ['title' => $page->title(), 'template' => 'shop']) ?>
|
<?php
|
||||||
|
$shopifyHandle = $page->shopifyHandle()->or($page->slug());
|
||||||
|
|
||||||
|
snippet('header', ['title' => $page->title(), 'template' => 'shop']);
|
||||||
|
?>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<nav class="store__nav">
|
<nav class="store__nav">
|
||||||
<a href="<?= $site->homePage()->url() ?>"><?= t('backToShop', 'Retour à la boutique') ?></a>
|
<a href="<?= $site->homePage()->url() ?>"><?= t('backToShop') ?></a>
|
||||||
</nav>
|
</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="col-left">
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
<h2 class="p__baseline-big"><?= $page->title()->html() ?></h2>
|
<h2 class="p__baseline-big" data-product-title></h2>
|
||||||
<p class="p__baseline-big"><?= $page->price() ?>€</p>
|
<p class="p__baseline-big" data-product-price></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="details">
|
<div class="details" data-product-details></div>
|
||||||
<?php if($page->details()->isNotEmpty()): ?>
|
|
||||||
<?= $page->details()->kt() ?>
|
<div class="product-options" data-product-options style="display: none;">
|
||||||
<?php endif ?>
|
<ul class="product-options__list" data-product-options-list></ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if($page->hasOptions()->toBool() && $page->optionValues()->isNotEmpty()): ?>
|
<?php snippet('buy-button') ?>
|
||||||
<div class="product-options">
|
|
||||||
<ul class="product-options__list">
|
|
||||||
<?php
|
|
||||||
$values = $page->optionValues()->split(',');
|
|
||||||
$optionSlug = $page->optionLabel()->slug();
|
|
||||||
foreach($values as $index => $value):
|
|
||||||
$value = trim($value);
|
|
||||||
$uniqueId = $optionSlug . '-' . Str::slug(strtolower($value));
|
|
||||||
?>
|
|
||||||
<li>
|
|
||||||
<input type="radio" id="<?= $uniqueId ?>" name="<?= $optionSlug ?>" value="<?= $value ?>" />
|
|
||||||
<label for="<?= $uniqueId ?>"><?= $value ?></label>
|
|
||||||
</li>
|
|
||||||
<?php endforeach ?>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<?php endif ?>
|
|
||||||
|
|
||||||
<div class="add-to-cart">
|
|
||||||
<button
|
|
||||||
class="btn__default snipcart-add-item"
|
|
||||||
data-item-id="<?= $page->slug() ?>"
|
|
||||||
data-item-price="<?= $page->price() ?>"
|
|
||||||
data-item-description="<?= $page->description()->excerpt(100) ?>"
|
|
||||||
data-item-image="<?= $page->images()->first() ? $page->images()->first()->url() : '' ?>"
|
|
||||||
data-item-name="<?= $page->title()->html() ?>"
|
|
||||||
data-item-shippable="true"
|
|
||||||
data-item-weight="<?= $page->weight()->or(0) ?>"
|
|
||||||
data-item-length="<?= $page->length()->or(0) ?>"
|
|
||||||
data-item-width="<?= $page->width()->or(0) ?>"
|
|
||||||
data-item-height="<?= $page->height()->or(0) ?>"
|
|
||||||
<?php
|
|
||||||
if($page->hasOptions()->toBool() && $page->optionValues()->isNotEmpty()):
|
|
||||||
$values = $page->optionValues()->split(',');
|
|
||||||
$trimmedValues = array_map('trim', $values);
|
|
||||||
$snipcartOptions = implode('|', $trimmedValues);
|
|
||||||
?>
|
|
||||||
data-item-custom1-name="<?= $page->optionLabel()->html() ?>"
|
|
||||||
data-item-custom1-options="<?= $snipcartOptions ?>"
|
|
||||||
data-item-custom1-required="true"
|
|
||||||
disabled
|
|
||||||
<?php endif; ?>
|
|
||||||
>
|
|
||||||
<span class="icon">
|
|
||||||
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="m14.523 18.787s4.501-4.505 6.255-6.26c.146-.146.219-.338.219-.53s-.073-.383-.219-.530c-1.753-1.754-6.255-6.258-6.255-6.258-.144-.145-.334-.217-.524-.217-.193 0-.385.074-.532.221-.293.292-.295.766-.004 1.056l4.978 4.978h-14.692c-.414 0-.75.336-.75.75s.336.75.75.75h14.692l-4.979 4.979c-.289.289-.286.762.006 1.054.148.148.341.222.533.222.19 0 .378-.072.522-.215z" fill-rule="nonzero" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<div class="txt" data-default-text="<?= t('addToCart', 'Ajouter au panier') ?>">
|
|
||||||
<?php if($page->hasOptions()->toBool() && $page->optionValues()->isNotEmpty()): ?>
|
|
||||||
<?= t('chooseOption', 'Choisissez une option') ?>
|
|
||||||
<?php else: ?>
|
|
||||||
<?= t('addToCart', 'Ajouter au panier') ?>
|
|
||||||
<?php endif ?>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="product-gallery swiper">
|
<div class="product-gallery swiper">
|
||||||
<div class="swiper-wrapper">
|
<div class="swiper-wrapper" data-product-images></div>
|
||||||
<?php
|
|
||||||
// Afficher uniquement les vraies images du produit
|
|
||||||
if ($page->hasFiles()):
|
|
||||||
foreach($page->files()->sortBy('sort', 'asc') as $image):
|
|
||||||
?>
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<figure>
|
|
||||||
<?php snippet('picture', [
|
|
||||||
'file' => $image,
|
|
||||||
'alt' => $page->title()->html(),
|
|
||||||
'preset' => 'product-detail',
|
|
||||||
'size' => 50,
|
|
||||||
'lazy' => false
|
|
||||||
]) ?>
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
endforeach;
|
|
||||||
endif;
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Navigation arrows -->
|
|
||||||
<div class="swiper-button-prev"></div>
|
<div class="swiper-button-prev"></div>
|
||||||
<div class="swiper-button-next"></div>
|
<div class="swiper-button-next"></div>
|
||||||
|
|
||||||
<!-- Pagination dots -->
|
|
||||||
<div class="swiper-pagination"></div>
|
<div class="swiper-pagination"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product-error" style="display: none;">
|
||||||
|
<p><?= t('productNotFound') ?></p>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<?php snippet('footer', ['scripts' => ['assets/js/product-size.js', 'assets/js/snipcart.js', 'assets/js/product-gallery.js']]) ?>
|
<?php snippet('structured-data-product') ?>
|
||||||
|
<?php snippet('footer', ['scripts' => ['product']]) ?>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue