Compare commits
No commits in common. "main" and "sliders" have entirely different histories.
63 changed files with 894 additions and 3702 deletions
|
|
@ -1,17 +1,7 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"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)"
|
||||
"WebSearch"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
set ftp:ssl-allow no
|
||||
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 accounts/ -x cache/ -x sessions/ site site
|
||||
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 kirby kirby
|
||||
mirror --reverse --verbose --ignore-time --parallel=10 vendor vendor
|
||||
quit
|
||||
|
|
|
|||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -51,9 +51,4 @@ Icon
|
|||
|
||||
# Guide d'intégration (contient des informations sensibles)
|
||||
# ---------------
|
||||
GUIDE-INTEGRATION-MONDIAL-RELAY.md
|
||||
|
||||
# Claude settings
|
||||
# ---------------
|
||||
.claude
|
||||
/.claude/*
|
||||
GUIDE-INTEGRATION-MONDIAL-RELAY.md
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
.product-purchase {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.product-stock-info {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stock-status {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stock-status.in-stock {
|
||||
color: #00cc00;
|
||||
}
|
||||
|
||||
.stock-status.low-stock {
|
||||
color: #ff9900;
|
||||
}
|
||||
|
||||
.stock-status.out-of-stock {
|
||||
color: #ff3333;
|
||||
}
|
||||
|
||||
.btn-add-to-cart {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
color: #000000;
|
||||
background-color: #00ff00;
|
||||
border: none;
|
||||
border-radius: 40px;
|
||||
padding: 12px 34px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.btn-add-to-cart:hover:not(:disabled) {
|
||||
background-color: #00e600;
|
||||
}
|
||||
|
||||
.btn-add-to-cart:focus {
|
||||
outline: 2px solid #00e600;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.btn-add-to-cart:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-add-to-cart.success {
|
||||
background-color: #00cc00;
|
||||
}
|
||||
|
||||
.btn-add-to-cart.error {
|
||||
background-color: #ff3333;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-add-to-cart.out-of-stock {
|
||||
background-color: #cccccc;
|
||||
color: #666666;
|
||||
}
|
||||
|
|
@ -1,258 +0,0 @@
|
|||
/* Cart Drawer Styles */
|
||||
.cart-drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
color: #000;
|
||||
|
||||
&.is-open {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
|
||||
.cart-drawer__panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
&__overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&__close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
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="thanks"],
|
||||
[data-template="support"],
|
||||
[data-template="home"] {
|
||||
[data-template="store"] {
|
||||
.p__baseline-big {
|
||||
font-family: var(--title);
|
||||
font-size: var(--fs-big);
|
||||
|
|
|
|||
|
|
@ -43,20 +43,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.header-left {
|
||||
.header-left,
|
||||
.header-right {
|
||||
width: 90px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -81,40 +75,4 @@
|
|||
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=thanks] .p__baseline-big,
|
||||
[data-template=support] .p__baseline-big,
|
||||
[data-template=home] .p__baseline-big {
|
||||
[data-template=store] .p__baseline-big {
|
||||
font-family: var(--title);
|
||||
font-size: var(--fs-big);
|
||||
font-weight: var(--fw-bold);
|
||||
|
|
@ -413,14 +413,14 @@ main {
|
|||
[data-template=subscription-newsletter] .p__baseline-big strong,
|
||||
[data-template=thanks] .p__baseline-big strong,
|
||||
[data-template=support] .p__baseline-big strong,
|
||||
[data-template=home] .p__baseline-big strong {
|
||||
[data-template=store] .p__baseline-big strong {
|
||||
font-weight: var(--fw-bolf);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
[data-template=subscription-newsletter] .p__baseline-big .link-don,
|
||||
[data-template=thanks] .p__baseline-big .link-don,
|
||||
[data-template=support] .p__baseline-big .link-don,
|
||||
[data-template=home] .p__baseline-big .link-don {
|
||||
[data-template=store] .p__baseline-big .link-don {
|
||||
display: block;
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
|
|
@ -428,7 +428,7 @@ main {
|
|||
[data-template=subscription-newsletter] .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=home] .p__baseline-big .link-don:hover {
|
||||
[data-template=store] .p__baseline-big .link-don:hover {
|
||||
-webkit-text-decoration: underline 2px;
|
||||
text-decoration: underline 2px;
|
||||
text-underline-offset: 4px;
|
||||
|
|
@ -436,7 +436,7 @@ main {
|
|||
[data-template=subscription-newsletter] .p__baseline,
|
||||
[data-template=thanks] .p__baseline,
|
||||
[data-template=support] .p__baseline,
|
||||
[data-template=home] .p__baseline {
|
||||
[data-template=store] .p__baseline {
|
||||
font-size: var(--fs-medium);
|
||||
font-weight: var(--fw-medium);
|
||||
line-height: 1.1;
|
||||
|
|
@ -447,7 +447,7 @@ main {
|
|||
[data-template=subscription-newsletter] .p__baseline,
|
||||
[data-template=thanks] .p__baseline,
|
||||
[data-template=support] .p__baseline,
|
||||
[data-template=home] .p__baseline {
|
||||
[data-template=store] .p__baseline {
|
||||
text-align: center;
|
||||
margin: var(--spacing) 0;
|
||||
}
|
||||
|
|
@ -455,7 +455,7 @@ main {
|
|||
[data-template=subscription-newsletter] .p__details,
|
||||
[data-template=thanks] .p__details,
|
||||
[data-template=support] .p__details,
|
||||
[data-template=home] .p__details {
|
||||
[data-template=store] .p__details {
|
||||
font-size: var(--fs-small);
|
||||
margin-bottom: 0.5em;
|
||||
color: var(--grey-400);
|
||||
|
|
@ -463,7 +463,7 @@ main {
|
|||
[data-template=subscription-newsletter] .section__heading,
|
||||
[data-template=thanks] .section__heading,
|
||||
[data-template=support] .section__heading,
|
||||
[data-template=home] .section__heading {
|
||||
[data-template=store] .section__heading {
|
||||
font-size: var(--fs-normal);
|
||||
font-weight: var(--fw-medium);
|
||||
line-height: 1;
|
||||
|
|
@ -477,8 +477,8 @@ main {
|
|||
[data-template=thanks] ol,
|
||||
[data-template=support] ul,
|
||||
[data-template=support] ol,
|
||||
[data-template=home] ul,
|
||||
[data-template=home] ol {
|
||||
[data-template=store] ul,
|
||||
[data-template=store] ol {
|
||||
margin-left: 3ch;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
|
@ -524,18 +524,13 @@ main {
|
|||
#site-header.is-shrinked .site-title {
|
||||
width: 80px !important;
|
||||
}
|
||||
#site-header .header-left {
|
||||
#site-header .header-left,
|
||||
#site-header .header-right {
|
||||
width: 90px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
#site-header .header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
}
|
||||
#site-header .header-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -558,36 +553,6 @@ main {
|
|||
#site-header #toggle-lang li.is-selected {
|
||||
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 {
|
||||
background-color: black;
|
||||
|
|
@ -975,36 +940,36 @@ body.is-fullscreen {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-template=home] .p__baseline-big {
|
||||
[data-template=store] .p__baseline-big {
|
||||
margin-top: calc(var(--spacing) * 2);
|
||||
}
|
||||
[data-template=home] #store__container {
|
||||
[data-template=store] #store__container {
|
||||
margin-top: calc(var(--spacing) * 2);
|
||||
margin-bottom: calc(var(--spacing) * 4);
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
}
|
||||
[data-template=home] #store__container .store__product {
|
||||
[data-template=store] #store__container .store__product {
|
||||
position: relative;
|
||||
}
|
||||
[data-template=home] #store__container .store__product figure {
|
||||
[data-template=store] #store__container .store__product figure {
|
||||
aspect-ratio: 4/3;
|
||||
background-color: var(--color-bg);
|
||||
background-color: var(--data-bg);
|
||||
margin-bottom: calc(var(--spacing) * 0.5);
|
||||
overflow: hidden;
|
||||
}
|
||||
[data-template=home] #store__container .store__product img {
|
||||
[data-template=store] #store__container .store__product img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-o-object-fit: contain;
|
||||
object-fit: contain;
|
||||
transition: var(--curve) 0.5s;
|
||||
}
|
||||
[data-template=home] #store__container .store__product a {
|
||||
[data-template=store] #store__container .store__product a {
|
||||
text-decoration: none;
|
||||
}
|
||||
[data-template=home] #store__container .store__product .link-block {
|
||||
[data-template=store] #store__container .store__product .link-block {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
|
@ -1013,23 +978,23 @@ body.is-fullscreen {
|
|||
left: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
[data-template=home] #store__container .store__product:hover figure {
|
||||
[data-template=store] #store__container .store__product:hover figure {
|
||||
overflow: hidden;
|
||||
}
|
||||
[data-template=home] #store__container .store__product:hover img {
|
||||
[data-template=store] #store__container .store__product:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
[data-template=home] #store__container .store__product:hover .line-1 {
|
||||
[data-template=store] #store__container .store__product:hover .line-1 {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@media screen and (max-width: 720px) {
|
||||
[data-template=home] #store__container .store__product {
|
||||
[data-template=store] #store__container .store__product {
|
||||
margin-top: calc(var(--spacing) * 1.5);
|
||||
margin-bottom: calc(var(--spacing) * 0.5);
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 720px) {
|
||||
[data-template=home] #store__container {
|
||||
[data-template=store] #store__container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
-moz-column-gap: calc(var(--padding-body) * 0.75);
|
||||
|
|
@ -1038,11 +1003,11 @@ body.is-fullscreen {
|
|||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
[data-template=home] #store__container .store__product {
|
||||
[data-template=store] #store__container .store__product {
|
||||
grid-column: span 2;
|
||||
}
|
||||
[data-template=home] #store__container .store__product:nth-of-type(1),
|
||||
[data-template=home] #store__container .store__product:nth-of-type(2) {
|
||||
[data-template=store] #store__container .store__product:nth-of-type(1),
|
||||
[data-template=store] #store__container .store__product:nth-of-type(2) {
|
||||
grid-column: span 3;
|
||||
}
|
||||
}
|
||||
|
|
@ -1054,8 +1019,11 @@ body.is-fullscreen {
|
|||
margin-right: auto;
|
||||
}
|
||||
|
||||
.product-content {
|
||||
display: contents;
|
||||
.section__product,
|
||||
.store__nav {
|
||||
max-width: 1200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.store__nav {
|
||||
|
|
@ -1067,17 +1035,11 @@ body.is-fullscreen {
|
|||
.store__nav a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.store__nav a::before {
|
||||
content: "← ";
|
||||
}
|
||||
.store__nav a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@media screen and (max-width: 720px) {
|
||||
.store__nav a {
|
||||
padding-top: 0;
|
||||
font-size: var(--fs-small);
|
||||
}
|
||||
.store__nav a::before {
|
||||
content: "← ";
|
||||
}
|
||||
|
||||
.section__product .details ul {
|
||||
|
|
@ -1086,7 +1048,122 @@ body.is-fullscreen {
|
|||
.section__product .details ul li {
|
||||
padding-bottom: 0.2em;
|
||||
}
|
||||
|
||||
.product-options__list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 2ch;
|
||||
}
|
||||
.product-options__list li {
|
||||
position: relative;
|
||||
}
|
||||
.product-options__list li input[type=radio] {
|
||||
position: fixed;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.product-options__list li label {
|
||||
font-family: var(--title);
|
||||
font-size: var(--fs-normal);
|
||||
height: 4ch;
|
||||
width: 4ch;
|
||||
border-radius: 50%;
|
||||
border: var(--border);
|
||||
border-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.product-options__list li input[type=radio]:checked + label {
|
||||
border-color: var(--color-txt);
|
||||
}
|
||||
.product-options__list li input[type=radio]:not(:checked) + label:hover {
|
||||
border-color: var(--grey-600);
|
||||
background-color: var(--grey-800);
|
||||
}
|
||||
|
||||
.product-gallery {
|
||||
position: relative;
|
||||
aspect-ratio: 4/3;
|
||||
}
|
||||
.product-gallery .swiper-slide {
|
||||
width: 100%;
|
||||
}
|
||||
.product-gallery .swiper-slide figure {
|
||||
aspect-ratio: 4/3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.product-gallery .swiper-slide figure img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-o-object-fit: contain;
|
||||
object-fit: contain;
|
||||
}
|
||||
.product-gallery .swiper-button-prev,
|
||||
.product-gallery .swiper-button-next {
|
||||
color: var(--color-txt);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.product-gallery .swiper-button-prev:after,
|
||||
.product-gallery .swiper-button-next:after {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.product-gallery .swiper-button-prev:hover,
|
||||
.product-gallery .swiper-button-next:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.product-gallery .swiper-pagination {
|
||||
position: relative;
|
||||
margin-top: calc(var(--spacing) * 0.5);
|
||||
bottom: 0;
|
||||
}
|
||||
.product-gallery .swiper-pagination .swiper-pagination-bullet {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--grey-600);
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.product-gallery .swiper-pagination .swiper-pagination-bullet:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.product-gallery .swiper-pagination .swiper-pagination-bullet-active {
|
||||
background: var(--color-txt);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.hero {
|
||||
margin-bottom: calc(var(--spacing) * 1);
|
||||
padding: calc(var(--spacing) * 0.5) 0;
|
||||
border-top: var(--border-light);
|
||||
border-bottom: var(--border-light);
|
||||
}
|
||||
.hero .p__baseline-big {
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.add-to-cart {
|
||||
margin: 0;
|
||||
border-bottom: var(--border-light);
|
||||
padding: calc(var(--spacing) * 0.5) 0;
|
||||
}
|
||||
|
||||
.product-options {
|
||||
border-bottom: var(--border-light);
|
||||
padding: calc(var(--spacing) * 0.25) 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 720px) {
|
||||
.store__nav a {
|
||||
padding-top: 0;
|
||||
font-size: var(--fs-small);
|
||||
}
|
||||
.section__product {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -1125,7 +1202,7 @@ body.is-fullscreen {
|
|||
}
|
||||
}
|
||||
@media screen and (min-width: 720px) {
|
||||
.section__product .product-content {
|
||||
.section__product {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: calc(var(--padding-body) * 2);
|
||||
|
|
@ -1144,123 +1221,10 @@ body.is-fullscreen {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.product-options {
|
||||
border-bottom: var(--border-light);
|
||||
padding: calc(var(--spacing) * 0.25) 0;
|
||||
}
|
||||
|
||||
.product-options__list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 2ch;
|
||||
}
|
||||
.product-options__list li {
|
||||
position: relative;
|
||||
}
|
||||
.product-options__list li input[type=radio] {
|
||||
position: fixed;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.product-options__list li input[type=radio]:checked + label {
|
||||
border-color: var(--color-txt);
|
||||
}
|
||||
.product-options__list li input[type=radio]:not(:checked) + label:hover {
|
||||
border-color: var(--grey-600);
|
||||
background-color: var(--grey-800);
|
||||
}
|
||||
.product-options__list li label {
|
||||
font-family: var(--title);
|
||||
font-size: var(--fs-normal);
|
||||
height: 4ch;
|
||||
width: 4ch;
|
||||
border-radius: 50%;
|
||||
border: var(--border);
|
||||
border-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.product-gallery {
|
||||
position: relative;
|
||||
aspect-ratio: 4/3;
|
||||
}
|
||||
.product-gallery .swiper-slide {
|
||||
width: 100%;
|
||||
}
|
||||
.product-gallery .swiper-slide figure {
|
||||
aspect-ratio: 4/3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.product-gallery .swiper-slide figure img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-o-object-fit: contain;
|
||||
object-fit: contain;
|
||||
}
|
||||
@media screen and (min-width: 720px) {
|
||||
.product-gallery .swiper-slide figure {
|
||||
width: calc(100% - 60px);
|
||||
}
|
||||
}
|
||||
.product-gallery .swiper-button-prev,
|
||||
.product-gallery .swiper-button-next {
|
||||
color: var(--color-txt);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.product-gallery .swiper-button-prev:after,
|
||||
.product-gallery .swiper-button-next:after {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.product-gallery .swiper-button-prev:hover,
|
||||
.product-gallery .swiper-button-next:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.product-gallery .swiper-pagination {
|
||||
position: relative;
|
||||
margin-top: calc(var(--spacing) * 0.5);
|
||||
bottom: 0;
|
||||
}
|
||||
.product-gallery .swiper-pagination .swiper-pagination-bullet {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--grey-600);
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.product-gallery .swiper-pagination .swiper-pagination-bullet:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.product-gallery .swiper-pagination .swiper-pagination-bullet.swiper-pagination-bullet-active {
|
||||
background: var(--color-txt);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.hero {
|
||||
margin-bottom: calc(var(--spacing) * 1);
|
||||
padding: calc(var(--spacing) * 0.5) 0;
|
||||
border-top: var(--border-light);
|
||||
border-bottom: var(--border-light);
|
||||
}
|
||||
.hero .p__baseline-big {
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.add-to-cart {
|
||||
margin: 0;
|
||||
border-bottom: var(--border-light);
|
||||
padding: calc(var(--spacing) * 0.5) 0;
|
||||
}
|
||||
|
||||
[data-template=thanks] .thanks-page {
|
||||
min-height: 60vh;
|
||||
display: flex;
|
||||
|
|
@ -1268,325 +1232,32 @@ body.is-fullscreen {
|
|||
justify-content: center;
|
||||
padding: calc(var(--spacing) * 4) var(--spacing);
|
||||
}
|
||||
[data-template=thanks] .thanks-page .thanks-content {
|
||||
[data-template=thanks] .thanks-content {
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
[data-template=thanks] .thanks-page .thanks-content h1 {
|
||||
[data-template=thanks] .thanks-content h1 {
|
||||
font-size: var(--fs-x-big);
|
||||
margin-bottom: calc(var(--spacing) * 2);
|
||||
}
|
||||
[data-template=thanks] .thanks-page .thanks-content .thanks-message {
|
||||
font-size: var(--fs-medium);
|
||||
line-height: 1.1;
|
||||
[data-template=thanks] .thanks-content .thanks-message {
|
||||
font-size: var(--fs-big);
|
||||
margin-bottom: calc(var(--spacing) * 3);
|
||||
line-height: 1.6;
|
||||
}
|
||||
[data-template=thanks] .thanks-page .thanks-content .thanks-message p {
|
||||
[data-template=thanks] .thanks-content .thanks-message p {
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
[data-template=thanks] .thanks-page .thanks-content .thanks-actions {
|
||||
width: -moz-max-content;
|
||||
width: max-content;
|
||||
[data-template=thanks] .thanks-content .thanks-actions {
|
||||
margin-top: calc(var(--spacing) * 3);
|
||||
}
|
||||
[data-template=thanks] #site-footer {
|
||||
border-top: none;
|
||||
margin-top: calc(var(--spacing) * 4);
|
||||
}
|
||||
|
||||
.product-purchase {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.product-stock-info {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stock-status {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stock-status.in-stock {
|
||||
color: #00cc00;
|
||||
}
|
||||
|
||||
.stock-status.low-stock {
|
||||
color: #ff9900;
|
||||
}
|
||||
|
||||
.stock-status.out-of-stock {
|
||||
color: #ff3333;
|
||||
}
|
||||
|
||||
.btn-add-to-cart {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
color: #000000;
|
||||
background-color: #00ff00;
|
||||
border: none;
|
||||
border-radius: 40px;
|
||||
padding: 12px 34px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.btn-add-to-cart:hover:not(:disabled) {
|
||||
background-color: #00e600;
|
||||
}
|
||||
|
||||
.btn-add-to-cart:focus {
|
||||
outline: 2px solid #00e600;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.btn-add-to-cart:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-add-to-cart.success {
|
||||
background-color: #00cc00;
|
||||
}
|
||||
|
||||
.btn-add-to-cart.error {
|
||||
background-color: #ff3333;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-add-to-cart.out-of-stock {
|
||||
background-color: #cccccc;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
/* Cart Drawer Styles */
|
||||
.cart-drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
color: #000;
|
||||
}
|
||||
.cart-drawer.is-open {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
.cart-drawer.is-open .cart-drawer__panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
.cart-drawer__overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
cursor: pointer;
|
||||
}
|
||||
.cart-drawer__panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.cart-drawer__panel {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
.cart-drawer__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
.cart-drawer__header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.cart-drawer__close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.cart-drawer__close:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.cart-drawer__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;
|
||||
.snipcart-modal__container {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
[data-template=subscription-newsletter] main {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -22,7 +22,6 @@
|
|||
@import "template/shop/layout";
|
||||
@import "template/shop/section--product";
|
||||
@import "template/shop/thanks";
|
||||
@import "components/shopify-buy-button.scss";
|
||||
@import "components/shopify-cart-drawer.scss";
|
||||
@import "template/shop/snipcart";
|
||||
|
||||
@import "template/subscription-newsletter/layout";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
[data-template="home"] {
|
||||
[data-template="store"] {
|
||||
|
||||
|
||||
.p__baseline-big {
|
||||
margin-top: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,12 @@
|
|||
.section__product,
|
||||
.store__nav{
|
||||
max-width: 1200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.section__product,
|
||||
.store__nav {
|
||||
max-width: 1200px;
|
||||
|
|
@ -5,9 +14,9 @@
|
|||
margin-right: auto;
|
||||
}
|
||||
|
||||
.product-content {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.store__nav {
|
||||
padding-top: calc(var(--spacing) * 1);
|
||||
|
|
@ -18,35 +27,174 @@
|
|||
a {
|
||||
text-decoration: none;
|
||||
|
||||
&::before {
|
||||
content: "← ";
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$small} {
|
||||
a {
|
||||
padding-top: 0;
|
||||
font-size: var(--fs-small);
|
||||
a::before {
|
||||
content: "← ";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.section__product .details {
|
||||
// margin-bottom: calc(var(--spacing) * 2);
|
||||
|
||||
ul{
|
||||
margin-left: 2ch;
|
||||
li{
|
||||
padding-bottom: 0.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section__product {
|
||||
.details {
|
||||
ul {
|
||||
margin-left: 2ch;
|
||||
|
||||
.product-options__list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 2ch;
|
||||
|
||||
li {
|
||||
padding-bottom: 0.2em;
|
||||
position: relative;
|
||||
|
||||
input[type="radio"] {
|
||||
position: fixed;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
label {
|
||||
font-family: var(--title);
|
||||
font-size: var(--fs-normal);
|
||||
height: 4ch;
|
||||
width: 4ch;
|
||||
border-radius: 50%;
|
||||
border: var(--border);
|
||||
border-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="radio"]:checked + label {
|
||||
border-color: var(--color-txt);
|
||||
}
|
||||
|
||||
input[type="radio"]:not(:checked) + label:hover {
|
||||
border-color: var(--grey-600);
|
||||
background-color: var(--grey-800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.product-gallery {
|
||||
position: relative;
|
||||
aspect-ratio: 4 / 3;
|
||||
|
||||
.swiper-slide {
|
||||
width: 100%;
|
||||
|
||||
figure {
|
||||
|
||||
aspect-ratio: 4 / 3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$small} {
|
||||
// Swiper navigation arrows
|
||||
.swiper-button-prev,
|
||||
.swiper-button-next {
|
||||
color: var(--color-txt);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
&:after {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
// Swiper pagination dots
|
||||
.swiper-pagination {
|
||||
position: relative;
|
||||
margin-top: calc(var(--spacing) * 0.5);
|
||||
bottom: 0;
|
||||
|
||||
.swiper-pagination-bullet {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--grey-600);
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-pagination-bullet-active {
|
||||
background: var(--color-txt);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.hero {
|
||||
margin-bottom: calc(var(--spacing) * 1);
|
||||
padding: calc(var(--spacing) * 0.5) 0;
|
||||
border-top: var(--border-light);
|
||||
border-bottom: var(--border-light);
|
||||
|
||||
.p__baseline-big {
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.add-to-cart {
|
||||
margin: 0;
|
||||
border-bottom: var(--border-light);
|
||||
padding: calc(var(--spacing) * 0.5) 0;
|
||||
}
|
||||
|
||||
.product-options {
|
||||
border-bottom: var(--border-light);
|
||||
padding: calc(var(--spacing) * 0.25) 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media #{$small} {
|
||||
.store__nav a {
|
||||
padding-top: 0;
|
||||
font-size: var(--fs-small);
|
||||
}
|
||||
|
||||
.section__product {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 10vh;
|
||||
|
|
@ -59,7 +207,6 @@
|
|||
margin-top: calc(var(--spacing) * 0.5);
|
||||
order: 1;
|
||||
}
|
||||
|
||||
figure {
|
||||
order: 2;
|
||||
margin-bottom: calc(var(--spacing) * 1);
|
||||
|
|
@ -79,25 +226,25 @@
|
|||
order: 5;
|
||||
}
|
||||
|
||||
.product-gallery {
|
||||
.product-gallery{
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
left: calc(var(--padding-body) * -1);
|
||||
left: calc(var(--padding-body)*-1);
|
||||
|
||||
.swiper-button-prev,
|
||||
.swiper-button-next {
|
||||
display: none;
|
||||
}
|
||||
.swiper-button-next{ display: none; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$small-up} {
|
||||
.product-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: calc(var(--padding-body) * 2);
|
||||
margin-bottom: calc(var(--spacing) * 3);
|
||||
}
|
||||
@media #{$small-up} {
|
||||
|
||||
|
||||
.section__product{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: calc(var(--padding-body)*2);
|
||||
margin-bottom: calc(var(--spacing)*3);
|
||||
|
||||
.details {
|
||||
margin-bottom: calc(var(--spacing) * 2);
|
||||
|
|
@ -108,140 +255,17 @@
|
|||
border-top: var(--border-light);
|
||||
}
|
||||
|
||||
.col-left {
|
||||
.col-left{
|
||||
min-height: 100%;
|
||||
padding-bottom: 40px;
|
||||
padding-bottom: 40px; //dots
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-options {
|
||||
border-bottom: var(--border-light);
|
||||
padding: calc(var(--spacing) * 0.25) 0;
|
||||
}
|
||||
|
||||
.product-options__list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 2ch;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
|
||||
input[type="radio"] {
|
||||
position: fixed;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&:checked + label {
|
||||
border-color: var(--color-txt);
|
||||
}
|
||||
|
||||
&:not(:checked) + label:hover {
|
||||
border-color: var(--grey-600);
|
||||
background-color: var(--grey-800);
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
font-family: var(--title);
|
||||
font-size: var(--fs-normal);
|
||||
height: 4ch;
|
||||
width: 4ch;
|
||||
border-radius: 50%;
|
||||
border: var(--border);
|
||||
border-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-gallery {
|
||||
position: relative;
|
||||
aspect-ratio: 4 / 3;
|
||||
|
||||
.swiper-slide {
|
||||
width: 100%;
|
||||
|
||||
figure {
|
||||
aspect-ratio: 4 / 3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$small-up} {
|
||||
figure {
|
||||
width: calc(100% - 60px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-button-prev,
|
||||
.swiper-button-next {
|
||||
color: var(--color-txt);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
&:after {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-pagination {
|
||||
position: relative;
|
||||
margin-top: calc(var(--spacing) * 0.5);
|
||||
bottom: 0;
|
||||
|
||||
.swiper-pagination-bullet {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--grey-600);
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&.swiper-pagination-bullet-active {
|
||||
background: var(--color-txt);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.product-gallery .swiper-slide figure{
|
||||
width: calc(100% - 60px);
|
||||
}
|
||||
}
|
||||
|
||||
.hero {
|
||||
margin-bottom: calc(var(--spacing) * 1);
|
||||
padding: calc(var(--spacing) * 0.5) 0;
|
||||
border-top: var(--border-light);
|
||||
border-bottom: var(--border-light);
|
||||
|
||||
.p__baseline-big {
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.add-to-cart {
|
||||
margin: 0;
|
||||
border-bottom: var(--border-light);
|
||||
padding: calc(var(--spacing) * 0.5) 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,30 +5,29 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: calc(var(--spacing) * 4) var(--spacing);
|
||||
}
|
||||
|
||||
.thanks-content {
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.thanks-content {
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
|
||||
h1 {
|
||||
margin-bottom: calc(var(--spacing) * 2);
|
||||
h1 {
|
||||
font-size: var(--fs-x-big);
|
||||
margin-bottom: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
||||
.thanks-message {
|
||||
font-size: var(--fs-big);
|
||||
margin-bottom: calc(var(--spacing) * 3);
|
||||
line-height: 1.6;
|
||||
|
||||
p {
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
}
|
||||
|
||||
.thanks-message {
|
||||
font-size: var(--fs-medium);
|
||||
line-height: 1.1;
|
||||
|
||||
p {
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
}
|
||||
|
||||
.thanks-actions {
|
||||
width: max-content;
|
||||
}
|
||||
.thanks-actions {
|
||||
margin-top: calc(var(--spacing) * 3);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,290 +0,0 @@
|
|||
/**
|
||||
* Cart Drawer Component
|
||||
* Manages the cart sidebar with add/remove/update functionality
|
||||
*/
|
||||
(function() {
|
||||
const drawer = document.getElementById('cart-drawer');
|
||||
const emptyState = document.querySelector('[data-cart-empty]');
|
||||
const itemsContainer = document.querySelector('[data-cart-items]');
|
||||
const checkoutBtn = document.querySelector('[data-cart-checkout]');
|
||||
const closeButtons = document.querySelectorAll('[data-cart-close]');
|
||||
const totalDisplay = document.querySelector('[data-cart-total]');
|
||||
const headerCartBtn = document.querySelector('[data-cart-open]');
|
||||
const headerCartCount = document.querySelector('[data-cart-count]');
|
||||
|
||||
// Get translated text
|
||||
const removeText = drawer.dataset.textRemove || 'Remove';
|
||||
|
||||
let currentCart = null;
|
||||
let cartInstance = null;
|
||||
|
||||
// Wait for ShopifyCart to be available
|
||||
function initCartDrawer() {
|
||||
if (typeof ShopifyCart === 'undefined') {
|
||||
setTimeout(initCartDrawer, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
cartInstance = new ShopifyCart({
|
||||
domain: 'nv7cqv-bu.myshopify.com',
|
||||
storefrontAccessToken: 'dec3d35a2554384d149c72927d1cfd1b'
|
||||
});
|
||||
|
||||
// Initialize event listeners
|
||||
setupEventListeners();
|
||||
|
||||
// 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();
|
||||
}
|
||||
})();
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
(function() {
|
||||
const cart = new ShopifyCart({
|
||||
domain: 'nv7cqv-bu.myshopify.com',
|
||||
storefrontAccessToken: 'dec3d35a2554384d149c72927d1cfd1b'
|
||||
});
|
||||
|
||||
const addToCartBtn = document.querySelector('[data-shopify-add-to-cart]');
|
||||
|
||||
if (!addToCartBtn) {
|
||||
return;
|
||||
}
|
||||
|
||||
const 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);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -1,306 +0,0 @@
|
|||
(async function () {
|
||||
const container = document.querySelector("[data-product-loader]");
|
||||
if (!container) return;
|
||||
|
||||
const handle = container.dataset.shopifyHandle;
|
||||
const language = container.dataset.language || "fr";
|
||||
const isEnglish = language === "en";
|
||||
const loadingState = container.querySelector(".product-loading");
|
||||
const contentState = container.querySelector(".product-content");
|
||||
const errorState = container.querySelector(".product-error");
|
||||
|
||||
try {
|
||||
const cart = new ShopifyCart({
|
||||
domain: "nv7cqv-bu.myshopify.com",
|
||||
storefrontAccessToken: "dec3d35a2554384d149c72927d1cfd1b",
|
||||
});
|
||||
|
||||
const product = await cart.getProductByHandle(handle);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
renderProduct(product, isEnglish);
|
||||
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);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
(async function() {
|
||||
const container = document.querySelector('[data-products-loader]');
|
||||
if (!container) return;
|
||||
|
||||
const language = (container.dataset.language || 'FR').toLowerCase();
|
||||
const isEnglish = language === 'en';
|
||||
|
||||
try {
|
||||
const cart = new ShopifyCart({
|
||||
domain: 'nv7cqv-bu.myshopify.com',
|
||||
storefrontAccessToken: 'dec3d35a2554384d149c72927d1cfd1b'
|
||||
});
|
||||
|
||||
const productsData = await cart.getAllProducts();
|
||||
const products = productsData.edges;
|
||||
|
||||
const productsHtml = products.map(edge => {
|
||||
const product = edge.node;
|
||||
const image = product.images.edges[0]?.node;
|
||||
const price = parseFloat(product.priceRange.minVariantPrice.amount).toFixed(2);
|
||||
const slug = product.handle;
|
||||
const productUrl = isEnglish ? `/en/${slug}` : `/${slug}`;
|
||||
const productTitle = isEnglish && product.titleEn?.value
|
||||
? product.titleEn.value
|
||||
: product.title;
|
||||
|
||||
return `
|
||||
<article class="store__product">
|
||||
<figure>
|
||||
${image ? `<img src="${image.url}" alt="${image.altText || productTitle}" loading="lazy" />` : ''}
|
||||
</figure>
|
||||
<p class="line-1">
|
||||
<a href="${productUrl}">${productTitle}</a>
|
||||
</p>
|
||||
<p class="price">${price}€</p>
|
||||
<a href="${productUrl}" class="link-block" aria-hidden="true"></a>
|
||||
</article>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = productsHtml;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading products:', error);
|
||||
container.innerHTML = '<p>Erreur lors du chargement des produits</p>';
|
||||
}
|
||||
})();
|
||||
|
|
@ -1,395 +0,0 @@
|
|||
/**
|
||||
* Shopify Storefront API Client
|
||||
* Custom implementation using Cart API (2026-01)
|
||||
*/
|
||||
class ShopifyCart {
|
||||
constructor(config) {
|
||||
this.domain = config.domain;
|
||||
this.storefrontAccessToken = config.storefrontAccessToken;
|
||||
this.apiVersion = '2026-01';
|
||||
this.endpoint = `https://${this.domain}/api/${this.apiVersion}/graphql.json`;
|
||||
this.cartId = null;
|
||||
this.cartItems = [];
|
||||
|
||||
// Load existing cart from localStorage
|
||||
this.loadCart();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make GraphQL request to Shopify Storefront API
|
||||
*/
|
||||
async query(query, variables = {}) {
|
||||
const response = await fetch(this.endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Storefront-Access-Token': this.storefrontAccessToken,
|
||||
},
|
||||
body: JSON.stringify({ query, variables }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.errors) {
|
||||
console.error('Shopify API Error:', result.errors);
|
||||
throw new Error(result.errors[0].message);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product information by ID
|
||||
*/
|
||||
async getProduct(productId) {
|
||||
const query = `
|
||||
query getProduct($id: ID!) {
|
||||
product(id: $id) {
|
||||
id
|
||||
title
|
||||
description
|
||||
availableForSale
|
||||
variants(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
price {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
availableForSale
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await this.query(query, {
|
||||
id: `gid://shopify/Product/${productId}`
|
||||
});
|
||||
|
||||
return data.product;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product by handle
|
||||
* @param {string} handle - Product handle (slug)
|
||||
*/
|
||||
async getProductByHandle(handle) {
|
||||
const query = `
|
||||
query getProductByHandle($handle: String!) {
|
||||
product(handle: $handle) {
|
||||
id
|
||||
handle
|
||||
title
|
||||
description
|
||||
descriptionHtml
|
||||
availableForSale
|
||||
tags
|
||||
titleEn: metafield(namespace: "custom", key: "title_en") {
|
||||
value
|
||||
}
|
||||
descriptionEn: metafield(namespace: "custom", key: "description_en") {
|
||||
value
|
||||
}
|
||||
priceRange {
|
||||
minVariantPrice {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
images(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
url
|
||||
altText
|
||||
width
|
||||
height
|
||||
}
|
||||
}
|
||||
}
|
||||
variants(first: 20) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
sku
|
||||
availableForSale
|
||||
price {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
selectedOptions {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await this.query(query, { handle });
|
||||
return data.product || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all products for listing page
|
||||
* @param {number} first - Number of products to fetch
|
||||
*/
|
||||
async getAllProducts(first = 20) {
|
||||
const query = `
|
||||
query getAllProducts($first: Int!) {
|
||||
products(first: $first, sortKey: TITLE) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
handle
|
||||
title
|
||||
description
|
||||
availableForSale
|
||||
titleEn: metafield(namespace: "custom", key: "title_en") {
|
||||
value
|
||||
}
|
||||
priceRange {
|
||||
minVariantPrice {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
images(first: 1) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
url
|
||||
altText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await this.query(query, { first });
|
||||
return data.products;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new cart
|
||||
*/
|
||||
async createCart(lines = []) {
|
||||
const query = `
|
||||
mutation cartCreate($input: CartInput!) {
|
||||
cartCreate(input: $input) {
|
||||
cart {
|
||||
id
|
||||
checkoutUrl
|
||||
lines(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
quantity
|
||||
merchandise {
|
||||
... on ProductVariant {
|
||||
id
|
||||
title
|
||||
price {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
product {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await this.query(query, {
|
||||
input: { lines }
|
||||
});
|
||||
|
||||
if (data.cartCreate.userErrors.length > 0) {
|
||||
throw new Error(data.cartCreate.userErrors[0].message);
|
||||
}
|
||||
|
||||
this.cartId = data.cartCreate.cart.id;
|
||||
this.saveCart();
|
||||
|
||||
return data.cartCreate.cart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add item to cart
|
||||
*/
|
||||
async addToCart(variantId, quantity = 1) {
|
||||
const lines = [{
|
||||
merchandiseId: `gid://shopify/ProductVariant/${variantId}`,
|
||||
quantity: quantity
|
||||
}];
|
||||
|
||||
let cart;
|
||||
|
||||
if (this.cartId) {
|
||||
// Add to existing cart
|
||||
const query = `
|
||||
mutation cartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {
|
||||
cartLinesAdd(cartId: $cartId, lines: $lines) {
|
||||
cart {
|
||||
id
|
||||
checkoutUrl
|
||||
lines(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
quantity
|
||||
merchandise {
|
||||
... on ProductVariant {
|
||||
id
|
||||
title
|
||||
price {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
product {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await this.query(query, {
|
||||
cartId: this.cartId,
|
||||
lines
|
||||
});
|
||||
|
||||
if (data.cartLinesAdd.userErrors.length > 0) {
|
||||
throw new Error(data.cartLinesAdd.userErrors[0].message);
|
||||
}
|
||||
|
||||
cart = data.cartLinesAdd.cart;
|
||||
} else {
|
||||
// Create new cart
|
||||
cart = await this.createCart(lines);
|
||||
}
|
||||
|
||||
this.cartItems = cart.lines.edges;
|
||||
return cart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 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
|
||||
document.addEventListener('snipcart.ready', function() {
|
||||
Snipcart.events.on('cart.confirmed', function(cartState) {
|
||||
Snipcart.execute('bind', 'order.completed', function(order) {
|
||||
// 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;
|
||||
window.location.href = langPrefix + '/thanks?order=' + order.token;
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,164 +0,0 @@
|
|||
# Archive Snipcart
|
||||
|
||||
Cette archive contient tous les fichiers et le code nécessaires pour réactiver l'intégration Snipcart si besoin.
|
||||
|
||||
## Date d'archivage
|
||||
13 janvier 2026
|
||||
|
||||
## Raison de l'archivage
|
||||
Remplacement par l'intégration Shopify Buy Button
|
||||
|
||||
---
|
||||
|
||||
## Fichiers archivés
|
||||
|
||||
### Fichiers JavaScript
|
||||
- `snipcart.js` - Loader Snipcart avec configuration de la clé API
|
||||
- `product-size.js` - Gestion des options produit (tailles, etc.) pour Snipcart
|
||||
|
||||
### Fichiers SCSS
|
||||
- `_snipcart.scss` - Styles pour le modal Snipcart
|
||||
|
||||
---
|
||||
|
||||
## Comment restaurer l'intégration Snipcart
|
||||
|
||||
### 1. Restaurer les fichiers JavaScript
|
||||
|
||||
#### Fichier: `assets/js/snipcart.js`
|
||||
Copier le fichier depuis l'archive:
|
||||
```bash
|
||||
cp assets/snipcart-archive/snipcart.js assets/js/
|
||||
```
|
||||
|
||||
#### Fichier: `assets/js/product-size.js`
|
||||
Copier le fichier depuis l'archive:
|
||||
```bash
|
||||
cp assets/snipcart-archive/product-size.js assets/js/
|
||||
```
|
||||
|
||||
### 2. Restaurer les styles SCSS
|
||||
|
||||
#### Fichier: `assets/css/template/shop/_snipcart.scss`
|
||||
Copier le fichier depuis l'archive:
|
||||
```bash
|
||||
cp assets/snipcart-archive/_snipcart.scss assets/css/template/shop/
|
||||
```
|
||||
|
||||
Puis décommenter l'import dans `assets/css/style.scss`:
|
||||
```scss
|
||||
// Décommenter cette ligne:
|
||||
@import 'template/shop/snipcart';
|
||||
```
|
||||
|
||||
### 3. Restaurer le template produit
|
||||
|
||||
#### Fichier: `site/templates/product.php`
|
||||
|
||||
1. **Restaurer le bouton "Ajouter au panier" avec les attributs Snipcart** (ligne ~40-78):
|
||||
- Décommenter tous les attributs `data-item-*` du bouton
|
||||
- Ajouter la classe `snipcart-add-item` au bouton
|
||||
|
||||
2. **Restaurer les scripts dans le footer** (ligne 114):
|
||||
```php
|
||||
<?php snippet('footer', ['scripts' => ['assets/js/product-size.js', 'assets/js/snipcart.js', 'assets/js/product-gallery.js']]) ?>
|
||||
```
|
||||
|
||||
### 4. Restaurer les routes et webhooks
|
||||
|
||||
#### Fichier: `site/config/config.php`
|
||||
|
||||
Décommenter les routes Snipcart (lignes 39-147):
|
||||
|
||||
1. **Route de validation produit** (`validate.json`):
|
||||
- Permet à Snipcart de valider les prix et stock
|
||||
- Route: `(:any)/validate.json`
|
||||
|
||||
2. **Webhook Snipcart**:
|
||||
- Gère les événements de commande (décrémente le stock)
|
||||
- Route: `snipcart-webhook`
|
||||
|
||||
### 5. Configuration Snipcart
|
||||
|
||||
#### Clé API publique
|
||||
La clé API publique Snipcart est dans `snipcart.js`:
|
||||
```javascript
|
||||
publicApiKey: 'NGU4ODQ3MjAtY2MzMC00MWEyLWI2YTMtNjBmNGYzMTBlOTZkNjM4OTY1NDY4OTE5MTQyMTI3'
|
||||
```
|
||||
|
||||
#### Configuration du webhook
|
||||
Pour que le webhook fonctionne, il faut:
|
||||
1. Configurer l'URL du webhook dans le dashboard Snipcart
|
||||
2. URL: `https://votre-domaine.com/snipcart-webhook`
|
||||
3. Événements à écouter: `order.completed`
|
||||
|
||||
### 6. Vérifications après restauration
|
||||
|
||||
- [ ] Les fichiers JS sont présents dans `assets/js/`
|
||||
- [ ] Le fichier SCSS est présent et importé
|
||||
- [ ] Les boutons "Ajouter au panier" ont la classe `snipcart-add-item`
|
||||
- [ ] Les attributs `data-item-*` sont présents sur les boutons
|
||||
- [ ] Les routes sont décommentées dans `config.php`
|
||||
- [ ] Le CSS de Snipcart est compilé
|
||||
- [ ] Le webhook est configuré dans le dashboard Snipcart
|
||||
- [ ] Les traductions sont restaurées dans `site/languages/en.php` et `fr.php`
|
||||
|
||||
---
|
||||
|
||||
## Fonctionnalités Snipcart implémentées
|
||||
|
||||
### Gestion des produits
|
||||
- Affichage du prix
|
||||
- Options de produit (tailles, couleurs, etc.)
|
||||
- Validation des options obligatoires
|
||||
- Images produit
|
||||
|
||||
### Gestion du panier
|
||||
- Ajout au panier
|
||||
- Gestion du stock
|
||||
- Calcul des frais de port (basé sur poids/dimensions)
|
||||
- Validation des prix côté serveur
|
||||
|
||||
### Gestion des commandes
|
||||
- Webhook pour décrémenter le stock automatiquement
|
||||
- Redirection vers page de remerciement après paiement
|
||||
- Token de commande dans l'URL
|
||||
|
||||
### Multi-langue
|
||||
- Support FR/EN
|
||||
- Redirection post-paiement avec détection de la langue
|
||||
|
||||
---
|
||||
|
||||
## Dépendances externes
|
||||
|
||||
### CDN Snipcart
|
||||
Snipcart est chargé depuis le CDN officiel:
|
||||
- Version: 3.0
|
||||
- JS: `https://cdn.snipcart.com/themes/v3.0/default/snipcart.js`
|
||||
- CSS: `https://cdn.snipcart.com/themes/v3.0/default/snipcart.css`
|
||||
|
||||
### Stratégie de chargement
|
||||
- `loadStrategy: 'on-user-interaction'`
|
||||
- Chargement différé pour optimiser les performances
|
||||
- Timeout: 2750ms
|
||||
|
||||
---
|
||||
|
||||
## Notes importantes
|
||||
|
||||
1. **Sécurité**: Le webhook devrait valider la signature Snipcart en production (voir commentaire dans `config.php`)
|
||||
|
||||
2. **Stock**: Le système décrémente automatiquement le stock via le webhook `order.completed`
|
||||
|
||||
3. **Validation**: Chaque produit expose une route `validate.json` pour que Snipcart puisse vérifier les prix
|
||||
|
||||
4. **Multi-langue**: La redirection post-paiement détecte automatiquement la langue depuis l'URL
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
Pour toute question sur Snipcart:
|
||||
- Documentation: https://docs.snipcart.com/
|
||||
- Support: https://snipcart.com/support
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
/**
|
||||
* Gestion de la sélection des options produit
|
||||
* Met à jour les attributs Snipcart et gère les classes CSS
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Initialise la gestion des options
|
||||
*/
|
||||
function initOptionSelector() {
|
||||
const optionsContainer = document.querySelector('.product-options');
|
||||
const addToCartButton = document.querySelector('.snipcart-add-item');
|
||||
|
||||
if (!addToCartButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Si pas d'options, le bouton est déjà actif
|
||||
if (!optionsContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const radios = optionsContainer.querySelectorAll('input[type="radio"]');
|
||||
|
||||
// Réinitialiser toutes les options (important pour le cache navigateur)
|
||||
radios.forEach(radio => {
|
||||
radio.checked = false;
|
||||
});
|
||||
|
||||
// Retirer la classe is-selected de tous les li
|
||||
const allLi = optionsContainer.querySelectorAll('li');
|
||||
allLi.forEach(li => li.classList.remove('is-selected'));
|
||||
|
||||
// S'assurer que le bouton est désactivé au départ
|
||||
addToCartButton.setAttribute('disabled', 'disabled');
|
||||
|
||||
// Écouter les changements de sélection
|
||||
radios.forEach(radio => {
|
||||
radio.addEventListener('change', function() {
|
||||
// Mettre à jour l'attribut Snipcart
|
||||
addToCartButton.setAttribute('data-item-custom1-value', this.value);
|
||||
|
||||
// Activer le bouton
|
||||
addToCartButton.removeAttribute('disabled');
|
||||
|
||||
// Changer le texte du bouton
|
||||
const buttonText = addToCartButton.querySelector('.txt');
|
||||
if (buttonText) {
|
||||
buttonText.textContent = buttonText.getAttribute('data-default-text') || 'Ajouter au panier';
|
||||
}
|
||||
|
||||
// Gérer la classe is-selected sur les li parents
|
||||
allLi.forEach(li => li.classList.remove('is-selected'));
|
||||
this.closest('li').classList.add('is-selected');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisation au chargement de la page
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', initOptionSelector);
|
||||
|
||||
})();
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
window.SnipcartSettings = {
|
||||
publicApiKey:
|
||||
'NGU4ODQ3MjAtY2MzMC00MWEyLWI2YTMtNjBmNGYzMTBlOTZkNjM4OTY1NDY4OTE5MTQyMTI3',
|
||||
loadStrategy: 'on-user-interaction',
|
||||
};
|
||||
|
||||
// Redirection après paiement réussi
|
||||
document.addEventListener('snipcart.ready', function() {
|
||||
Snipcart.events.on('cart.confirmed', function(cartState) {
|
||||
// Détecter la langue actuelle depuis l'URL
|
||||
const currentPath = window.location.pathname;
|
||||
const langMatch = currentPath.match(/^\/([a-z]{2})(\/|$)/);
|
||||
const langPrefix = langMatch ? '/' + langMatch[1] : '';
|
||||
|
||||
window.location.href = langPrefix + '/thanks?order=' + cartState.token;
|
||||
});
|
||||
});
|
||||
|
||||
(() => {
|
||||
var c, d;
|
||||
(d = (c = window.SnipcartSettings).version) != null || (c.version = '3.0');
|
||||
var s, S;
|
||||
(S = (s = window.SnipcartSettings).timeoutDuration) != null ||
|
||||
(s.timeoutDuration = 2750);
|
||||
var l, p;
|
||||
(p = (l = window.SnipcartSettings).domain) != null ||
|
||||
(l.domain = 'cdn.snipcart.com');
|
||||
var w, u;
|
||||
(u = (w = window.SnipcartSettings).protocol) != null ||
|
||||
(w.protocol = 'https');
|
||||
var f =
|
||||
window.SnipcartSettings.version.includes('v3.0.0-ci') ||
|
||||
(window.SnipcartSettings.version != '3.0' &&
|
||||
window.SnipcartSettings.version.localeCompare('3.4.0', void 0, {
|
||||
numeric: !0,
|
||||
sensitivity: 'base',
|
||||
}) === -1),
|
||||
m = ['focus', 'mouseover', 'touchmove', 'scroll', 'keydown'];
|
||||
window.LoadSnipcart = o;
|
||||
document.readyState === 'loading'
|
||||
? document.addEventListener('DOMContentLoaded', r)
|
||||
: r();
|
||||
function r() {
|
||||
window.SnipcartSettings.loadStrategy
|
||||
? window.SnipcartSettings.loadStrategy === 'on-user-interaction' &&
|
||||
(m.forEach((t) => document.addEventListener(t, o)),
|
||||
setTimeout(o, window.SnipcartSettings.timeoutDuration))
|
||||
: o();
|
||||
}
|
||||
var a = !1;
|
||||
function o() {
|
||||
if (a) return;
|
||||
a = !0;
|
||||
let t = document.getElementsByTagName('head')[0],
|
||||
e = document.querySelector('#snipcart'),
|
||||
i = document.querySelector(
|
||||
`src[src^="${window.SnipcartSettings.protocol}://${window.SnipcartSettings.domain}"][src$="snipcart.js"]`
|
||||
),
|
||||
n = document.querySelector(
|
||||
`link[href^="${window.SnipcartSettings.protocol}://${window.SnipcartSettings.domain}"][href$="snipcart.css"]`
|
||||
);
|
||||
e ||
|
||||
((e = document.createElement('div')),
|
||||
(e.id = 'snipcart'),
|
||||
e.setAttribute('hidden', 'true'),
|
||||
document.body.appendChild(e)),
|
||||
v(e),
|
||||
i ||
|
||||
((i = document.createElement('script')),
|
||||
(i.src = `${window.SnipcartSettings.protocol}://${window.SnipcartSettings.domain}/themes/v${window.SnipcartSettings.version}/default/snipcart.js`),
|
||||
(i.async = !0),
|
||||
t.appendChild(i)),
|
||||
n ||
|
||||
((n = document.createElement('link')),
|
||||
(n.rel = 'stylesheet'),
|
||||
(n.type = 'text/css'),
|
||||
(n.href = `${window.SnipcartSettings.protocol}://${window.SnipcartSettings.domain}/themes/v${window.SnipcartSettings.version}/default/snipcart.css`),
|
||||
t.prepend(n)),
|
||||
m.forEach((g) => document.removeEventListener(g, o));
|
||||
}
|
||||
function v(t) {
|
||||
!f ||
|
||||
((t.dataset.apiKey = window.SnipcartSettings.publicApiKey),
|
||||
window.SnipcartSettings.addProductBehavior &&
|
||||
(t.dataset.configAddProductBehavior =
|
||||
window.SnipcartSettings.addProductBehavior),
|
||||
window.SnipcartSettings.modalStyle &&
|
||||
(t.dataset.configModalStyle = window.SnipcartSettings.modalStyle),
|
||||
window.SnipcartSettings.currency &&
|
||||
(t.dataset.currency = window.SnipcartSettings.currency),
|
||||
window.SnipcartSettings.templatesUrl &&
|
||||
(t.dataset.templatesUrl = window.SnipcartSettings.templatesUrl));
|
||||
}
|
||||
})();
|
||||
52
content/1_tshirt-index-01/product.en.txt
Normal file
52
content/1_tshirt-index-01/product.en.txt
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
Title: T-shirt Index 01
|
||||
|
||||
----
|
||||
|
||||
Price: 35
|
||||
|
||||
----
|
||||
|
||||
Description: <p>T-shirt de soutien à Index, 100% coton</p>
|
||||
|
||||
----
|
||||
|
||||
Details:
|
||||
|
||||
[
|
||||
{
|
||||
"content": {
|
||||
"text": "<p>100% cotton</p>"
|
||||
},
|
||||
"id": "detail1",
|
||||
"isHidden": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"text": "<p>Lorem ipsum dolor sit amet</p>"
|
||||
},
|
||||
"id": "detail2",
|
||||
"isHidden": false,
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
|
||||
----
|
||||
|
||||
Options:
|
||||
|
||||
-
|
||||
label: Size
|
||||
values: XS, S, M, L, XL
|
||||
|
||||
----
|
||||
|
||||
Snipcartid: tshirt-01
|
||||
|
||||
----
|
||||
|
||||
Backgroundcolor: #ffffff
|
||||
|
||||
----
|
||||
|
||||
Template: product
|
||||
56
content/1_tshirt-index-01/product.fr.txt
Normal file
56
content/1_tshirt-index-01/product.fr.txt
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
Title: T-shirt Index 01
|
||||
|
||||
----
|
||||
|
||||
Price: 35
|
||||
|
||||
----
|
||||
|
||||
Stock: 10
|
||||
|
||||
----
|
||||
|
||||
Description: T-shirt de soutien à Index, 100% coton
|
||||
|
||||
----
|
||||
|
||||
Details: <p>T-shirt en coton organique avec impression sérigraphique.<br>Marquage sur la face avant : logo « INDEX » de 10 cm de large.</p><ul><li><p>100 % coton biologique</p></li><li><p>Grammage : 180 g/m²</p></li><li><p>Jersey simple au toucher très doux</p></li><li><p>Excellente tenue dans le temps</p></li><li><p>Bande de propreté intérieure au col</p></li><li><p>Surpiqûres doubles en bas de manches et en bas de corps</p></li></ul><p>Envoi uniquement via Mondial Relay vers la France, la Belgique et la Suisse.</p>
|
||||
|
||||
----
|
||||
|
||||
Hasoptions: true
|
||||
|
||||
----
|
||||
|
||||
Optionlabel: Taille
|
||||
|
||||
----
|
||||
|
||||
Optionvalues: XS, S, M, L, XL
|
||||
|
||||
----
|
||||
|
||||
Options:
|
||||
|
||||
-
|
||||
label: Taille
|
||||
values: XS, S, M, L, XL
|
||||
-
|
||||
label: Couleur
|
||||
values: Rouge, Vert
|
||||
|
||||
----
|
||||
|
||||
Snipcartid: tshirt-01
|
||||
|
||||
----
|
||||
|
||||
Backgroundcolor: #ffffff
|
||||
|
||||
----
|
||||
|
||||
Template: product
|
||||
|
||||
----
|
||||
|
||||
Uuid: udrrfizhayqixfoo
|
||||
BIN
content/1_tshirt-index-01/tshirt-01.png
Normal file
BIN
content/1_tshirt-index-01/tshirt-01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
9
content/1_tshirt-index-01/tshirt-01.png.fr.txt
Normal file
9
content/1_tshirt-index-01/tshirt-01.png.fr.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
Sort: 1
|
||||
|
||||
----
|
||||
|
||||
Uuid: deupkqq83jvloz0r
|
||||
|
||||
----
|
||||
|
||||
Template: image
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
Title: Error
|
||||
|
||||
----
|
||||
|
||||
Template: default
|
||||
|
|
@ -1,5 +1 @@
|
|||
Title: Erreur
|
||||
|
||||
----
|
||||
|
||||
Uuid: kcrqkszqasludg2h
|
||||
Uuid: kcrqkszqasludg2h
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
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,7 +7,3 @@ Text:
|
|||
Votre commande a été confirmée ! Vous allez recevoir un email de confirmation sous peu.
|
||||
|
||||
Merci pour votre achat.
|
||||
|
||||
----
|
||||
|
||||
Template: thanks
|
||||
|
|
|
|||
|
|
@ -1 +1,9 @@
|
|||
Title: Thanks
|
||||
Title: Thank you for your order
|
||||
|
||||
----
|
||||
|
||||
Text:
|
||||
|
||||
Your order has been confirmed! You will receive a confirmation email shortly.
|
||||
|
||||
Thank you for your purchase.
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
<!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,21 +1,134 @@
|
|||
title: Product
|
||||
title:
|
||||
en: Product
|
||||
fr: Produit
|
||||
icon: cart
|
||||
|
||||
columns:
|
||||
- width: 1/1
|
||||
fields:
|
||||
info:
|
||||
type: info
|
||||
text:
|
||||
en: "Product data (title, description, images, price) is managed in Shopify Admin. This Kirby page only serves for routing."
|
||||
fr: "Les données produit (titre, description, images, prix) sont gérées dans Shopify Admin. Cette page Kirby sert uniquement au routing."
|
||||
tabs:
|
||||
content:
|
||||
label:
|
||||
en: Content
|
||||
fr: Contenu
|
||||
columns:
|
||||
- width: 2/3
|
||||
sections:
|
||||
main:
|
||||
type: fields
|
||||
fields:
|
||||
price:
|
||||
label:
|
||||
en: Price (€)
|
||||
fr: Prix (€)
|
||||
type: number
|
||||
min: 0
|
||||
step: 0.01
|
||||
required: true
|
||||
translate: false
|
||||
width: 1/4
|
||||
stock:
|
||||
label: Stock
|
||||
type: number
|
||||
min: 0
|
||||
default: 0
|
||||
help:
|
||||
en: Edit through french version
|
||||
fr: Partagé entre les versions FR et EN
|
||||
translate: false
|
||||
width: 1/4
|
||||
space:
|
||||
type: gap
|
||||
width: 2/4
|
||||
weight:
|
||||
label:
|
||||
en: Weight (g)
|
||||
fr: Poids (g)
|
||||
type: number
|
||||
min: 0
|
||||
default: 0
|
||||
help:
|
||||
en: Weight in grams for shipping calculation
|
||||
fr: Poids en grammes pour le calcul de la livraison
|
||||
translate: false
|
||||
width: 1/4
|
||||
length:
|
||||
label:
|
||||
en: Length (cm)
|
||||
fr: Longueur (cm)
|
||||
type: number
|
||||
min: 0
|
||||
default: 0
|
||||
help:
|
||||
en: Package length in centimeters
|
||||
fr: Longueur du colis en centimètres
|
||||
translate: false
|
||||
width: 1/4
|
||||
width:
|
||||
label:
|
||||
en: Width (cm)
|
||||
fr: Largeur (cm)
|
||||
type: number
|
||||
min: 0
|
||||
default: 0
|
||||
help:
|
||||
en: Package width in centimeters
|
||||
fr: Largeur du colis en centimètres
|
||||
translate: false
|
||||
width: 1/4
|
||||
height:
|
||||
label:
|
||||
en: Height (cm)
|
||||
fr: Hauteur (cm)
|
||||
type: number
|
||||
min: 0
|
||||
default: 0
|
||||
help:
|
||||
en: Package height in centimeters
|
||||
fr: Hauteur du colis en centimètres
|
||||
translate: false
|
||||
width: 1/4
|
||||
description:
|
||||
label: Description panier
|
||||
type: writer
|
||||
help: Visible dans le panier seulement.
|
||||
details:
|
||||
label:
|
||||
en: Details
|
||||
fr: Détails
|
||||
type: writer
|
||||
hasOptions:
|
||||
label:
|
||||
en: Options
|
||||
fr: Options
|
||||
type: toggle
|
||||
default: false
|
||||
translate: false
|
||||
width: 1/7
|
||||
optionLabel:
|
||||
label:
|
||||
en: Option label
|
||||
fr: Libellé de l'option
|
||||
type: text
|
||||
width: 3/7
|
||||
when:
|
||||
hasOptions: true
|
||||
optionValues:
|
||||
label:
|
||||
en: Option values
|
||||
fr: Valeurs de l'option
|
||||
type: tags
|
||||
help:
|
||||
en: "Comma-separated values (e.g.: XS, S, M, L, XL)"
|
||||
fr: "Valeurs séparées par des virgules (ex: XS, S, M, L, XL)"
|
||||
translate: false
|
||||
when:
|
||||
hasOptions: true
|
||||
width: 3/7
|
||||
|
||||
shopifyHandle:
|
||||
label:
|
||||
en: Shopify Handle
|
||||
fr: Shopify Handle
|
||||
type: text
|
||||
help:
|
||||
en: "Product handle from Shopify (e.g. tshirt-index-01). If empty, uses the page slug."
|
||||
fr: "Handle du produit Shopify (ex: tshirt-index-01). Si vide, utilise le slug de la page Kirby."
|
||||
placeholder: tshirt-index-01
|
||||
- width: 1/3
|
||||
sections:
|
||||
images:
|
||||
type: files
|
||||
headline:
|
||||
en: Product Images
|
||||
fr: Images du produit
|
||||
template: image
|
||||
layout: cards
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
title: Site
|
||||
|
||||
sections:
|
||||
shopify:
|
||||
type: fields
|
||||
fields:
|
||||
shopifyRefreshButton:
|
||||
type: shopify-refresh
|
||||
label:
|
||||
en: Shopify Products
|
||||
fr: Produits Shopify
|
||||
pages:
|
||||
type: pages
|
||||
headline:
|
||||
en: Products
|
||||
fr: Produits
|
||||
template: product
|
||||
sortBy: title asc
|
||||
info: "{{ page.stock }} en stock"
|
||||
layout: cardlets
|
||||
image:
|
||||
query: page.files.first
|
||||
cover: true
|
||||
|
|
|
|||
|
|
@ -1,142 +1,10 @@
|
|||
<?php
|
||||
|
||||
require_once __DIR__ . '/shopify.php';
|
||||
|
||||
use Kirby\Cms\Page;
|
||||
|
||||
return [
|
||||
'debug' => 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' => [
|
||||
'quality' => 85,
|
||||
'format' => 'webp',
|
||||
|
|
@ -167,4 +35,114 @@ 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);
|
||||
}
|
||||
]
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<?php
|
||||
|
||||
return function ($page, $kirby) {
|
||||
$shopifyHandle = $page->shopifyHandle()->or($page->slug())->value();
|
||||
$language = $kirby->language()->code();
|
||||
|
||||
return [
|
||||
'shopifyHandle' => $shopifyHandle,
|
||||
'language' => $language
|
||||
];
|
||||
};
|
||||
|
|
@ -13,24 +13,7 @@ return [
|
|||
'backToShop' => 'Back to shop',
|
||||
'supportText' => 'To support us, you can also',
|
||||
'makeDonation' => 'make a donation',
|
||||
|
||||
// Shop / Cart
|
||||
'addToCart' => 'Add to cart',
|
||||
'cart' => 'Cart',
|
||||
'cartEmpty' => 'Your cart is empty',
|
||||
'total' => 'Total',
|
||||
'checkout' => 'Checkout',
|
||||
'remove' => 'Remove',
|
||||
'inStock' => 'In stock',
|
||||
'outOfStock' => 'Out of stock',
|
||||
'addingToCart' => 'Adding...',
|
||||
'addedToCart' => 'Added! ✓',
|
||||
'errorAddToCart' => 'Error - Try again',
|
||||
'closeCart' => 'Close cart',
|
||||
'loading' => 'Loading...',
|
||||
'productNotFound' => 'Product not found',
|
||||
'selectVariant' => 'Select',
|
||||
'chooseOption' => 'Choose an option',
|
||||
|
||||
// Blueprints - Home
|
||||
'home.title' => 'Home',
|
||||
|
|
|
|||
|
|
@ -13,24 +13,7 @@ return [
|
|||
'backToShop' => 'Retour à la boutique',
|
||||
'supportText' => 'Pour nous soutenir, vous pouvez aussi',
|
||||
'makeDonation' => 'faire un don',
|
||||
|
||||
// Shop / Cart
|
||||
'addToCart' => 'Ajouter au panier',
|
||||
'cart' => 'Panier',
|
||||
'cartEmpty' => 'Votre panier est vide',
|
||||
'total' => 'Total',
|
||||
'checkout' => 'Passer commande',
|
||||
'remove' => 'Retirer',
|
||||
'inStock' => 'En stock',
|
||||
'outOfStock' => 'Rupture de stock',
|
||||
'addingToCart' => 'Ajout en cours...',
|
||||
'addedToCart' => 'Ajouté ! ✓',
|
||||
'errorAddToCart' => 'Erreur - Réessayer',
|
||||
'closeCart' => 'Fermer le panier',
|
||||
'loading' => 'Chargement...',
|
||||
'productNotFound' => 'Produit non trouvé',
|
||||
'selectVariant' => 'Choisir',
|
||||
'chooseOption' => 'Choisissez une option',
|
||||
|
||||
// Blueprints - Home
|
||||
'home.title' => 'Accueil',
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# 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
14
site/plugins/shopify-refresh-button/.gitignore
vendored
|
|
@ -1,14 +0,0 @@
|
|||
# OS files
|
||||
.DS_Store
|
||||
|
||||
# npm modules
|
||||
/node_modules
|
||||
|
||||
# Parcel cache folder
|
||||
.cache
|
||||
|
||||
# Composer files
|
||||
/vendor
|
||||
|
||||
# kirbyup temp development entry
|
||||
/index.dev.mjs
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
# 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)
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# 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.*
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"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
66
site/plugins/shopify-refresh-button/composer.lock
generated
|
|
@ -1,66 +0,0 @@
|
|||
{
|
||||
"_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"
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
(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}})})();
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
<?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()
|
||||
];
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"scripts": {
|
||||
"dev": "npx -y kirbyup src/index.js --watch",
|
||||
"serve": "npx -y kirbyup serve src/index.js",
|
||||
"build": "npx -y kirbyup src/index.js"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import ShopifyRefreshButton from "./components/ShopifyRefreshButton.vue";
|
||||
|
||||
window.panel.plugin("index/shopify-refresh-button", {
|
||||
fields: {
|
||||
"shopify-refresh": ShopifyRefreshButton
|
||||
}
|
||||
});
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<div class="product-purchase">
|
||||
<div class="product-stock-info">
|
||||
<p data-product-stock class="stock-status"></p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn-add-to-cart"
|
||||
data-shopify-add-to-cart
|
||||
data-product-id="15689076179317"
|
||||
data-variant-id=""
|
||||
data-text-add="<?= t('addToCart') ?>"
|
||||
data-text-adding="<?= t('addingToCart') ?>"
|
||||
data-text-added="<?= t('addedToCart') ?>"
|
||||
data-text-error="<?= t('errorAddToCart') ?>"
|
||||
data-text-out-of-stock="<?= t('outOfStock') ?>"
|
||||
data-text-in-stock="<?= t('inStock') ?>"
|
||||
>
|
||||
<?= t('addToCart') ?>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<div id="cart-drawer" class="cart-drawer" data-text-remove="<?= t('remove') ?>">
|
||||
<div class="cart-drawer__overlay" data-cart-close></div>
|
||||
<div class="cart-drawer__panel">
|
||||
<div class="cart-drawer__header">
|
||||
<h3><?= t('cart') ?></h3>
|
||||
<button class="cart-drawer__close" data-cart-close aria-label="<?= t('closeCart') ?>">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="cart-drawer__content">
|
||||
<div class="cart-drawer__empty" data-cart-empty>
|
||||
<p><?= t('cartEmpty') ?></p>
|
||||
</div>
|
||||
|
||||
<div class="cart-drawer__items" data-cart-items></div>
|
||||
</div>
|
||||
|
||||
<div class="cart-drawer__footer">
|
||||
<div class="cart-drawer__total">
|
||||
<span class="cart-drawer__total-label"><?= t('total') ?></span>
|
||||
<span class="cart-drawer__total-amount" data-cart-total>0,00 €</span>
|
||||
</div>
|
||||
<button class="cart-drawer__checkout-btn" data-cart-checkout>
|
||||
<?= t('checkout') ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
<?php snippet('cart-drawer') ?>
|
||||
|
||||
<footer id="site-footer">
|
||||
<div class="site-footer__container">
|
||||
<div class="footer__mentions">
|
||||
|
|
@ -12,17 +10,10 @@
|
|||
</footer>
|
||||
|
||||
<script src="<?= url('assets/js/onload.js') ?>"></script>
|
||||
<script src="<?= url('assets/js/shopify-cart.js') ?>"></script>
|
||||
|
||||
<?php if ($scripts ?? null): ?>
|
||||
<?php if (in_array('product', $scripts)): ?>
|
||||
<script src="<?= url('assets/js/product-loader.js') ?>"></script>
|
||||
<script src="<?= url('assets/js/product-add-to-cart.js') ?>"></script>
|
||||
<?php endif ?>
|
||||
<?php else: ?>
|
||||
<script src="<?= url('assets/js/products-list-loader.js') ?>"></script>
|
||||
<?php if(isset($scripts) && is_array($scripts)): ?>
|
||||
<?php foreach($scripts as $script): ?>
|
||||
<script src="<?= url($script) ?>"></script>
|
||||
<?php endforeach ?>
|
||||
<?php endif ?>
|
||||
|
||||
<script src="<?= url('assets/js/cart-drawer.js') ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="<?= $kirby->language()->code() ?>">
|
||||
<head>
|
||||
<?php snippet('seo') ?>
|
||||
<meta charset="UTF-8">
|
||||
<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" />
|
||||
<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/css/style.css') ?>" />
|
||||
<link rel="stylesheet" type="text/css" href="<?= url('assets/css/cart-drawer.css') ?>" />
|
||||
</head>
|
||||
<body data-template="<?= $page->template() ?>">
|
||||
<body data-template="<?= $template ?? 'default' ?>">
|
||||
<header id="site-header">
|
||||
<div class="header-left"></div>
|
||||
|
||||
|
|
@ -43,9 +46,5 @@
|
|||
</li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
|
||||
<button class="header-cart-btn" data-cart-open aria-label="<?= t('cart') ?>">
|
||||
<?= t('cart') ?> <span class="header-cart-count" data-cart-count></span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
<?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') ?>">
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
<?= '<?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>
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,24 +1,35 @@
|
|||
<?php snippet('header', ['title' => $site->title(), 'template' => 'store']) ?>
|
||||
|
||||
<main>
|
||||
<p class="p__baseline-big">
|
||||
<?= $page->baseline()->or('Bienvenue sur la boutique de soutien à Index') ?>
|
||||
</p>
|
||||
<main>
|
||||
<p class="p__baseline-big">
|
||||
<?= $page->baseline()->or('Bienvenue sur la boutique de soutien à Index') ?>
|
||||
</p>
|
||||
|
||||
<section id="store__container"
|
||||
data-products-loader
|
||||
data-language="<?= strtoupper($kirby->language()->code()) ?>">
|
||||
<section id="store__container">
|
||||
<?php foreach($site->children()->listed() as $product): ?>
|
||||
<article class="store__product">
|
||||
<figure>
|
||||
<?php if($cover = $product->files()->sort()->first()): ?>
|
||||
<?php snippet('picture', [
|
||||
'file' => $cover,
|
||||
'alt' => $product->title()->html(),
|
||||
'preset' => 'product-card',
|
||||
'size' => 25,
|
||||
'lazy' => true
|
||||
]) ?>
|
||||
<?php endif ?>
|
||||
</figure>
|
||||
<p class="line-1"><a href="<?= $product->url() ?>"><?= $product->title()->html() ?></a></p>
|
||||
<p class="price"><?= $product->price() ?>€</p>
|
||||
<a href="<?= $product->url() ?>" class="link-block" aria-hidden="true"></a>
|
||||
</article>
|
||||
<?php endforeach ?>
|
||||
</section>
|
||||
|
||||
<div class="products-loading">
|
||||
<p><?= t('loading') ?></p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<p class="p__baseline-big">
|
||||
<?= t('supportText') ?>
|
||||
<a href="https://soutenir.index.ngo" class="link-don"><?= t('makeDonation') ?></a>
|
||||
</p>
|
||||
</main>
|
||||
<p class="p__baseline-big">
|
||||
<?= t('supportText', 'Pour nous soutenir, vous pouvez aussi') ?>
|
||||
<a href="https://soutenir.index.ngo" class="link-don"><?= t('makeDonation', 'faire un don') ?></a>
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<?php snippet('footer') ?>
|
||||
|
|
|
|||
|
|
@ -1,52 +1,115 @@
|
|||
<?php
|
||||
$shopifyHandle = $page->shopifyHandle()->or($page->slug());
|
||||
<?php snippet('header', ['title' => $page->title(), 'template' => 'shop']) ?>
|
||||
|
||||
snippet('header', ['title' => $page->title(), 'template' => 'shop']);
|
||||
?>
|
||||
<main>
|
||||
<nav class="store__nav">
|
||||
<a href="<?= $site->homePage()->url() ?>"><?= t('backToShop', 'Retour à la boutique') ?></a>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<nav class="store__nav">
|
||||
<a href="<?= $site->homePage()->url() ?>"><?= t('backToShop') ?></a>
|
||||
</nav>
|
||||
|
||||
<section class="section__product"
|
||||
data-product-loader
|
||||
data-shopify-handle="<?= $shopifyHandle ?>"
|
||||
data-language="<?= $kirby->language()->code() ?>">
|
||||
|
||||
<div class="product-loading">
|
||||
<p><?= t('loading') ?></p>
|
||||
</div>
|
||||
|
||||
<div class="product-content" style="display: none;">
|
||||
<section class="section__product">
|
||||
<div class="col-left">
|
||||
<div class="hero">
|
||||
<h2 class="p__baseline-big" data-product-title></h2>
|
||||
<p class="p__baseline-big" data-product-price></p>
|
||||
<h2 class="p__baseline-big"><?= $page->title()->html() ?></h2>
|
||||
<p class="p__baseline-big"><?= $page->price() ?>€</p>
|
||||
</div>
|
||||
|
||||
<div class="details" data-product-details></div>
|
||||
|
||||
<div class="product-options" data-product-options style="display: none;">
|
||||
<ul class="product-options__list" data-product-options-list></ul>
|
||||
<div class="details">
|
||||
<?php if($page->details()->isNotEmpty()): ?>
|
||||
<?= $page->details()->kt() ?>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
|
||||
<?php snippet('buy-button') ?>
|
||||
<?php if($page->hasOptions()->toBool() && $page->optionValues()->isNotEmpty()): ?>
|
||||
<div class="product-options">
|
||||
<ul class="product-options__list">
|
||||
<?php
|
||||
$values = $page->optionValues()->split(',');
|
||||
$optionSlug = $page->optionLabel()->slug();
|
||||
foreach($values as $index => $value):
|
||||
$value = trim($value);
|
||||
$uniqueId = $optionSlug . '-' . Str::slug(strtolower($value));
|
||||
?>
|
||||
<li>
|
||||
<input type="radio" id="<?= $uniqueId ?>" name="<?= $optionSlug ?>" value="<?= $value ?>" />
|
||||
<label for="<?= $uniqueId ?>"><?= $value ?></label>
|
||||
</li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<div class="add-to-cart">
|
||||
<button
|
||||
class="btn__default snipcart-add-item"
|
||||
data-item-id="<?= $page->slug() ?>"
|
||||
data-item-price="<?= $page->price() ?>"
|
||||
data-item-description="<?= $page->description()->excerpt(100) ?>"
|
||||
data-item-image="<?= $page->images()->first() ? $page->images()->first()->url() : '' ?>"
|
||||
data-item-name="<?= $page->title()->html() ?>"
|
||||
data-item-shippable="true"
|
||||
data-item-weight="<?= $page->weight()->or(0) ?>"
|
||||
data-item-length="<?= $page->length()->or(0) ?>"
|
||||
data-item-width="<?= $page->width()->or(0) ?>"
|
||||
data-item-height="<?= $page->height()->or(0) ?>"
|
||||
<?php
|
||||
if($page->hasOptions()->toBool() && $page->optionValues()->isNotEmpty()):
|
||||
$values = $page->optionValues()->split(',');
|
||||
$trimmedValues = array_map('trim', $values);
|
||||
$snipcartOptions = implode('|', $trimmedValues);
|
||||
?>
|
||||
data-item-custom1-name="<?= $page->optionLabel()->html() ?>"
|
||||
data-item-custom1-options="<?= $snipcartOptions ?>"
|
||||
data-item-custom1-required="true"
|
||||
disabled
|
||||
<?php endif; ?>
|
||||
>
|
||||
<span class="icon">
|
||||
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m14.523 18.787s4.501-4.505 6.255-6.26c.146-.146.219-.338.219-.53s-.073-.383-.219-.530c-1.753-1.754-6.255-6.258-6.255-6.258-.144-.145-.334-.217-.524-.217-.193 0-.385.074-.532.221-.293.292-.295.766-.004 1.056l4.978 4.978h-14.692c-.414 0-.75.336-.75.75s.336.75.75.75h14.692l-4.979 4.979c-.289.289-.286.762.006 1.054.148.148.341.222.533.222.19 0 .378-.072.522-.215z" fill-rule="nonzero" />
|
||||
</svg>
|
||||
</span>
|
||||
<div class="txt" data-default-text="<?= t('addToCart', 'Ajouter au panier') ?>">
|
||||
<?php if($page->hasOptions()->toBool() && $page->optionValues()->isNotEmpty()): ?>
|
||||
<?= t('chooseOption', 'Choisissez une option') ?>
|
||||
<?php else: ?>
|
||||
<?= t('addToCart', 'Ajouter au panier') ?>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-gallery swiper">
|
||||
<div class="swiper-wrapper" data-product-images></div>
|
||||
<div class="swiper-wrapper">
|
||||
<?php
|
||||
// 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-next"></div>
|
||||
|
||||
<!-- Pagination dots -->
|
||||
<div class="swiper-pagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="product-error" style="display: none;">
|
||||
<p><?= t('productNotFound') ?></p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<?php snippet('structured-data-product') ?>
|
||||
<?php snippet('footer', ['scripts' => ['product']]) ?>
|
||||
<?php snippet('footer', ['scripts' => ['assets/js/product-size.js', 'assets/js/snipcart.js', 'assets/js/product-gallery.js']]) ?>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue