Compare commits

..

No commits in common. "main" and "sliders" have entirely different histories.

63 changed files with 894 additions and 3702 deletions

View file

@ -1,17 +1,7 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"WebSearch", "WebSearch"
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(find:*)",
"Bash(curl:*)",
"WebFetch(domain:snipcart.com)",
"Bash(grep:*)",
"Bash(npm run build:*)",
"Bash(php test-shopify.php:*)",
"WebFetch(domain:getkirby.com)",
"WebFetch(domain:forum.getkirby.com)"
] ]
} }
} }

View file

@ -26,7 +26,7 @@ jobs:
set ftp:ssl-allow no set ftp:ssl-allow no
open -u $USERNAME,$PASSWORD $PRODUCTION_HOST open -u $USERNAME,$PASSWORD $PRODUCTION_HOST
mirror --reverse --verbose --ignore-time --parallel=10 -x local/ assets assets mirror --reverse --verbose --ignore-time --parallel=10 -x local/ assets assets
mirror --reverse --verbose --ignore-time --parallel=10 -x accounts/ -x cache/ -x sessions/ 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 kirby kirby
mirror --reverse --verbose --ignore-time --parallel=10 vendor vendor mirror --reverse --verbose --ignore-time --parallel=10 vendor vendor
quit quit

7
.gitignore vendored
View file

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

View file

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

View file

@ -1,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;
}
}
}

View file

