first commit
Some checks are pending
Deploy / Deploy to Production (push) Waiting to run

This commit is contained in:
isUnknown 2025-12-10 15:12:06 +01:00
commit a3620a1f5f
1042 changed files with 226722 additions and 0 deletions

0
site/accounts/index.html Normal file
View file

View file

@ -0,0 +1,21 @@
title: Default Page
columns:
main:
width: 2/3
sections:
fields:
type: fields
fields:
text:
type: textarea
size: huge
sidebar:
width: 1/3
sections:
pages:
type: pages
template: default
files:
type: files

View file

@ -0,0 +1,30 @@
title:
en: Home
fr: Accueil
icon: shop
columns:
- width: 2/3
sections:
products:
type: pages
headline:
en: Products
fr: Produits
template: product
sortBy: title asc
info: "{{ page.stock }} en stock"
layout: cardlets
image:
query: page.files.first
cover: true
- width: 1/3
sections:
settings:
type: fields
fields:
baseline:
label: Baseline
type: textarea
size: small

View file

@ -0,0 +1,79 @@
title:
en: Product
fr: Produit
icon: cart
tabs:
content:
label:
en: Content
fr: Contenu
columns:
- width: 2/3
sections:
main:
type: fields
fields:
price:
label:
en: Price (€)
fr: Prix (€)
type: number
min: 0
step: 0.01
required: true
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
description:
label: Description
type: writer
details:
label:
en: Details
fr: Détails
type: writer
hasOptions:
label:
en: Options
fr: Options
type: toggle
default: 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)"
when:
hasOptions: true
width: 3/7
- width: 1/3
sections:
images:
type: files
headline:
en: Product Images
fr: Images du produit
template: image
layout: cards

15
site/blueprints/site.yml Normal file
View file

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

0
site/cache/index.html vendored Normal file
View file

63
site/config/config.php Normal file
View file

@ -0,0 +1,63 @@
<?php
return [
'debug' => true,
'languages' => true,
'routes' => [
[
'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'])) {
foreach ($order['items'] as $item) {
$productId = $item['id'] ?? null;
$quantity = $item['quantity'] ?? 0;
if ($productId && $quantity > 0) {
// Trouver le produit par son snipcartId
$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 dans toutes les langues
$product->update([
'stock' => $newStock
]);
kirby()->impersonate('kirby');
break;
}
}
}
}
}
}
return Response::json(['status' => 'success'], 200);
}
]
]
];

39
site/languages/en.php Normal file
View file

@ -0,0 +1,39 @@
<?php
return [
'code' => 'en',
'default' => false,
'direction' => 'ltr',
'locale' => 'en_US',
'name' => 'English',
'url' => '/en',
'translations' => [
// General
'backToHome' => 'Back to home',
'backToShop' => 'Back to shop',
'supportText' => 'To support us, you&nbsp;can&nbsp;also',
'makeDonation' => 'make a donation',
'addToCart' => 'Add to cart',
// Blueprints - Home
'home.title' => 'Home',
'home.baseline.label' => 'Baseline',
'home.products.headline' => 'Products',
'home.settings.headline' => 'Settings',
// Blueprints - Product
'product.title' => 'Product',
'product.description.label' => 'Description',
'product.details.label' => 'Details',
'product.details.item.label' => 'Detail',
'product.price.label' => 'Price (€)',
'product.backgroundColor.label' => 'Background Color',
'product.stock.label' => 'Stock',
'product.stock.help' => 'Shared between FR and EN versions',
'product.snipcartId.label' => 'Snipcart ID',
'product.sizes.label' => 'Available Sizes',
'product.images.headline' => 'Product Images',
'product.meta.headline' => 'Metadata',
'product.tab.content' => 'Content',
]
];

39
site/languages/fr.php Normal file
View file