@ -1,7 +1,7 @@
[data-template="subscription-newsletter"], [data-template="subscription-newsletter"],
[data-template="thanks"], [data-template="thanks"],
[data-template="support"], [data-template="support"],
[data-template="home"] { [data-template="store"] {
.p__baseline-big { .p__baseline-big {
font-family: var(--title); font-family: var(--title);
font-size: var(--fs-big); font-size: var(--fs-big);

View file

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

View file

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

File diff suppressed because one or more lines are too long

View file

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

View file

@ -1,4 +1,6 @@
[data-template="home"] { [data-template="store"] {
.p__baseline-big { .p__baseline-big {
margin-top: calc(var(--spacing) * 2); margin-top: calc(var(--spacing) * 2);
} }

View file

@ -1,3 +1,12 @@
.section__product,
.store__nav{
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
.section__product, .section__product,
.store__nav { .store__nav {
max-width: 1200px; max-width: 1200px;
@ -5,9 +14,9 @@
margin-right: auto; margin-right: auto;
} }
.product-content {
display: contents;
}
.store__nav { .store__nav {
padding-top: calc(var(--spacing) * 1); padding-top: calc(var(--spacing) * 1);
@ -18,35 +27,174 @@
a { a {
text-decoration: none; text-decoration: none;
&::before {
content: "";
}
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
} }
} }
@media #{$small} { a::before {
a { content: "";
padding-top: 0; }
font-size: var(--fs-small); }
.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 { 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; display: flex;
flex-direction: column; flex-direction: column;
margin-bottom: 10vh; margin-bottom: 10vh;
@ -59,7 +207,6 @@
margin-top: calc(var(--spacing) * 0.5); margin-top: calc(var(--spacing) * 0.5);
order: 1; order: 1;
} }
figure { figure {
order: 2; order: 2;
margin-bottom: calc(var(--spacing) * 1); margin-bottom: calc(var(--spacing) * 1);
@ -79,25 +226,25 @@
order: 5; order: 5;
} }
.product-gallery { .product-gallery{
width: 100vw; width: 100vw;
position: relative; position: relative;
left: calc(var(--padding-body) * -1); left: calc(var(--padding-body)*-1);
.swiper-button-prev, .swiper-button-prev,
.swiper-button-next { .swiper-button-next{ display: none; }
display: none;
}
} }
} }
}
@media #{$small-up} { @media #{$small-up} {
.product-content {
display: grid;
grid-template-columns: 1fr 1fr; .section__product{
gap: calc(var(--padding-body) * 2); display: grid;
margin-bottom: calc(var(--spacing) * 3); grid-template-columns: 1fr 1fr;
} gap: calc(var(--padding-body)*2);
margin-bottom: calc(var(--spacing)*3);
.details { .details {
margin-bottom: calc(var(--spacing) * 2); margin-bottom: calc(var(--spacing) * 2);
@ -108,140 +255,17 @@
border-top: var(--border-light); border-top: var(--border-light);
} }
.col-left { .col-left{
min-height: 100%; min-height: 100%;
padding-bottom: 40px; padding-bottom: 40px; //dots
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
}
}
.product-options {
border-bottom: var(--border-light);
padding: calc(var(--spacing) * 0.25) 0;
}
.product-options__list {
list-style: none;
display: flex;
gap: 2ch;
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, .product-gallery .swiper-slide figure{
.swiper-button-next { width: calc(100% - 60px);
color: var(--color-txt);
width: 20px;
height: 20px;
&:after {
font-size: 20px;
font-weight: bold;
}
&:hover {
opacity: 0.7;
}
}
.swiper-pagination {
position: relative;
margin-top: calc(var(--spacing) * 0.5);
bottom: 0;
.swiper-pagination-bullet {
width: 8px;
height: 8px;
background: var(--grey-600);
opacity: 0.5;
transition: opacity 0.3s;
&:hover {
opacity: 0.7;
}
&.swiper-pagination-bullet-active {
background: var(--color-txt);
opacity: 1;
}
}
} }
} }
.hero {
margin-bottom: calc(var(--spacing) * 1);
padding: calc(var(--spacing) * 0.5) 0;
border-top: var(--border-light);
border-bottom: var(--border-light);
.p__baseline-big {
margin: 0;
text-align: left;
}
}
.add-to-cart {
margin: 0;
border-bottom: var(--border-light);
padding: calc(var(--spacing) * 0.5) 0;
}

View file

@ -5,30 +5,29 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: calc(var(--spacing) * 4) var(--spacing); padding: calc(var(--spacing) * 4) var(--spacing);
}
.thanks-content { .thanks-content {
text-align: center; text-align: center;
max-width: 600px; max-width: 600px;
display: flex;
flex-direction: column;
align-items: center;
h1 { h1 {
margin-bottom: calc(var(--spacing) * 2); 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 { .thanks-actions {
font-size: var(--fs-medium); margin-top: calc(var(--spacing) * 3);
line-height: 1.1;
p {
margin-bottom: var(--spacing);
}
}
.thanks-actions {
width: max-content;
}
} }
} }

View file

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

View file

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

View file

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

View file

@ -1,47 +0,0 @@
(async function() {
const container = document.querySelector('[data-products-loader]');
if (!container) return;
const language = (container.dataset.language || 'FR').toLowerCase();
const isEnglish = language === 'en';
try {
const cart = new ShopifyCart({
domain: 'nv7cqv-bu.myshopify.com',
storefrontAccessToken: 'dec3d35a2554384d149c72927d1cfd1b'
});
const productsData = await cart.getAllProducts();
const products = productsData.edges;
const productsHtml = products.map(edge => {
const product = edge.node;
const image = product.images.edges[0]?.node;
const price = parseFloat(product.priceRange.minVariantPrice.amount).toFixed(2);
const slug = product.handle;
const productUrl = isEnglish ? `/en/${slug}` : `/${slug}`;
const productTitle = isEnglish && product.titleEn?.value
? product.titleEn.value
: product.title;
return `
<article class="store__product">
<figure>
${image ? `<img src="${image.url}" alt="${image.altText || productTitle}" loading="lazy" />` : ''}
</figure>
<p class="line-1">
<a href="${productUrl}">${productTitle}</a>
</p>
<p class="price">${price}</p>
<a href="${productUrl}" class="link-block" aria-hidden="true"></a>
</article>
`;
}).join('');
container.innerHTML = productsHtml;
} catch (error) {
console.error('Error loading products:', error);
container.innerHTML = '<p>Erreur lors du chargement des produits</p>';
}
})();

View file

@ -1,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;

View file

@ -6,13 +6,13 @@ window.SnipcartSettings = {
// Redirection après paiement réussi // Redirection après paiement réussi
document.addEventListener('snipcart.ready', function() { document.addEventListener('snipcart.ready', function() {
Snipcart.events.on('cart.confirmed', function(cartState) { Snipcart.execute('bind', 'order.completed', function(order) {
// Détecter la langue actuelle depuis l'URL // Détecter la langue actuelle depuis l'URL
const currentPath = window.location.pathname; const currentPath = window.location.pathname;
const langMatch = currentPath.match(/^\/([a-z]{2})(\/|$)/); const langMatch = currentPath.match(/^\/([a-z]{2})(\/|$)/);
const langPrefix = langMatch ? '/' + langMatch[1] : ''; const langPrefix = langMatch ? '/' + langMatch[1] : '';
window.location.href = langPrefix + '/thanks?order=' + cartState.token; window.location.href = langPrefix + '/thanks?order=' + order.token;
}); });
}); });

View file

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

View file

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

View file

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

View file

@ -0,0 +1,52 @@
Title: T-shirt Index 01
----
Price: 35
----
Description: <p>T-shirt de soutien à Index, 100% coton</p>
----
Details:
[
{
"content": {
"text": "<p>100% cotton</p>"
},
"id": "detail1",
"isHidden": false,
"type": "text"
},
{
"content": {
"text": "<p>Lorem ipsum dolor sit amet</p>"
},
"id": "detail2",
"isHidden": false,
"type": "text"
}
]
----
Options:
-
label: Size
values: XS, S, M, L, XL
----
Snipcartid: tshirt-01
----
Backgroundcolor: #ffffff
----
Template: product

View file

@ -0,0 +1,56 @@
Title: T-shirt Index 01
----
Price: 35
----
Stock: 10
----
Description: T-shirt de soutien à Index, 100% coton
----
Details: <p>T-shirt en coton organique avec impression sérigraphique.<br>Marquage sur la face avant : logo « INDEX » de 10 cm de large.</p><ul><li><p>100 % coton biologique</p></li><li><p>Grammage : 180 g/m²</p></li><li><p>Jersey simple au toucher très doux</p></li><li><p>Excellente tenue dans le temps</p></li><li><p>Bande de propreté intérieure au col</p></li><li><p>Surpiqûres doubles en bas de manches et en bas de corps</p></li></ul><p>Envoi uniquement via Mondial Relay vers la France, la Belgique et la Suisse.</p>
----
Hasoptions: true
----
Optionlabel: Taille
----
Optionvalues: XS, S, M, L, XL
----
Options:
-
label: Taille
values: XS, S, M, L, XL
-
label: Couleur
values: Rouge, Vert
----
Snipcartid: tshirt-01
----
Backgroundcolor: #ffffff
----
Template: product
----
Uuid: udrrfizhayqixfoo

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View file

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

View file

@ -1,5 +0,0 @@
Title: Error
----
Template: default

View file

@ -1,5 +1 @@
Title: Erreur Uuid: kcrqkszqasludg2h
----
Uuid: kcrqkszqasludg2h

View file

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

View file

@ -7,7 +7,3 @@ Text:
Votre commande a été confirmée ! Vous allez recevoir un email de confirmation sous peu. Votre commande a été confirmée ! Vous allez recevoir un email de confirmation sous peu.
Merci pour votre achat. Merci pour votre achat.
----
Template: thanks

View file

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

View file

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

View file

@ -1,21 +1,134 @@
title: Product title:
en: Product
fr: Produit
icon: cart icon: cart
columns: tabs:
- width: 1/1 content:
fields: label:
info: en: Content
type: info fr: Contenu
text: columns:
en: "Product data (title, description, images, price) is managed in Shopify Admin. This Kirby page only serves for routing." - width: 2/3
fr: "Les données produit (titre, description, images, prix) sont gérées dans Shopify Admin. Cette page Kirby sert uniquement au routing." 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: - width: 1/3
label: sections:
en: Shopify Handle images:
fr: Shopify Handle type: files
type: text headline:
help: en: Product Images
en: "Product handle from Shopify (e.g. tshirt-index-01). If empty, uses the page slug." fr: Images du produit
fr: "Handle du produit Shopify (ex: tshirt-index-01). Si vide, utilise le slug de la page Kirby." template: image
placeholder: tshirt-index-01 layout: cards

View file

@ -1,11 +1,15 @@
title: Site title: Site
sections: sections:
shopify: pages:
type: fields type: pages
fields: headline:
shopifyRefreshButton: en: Products
type: shopify-refresh fr: Produits
label: template: product
en: Shopify Products sortBy: title asc
fr: Produits Shopify info: "{{ page.stock }} en stock"
layout: cardlets
image:
query: page.files.first
cover: true

View file

@ -1,142 +1,10 @@
<?php <?php
require_once __DIR__ . '/shopify.php';
use Kirby\Cms\Page;
return [ return [
'debug' => true, 'debug' => true,
'languages' => true, 'languages' => true,
'cache' => [
'shopify' => true
],
'routes' => [
// Sitemap
[
'pattern' => 'sitemap.xml',
'action' => function() {
$sitemap = page('home');
return new Kirby\Cms\Response(
snippet('sitemap', ['page' => $sitemap], true),
'application/xml'
);
}
],
// English homepage
[
'pattern' => 'en',
'action' => function() {
$home = page('home');
if ($home) {
site()->visit($home, 'en');
return $home;
}
return null;
}
],
// English thanks page
[
'pattern' => 'en/thanks',
'action' => function() {
$thanks = page('thanks');
if ($thanks) {
site()->visit($thanks, 'en');
return $thanks;
}
return null;
}
],
// English error page
[
'pattern' => 'en/error',
'action' => function() {
$error = page('error');
if ($error) {
site()->visit($error, 'en');
return $error;
}
return null;
}
],
// French thanks page
[
'pattern' => 'thanks',
'action' => function() {
return page('thanks');
}
],
// French error page
[
'pattern' => 'error',
'action' => function() {
return page('error');
}
],
// French products (default)
[
'pattern' => '(:any)',
'action' => function($slug) {
if (in_array($slug, ['home', 'error', 'thanks'])) {
return null;
}
$products = getShopifyProducts();
foreach ($products as $product) {
if ($product['handle'] === $slug) {
$page = Page::factory([
'slug' => $product['handle'],
'template' => 'product',
'content' => [
'title' => $product['title'],
'shopifyHandle' => $product['handle'],
'uuid' => $product['id']
]
]);
site()->visit($page, 'fr');
return $page;
}
}
return null;
}
],
// English products
[
'pattern' => 'en/(:any)',
'action' => function($slug) {
if (in_array($slug, ['home', 'error', 'thanks'])) {
return null;
}
$products = getShopifyProducts();
foreach ($products as $product) {
if ($product['handle'] === $slug) {
$page = Page::factory([
'slug' => $product['handle'],
'template' => 'product',
'content' => [
'title' => $product['title'],
'shopifyHandle' => $product['handle'],
'uuid' => $product['id']
]
]);
site()->visit($page, 'en');
return $page;
}
}
return null;
}
]
],
'thumbs' => [ 'thumbs' => [
'quality' => 85, 'quality' => 85,
'format' => 'webp', 'format' => 'webp',
@ -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);
}
]
]
]; ];

View file

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

View file

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

View file

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

View file

@ -13,24 +13,7 @@ return [
'backToShop' => 'Retour à la boutique', 'backToShop' => 'Retour à la boutique',
'supportText' => 'Pour nous soutenir, vous&nbsp;pouvez&nbsp;aussi', 'supportText' => 'Pour nous soutenir, vous&nbsp;pouvez&nbsp;aussi',
'makeDonation' => 'faire un don', 'makeDonation' => 'faire un don',
// Shop / Cart
'addToCart' => 'Ajouter au panier', 'addToCart' => 'Ajouter au panier',
'cart' => 'Panier',
'cartEmpty' => 'Votre panier est vide',
'total' => 'Total',
'checkout' => 'Passer commande',
'remove' => 'Retirer',
'inStock' => 'En stock',
'outOfStock' => 'Rupture de stock',
'addingToCart' => 'Ajout en cours...',
'addedToCart' => 'Ajouté ! ✓',
'errorAddToCart' => 'Erreur - Réessayer',
'closeCart' => 'Fermer le panier',
'loading' => 'Chargement...',
'productNotFound' => 'Produit non trouvé',
'selectVariant' => 'Choisir',
'chooseOption' => 'Choisissez une option',
// Blueprints - Home // Blueprints - Home
'home.title' => 'Accueil', 'home.title' => 'Accueil',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +0,0 @@
import ShopifyRefreshButton from "./components/ShopifyRefreshButton.vue";
window.panel.plugin("index/shopify-refresh-button", {
fields: {
"shopify-refresh": ShopifyRefreshButton
}
});

View file

@ -1,20 +0,0 @@
<div class="product-purchase">
<div class="product-stock-info">
<p data-product-stock class="stock-status"></p>
</div>
<button
class="btn-add-to-cart"
data-shopify-add-to-cart
data-product-id="15689076179317"
data-variant-id=""
data-text-add="<?= t('addToCart') ?>"
data-text-adding="<?= t('addingToCart') ?>"
data-text-added="<?= t('addedToCart') ?>"
data-text-error="<?= t('errorAddToCart') ?>"
data-text-out-of-stock="<?= t('outOfStock') ?>"
data-text-in-stock="<?= t('inStock') ?>"
>
<?= t('addToCart') ?>
</button>
</div>

View file

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

View file

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

View file

@ -1,5 +1,3 @@
<?php snippet('cart-drawer') ?>
<footer id="site-footer"> <footer id="site-footer">
<div class="site-footer__container"> <div class="site-footer__container">
<div class="footer__mentions"> <div class="footer__mentions">
@ -12,17 +10,10 @@
</footer> </footer>
<script src="<?= url('assets/js/onload.js') ?>"></script> <script src="<?= url('assets/js/onload.js') ?>"></script>
<script src="<?= url('assets/js/shopify-cart.js') ?>"></script> <?php if(isset($scripts) && is_array($scripts)): ?>
<?php foreach($scripts as $script): ?>
<?php if ($scripts ?? null): ?> <script src="<?= url($script) ?>"></script>
<?php if (in_array('product', $scripts)): ?> <?php endforeach ?>
<script src="<?= url('assets/js/product-loader.js') ?>"></script>
<script src="<?= url('assets/js/product-add-to-cart.js') ?>"></script>
<?php endif ?>
<?php else: ?>
<script src="<?= url('assets/js/products-list-loader.js') ?>"></script>
<?php endif ?> <?php endif ?>
<script src="<?= url('assets/js/cart-drawer.js') ?>"></script>
</body> </body>
</html> </html>

View file

@ -1,16 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="<?= $kirby->language()->code() ?>"> <html lang="<?= $kirby->language()->code() ?>">
<head> <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" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.css" />
<script src="https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.js"></script>
<link rel="stylesheet" type="text/css" href="<?= url('assets/fonts/stylesheet.css') ?>?version-cache-prevent<?= rand(0, 1000)?>" /> <link rel="stylesheet" type="text/css" href="<?= url('assets/fonts/stylesheet.css') ?>?version-cache-prevent<?= rand(0, 1000)?>" />
<link rel="stylesheet" type="text/css" href="<?= url('assets/css/style.css') ?>" /> <link rel="stylesheet" type="text/css" href="<?= url('assets/css/style.css') ?>" />
<link rel="stylesheet" type="text/css" href="<?= url('assets/css/cart-drawer.css') ?>" />
</head> </head>
<body data-template="<?= $page->template() ?>"> <body data-template="<?= $template ?? 'default' ?>">
<header id="site-header"> <header id="site-header">
<div class="header-left"></div> <div class="header-left"></div>
@ -43,9 +46,5 @@
</li> </li>
<?php endforeach ?> <?php endforeach ?>
</ul> </ul>
<button class="header-cart-btn" data-cart-open aria-label="<?= t('cart') ?>">
<?= t('cart') ?> <span class="header-cart-count" data-cart-count></span>
</button>
</div> </div>
</header> </header>

View file

@ -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') ?>">

View file

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

View file

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

View file

@ -1,24 +1,35 @@
<?php snippet('header', ['title' => $site->title(), 'template' => 'store']) ?> <?php snippet('header', ['title' => $site->title(), 'template' => 'store']) ?>
<main> <main>
<p class="p__baseline-big"> <p class="p__baseline-big">
<?= $page->baseline()->or('Bienvenue sur la boutique de soutien à Index') ?> <?= $page->baseline()->or('Bienvenue sur la boutique de soutien à Index') ?>
</p> </p>
<section id="store__container" <section id="store__container">
data-products-loader <?php foreach($site->children()->listed() as $product): ?>
data-language="<?= strtoupper($kirby->language()->code()) ?>"> <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 class="p__baseline-big">
<p><?= t('loading') ?></p> <?= t('supportText', 'Pour nous soutenir, vous&nbsp;pouvez&nbsp;aussi') ?>
</div> <a href="https://soutenir.index.ngo" class="link-don"><?= t('makeDonation', 'faire un don') ?></a>
</p>
</section> </main>
<p class="p__baseline-big">
<?= t('supportText') ?>
<a href="https://soutenir.index.ngo" class="link-don"><?= t('makeDonation') ?></a>
</p>
</main>
<?php snippet('footer') ?> <?php snippet('footer') ?>

View file

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