@ -0,0 +1,39 @@
<?php
return [
'code' => 'fr',
'default' => true,
'direction' => 'ltr',
'locale' => 'fr_FR',
'name' => 'Français',
'url' => '/',
'translations' => [
// Général
'backToHome' => 'Retour à l\'accueil',
'backToShop' => 'Retour à la boutique',
'supportText' => 'Pour nous soutenir, vous&nbsp;pouvez&nbsp;aussi',
'makeDonation' => 'faire un don',
'addToCart' => 'Ajouter au panier',
// Blueprints - Home
'home.title' => 'Accueil',
'home.baseline.label' => 'Baseline',
'home.products.headline' => 'Produits',
'home.settings.headline' => 'Paramètres',
// Blueprints - Product
'product.title' => 'Produit',
'product.description.label' => 'Description',
'product.details.label' => 'Détails',
'product.details.item.label' => 'Détail',
'product.price.label' => 'Prix (€)',
'product.backgroundColor.label' => 'Couleur de fond',
'product.stock.label' => 'Stock',
'product.stock.help' => 'Partagé entre les versions FR et EN',
'product.snipcartId.label' => 'Identifiant Snipcart',
'product.sizes.label' => 'Tailles disponibles',
'product.images.headline' => 'Images du produit',
'product.meta.headline' => 'Métadonnées',
'product.tab.content' => 'Contenu',
]
];

0
site/sessions/index.html Normal file
View file

0
site/snippets/index.html Normal file
View file

View file

@ -0,0 +1 @@
<h1><?= $page->title() ?></h1>

91
site/templates/home.php Normal file
View file

@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="<?= $kirby->language()->code() ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title><?= $site->title() ?> | Index.ngo</title>
<link rel="icon" type="image/png" href="<?= url('assets/favicon.png') ?>"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.css" />
<script src="https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.js"></script>
<link rel="stylesheet" type="text/css" href="<?= url('assets/fonts/stylesheet.css') ?>" />
<link rel="stylesheet" type="text/css" href="<?= url('assets/css/style.css') ?>" />
</head>
<body data-template="store">
<header id="site-header">
<div class="header-left"></div>
<div class="header-center">
<h1 class="site-title">
<a href="<?= $site->url() ?>" aria-label="<?= t('backToHome', 'Retour à l\'accueil') ?>">
<svg width="100%" height="100%" viewBox="0 0 162 29" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2;">
<title>Index.ngo</title>
<g transform="matrix(1.04516,0,0,0.659091,57.4839,-6.59091)">
<rect x="-55" y="10" width="155" height="44" style="fill: none" />
<clipPath id="_clip1">
<rect x="-55" y="10" width="155" height="44" />
</clipPath>
<g clip-path="url(#_clip1)">
<g transform="matrix(0.95679,0,0,1.51724,-55,10)">
<path d="M162,29L148.198,29L141.174,20.767L134.15,29L91.184,29L91.184,0.004L120.653,0.004L120.653,7.351L102.637,7.351L102.637,10.867L120.137,10.867L120.137,18.13L102.637,18.13L102.637,21.606L120.926,21.606L120.926,28.951L134.273,14.414L120.807,0L134.56,0L141.388,7.767L147.76,0L161.201,0L148.236,13.79L161.996,28.997L162,29ZM68.58,29L54.224,29L54.224,0.004L68.637,0.004C74.672,0.004 78.31,0.004 82.046,2.045C86.259,4.379 88.674,8.889 88.674,14.417C88.674,19.406 86.862,23.405 83.427,25.975C79.463,29 75.345,29 68.58,29ZM49.819,29L38.775,29L31.499,19.815C29.746,17.735 28.088,15.545 27.307,14.495C27.387,15.813 27.524,17.238 27.524,20.499L27.524,29L15.965,29L15.965,0.004L27.009,0.004L33.798,8.349C36.223,11.121 37.709,12.993 38.393,13.881C38.347,12.615 38.26,9.911 38.26,6.84L38.26,0.004L49.819,0.004L49.819,29ZM11.559,29L0,29L0,0.004L11.559,0.004L11.559,29ZM65.784,21.818L67.904,21.818C70.918,21.818 73.067,21.818 74.728,20.531C76.074,19.491 76.845,17.308 76.845,14.541C76.845,11.526 76.084,9.541 74.525,8.476C72.895,7.411 71.461,7.224 67.578,7.224L65.784,7.224L65.784,21.818Z" style="fill-rule: nonzero" />
</g>
</g>
</g>
</svg>
</a>
</h1>
</div>
<div class="header-right">
<ul id="toggle-lang">
<?php foreach($kirby->languages() as $language): ?>
<li<?= $kirby->language() == $language ? ' class="is-selected"' : '' ?>>
<a href="<?= $page->url($language->code()) ?>"><?= $language->code() === 'fr' ? 'Fr' : 'En' ?></a>
</li>
<?php endforeach ?>
</ul>
</div>
</header>
<main>
<p class="p__baseline-big">
<?= $page->baseline()->or('Bienvenue sur la boutique de soutien à Index') ?>
</p>
<section id="store__container">
<?php foreach($site->children()->listed() as $product): ?>
<article class="store__product">
<figure>
<?php if($image = $product->images()->first()): ?>
<img src="<?= $image->url() ?>" alt="<?= $product->title()->html() ?>" />
<?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>
<p class="p__baseline-big">
<?= t('supportText', 'Pour nous soutenir, vous&nbsp;pouvez&nbsp;aussi') ?>
<a href="https://soutenir.index.ngo" class="link-don"><?= t('makeDonation', 'faire un don') ?></a>
</p>
</main>
<footer id="site-footer">
<div class="site-footer__container">
<div class="footer__mentions">
<p class="p__small">
© <?= date('Y') ?> Index Investigation |
<a target="_blank" href="https://www.index.ngo/mentions-legales/">Mentions légales</a>
</p>
</div>
</div>
</footer>
<script src="<?= url('assets/js/onload.js') ?>"></script>
</body>
</html>

143
site/templates/product.php Normal file
View file

@ -0,0 +1,143 @@
<!DOCTYPE html>
<html lang="<?= $kirby->language()->code() ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title><?= $page->title() ?> | Index.ngo</title>
<link rel="icon" type="image/png" href="<?= url('assets/favicon.png') ?>"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.css" />
<script src="https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.js"></script>
<link rel="stylesheet" type="text/css" href="<?= url('assets/fonts/stylesheet.css') ?>" />
<link rel="stylesheet" type="text/css" href="<?= url('assets/css/style.css') ?>" />
</head>
<body data-template="shop">
<header id="site-header">
<div class="header-left"></div>
<div class="header-center">
<h1 class="site-title">
<a href="<?= $site->url() ?>" aria-label="<?= t('backToHome', 'Retour à l\'accueil') ?>">
<svg width="100%" height="100%" viewBox="0 0 162 29" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2;">
<title>Index.ngo</title>
<g transform="matrix(1.04516,0,0,0.659091,57.4839,-6.59091)">
<rect x="-55" y="10" width="155" height="44" style="fill: none" />
<clipPath id="_clip1">
<rect x="-55" y="10" width="155" height="44" />
</clipPath>
<g clip-path="url(#_clip1)">
<g transform="matrix(0.95679,0,0,1.51724,-55,10)">
<path d="M162,29L148.198,29L141.174,20.767L134.15,29L91.184,29L91.184,0.004L120.653,0.004L120.653,7.351L102.637,7.351L102.637,10.867L120.137,10.867L120.137,18.13L102.637,18.13L102.637,21.606L120.926,21.606L120.926,28.951L134.273,14.414L120.807,0L134.56,0L141.388,7.767L147.76,0L161.201,0L148.236,13.79L161.996,28.997L162,29ZM68.58,29L54.224,29L54.224,0.004L68.637,0.004C74.672,0.004 78.31,0.004 82.046,2.045C86.259,4.379 88.674,8.889 88.674,14.417C88.674,19.406 86.862,23.405 83.427,25.975C79.463,29 75.345,29 68.58,29ZM49.819,29L38.775,29L31.499,19.815C29.746,17.735 28.088,15.545 27.307,14.495C27.387,15.813 27.524,17.238 27.524,20.499L27.524,29L15.965,29L15.965,0.004L27.009,0.004L33.798,8.349C36.223,11.121 37.709,12.993 38.393,13.881C38.347,12.615 38.26,9.911 38.26,6.84L38.26,0.004L49.819,0.004L49.819,29ZM11.559,29L0,29L0,0.004L11.559,0.004L11.559,29ZM65.784,21.818L67.904,21.818C70.918,21.818 73.067,21.818 74.728,20.531C76.074,19.491 76.845,17.308 76.845,14.541C76.845,11.526 76.084,9.541 74.525,8.476C72.895,7.411 71.461,7.224 67.578,7.224L65.784,7.224L65.784,21.818Z" style="fill-rule: nonzero" />
</g>
</g>
</g>
</svg>
</a>
</h1>
</div>
<div class="header-right">
<ul id="toggle-lang">
<?php foreach($kirby->languages() as $language): ?>
<li<?= $kirby->language() == $language ? ' class="is-selected"' : '' ?>>
<a href="<?= $page->url($language->code()) ?>"><?= $language->code() === 'fr' ? 'Fr' : 'En' ?></a>
</li>
<?php endforeach ?>
</ul>
</div>
</header>
<main>
<nav class="store__nav">
<a href="<?= $site->homePage()->url() ?>"><?= t('backToShop', 'Retour à la boutique') ?></a>
</nav>
<section class="section__product">
<div class="col-left">
<div class="hero">
<h2 class="p__baseline-big"><?= $page->title()->html() ?></h2>
<p class="p__baseline-big"><?= $page->price() ?>€</p>
</div>
<div class="details">
<?php if($page->details()->isNotEmpty()): ?>
<?= $page->details()->toBlocks() ?>
<?php endif ?>
</div>
<?php if($page->hasOptions()->toBool() && $page->optionValues()->isNotEmpty()): ?>
<div class="product-options">
<ul class="product-options__list">
<?php
$values = $page->optionValues()->split(',');
$optionSlug = $page->optionLabel()->slug();
foreach($values as $index => $value):
$value = trim($value);
$uniqueId = $optionSlug . '-' . Str::slug(strtolower($value));
?>
<li>
<input type="radio" id="<?= $uniqueId ?>" name="<?= $optionSlug ?>" value="<?= $value ?>" />
<label for="<?= $uniqueId ?>"><?= $value ?></label>
</li>
<?php endforeach ?>
</ul>
</div>
<?php endif ?>
<div class="add-to-cart">
<button
class="btn__default snipcart-add-item"
data-item-id="<?= $page->slug() ?>"
data-item-price="<?= $page->price() ?>"
data-item-description="<?= $page->description()->excerpt(100) ?>"
data-item-image="<?= $page->images()->first() ? $page->images()->first()->url() : '' ?>"
data-item-name="<?= $page->title()->html() ?>"
data-item-url="<?= $page->url() ?>/validate.json"
<?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-.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"><?= t('addToCart', 'Ajouter au panier') ?></div>
</button>
</div>
</div>
<figure>
<?php if($image = $page->images()->first()): ?>
<img src="<?= $image->url() ?>" alt="<?= $page->title()->html() ?>" />
<?php endif ?>
</figure>
</section>
</main>
<footer id="site-footer">
<div class="site-footer__container">
<div class="footer__mentions">
<p class="p__small">
© <?= date('Y') ?> Index Investigation |
<a target="_blank" href="https://www.index.ngo/mentions-legales/">Mentions légales</a>
</p>
</div>
</div>
</footer>
<script src="<?= url('assets/js/onload.js') ?>"></script>
<script src="<?= url('assets/js/product-size.js') ?>"></script>
<script src="<?= url('assets/js/snipcart.js') ?>"></script>
</body>
</html>

View file

@ -0,0 +1,46 @@
<?php
// Template pour valider les produits avec Snipcart
// Ce fichier sera appelé par Snipcart pour valider le stock avant l'ajout au panier
header('Content-Type: application/json');
$product = $page;
// Vérifier que c'est bien une page produit
if ($product->intendedTemplate() !== 'product') {
http_response_code(404);
echo json_encode(['error' => 'Product not found']);
exit;
}
// Récupérer le stock actuel
$stock = (int) $product->stock()->value();
// Préparer la réponse JSON pour Snipcart
$response = [
'id' => $product->slug(),
'price' => (float) $product->price()->value(),
'url' => $product->url() . '/validate.json',
'name' => $product->title()->value(),
'description' => $product->description()->value(),
'image' => $product->images()->first() ? $product->images()->first()->url() : '',
'inventory' => $stock,
'stock' => $stock
];
// Ajouter les options si disponibles
if ($product->hasOptions()->toBool() && $product->optionValues()->isNotEmpty()) {
$values = $product->optionValues()->split(',');
$trimmedValues = array_map('trim', $values);
$snipcartOptions = implode('|', $trimmedValues);
$response['customFields'] = [
[
'name' => $product->optionLabel()->value(),
'options' => $snipcartOptions,
'required' => true
]
];
}
echo json_encode($response);