2026-04-10

- corvée: met à jour les deps
- corvée: formate
This commit is contained in:
gcch 2026-04-10 17:21:57 +02:00
commit d50de6d534
85 changed files with 132090 additions and 31346 deletions

View file

@ -22,11 +22,12 @@ $templates = ['404.twig'];
*
* @throws Exception une exception est levée s'il est impossible d'obtenir la date de modification du fichier à charger
*/
function load_page_resources(): void {
Resource::enqueue_style_file(
handle: 'haiku-atelier-2024-styles-page-a-propos',
path: '/assets/css/pages/page-modele-simple.css',
);
function load_page_resources(): void
{
Resource::enqueue_style_file(
handle: 'haiku-atelier-2024-styles-page-a-propos',
path: '/assets/css/pages/page-modele-simple.css',
);
}
add_action('wp_enqueue_scripts', load_page_resources(...));

View file

@ -1,4 +1,4 @@
jQuery(document).ready(function ($) {
jQuery(document).ready(function($) {
"use strict";
/**
@ -9,7 +9,7 @@ jQuery(document).ready(function ($) {
* @link https://github.com/maddisondesigns
*/
$(".customize-control-tinymce-editor").each(function () {
$(".customize-control-tinymce-editor").each(function() {
// Get the toolbar strings that were passed from the PHP Class
const tinyMCEToolbar1String = _wpCustomizeSettings.controls[$(this).attr("id")].skyrockettinymcetoolbar1;
const tinyMCEToolbar2String = _wpCustomizeSettings.controls[$(this).attr("id")].skyrockettinymcetoolbar2;
@ -19,14 +19,14 @@ jQuery(document).ready(function ($) {
mediaButtons: tinyMCEMediaButtons,
quicktags: true,
tinymce: {
wpautop: true,
toolbar1: tinyMCEToolbar1String,
toolbar2: tinyMCEToolbar2String,
wpautop: true,
},
});
});
$(document).on("tinymce-editor-init", function (event, editor) {
editor.on("change", function (e) {
$(document).on("tinymce-editor-init", function(event, editor) {
editor.on("change", function(e) {
tinyMCE.triggerSave();
$("#" + editor.id).trigger("change");
});

View file

@ -17,14 +17,14 @@ $context = Timber::context();
$templates = ['accueil.twig'];
add_action('wp_enqueue_scripts', function (): void {
Resource::enqueue_style_file(
handle: 'haiku-atelier-2024-styles-page-accueil',
path: '/assets/css/pages/page-accueil.css',
);
Resource::enqueue_script_module_file(
id: 'haiku-atelier-2024-scripts-page-accueil',
path: '/assets/js/scripts-page-accueil.js',
);
Resource::enqueue_style_file(
handle: 'haiku-atelier-2024-styles-page-accueil',
path: '/assets/css/pages/page-accueil.css',
);
Resource::enqueue_script_module_file(
id: 'haiku-atelier-2024-scripts-page-accueil',
path: '/assets/js/scripts-page-accueil.js',
);
});
Timber::render(data: $context, filenames: $templates);

View file

@ -24,20 +24,20 @@ $templates = ['a-propos.twig'];
$image_dimensions = getimagesize(filename: get_template_directory() . '/assets/img/about/haikuabout.png');
if (is_bool($image_dimensions)) {
throw new Exception("Impossible d'obtenir les dimensions de l'image principale de la page.");
throw new Exception("Impossible d'obtenir les dimensions de l'image principale de la page.");
}
$context['image_dimensions'] = $image_dimensions;
add_action('wp_enqueue_scripts', function (): void {
Resource::enqueue_style_file(
handle: 'haiku-atelier-2024-styles-page-a-propos',
path: '/assets/css/pages/page-a-propos.css',
);
Resource::enqueue_script_module_file(
id: 'haiku-atelier-2024-scripts-page-a-propos',
path: '/assets/js/scripts-page-a-propos.js',
);
Resource::enqueue_style_file(
handle: 'haiku-atelier-2024-styles-page-a-propos',
path: '/assets/css/pages/page-a-propos.css',
);
Resource::enqueue_script_module_file(
id: 'haiku-atelier-2024-scripts-page-a-propos',
path: '/assets/js/scripts-page-a-propos.js',
);
});
Timber::render(data: $context, filenames: $templates);

View file

@ -22,20 +22,22 @@ use WC_Session_Handler;
header('Content-Type: application/json; charset=utf-8');
// TODO: Appliquer le bon calcul pour les montants vs. percentages
function get_discount_amount(WC_Coupon $coupon) {
if ($coupon->get_discount_type() === 'fixed_cart') {
return $coupon->get_amount() * 100;
} else {
return $coupon->get_amount();
}
function get_discount_amount(WC_Coupon $coupon)
{
if ($coupon->get_discount_type() === 'fixed_cart') {
return $coupon->get_amount() * 100;
} else {
return $coupon->get_amount();
}
}
function get_discount_duration(WC_Coupon $coupon): string {
if ($coupon->get_discount_type() === 'fixed_cart') {
return 'once';
} else {
return 'forever';
}
function get_discount_duration(WC_Coupon $coupon): string
{
if ($coupon->get_discount_type() === 'fixed_cart') {
return 'once';
} else {
return 'forever';
}
}
// Récupère les informations nécessaires
@ -44,37 +46,37 @@ $session_wc = WC()->session;
/** @var array<string,string> $urls URLs utilisables pour rediriger l'Utilisateur. */
$urls = [
'accueil' => get_page_link(get_page_by_path('home')),
'succes_commande' => get_page_link(get_page_by_path('successful-order')),
'echec_commande' => get_page_link(get_page_by_path('failed-order')),
'accueil' => get_page_link(get_page_by_path('home')),
'succes_commande' => get_page_link(get_page_by_path('successful-order')),
'echec_commande' => get_page_link(get_page_by_path('failed-order')),
];
// Redirige à la page d'accueil si le Panier est vide
if (WC()->cart->is_empty()) {
header('Location: ' . $urls['accueil']);
header('Location: ' . $urls['accueil']);
return;
return;
}
// Vérifie que les paramètres d'URLs nécessaires soient présents
/** @var string $order_id */
$order_id = $_GET['order_id'];
if (!$order_id) {
$reponse = ['succes' => false, 'status' => 'order_key is missing'];
echo json_encode($reponse);
http_response_code(400);
$reponse = ['succes' => false, 'status' => 'order_key is missing'];
echo json_encode($reponse);
http_response_code(400);
return;
return;
}
/** @var string $order_key */
$order_key = $_GET['order_key'];
if (!$order_key) {
$reponse = ['succes' => false, 'status' => 'order_key is missing'];
echo json_encode($reponse);
http_response_code(400);
$reponse = ['succes' => false, 'status' => 'order_key is missing'];
echo json_encode($reponse);
http_response_code(400);
return;
return;
}
// Récupère le Panier et l'Email du Client
@ -86,29 +88,29 @@ $email_client = WC()->session->get('customer')['email'];
/** @var list<Product> $articles */
$articles = collect($panier->get_cart())
->map(static function ($article_panier) {
$titre_produit = match ('variable' === $article_panier['data']?->get_type()) {
true => $article_panier['data']?->get_title()
. ' ('
. explode(': ', (string) $article_panier['data']?->get_attribute_summary())[1]
. ')',
false => $article_panier['data']?->get_title(),
};
->map(static function ($article_panier) {
$titre_produit = match ('variable' === $article_panier['data']?->get_type()) {
true => $article_panier['data']?->get_title()
. ' ('
. explode(': ', (string) $article_panier['data']?->get_attribute_summary())[1]
. ')',
false => $article_panier['data']?->get_title(),
};
return [
'price_data' => [
'currency' => 'EUR',
'product_data' => [
'name' => $titre_produit,
'images' => [wp_get_attachment_image_url($article_panier['data']?->get_image_id())],
],
'unit_amount' => $article_panier['data']?->get_price() * 100,
],
'quantity' => $article_panier['quantity'],
];
})
->values()
->toArray();
return [
'price_data' => [
'currency' => 'EUR',
'product_data' => [
'name' => $titre_produit,
'images' => [wp_get_attachment_image_url($article_panier['data']?->get_image_id())],
],
'unit_amount' => $article_panier['data']?->get_price() * 100,
],
'quantity' => $article_panier['quantity'],
];
})
->values()
->toArray();
// Récupère la Commande et la Méthode de Livraison
/** @var WC_Order $commande */
@ -118,7 +120,7 @@ $methode_livraison = ['nom' => $commande->get_shipping_method(), 'cout' => $comm
// Le nom de la méthode de livraison ne peut être une chaîne vide.
if (empty($methode_livraison['nom'])) {
$methode_livraison['nom'] = 'Free';
$methode_livraison['nom'] = 'Free';
}
// Sélectionne la clé API Stripe
@ -127,39 +129,39 @@ Stripe::setApiKey(Config::get('STRIPE_API_SECRET'));
// Met à jour les Codes promos
$coupons_stripe = collect(Coupon::all()->data);
$coupons_wc = collect(WC()->cart->get_coupons())
->map(static fn(WC_Coupon $coupon): array => [
'currency' => 'EUR',
'duration' => get_discount_duration($coupon),
'fixed_cart' === $coupon->get_discount_type() ? 'amount_off' : 'percent_off' => get_discount_amount($coupon),
'id' => $coupon->get_code(),
'name' => $coupon->get_code(),
])
->each(static function (array $item) use ($coupons_stripe): void {
// Si le code promo n'existe pas, le créer
if (!$coupons_stripe->contains('name', $item['name'])) {
Coupon::create($item);
}
});
->map(static fn(WC_Coupon $coupon): array => [
'currency' => 'EUR',
'duration' => get_discount_duration($coupon),
'fixed_cart' === $coupon->get_discount_type() ? 'amount_off' : 'percent_off' => get_discount_amount($coupon),
'id' => $coupon->get_code(),
'name' => $coupon->get_code(),
])
->each(static function (array $item) use ($coupons_stripe): void {
// Si le code promo n'existe pas, le créer
if (!$coupons_stripe->contains('name', $item['name'])) {
Coupon::create($item);
}
});
$reductions_stripe = $coupons_wc
->map(static fn($coupon): array => ['coupon' => $coupon['name']])
->values()
->toArray();
->map(static fn($coupon): array => ['coupon' => $coupon['name']])
->values()
->toArray();
/** @var Session $session_checkout_stripe */
$session_checkout_stripe = Session::create([
'cancel_url' => $urls['echec_commande'],
'customer_email' => $email_client,
'discounts' => $reductions_stripe,
'line_items' => $articles,
'mode' => 'payment',
'success_url' => $urls['succes_commande'] . '?session_id={CHECKOUT_SESSION_ID}',
'metadata' => ['order_id' => $order_id, 'order_key' => $order_key],
'shipping_options' => [['shipping_rate_data' => [
'display_name' => $methode_livraison['nom'],
'fixed_amount' => ['amount' => $methode_livraison['cout'], 'currency' => 'EUR'],
'tax_behavior' => 'inclusive',
'type' => 'fixed_amount',
]]],
'cancel_url' => $urls['echec_commande'],
'customer_email' => $email_client,
'discounts' => $reductions_stripe,
'line_items' => $articles,
'mode' => 'payment',
'success_url' => $urls['succes_commande'] . '?session_id={CHECKOUT_SESSION_ID}',
'metadata' => ['order_id' => $order_id, 'order_key' => $order_key],
'shipping_options' => [['shipping_rate_data' => [
'display_name' => $methode_livraison['nom'],
'fixed_amount' => ['amount' => $methode_livraison['cout'], 'currency' => 'EUR'],
'tax_behavior' => 'inclusive',
'type' => 'fixed_amount',
]]],
], ['idempotency_key' => Uuid::v4()]);
// echo json_encode($session_checkout_stripe);
header('HTTP/1.1 303 See Other');

View file

@ -17,10 +17,10 @@ $context = Timber::context();
$templates = ['echec-commande.twig'];
add_action('wp_enqueue_scripts', function (): void {
Resource::enqueue_style_file(
handle: 'haiku-atelier-2024-styles-page-modele-simple',
path: '/assets/css/pages/page-modele-simple.css',
);
Resource::enqueue_style_file(
handle: 'haiku-atelier-2024-styles-page-modele-simple',
path: '/assets/css/pages/page-modele-simple.css',
);
});
// Rendu

View file

@ -17,10 +17,10 @@ $context = Timber::context();
$templates = ['cgv.twig'];
add_action('wp_enqueue_scripts', function (): void {
Resource::enqueue_style_file(
handle: '/assets/css/pages/page-modele-simple.css',
path: '/assets/css/pages/page-modele-simple.css',
);
Resource::enqueue_style_file(
handle: '/assets/css/pages/page-modele-simple.css',
path: '/assets/css/pages/page-modele-simple.css',
);
});
// Rendu

View file

@ -45,6 +45,7 @@ $maximum_price = collect($product->variations)->max('price');
$same_collection_products = Product::get_same_collection_products($product->collection)($product->id)
|> function (/** @var list<WC_Product>|stdClass */ mixed $products): array {
assert(is_array($products), 'Les Produits de la même collection doivent être un tableau.');
return $products;
}
|> (static fn(/** @var list<WC_Product> */ array $products): array => Arr::map(

View file

@ -170,6 +170,7 @@ final class StarterSite extends Site {
public function maj_environnement_twig(array $options): array {
return $options;
}
// public function charge_traductions_theme(): void {
// load_theme_textdomain("haiku-atelier-2024", get_template_directory() . "/languages");
// }

View file

@ -9,36 +9,39 @@ declare(strict_types=1);
use Carbon_Fields\Container;
use Carbon_Fields\Field;
function cree_champs_personnalises_produit(): void {
Container::make('post_meta', "Product's Details")
->where('post_type', '=', 'product')
->add_fields([
// Galerie des photos Produit
Field::make('media_gallery', 'photos_colonne_gauche', __('Left Column Photos'))
->set_type(['image'])
->set_duplicates_allowed(false),
// Galerie des photos portées
Field::make('media_gallery', 'photos_colonne_droite', __('Right Column Photos'))
->set_type(['image'])
->set_duplicates_allowed(false),
// Texte des détails du Produit
Field::make('rich_text', 'haiku_details_produit', __("Product's Details")),
function cree_champs_personnalises_produit(): void
{
Container::make('post_meta', "Product's Details")
->where('post_type', '=', 'product')
->add_fields([
// Galerie des photos Produit
Field::make('media_gallery', 'photos_colonne_gauche', __('Left Column Photos'))
->set_type(['image'])
->set_duplicates_allowed(false),
// Galerie des photos portées
Field::make('media_gallery', 'photos_colonne_droite', __('Right Column Photos'))
->set_type(['image'])
->set_duplicates_allowed(false),
// Texte des détails du Produit
Field::make('rich_text', 'haiku_details_produit', __("Product's Details")),
]);
}
function cree_champ_personnalise_commande($order): void
{
woocommerce_wp_text_input([
'id' => 'tracking_number',
'label' => 'Tracking Number:',
'value' => $order->get_meta('tracking_number'),
'wrapper_class' => 'form-field-wide',
]);
}
function cree_champ_personnalise_commande($order): void {
woocommerce_wp_text_input([
'id' => 'tracking_number',
'label' => 'Tracking Number:',
'value' => $order->get_meta('tracking_number'),
'wrapper_class' => 'form-field-wide',
]);
}
function maj_champ_personnalise_commande($order_id): void {
$order = wc_get_order($order_id);
$order->update_meta_data('tracking_number', wc_clean($_POST['tracking_number']));
$order->save();
function maj_champ_personnalise_commande($order_id): void
{
$order = wc_get_order($order_id);
$order->update_meta_data('tracking_number', wc_clean($_POST['tracking_number']));
$order->save();
}
add_action('carbon_fields_register_fields', 'cree_champs_personnalises_produit');

View file

@ -8,69 +8,74 @@
declare(strict_types=1);
function enregistre_controle_personnalise_tinymce(): void {
/**
* TinyMCE Custom Control.
*
* @author Anthony Hortin <http://maddisondesigns.com>
* @license http://www.gnu.org/licenses/gpl-2.0.html
*
* @see https://github.com/maddisondesigns
*/
final class ControlesPersonnalises extends WP_Customize_Control {
/** The type of control being rendered. */
public $type = 'editeur_tinymce';
function enregistre_controle_personnalise_tinymce(): void
{
/**
* Enqueue our scripts and styles.
* TinyMCE Custom Control.
*
* @author Anthony Hortin <http://maddisondesigns.com>
* @license http://www.gnu.org/licenses/gpl-2.0.html
*
* @see https://github.com/maddisondesigns
*/
public function enqueue(): void {
wp_enqueue_script(
handle: 'controle-personnalise-tinymce',
src: get_template_directory_uri() . '/assets/vendor/controle-personnalise-tinymce.js',
deps: ['jquery'],
ver: '1.3',
args: true,
);
wp_enqueue_editor();
}
final class ControlesPersonnalises extends WP_Customize_Control
{
/** The type of control being rendered. */
public $type = 'editeur_tinymce';
/**
* Render the control in the customizer.
*/
public function render_content(): void { ?>
/**
* Enqueue our scripts and styles.
*/
public function enqueue(): void
{
wp_enqueue_script(
handle: 'controle-personnalise-tinymce',
src: get_template_directory_uri() . '/assets/vendor/controle-personnalise-tinymce.js',
deps: ['jquery'],
ver: '1.3',
args: true,
);
wp_enqueue_editor();
}
/**
* Render the control in the customizer.
*/
public function render_content(): void
{ ?>
<div class="tinymce-control">
<span class="customize-control-title"><?php echo esc_html($this->label); ?></span>
<?php if (!empty($this->description)) { ?>
<span class="customize-control-description"><?php echo esc_html($this->description); ?></span>
<?php } ?>
<textarea id="<?php echo
esc_attr($this->id)
esc_attr($this->id)
; ?>" class="customize-control-tinymce-editor" <?php $this->link(); ?>><?php echo
esc_html($this->value())
esc_html($this->value())
; ?></textarea>
</div>
<?php }
/**
* Pass our TinyMCE toolbar string to JavaScript.
*/
public function to_json(): void {
parent::to_json();
/**
* Pass our TinyMCE toolbar string to JavaScript.
*/
public function to_json(): void
{
parent::to_json();
$this->json['skyrockettinymcetoolbar1'] = isset($this->input_attrs['toolbar1'])
? esc_attr($this->input_attrs['toolbar1'])
: 'bold italic bullist numlist alignleft aligncenter alignright link';
$this->json['skyrockettinymcetoolbar1'] = isset($this->input_attrs['toolbar1'])
? esc_attr($this->input_attrs['toolbar1'])
: 'bold italic bullist numlist alignleft aligncenter alignright link';
$this->json['skyrockettinymcetoolbar2'] = isset($this->input_attrs['toolbar2'])
? esc_attr($this->input_attrs['toolbar2'])
: '';
$this->json['skyrocketmediabuttons'] = isset($this->input_attrs['mediaButtons'])
&& $this->input_attrs['mediaButtons'] === true
? true
: false;
$this->json['skyrockettinymcetoolbar2'] = isset($this->input_attrs['toolbar2'])
? esc_attr($this->input_attrs['toolbar2'])
: '';
$this->json['skyrocketmediabuttons'] = isset($this->input_attrs['mediaButtons'])
&& $this->input_attrs['mediaButtons'] === true
? true
: false;
}
}
}
}
add_action('customize_register', 'enregistre_controle_personnalise_tinymce');

View file

@ -11,112 +11,116 @@ use function is_float;
use function is_int;
use function is_string;
final readonly class Cart {
public function __construct() {}
final readonly class Cart
{
public function __construct() {}
/** La valeur par défaut d'une donnée invalide du Panier. */
private const string DEFAULT_VALUE = '0.00';
/** La valeur par défaut d'une donnée invalide du Panier. */
private const string DEFAULT_VALUE = '0.00';
/**
* Retourne la liste des pays acceptés pour la livraison.
*
* @return array<int,string>
*/
public static function get_allowed_countries(): array {
return [
'AD',
'AL',
'AM',
'AR',
'AT',
'AU',
'BA',
'BE',
'BG',
'BR',
'CA',
'CH',
'CL',
'CR',
'CU',
'CY',
'CZ',
'DE',
'DK',
'DZ',
'EE',
'EG',
'ES',
'FI',
'FR',
'GF',
'GP',
'GR',
'HR',
'HU',
'IE',
'IS',
'IT',
'JP',
'KR',
'LB',
'LI',
'LT',
'LU',
'LV',
'MA',
'MD',
'ME',
'MF',
'MQ',
'MT',
'MX',
'NC',
'NL',
'NO',
'NZ',
'PF',
'PL',
'PM',
'PS',
'PT',
'RE',
'RO',
'SE',
'SI',
'SK',
'SM',
'TN',
'TR',
'TW',
'US',
'YT',
'ZA',
];
}
public static function parse_cart_value(int|float|string|bool $cart_value): string {
if (is_int($cart_value) || is_float($cart_value)) {
return self::format_number($cart_value);
/**
* Retourne la liste des pays acceptés pour la livraison.
*
* @return array<int,string>
*/
public static function get_allowed_countries(): array
{
return [
'AD',
'AL',
'AM',
'AR',
'AT',
'AU',
'BA',
'BE',
'BG',
'BR',
'CA',
'CH',
'CL',
'CR',
'CU',
'CY',
'CZ',
'DE',
'DK',
'DZ',
'EE',
'EG',
'ES',
'FI',
'FR',
'GF',
'GP',
'GR',
'HR',
'HU',
'IE',
'IS',
'IT',
'JP',
'KR',
'LB',
'LI',
'LT',
'LU',
'LV',
'MA',
'MD',
'ME',
'MF',
'MQ',
'MT',
'MX',
'NC',
'NL',
'NO',
'NZ',
'PF',
'PL',
'PM',
'PS',
'PT',
'RE',
'RO',
'SE',
'SI',
'SK',
'SM',
'TN',
'TR',
'TW',
'US',
'YT',
'ZA',
];
}
if (is_string($cart_value)) {
$number = Number::parseInt($cart_value);
$number = is_bool($number) ? 0 : $number;
public static function parse_cart_value(int|float|string|bool $cart_value): string
{
if (is_int($cart_value) || is_float($cart_value)) {
return self::format_number($cart_value);
}
return self::format_number($number);
if (is_string($cart_value)) {
$number = Number::parseInt($cart_value);
$number = is_bool($number) ? 0 : $number;
return self::format_number($number);
}
return '0.00';
}
return '0.00';
}
private static function format_number(int|float $number): string {
$formatted_number = Number::format(
number: $number,
// precision et max_precision sont mutuellement exclusifs.
precision: 2,
locale: 'fr',
);
return is_bool($formatted_number) ? self::DEFAULT_VALUE : $formatted_number;
}
private static function format_number(int|float $number): string
{
$formatted_number = Number::format(
number: $number,
// precision et max_precision sont mutuellement exclusifs.
precision: 2,
locale: 'fr',
);
return is_bool($formatted_number) ? self::DEFAULT_VALUE : $formatted_number;
}
}

View file

@ -6,32 +6,34 @@ namespace HaikuAtelier\Data;
use WC_Product;
final readonly class ProductVariation {
/**
* @param int $id L'ID de la Variation
* @param string $price Le prix de la Variation
* @param list<ProductVariationAttribute> $attributes Les attributs appliqués à la Variation
*/
private function __construct(
public int $id,
public string $price,
public array $attributes,
) {}
final readonly class ProductVariation
{
/**
* @param int $id L'ID de la Variation
* @param string $price Le prix de la Variation
* @param list<ProductVariationAttribute> $attributes Les attributs appliqués à la Variation
*/
private function __construct(
public int $id,
public string $price,
public array $attributes,
) {}
/**
* Créé une nouvelle instance de `ProductVariation` à partir d'un `WC_Product`.
*/
public static function new(WC_Product $product): self {
$id = $product->get_id();
$price = $product->get_price();
/** @var list<ProductVariationAttribute> */
$attributes = array_map(
/** @phpstan-ignore argument.type (Impossible à satisfaire) */
static fn(string $key, string $value) => new ProductVariationAttribute($key, $value),
array_keys($product->get_attributes()),
array_values($product->get_attributes()),
);
/**
* Créé une nouvelle instance de `ProductVariation` à partir d'un `WC_Product`.
*/
public static function new(WC_Product $product): self
{
$id = $product->get_id();
$price = $product->get_price();
/** @var list<ProductVariationAttribute> */
$attributes = array_map(
/** @phpstan-ignore argument.type (Impossible à satisfaire) */
static fn(string $key, string $value) => new ProductVariationAttribute($key, $value),
array_keys($product->get_attributes()),
array_values($product->get_attributes()),
);
return new self($id, $price, $attributes);
}
return new self($id, $price, $attributes);
}
}

View file

@ -4,13 +4,14 @@ declare(strict_types=1);
namespace HaikuAtelier\Data;
final readonly class ProductVariationAttribute {
/**
* @param string $attribute Le slug de l'Attribut
* @param string $value Le slug de la valeur de l'Attribut
*/
public function __construct(
public string $attribute,
public string $value,
) {}
final readonly class ProductVariationAttribute
{
/**
* @param string $attribute Le slug de l'Attribut
* @param string $value Le slug de la valeur de l'Attribut
*/
public function __construct(
public string $attribute,
public string $value,
) {}
}

View file

@ -9,8 +9,9 @@ declare(strict_types=1);
namespace HaikuAtelier;
// Désactive divers transformations du contenu par WordPress
function desactive_wpautop(): void {
remove_filter('the_content', 'wpautop');
function desactive_wpautop(): void
{
remove_filter('the_content', 'wpautop');
}
/**
@ -20,16 +21,17 @@ function desactive_wpautop(): void {
*
* @return array<string, bool> le même tableau avec des configurations en plus
*/
function desactive_transformation_contenu_tinymce(array $configuration): array {
// Ne supprime pas les retours à la ligne
$configuration['remove_linebreaks'] = false;
// Convertis les caractères de retours à la ligne en <br>
$configuration['convert_newlines_to_brs'] = true;
// Supprime les <br> redondants
$configuration['remove_redundant_brs'] = false;
function desactive_transformation_contenu_tinymce(array $configuration): array
{
// Ne supprime pas les retours à la ligne
$configuration['remove_linebreaks'] = false;
// Convertis les caractères de retours à la ligne en <br>
$configuration['convert_newlines_to_brs'] = true;
// Supprime les <br> redondants
$configuration['remove_redundant_brs'] = false;
// Retourne $configuration à WordPress
return $configuration;
// Retourne $configuration à WordPress
return $configuration;
}
/**
@ -39,19 +41,22 @@ function desactive_transformation_contenu_tinymce(array $configuration): array {
*
* @return array<string, string> le même tableau avec SVG en plus
*/
function autorise_import_svg_mediatheque(array $file_types): array {
$new_filetypes = [];
$new_filetypes['svg'] = 'image/svg+xml';
function autorise_import_svg_mediatheque(array $file_types): array
{
$new_filetypes = [];
$new_filetypes['svg'] = 'image/svg+xml';
return [...$file_types, ...$new_filetypes];
return [...$file_types, ...$new_filetypes];
}
function retire_motifs_blocs_gutenberg(): void {
remove_theme_support('core-block-patterns');
function retire_motifs_blocs_gutenberg(): void
{
remove_theme_support('core-block-patterns');
}
function retire_styles_core_block(): void {
wp_dequeue_style('core-block-supports');
function retire_styles_core_block(): void
{
wp_dequeue_style('core-block-supports');
}
// Désactive les appels à l'API de la mise à jour des traductions

View file

@ -14,32 +14,33 @@ use function register_taxonomy;
/**
* Enregistre la Taxonomie « Collection ».
*/
function enregistre_taxonomie_collection(): void {
$labels = [
'add_new_item' => __('Add New Collection'),
'all_items' => __('All Collections'),
'edit_item' => __('Edit Collection'),
'menu_name' => __('Collections'),
'name' => __('Collections'),
'new_item_name' => __('New Collection Name'),
'search_items' => __('Search Collections'),
'singular_name' => __('Collection'),
'update_item' => __('Update Collection'),
];
$args = [
'description' => __('An ensemble of pieces thematically or chronologically grouped together.'),
'hierarchical' => false,
'labels' => $labels,
'publicly_queryable' => false,
'query_var' => true,
'rewrite' => ['slug' => 'collection'],
'show_admin_column' => true,
'show_in_menu' => true,
'show_in_quick_edit' => true,
'show_ui' => true,
];
function enregistre_taxonomie_collection(): void
{
$labels = [
'add_new_item' => __('Add New Collection'),
'all_items' => __('All Collections'),
'edit_item' => __('Edit Collection'),
'menu_name' => __('Collections'),
'name' => __('Collections'),
'new_item_name' => __('New Collection Name'),
'search_items' => __('Search Collections'),
'singular_name' => __('Collection'),
'update_item' => __('Update Collection'),
];
$args = [
'description' => __('An ensemble of pieces thematically or chronologically grouped together.'),
'hierarchical' => false,
'labels' => $labels,
'publicly_queryable' => false,
'query_var' => true,
'rewrite' => ['slug' => 'collection'],
'show_admin_column' => true,
'show_in_menu' => true,
'show_in_quick_edit' => true,
'show_ui' => true,
];
register_taxonomy('collection', ['product'], $args);
register_taxonomy('collection', ['product'], $args);
}
add_action('init', enregistre_taxonomie_collection(...));

View file

@ -14,46 +14,50 @@ use function is_array;
use function Psl\Option\none;
use function Psl\Option\some;
final readonly class Post {
/**
* @return Option\Option<mixed>
*/
public static function get_post_meta(int $post_id, string $key): Option\Option {
/** @var false|mixed|string */
$value = get_post_meta($post_id, $key, true);
final readonly class Post
{
/**
* @return Option\Option<mixed>
*/
public static function get_post_meta(int $post_id, string $key): Option\Option
{
/** @var false|mixed|string */
$value = get_post_meta($post_id, $key, true);
if ($value === false) {
return none();
if ($value === false) {
return none();
}
return some($value);
}
return some($value);
}
/**
* @return Option\Option<array<mixed>>
*/
public static function get_post_meta_array(int $post_id, string $key): Option\Option
{
/** @var array<mixed>|false */
$value = get_post_meta($post_id, $key, false);
/**
* @return Option\Option<array<mixed>>
*/
public static function get_post_meta_array(int $post_id, string $key): Option\Option {
/** @var array<mixed>|false */
$value = get_post_meta($post_id, $key, false);
if (is_array($value)) {
return some($value);
}
if (is_array($value)) {
return some($value);
return none();
}
return none();
}
/**
* @return Option\Option<array<mixed>>
*/
public static function get_terms(int $post_id, string $taxonomy_name): Option\Option
{
/** @var false|list<WP_Term>|WP_Error */
$terms = get_the_terms($post_id, $taxonomy_name);
/**
* @return Option\Option<array<mixed>>
*/
public static function get_terms(int $post_id, string $taxonomy_name): Option\Option {
/** @var false|list<WP_Term>|WP_Error */
$terms = get_the_terms($post_id, $taxonomy_name);
if (is_array($terms)) {
return some($terms);
}
if (is_array($terms)) {
return some($terms);
return none();
}
return none();
}
}

View file

@ -7,13 +7,11 @@ import { getOptionOrThrowWithError } from "./utils.ts";
type ParentElement = Document | Element;
const getFirstSelectorFromParent =
(parent: ParentElement) =>
<E extends Element = Element>(selector: string): Option.Option<NonNullable<E>> =>
(parent: ParentElement) => <E extends Element = Element>(selector: string): Option.Option<NonNullable<E>> =>
Option.fromNullishOr(parent.querySelector<E>(selector));
const getFirstSelectorFromParentOrThrow =
(parent: ParentElement) =>
<E extends Element = Element>(selector: string): NonNullable<E> =>
(parent: ParentElement) => <E extends Element = Element>(selector: string): NonNullable<E> =>
pipe(
getFirstSelectorFromParent(parent)<E>(selector),
getOptionOrThrowWithError(`Il n'y a pas d'Élément dans le parent avec le sélecteur suivant : ${selector}.`),
@ -29,8 +27,7 @@ const getFirstSelectorFromDocumentOrThrow = <E extends Element = Element>(select
);
const getAllSelectorFromParent =
(parent: ParentElement) =>
<E extends Element = Element>(selector: string): Option.Option<NonEmptyReadonlyArray<E>> =>
(parent: ParentElement) => <E extends Element = Element>(selector: string): Option.Option<NonEmptyReadonlyArray<E>> =>
pipe(
parent.querySelectorAll<E>(selector),
// Convertis NodeListOf en Array.

View file

@ -1,9 +1,7 @@
import { Option, pipe } from "effect";
export const getOptionOrThrowWithError =
(message: string) =>
<T>(option: Option.Option<T>): T =>
pipe(
option,
Option.getOrThrowWith(() => new Error(message)),
);
export const getOptionOrThrowWithError = (message: string) => <T>(option: Option.Option<T>): T =>
pipe(
option,
Option.getOrThrowWith(() => new Error(message)),
);

View file

@ -1,18 +1,12 @@
export const forEach =
<T>(fn: (_1: T) => void) =>
(xs: Array<T>): void => {
xs.forEach(fn);
};
export const forEach = <T>(fn: (_1: T) => void) => (xs: Array<T>): void => {
xs.forEach(fn);
};
export const forEachWithIndex =
<T>(fn: (_1: T, _2: number) => void) =>
(xs: Array<T>): void => {
xs.forEach(fn);
};
export const forEachWithIndex = <T>(fn: (_1: T, _2: number) => void) => (xs: Array<T>): void => {
xs.forEach(fn);
};
export const map =
<T>(fn: (_1: T) => void) =>
(xs: Array<T>): Array<T> => {
xs.map(fn);
return xs;
};
export const map = <T>(fn: (_1: T) => void) => (xs: Array<T>): Array<T> => {
xs.map(fn);
return xs;
};

View file

@ -18,26 +18,22 @@ import {
} from "./erreurs";
export const recupereElementAvecSelecteur =
(parent: ParentElement) =>
<E extends Element = Element>(selecteur: string): Either<SyntaxError, E> =>
(parent: ParentElement) => <E extends Element = Element>(selecteur: string): Either<SyntaxError, E> =>
Either
// Retourne une SyntaxError dans un Left si le sélecteur est invalide
.encase(() => parent.querySelector<E>(selecteur))
// Transforme le Left en une erreur plus sympathique
.mapLeft((_) => creeSyntaxError(ERREUR_SYNTAXE_INVALIDE(selecteur)))
.mapLeft(_ => creeSyntaxError(ERREUR_SYNTAXE_INVALIDE(selecteur)))
// Retourne une SyntaxError si l'Élément est null
.chain((e: E | null) =>
G.isNotNullable(e) ? Right(e) : Left(creeSyntaxError(ERREUR_DOM_INEXISTANT(selecteur))),
);
.chain((e: E | null) => G.isNotNullable(e) ? Right(e) : Left(creeSyntaxError(ERREUR_DOM_INEXISTANT(selecteur))));
export const getDOMElementsWithSelector =
(parent: ParentElement) =>
<E extends Element = Element>(selecteur: string): Either<SyntaxError, Array<E>> =>
(parent: ParentElement) => <E extends Element = Element>(selecteur: string): Either<SyntaxError, Array<E>> =>
Either
// Retourne une SyntaxError dans un Left si le sélecteur est invalide
.encase(() => pipe(parent.querySelectorAll<E>(selecteur), Array.from<E>))
// Transforme le Left en une erreur plus sympathique
.mapLeft((_) => creeSyntaxError(ERREUR_SYNTAXE_INVALIDE(selecteur)))
.mapLeft(_ => creeSyntaxError(ERREUR_SYNTAXE_INVALIDE(selecteur)))
// Retourne une SyntaxError si le tableau est vide
.chain((e: Array<E>) => (A.isEmpty(e) ? Left(creeSyntaxError(ERREUR_DOM_INEXISTANT(selecteur))) : Right(e)));
@ -55,20 +51,18 @@ export const recupereElementsOuLeve = <E extends Element = Element>(
Right: identity,
});
export const majElementInnerHtml =
<T extends HTMLElement>(element: T) =>
(innerHtml: string) => {
element.innerHTML = innerHtml;
return element;
};
export const majElementInnerHtml = <T extends HTMLElement>(element: T) => (innerHtml: string) => {
element.innerHTML = innerHtml;
return element;
};
// Merci facon (https://github.com/terkelg/facon)
export const html = (strings: TemplateStringsArray, ...args: Array<string>) =>
pipe(
document.createElement("template"),
(template) =>
template =>
majElementInnerHtml(template)(args.reduce((prev, value, i) => prev + value + strings[i + 1], strings[0])),
(template) => template.content,
template => template.content,
);
/**
@ -86,10 +80,8 @@ export const safeJsonParse = (chaine: string): Either<SyntaxError, JSONValue> =>
*
* @returns Un booléen
*/
export const targetMatchesSelector = <E extends HTMLElement = HTMLElement>(
cible: EventTarget | null,
selecteur: string,
): cible is E => cible !== null && (cible as HTMLElement).matches(selecteur);
export const targetMatchesSelector = (cible: EventTarget | null, selecteur: string): cible is HTMLElement =>
(cible as HTMLElement)?.matches(selecteur);
export const recupereElementsDocumentEither: <E extends Element = Element>(
selecteur: string,
@ -105,13 +97,11 @@ export const recupereElementDocumentEither: <E extends Element = Element>(select
* @throws Une SyntaxError si l'Élément n'est pas trouvé.
* @returns Un Élément.
*/
export const mustGetEleInDocument = <E extends Element = Element>(selecteur: string): E =>
pipe(recupereElementDocumentEither<E>(selecteur), recupereElementOuLeve);
export const mustGetEleInDocument = (selecteur: string): Element =>
pipe(recupereElementDocumentEither<Element>(selecteur), recupereElementOuLeve);
export const mustGetEleInParent =
(parent: ParentElement) =>
<E extends HTMLElement>(selector: string) =>
pipe(recupereElementAvecSelecteur(parent)<E>(selector), recupereElementOuLeve);
export const mustGetEleInParent = (parent: ParentElement) => (selector: string) =>
pipe(recupereElementAvecSelecteur(parent)<HTMLElement>(selector), recupereElementOuLeve);
/**
* Fonction utilitaire pour récupérer des Éléments selon un sélecteur au sein du Document.
@ -138,11 +128,11 @@ export const setButtonLoadingState = (button: HTMLButtonElement, isLoading: bool
};
export const estErreurHttp = (erreur: unknown) =>
erreur instanceof BadRequestError ||
erreur instanceof ForbiddenError ||
erreur instanceof NotFoundError ||
erreur instanceof ServerError ||
erreur instanceof UnauthorizedError;
erreur instanceof BadRequestError
|| erreur instanceof ForbiddenError
|| erreur instanceof NotFoundError
|| erreur instanceof ServerError
|| erreur instanceof UnauthorizedError;
export const estErreurFetch = (erreur: unknown) =>
erreur instanceof DOMException || erreur instanceof TypeError || erreur instanceof Error;

View file

@ -78,7 +78,7 @@ export const Erreur = (message: string): Error => new Error(message);
export const ErreurInconnue = (erreur: unknown): UnknownError => new UnknownError(erreur);
export const ErreurEntreeInexistante = (message: string): NonExistingKeyError => new NonExistingKeyError(message);
export const leveErreur = <E extends Error = Error>(erreur: E): never => {
export const leveErreur = (erreur: Error): never => {
throw erreur;
};
export const leveBadRequestError = (erreur: WCErrorBody): never => {
@ -106,7 +106,7 @@ export const leveNonExistingKeyError = (message: string): never => {
* @param erreur
* @returns L'ID Sentry de l'évènement capturé.
*/
export const reporteErreur = <E extends Error>(erreur: E): string => captureException(erreur);
export const reporteErreur = (erreur: Error): string => captureException(erreur);
/**
* Reporte une Erreur, sous forme d'erreur console et au service GlitchTip, puis la lève sous forme
@ -115,12 +115,12 @@ export const reporteErreur = <E extends Error>(erreur: E): string => captureExce
* @param erreur
* @returns never Lève une Erreur et ne retourne donc rien.
*/
export const reporteEtLeveErreur = <E extends Error>(erreur: E): never => {
export const reporteEtLeveErreur = (erreur: Error): never => {
reporteErreur(erreur);
throw erreur;
};
export const reporteEtJournaliseErreur = <E extends Error>(erreur: E): void => {
export const reporteEtJournaliseErreur = (erreur: Error): void => {
reporteErreur(erreur);
console.error(erreur);
if (erreur instanceof ValiError) {

View file

@ -7,12 +7,12 @@ export const CODE_PROMO_MAJ_EVENT = new CustomEvent(CODE_PROMO_MAJ, {});
// Interfaces
export type UpdatedShippingRatesEvent = {
export type UpdatedShippingRatesEvent = Event & {
detail: { refresh_methods: boolean; shipping_rates: ReadonlyArray<WCStoreShippingRateShippingRate> };
} & Event;
export type UpdatedTotalsEvent = {
};
export type UpdatedTotalsEvent = Event & {
detail: { totals: WCStoreCartTotals };
} & Event;
};
// Méthodes

View file

@ -1,9 +1,7 @@
import type { Constructor } from "./types/classes";
const estElement =
<T extends HTMLElement>(typeElement: Constructor<T>) =>
(element: unknown): element is T =>
element instanceof typeElement;
const estElement = <T extends HTMLElement>(typeElement: Constructor<T>) => (element: unknown): element is T =>
element instanceof typeElement;
export const estHTMLSelectElement = estElement<HTMLSelectElement>(HTMLSelectElement);

View file

@ -55,30 +55,31 @@ export const emetMessageMajContenuPanier = (args: MessageMajContenuPanierDonnees
* @param message Le message émis.
* @return void
*/
export const emetUniqueMessageBroadcastChannel = <M>(nomCanal: string, message: M): void =>
export const emetUniqueMessageBroadcastChannel = (nomCanal: string, message: unknown): void => {
pipe(
new BroadcastChannel(nomCanal),
(canal) => canalPostMessage(canal, message),
(canal) => canal.close(),
canal => canalPostMessage(canal, message),
canal => canal.close(),
);
};
// Validations
export const valideMessageMajBoutonPanier = (
evenementMessage: MessageEvent<unknown>,
): Either<ValiError<typeof MessageMajBoutonPanierSchema>, MessageMajBoutonPanier> =>
Either.of<ValiError<typeof MessageMajBoutonPanierSchema>, MessageMajBoutonPanier>(
Either.of<ValiError<typeof MessageMajBoutonPanierSchema>>(
parse(MessageMajBoutonPanierSchema, evenementMessage.data),
).ifLeft((erreur) => reporteErreur(erreur));
).ifLeft(erreur => reporteErreur(erreur));
export const valideMessageMajContenuPanier = (
evenementMessage: MessageEvent<unknown>,
): Either<ValiError<typeof MessageMajContenuPanierSchema>, MessageMajContenuPanier> =>
Either.of<ValiError<typeof MessageMajContenuPanierSchema>, MessageMajContenuPanier>(
Either.of<ValiError<typeof MessageMajContenuPanierSchema>>(
parse(MessageMajContenuPanierSchema, evenementMessage.data),
).ifLeft((erreur) => reporteErreur(erreur));
).ifLeft(erreur => reporteErreur(erreur));
// Correspondances
export const reponseEstCodeErreurWC = (reponse: SimplifiedResponse, codeErreurWC: string): boolean =>
safeSchemaParse(reponse, WCErrorSchema)
.map((v) => v.body.code === codeErreurWC)
.map(v => v.body.code === codeErreurWC)
.orDefault(false);

View file

@ -5,10 +5,10 @@ export const estEntreDeuxNombres = (nombre: number, min: number, max: number): b
export const diviseParCent = (nombre: number | string): number => Number(nombre) / 100;
export const arrondisADeuxDecimales = (nombre: number | string) => pipe(Number(nombre), (n) => n.toFixed(2));
export const arrondisADeuxDecimales = (nombre: number | string) => pipe(Number(nombre), n => n.toFixed(2));
export const arrondisAZeroOuDeuxDecimales = (nombre: number | string): string =>
pipe(Number(nombre), (n) => (n / Math.round(n) === 1 ? n.toFixed(0) : n.toFixed(2)));
pipe(Number(nombre), n => (n / Math.round(n) === 1 ? n.toFixed(0) : n.toFixed(2)));
export const inverseNombre = (nombre: number | string): number => Number(nombre) * -1;

View file

@ -38,7 +38,7 @@ type ArgumentsPostBackendWC = {
// Fetch
export const getBackend = (args: ArgumentsGetBackendWC): Promise<Response> =>
export const getBackend = async (args: ArgumentsGetBackendWC): Promise<Response> =>
fetch(args.route, {
credentials: "same-origin",
headers: {
@ -53,7 +53,7 @@ export const getBackend = (args: ArgumentsGetBackendWC): Promise<Response> =>
signal: AbortSignal.timeout(5000),
});
export const getBackendAvecParametresUrl = (args: ArgumentsGetBackendWC): Promise<Response> =>
export const getBackendAvecParametresUrl = async (args: ArgumentsGetBackendWC): Promise<Response> =>
fetch(`${args.route}?${args.searchParams}`, {
credentials: "same-origin",
headers: {
@ -68,7 +68,7 @@ export const getBackendAvecParametresUrl = (args: ArgumentsGetBackendWC): Promis
signal: AbortSignal.timeout(5000),
});
export const deleteBackend = (args: ArgumentsDeleteBackendWC): Promise<Response> =>
export const deleteBackend = async (args: ArgumentsDeleteBackendWC): Promise<Response> =>
fetch(args.route, {
credentials: "same-origin",
headers: {
@ -83,7 +83,7 @@ export const deleteBackend = (args: ArgumentsDeleteBackendWC): Promise<Response>
signal: AbortSignal.timeout(5000),
});
export const postBackend = (args: ArgumentsPostBackendWC): Promise<Response> =>
export const postBackend = async (args: ArgumentsPostBackendWC): Promise<Response> =>
fetch(args.route, {
body: args.corps,
credentials: "same-origin",
@ -101,7 +101,7 @@ export const postBackend = (args: ArgumentsPostBackendWC): Promise<Response> =>
export const prefilledPostBackend =
(nonce: string, authString?: string) =>
(route: string, body: BodyInit, needsAuthString: boolean): Promise<Response> =>
async (route: string, body: BodyInit, needsAuthString: boolean): Promise<Response> =>
fetch(route, {
body: body,
credentials: "same-origin",
@ -127,9 +127,9 @@ export const newPartialResponse = async (reponse: Response): Promise<SimplifiedR
export const traiteErreursBackendWooCommerce = (rs: SimplifiedResponse): HttpCodeErrors =>
match(rs)
.with({ status: 400 }, () => new BadRequestError())
.with({ status: 401 }, () => new UnauthorizedError())
.with({ status: 403 }, () => new ForbiddenError())
.with({ status: 404 }, () => new NotFoundError())
.with({ status: 500 }, () => new ServerError())
.otherwise((rs) => new Error(String(rs.status)));
["with"]({ status: 400 }, () => new BadRequestError())
["with"]({ status: 401 }, () => new UnauthorizedError())
["with"]({ status: 403 }, () => new ForbiddenError())
["with"]({ status: 404 }, () => new NotFoundError())
["with"]({ status: 500 }, () => new ServerError())
.otherwise(rs => new Error(String(rs.status)));

View file

@ -7,7 +7,5 @@ import { Maybe } from "purify-ts";
*/
export const first = <T>(xs: Array<T>): Maybe<T> => Maybe.fromNullable(xs.at(0));
export const find =
<T>(predicateFn: (_1: T) => boolean) =>
(xs: Array<T>): Maybe<T> =>
Maybe.fromNullable(xs.find(predicateFn));
export const find = <T>(predicateFn: (_1: T) => boolean) => (xs: Array<T>): Maybe<T> =>
Maybe.fromNullable(xs.find(predicateFn));

View file

@ -20,24 +20,24 @@ export const WCStoreCartItemTotalsSchema = v.object({
});
export const WCStoreCartItemSchema = v.object({
backorders_allowed: v.boolean(),
catalog_visibility: v.enum(CATALOG_VISIBILITIES),
backorders_allowed: v["boolean"](),
catalog_visibility: v["enum"](CATALOG_VISIBILITIES),
description: v.string(),
extensions: v.unknown(),
id: v.number(),
images: v.array(v.unknown()),
item_data: v.array(v.unknown()),
key: v.string(),
low_stock_remaining: v.union([v.number(), v.null()]),
low_stock_remaining: v.union([v.number(), v["null"]()]),
name: v.string(),
permalink: v.pipe(v.string(), v.url()),
prices: v.unknown(),
quantity: v.number(),
quantity_limits: v.unknown(),
short_description: v.string(),
show_backorder_badge: v.boolean(),
show_backorder_badge: v["boolean"](),
sku: v.string(),
sold_individually: v.boolean(),
sold_individually: v["boolean"](),
totals: WCStoreCartItemTotalsSchema,
type: v.string(),
variation: v.array(v.unknown()),
@ -60,10 +60,10 @@ export const WCStoreCartTotalsSchema = v.object({
total_items_tax: v.string(),
total_price: v.pipe(v.union([v.string(), v.number()]), v.transform(Number)),
total_shipping: v.pipe(
v.union([v.string(), v.number(), v.null()]),
v.transform((n) => (n ? Number(n) : 0)),
v.union([v.string(), v.number(), v["null"]()]),
v.transform(n => (n ? Number(n) : 0)),
),
total_shipping_tax: v.union([v.string(), v.null()]),
total_shipping_tax: v.union([v.string(), v["null"]()]),
total_tax: v.string(),
});
@ -75,12 +75,12 @@ export const WCStoreCartSchema = v.object({
errors: v.unknown(),
extensions: v.unknown(),
fees: v.unknown(),
has_calculated_shipping: v.boolean(),
has_calculated_shipping: v["boolean"](),
items: v.array(WCStoreCartItemSchema),
items_count: v.pipe(v.number(), v.integer()),
items_weight: v.pipe(v.number(), v.integer()),
needs_payment: v.boolean(),
needs_shipping: v.boolean(),
needs_payment: v["boolean"](),
needs_shipping: v["boolean"](),
payment_methods: v.unknown(),
payment_requirements: v.unknown(),
shipping_address: WCStoreShippingAddressSchema,

View file

@ -26,7 +26,7 @@ export const WCStoreShippingRateShippingRateSchema = v.object({
name: v.string(),
price: v.pipe(v.union([v.string(), v.number()]), v.transform(Number)),
rate_id: v.string(),
selected: v.boolean(),
selected: v["boolean"](),
taxes: v.string(),
});

View file

@ -24,14 +24,14 @@ export const WCAddressErrorSchema = v.object({
billing: v.optional(
v.object({
code: v.string(),
data: v.union([v.null(), v.string()]),
data: v.union([v["null"](), v.string()]),
message: v.string(),
}),
),
shipping: v.optional(
v.object({
code: v.string(),
data: v.union([v.null(), v.string()]),
data: v.union([v["null"](), v.string()]),
message: v.string(),
}),
),

View file

@ -17,36 +17,36 @@ import {
export const WCProductsArgsSchema = v.object({
// Date ISO8601
after: v.optional(v.optional(v.string())),
attribute_relation: v.optional(v.enum(ATTRIBUTES_RELATIONS)),
attribute_relation: v.optional(v["enum"](ATTRIBUTES_RELATIONS)),
attributes: v.optional(v.array(v.unknown())),
// Date ISO8601
before: v.optional(v.string()),
catalog_visibility: v.optional(v.enum(CATALOG_VISIBILITIES)),
catalog_visibility: v.optional(v["enum"](CATALOG_VISIBILITIES)),
category: v.optional(v.string()),
category_operator: v.optional(v.enum(CATEGORY_OPERATORS)),
context: v.optional(v.enum(PRODUCTS_CONTEXTES)),
date_column: v.optional(v.enum(DATE_COLUMN_VALUES)),
category_operator: v.optional(v["enum"](CATEGORY_OPERATORS)),
context: v.optional(v["enum"](PRODUCTS_CONTEXTES)),
date_column: v.optional(v["enum"](DATE_COLUMN_VALUES)),
exclude: v.optional(v.array(v.pipe(v.number(), v.integer()))),
featured: v.optional(v.boolean()),
featured: v.optional(v["boolean"]()),
include: v.optional(v.array(v.pipe(v.number(), v.integer()))),
max_price: v.optional(v.string()),
min_price: v.optional(v.string()),
offset: v.optional(v.number()),
on_sale: v.optional(v.boolean()),
order: v.optional(v.enum(ORDER_VALUES)),
orderby: v.optional(v.enum(ORDERBY_VALUES)),
on_sale: v.optional(v["boolean"]()),
order: v.optional(v["enum"](ORDER_VALUES)),
orderby: v.optional(v["enum"](ORDERBY_VALUES)),
page: v.optional(v.pipe(v.number(), v.minValue(1))),
parent: v.optional(v.array(v.pipe(v.number(), v.integer()))),
parent_exclude: v.optional(v.array(v.pipe(v.number(), v.integer()))),
per_page: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(100))),
rating: v.optional(v.array(v.enum(RATINGS))),
rating: v.optional(v.array(v["enum"](RATINGS))),
search: v.optional(v.string()),
sku: v.optional(v.string()),
slug: v.optional(v.string()),
stock_status: v.optional(v.array(v.enum(STOCK_STATUSES))),
stock_status: v.optional(v.array(v["enum"](STOCK_STATUSES))),
tag: v.optional(v.string()),
tag_operator: v.optional(v.enum(TAG_OPERATORS)),
type: v.optional(v.enum(PRODUCT_TYPES)),
tag_operator: v.optional(v["enum"](TAG_OPERATORS)),
type: v.optional(v["enum"](PRODUCT_TYPES)),
});
export const WCProductSchema = v.object({
@ -70,7 +70,7 @@ export const WCProductSchema = v.object({
),
description: v.string(),
extensions: v.unknown(),
has_options: v.boolean(),
has_options: v["boolean"](),
id: v.number(),
images: v.array(
v.object({
@ -83,12 +83,12 @@ export const WCProductSchema = v.object({
thumbnail: v.string(),
}),
),
is_in_stock: v.boolean(),
is_on_backorder: v.boolean(),
is_purchasable: v.boolean(),
low_stock_remaining: v.union([v.number(), v.null()]),
is_in_stock: v["boolean"](),
is_on_backorder: v["boolean"](),
is_purchasable: v["boolean"](),
low_stock_remaining: v.union([v.number(), v["null"]()]),
name: v.string(),
on_sale: v.boolean(),
on_sale: v["boolean"](),
parent: v.number(),
permalink: v.string(),
price_html: v.string(),
@ -109,7 +109,7 @@ export const WCProductSchema = v.object({
short_description: v.string(),
sku: v.string(),
slug: v.string(),
sold_individually: v.boolean(),
sold_individually: v["boolean"](),
tags: v.array(v.string()),
type: v.string(),
variation: v.unknown(),

View file

@ -14,7 +14,7 @@ export const WCV3OrdersCouponLineSchema = v.object({
discount: v.string(),
discount_tax: v.string(),
discount_type: v.string(),
free_shipping: v.boolean(),
free_shipping: v["boolean"](),
id: v.pipe(v.number(), v.integer()),
meta_data: v.array(WCV3OrdersCouponLineMetaDataSchema),
nominal_amount: v.number(),
@ -37,7 +37,7 @@ export const WCV3OrdersFeeLineSchema = v.object({
meta_data: v.array(WCV3OrdersFeeLineMetaDataSchema),
name: v.string(),
tax_class: v.string(),
tax_status: v.enum(TAX_STATUSES),
tax_status: v["enum"](TAX_STATUSES),
taxes: v.array(WCV3OrdersFeeLineTaxSchema),
total: v.string(),
total_tax: v.string(),
@ -88,7 +88,7 @@ export const WCV3OrdersLineItemSchema = v.object({
image: v.optional(WCV3OrdersLineItemImageSchema),
meta_data: v.optional(v.array(WCV3OrdersLineItemMetaDataSchema)),
name: v.optional(v.string()),
parent_name: v.optional(v.union([v.string(), v.null()])),
parent_name: v.optional(v.union([v.string(), v["null"]()])),
price: v.optional(v.number()),
product_id: v.optional(v.pipe(v.number(), v.integer())),
quantity: v.optional(v.pipe(v.number(), v.integer())),
@ -116,14 +116,14 @@ export const WCV3OrdersArgsSchema = v.object({
customer_note: v.optional(v.string()),
fee_lines: v.optional(v.array(WCV3OrdersFeeLineSchema)),
line_items: v.optional(v.array(WCV3OrdersLineItemSchema)),
manual_update: v.optional(v.boolean()),
manual_update: v.optional(v["boolean"]()),
parent_id: v.optional(v.pipe(v.number(), v.integer())),
payment_method: v.optional(v.string()),
payment_method_title: v.optional(v.string()),
set_paid: v.optional(v.boolean()),
set_paid: v.optional(v["boolean"]()),
shipping: v.optional(WCStoreShippingAddressSchema),
shipping_lines: v.optional(v.array(WCV3OrdersShippingLineSchema)),
status: v.optional(v.enum(ORDER_STATUSES)),
status: v.optional(v["enum"](ORDER_STATUSES)),
transaction_id: v.optional(v.string()),
});
@ -139,37 +139,37 @@ export const WCV3OrderSchema = v.object({
customer_ip_address: v.string(),
customer_note: v.string(),
customer_user_agent: v.string(),
date_completed: v.union([v.string(), v.null()]),
date_completed_gmt: v.union([v.string(), v.null()]),
date_completed: v.union([v.string(), v["null"]()]),
date_completed_gmt: v.union([v.string(), v["null"]()]),
// Date
date_created: v.string(),
date_created_gmt: v.string(),
date_modified: v.string(),
date_modified_gmt: v.string(),
date_paid: v.union([v.string(), v.null()]),
date_paid_gmt: v.union([v.string(), v.null()]),
date_paid: v.union([v.string(), v["null"]()]),
date_paid_gmt: v.union([v.string(), v["null"]()]),
discount_tax: v.string(),
discount_total: v.string(),
fee_lines: v.array(WCV3OrdersFeeLineSchema),
id: v.pipe(v.number(), v.integer()),
is_editable: v.boolean(),
is_editable: v["boolean"](),
line_items: v.array(WCV3OrdersLineItemSchema),
meta_data: v.unknown(),
needs_payment: v.boolean(),
needs_processing: v.boolean(),
needs_payment: v["boolean"](),
needs_processing: v["boolean"](),
number: v.string(),
order_key: v.string(),
parent_id: v.pipe(v.number(), v.integer()),
payment_method: v.string(),
payment_method_title: v.string(),
payment_url: v.string(),
prices_include_tax: v.boolean(),
prices_include_tax: v["boolean"](),
refunds: v.array(v.unknown()),
shipping: WCStoreShippingAddressSchema,
shipping_lines: v.array(WCV3OrdersShippingLineSchema),
shipping_tax: v.string(),
shipping_total: v.string(),
status: v.enum(ORDER_STATUSES),
status: v["enum"](ORDER_STATUSES),
tax_lines: v.array(v.unknown()),
total: v.string(),
total_tax: v.string(),

View file

@ -21,20 +21,20 @@ export const WCV3ProductsArgsSchema = v.object({
// Date ISO8601
after: v.optional(v.string()),
attribute: v.optional(v.string()),
attribute_relation: v.optional(v.enum(ATTRIBUTES_RELATIONS)),
attribute_relation: v.optional(v["enum"](ATTRIBUTES_RELATIONS)),
attribute_term: v.optional(v.string()),
attributes: v.optional(v.array(v.unknown())),
// Date ISO8601
before: v.optional(v.string()),
catalog_visibility: v.optional(v.enum(CATALOG_VISIBILITIES)),
catalog_visibility: v.optional(v["enum"](CATALOG_VISIBILITIES)),
category: v.optional(v.string()),
category_operator: v.optional(v.enum(CATEGORY_OPERATORS)),
context: v.optional(v.enum(PRODUCTS_CONTEXTES)),
date_column: v.optional(v.enum(DATE_COLUMN_VALUES)),
dates_are_gmt: v.optional(v.boolean()),
category_operator: v.optional(v["enum"](CATEGORY_OPERATORS)),
context: v.optional(v["enum"](PRODUCTS_CONTEXTES)),
date_column: v.optional(v["enum"](DATE_COLUMN_VALUES)),
dates_are_gmt: v.optional(v["boolean"]()),
exclude: v.optional(v.array(v.pipe(v.number(), v.integer()))),
exclude_meta: v.optional(v.array(v.string())),
featured: v.optional(v.boolean()),
featured: v.optional(v["boolean"]()),
include: v.optional(v.array(v.pipe(v.number(), v.integer()))),
include_meta: v.optional(v.array(v.string())),
max_price: v.optional(v.string()),
@ -44,24 +44,24 @@ export const WCV3ProductsArgsSchema = v.object({
// Date ISO8601
modified_before: v.optional(v.string()),
offset: v.optional(v.pipe(v.number(), v.integer())),
on_sale: v.optional(v.boolean()),
order: v.optional(v.enum(ORDER_VALUES)),
orderby: v.optional(v.enum(ORDERBY_VALUES)),
on_sale: v.optional(v["boolean"]()),
order: v.optional(v["enum"](ORDER_VALUES)),
orderby: v.optional(v["enum"](ORDERBY_VALUES)),
page: v.optional(v.pipe(v.number(), v.minValue(1))),
parent: v.optional(v.array(v.pipe(v.number(), v.integer()))),
parent_exclude: v.optional(v.array(v.pipe(v.number(), v.integer()))),
per_page: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(100))),
rating: v.optional(v.array(v.enum(RATINGS))),
rating: v.optional(v.array(v["enum"](RATINGS))),
search: v.optional(v.string()),
search_sku: v.optional(v.string()),
shipping_class: v.optional(v.string()),
sku: v.optional(v.string()),
slug: v.optional(v.string()),
status: v.optional(v.enum(PRODUCT_STATUTES)),
stock_status: v.optional(v.array(v.enum(STOCK_STATUSES))),
status: v.optional(v["enum"](PRODUCT_STATUTES)),
stock_status: v.optional(v.array(v["enum"](STOCK_STATUSES))),
tag: v.optional(v.string()),
tag_operator: v.optional(v.enum(TAG_OPERATORS)),
type: v.optional(v.enum(PRODUCT_TYPES)),
tag_operator: v.optional(v["enum"](TAG_OPERATORS)),
type: v.optional(v["enum"](PRODUCT_TYPES)),
});
export const WCV3ProductDownloadsSchema = v.object({
@ -99,8 +99,8 @@ export const WCV3ProductAttributeSchema = v.object({
name: v.string(),
options: v.array(v.string()),
position: v.pipe(v.number(), v.integer()),
variation: v.boolean(),
visible: v.boolean(),
variation: v["boolean"](),
visible: v["boolean"](),
});
export const WCV3ProductDefaultAttributeSchema = v.object({
id: v.pipe(v.number(), v.integer()),
@ -116,46 +116,46 @@ export const WCV3ProductMetaDataSchema = v.object({
export const WCV3ProductSchema = v.object({
attributes: v.array(WCV3ProductAttributeSchema),
average_rating: v.string(),
backordered: v.boolean(),
backorders: v.enum(BACKORDERS_SETTINGS),
backorders_allowed: v.boolean(),
backordered: v["boolean"](),
backorders: v["enum"](BACKORDERS_SETTINGS),
backorders_allowed: v["boolean"](),
button_text: v.string(),
catalog_visibility: v.enum(CATALOG_VISIBILITIES),
catalog_visibility: v["enum"](CATALOG_VISIBILITIES),
categories: v.array(WCV3ProductCategorySchema),
cross_sell_ids: v.array(v.pipe(v.number(), v.integer())),
date_created: v.string(),
date_created_gmt: v.string(),
date_modified: v.string(),
date_modified_gmt: v.string(),
date_on_sale_from: v.union([v.string(), v.null()]),
date_on_sale_from_gmt: v.union([v.string(), v.null()]),
date_on_sale_to: v.union([v.string(), v.null()]),
date_on_sale_to_gmt: v.union([v.string(), v.null()]),
date_on_sale_from: v.union([v.string(), v["null"]()]),
date_on_sale_from_gmt: v.union([v.string(), v["null"]()]),
date_on_sale_to: v.union([v.string(), v["null"]()]),
date_on_sale_to_gmt: v.union([v.string(), v["null"]()]),
default_attributes: v.array(WCV3ProductDefaultAttributeSchema),
description: v.string(),
dimensions: WCV3ProductDimensionsSchema,
download_expiry: v.number(),
download_limit: v.number(),
downloadable: v.boolean(),
downloadable: v["boolean"](),
downloads: v.array(WCV3ProductDownloadsSchema),
external_url: v.string(),
featured: v.boolean(),
featured: v["boolean"](),
generated_slug: v.optional(v.string()),
global_unique_id: v.string(),
grouped_products: v.array(v.pipe(v.number(), v.integer())),
has_options: v.boolean(),
has_options: v["boolean"](),
id: v.pipe(v.number(), v.integer()),
// NOTE: Ajouté par mes soins
image_repos: v.union([v.string(), v.null()]),
image_repos: v.union([v.string(), v["null"]()]),
// NOTE: Ajouté par mes soins
image_survol: v.union([v.string(), v.null()]),
image_survol: v.union([v.string(), v["null"]()]),
images: v.array(WCV3ProductImageSchema),
low_stock_amount: v.union([v.number(), v.null()]),
manage_stock: v.boolean(),
low_stock_amount: v.union([v.number(), v["null"]()]),
manage_stock: v["boolean"](),
menu_order: v.pipe(v.number(), v.integer()),
meta_data: v.array(WCV3ProductMetaDataSchema),
name: v.string(),
on_sale: v.boolean(),
on_sale: v["boolean"](),
parent_id: v.pipe(v.number(), v.integer()),
permalink: v.pipe(v.string(), v.url()),
permalink_template: v.optional(v.string()),
@ -163,32 +163,32 @@ export const WCV3ProductSchema = v.object({
price: v.string(),
price_html: v.string(),
prix_maximal: v.string(),
purchasable: v.boolean(),
purchasable: v["boolean"](),
purchase_note: v.string(),
rating_count: v.pipe(v.number(), v.integer()),
regular_price: v.string(),
related_ids: v.array(v.pipe(v.number(), v.integer())),
reviews_allowed: v.boolean(),
reviews_allowed: v["boolean"](),
sale_price: v.string(),
shipping_class: v.string(),
shipping_class_id: v.pipe(v.number(), v.integer()),
shipping_required: v.boolean(),
shipping_taxable: v.boolean(),
shipping_required: v["boolean"](),
shipping_taxable: v["boolean"](),
short_description: v.string(),
sku: v.string(),
slug: v.string(),
sold_individually: v.boolean(),
status: v.enum(PRODUCT_STATUTES),
stock_quantity: v.union([v.number(), v.null()]),
stock_status: v.enum(STOCK_STATUSES),
sold_individually: v["boolean"](),
status: v["enum"](PRODUCT_STATUTES),
stock_quantity: v.union([v.number(), v["null"]()]),
stock_status: v["enum"](STOCK_STATUSES),
tags: v.array(WCV3ProductTagSchema),
tax_class: v.string(),
tax_status: v.enum(TAX_STATUTES),
tax_status: v["enum"](TAX_STATUTES),
total_sales: v.pipe(v.number(), v.integer()),
type: v.enum(PRODUCT_TYPES),
type: v["enum"](PRODUCT_TYPES),
upsell_ids: v.array(v.pipe(v.number(), v.integer())),
variations: v.array(v.pipe(v.number(), v.integer())),
virtual: v.boolean(),
virtual: v["boolean"](),
weight: v.string(),
});

View file

@ -5,7 +5,7 @@ import * as v from "valibot";
import { TYPES_MESSAGES } from "../../constantes/messages.ts";
import { WCStoreCartItemSchema } from "./api/cart.ts";
export const TypesMessagesSchema = v.enum(TYPES_MESSAGES);
export const TypesMessagesSchema = v["enum"](TYPES_MESSAGES);
export const MessageMajBoutonPanierDonneesSchema = v.object({
quantiteProduits: v.number(),

View file

@ -35,4 +35,4 @@ export const getSessionStorageByKey = <S extends GenericSchema>(key: string, sch
export const setSessionStorageByKey =
<S extends GenericSchema>(key: string, schema: S) =>
(value: unknown): Either<DOMException | ValiError<S>, InferOutput<S>> =>
safeSchemaParse(value, schema).chain((v) => eitherSetSessionStorage(key, v));
safeSchemaParse(value, schema).chain(v => eitherSetSessionStorage(key, v));

View file

@ -1 +1 @@
export type Constructor<T> = new (...args: Array<unknown>) => T;
export type Constructor<T> = new(...args: Array<unknown>) => T;

View file

@ -7,9 +7,7 @@ import { CleNonTrouveError } from "./erreurs";
/**
* TODO
*/
export const propEither =
<T, K extends keyof T>(cle: K) =>
(donnees: T): Either<CleNonTrouveError, T[K]> =>
Maybe.fromNullable(D.getUnsafe(donnees, cle)).toEither(
new CleNonTrouveError(`La clé « ${String(cle)} » n'a pas été trouvé dans l'objet.`),
);
export const propEither = <T, K extends keyof T>(cle: K) => (donnees: T): Either<CleNonTrouveError, T[K]> =>
Maybe.fromNullable(D.getUnsafe(donnees, cle)).toEither(
new CleNonTrouveError(`La clé « ${String(cle)} » n'a pas été trouvé dans l'objet.`),
);

View file

@ -12,6 +12,5 @@ export const safeSchemaParse = <Schema extends GenericSchema>(
): Either<ValiError<Schema>, InferOutput<Schema>> => Either.encase(() => parse(schema, valeur));
export const safeSchemaParseCurried =
<S extends GenericSchema>(schema: S) =>
(valeur: unknown): Either<ValiError<S>, InferOutput<S>> =>
<S extends GenericSchema>(schema: S) => (valeur: unknown): Either<ValiError<S>, InferOutput<S>> =>
Either.encase(() => parse(schema, valeur));

View file

@ -8,15 +8,16 @@ import { ValiError } from "valibot";
import type { AnySchema } from "valibot";
import type { WCStoreBillingAddress, WCStoreShippingAddress } from "../lib/types/api/adresses.ts";
import type { WCStoreCart, WCStoreShippingRate, WCStoreShippingRateShippingRate } from "../lib/types/api/cart.ts";
import type { WCStoreCartUpdateCustomerArgs } from "../lib/types/api/cart-update-customer.ts";
import type { WCStoreCart, WCStoreShippingRate, WCStoreShippingRateShippingRate } from "../lib/types/api/cart.ts";
import type { WCV3Order, WCV3OrdersArgs } from "../lib/types/api/v3/orders.ts";
import type { GenericPageState } from "../lib/types/pages.ts";
import type { FetchErrors, HttpCodeErrors } from "../lib/types/reseau.ts";
import { Console, Effect, Stream } from "effect";
import { ReadonlyRecord } from "effect/Record";
import { ROUTE_API_MAJ_CLIENT, ROUTE_API_NOUVELLE_COMMANDES } from "../constantes/api.ts";
import { ATTRIBUT_CHARGEMENT, ATTRIBUT_LIVRAISON_VALIDEE } from "../constantes/dom.ts";
import { NOM_CANAL_REVALIDATION_LIVRAISON } from "../constantes/messages.ts";
import {
ERREUR_ADRESSE_GENERIQUE,
ERREUR_ADRESSE_MAUVAIS_CODE_POSTAL,
@ -24,6 +25,7 @@ import {
ERREUR_GENERIQUE_RESEAU,
ERREUR_GENERIQUE_SOUMISSION_ADRESSES,
} from "../constantes/messages-utilisateur.ts";
import { NOM_CANAL_REVALIDATION_LIVRAISON } from "../constantes/messages.ts";
import { estErreurFetch, estErreurHttp, setButtonLoadingState } from "../lib/dom.ts";
import { reporteEtJournaliseErreur } from "../lib/erreurs.ts";
import { ErreurAdresseInvalide } from "../lib/erreurs/adresses.ts";
@ -36,15 +38,13 @@ import { emetUniqueMessageBroadcastChannel } from "../lib/messages.ts";
import { diviseParCent } from "../lib/nombres.ts";
import { newPartialResponse, prefilledPostBackend, safeFetch, traiteErreursBackendWooCommerce } from "../lib/reseau.ts";
import { find, first } from "../lib/safe-arrays.ts";
import { WCStoreCartSchema } from "../lib/schemas/api/cart.ts";
import { WCStoreCartUpdateCustomerArgsSchema } from "../lib/schemas/api/cart-update-customer.ts";
import { WCStoreCartSchema } from "../lib/schemas/api/cart.ts";
import { estWCAddressError } from "../lib/schemas/api/erreurs.ts";
import { WCV3OrdersArgsSchema, WCV3OrderSchema } from "../lib/schemas/api/v3/orders.ts";
import { safeSchemaParse } from "../lib/validation.ts";
import { E } from "./scripts-page-panier-elements.ts";
import { getShippingRatesLS } from "./scripts-page-panier-local-storage.ts";
import { ReadonlyRecord } from "effect/Record";
import { Console, Effect, Stream } from "effect";
type Addresses = {
billing_address: WCStoreBillingAddress;
@ -60,7 +60,7 @@ const postBackend = prefilledPostBackend(ETATS_PAGE.nonce, ETATS_PAGE.authString
*
* @returns Un `Effect` ne retournant rien et ne pouvant échouer.
*/
export const initCartFormEventEmitters = Effect.fn("initCartFormEventEmitters")(function* () {
export const initCartFormEventEmitters = Effect.fn("initCartFormEventEmitters")(function*() {
return yield* pipe(
Stream.fromEventListener(E.FORMULAIRE_PANIER, "change"),
Stream.tap((event: Event) => {
@ -125,7 +125,7 @@ export const initShippingCalculationButton = (): void => {
.fromFalsy(E.FORMULAIRE_PANIER.checkValidity())
// Ne fais rien si la livraison a déjà été validée
.chainNullable((): boolean | undefined =>
E.BOUTON_ACTIONS_FORMULAIRE.hasAttribute(ATTRIBUT_LIVRAISON_VALIDEE) ? undefined : true,
E.BOUTON_ACTIONS_FORMULAIRE.hasAttribute(ATTRIBUT_LIVRAISON_VALIDEE) ? undefined : true
)
.ifJust((): void => {
event.preventDefault();
@ -133,8 +133,8 @@ export const initShippingCalculationButton = (): void => {
/** Les données du Formulaire transformées pour la requête vers le Backend. */
const formArgs: WCStoreCartUpdateCustomerArgs = pipe(
Object.fromEntries(new FormData(E.FORMULAIRE_PANIER)) as Record<string, string>,
(fields) => dictMap(fields, stringTrim),
(fields) => getAddressesFromForm(fields, E.BOUTON_SEPARATION_ADRESSES.checked),
fields => dictMap(fields, stringTrim),
fields => getAddressesFromForm(fields, E.BOUTON_SEPARATION_ADRESSES.checked),
);
// Réalise la requête et traite sa réponse
@ -142,7 +142,7 @@ export const initShippingCalculationButton = (): void => {
// Désactive le Bouton pour empêcher des requêtes concurrentes
.ifRight((): void => setButtonLoadingState(E.BOUTON_ACTIONS_FORMULAIRE, true))
.chain((args: WCStoreCartUpdateCustomerArgs) =>
safeFetch(postBackend(ROUTE_API_MAJ_CLIENT, JSON.stringify(args), false)),
safeFetch(postBackend(ROUTE_API_MAJ_CLIENT, JSON.stringify(args), false))
)
.chain((rs: Response) =>
EitherAsync<ErreurAdresseInvalide | HttpCodeErrors, unknown>(
@ -151,18 +151,18 @@ export const initShippingCalculationButton = (): void => {
.with({ status: 200 }, (rs): unknown => rs.body)
.with(
{
body: P.when((body) => estWCAddressError(body)),
body: P.when(body => estWCAddressError(body)),
status: 400,
},
(rs): never => throwE(new ErreurAdresseInvalide(rs.body.data.params)),
)
.otherwise((rs): never => throwE(traiteErreursBackendWooCommerce(rs))),
),
)
)
.chain((b: unknown) => EitherAsync.liftEither(safeSchemaParse(b, WCStoreCartSchema)))
.ifRight((cart: WCStoreCart): void => {
/** La méthode de livraison sélectionnée dans le SessionStorage */
const oldSelectedRateLS = getShippingRatesLS().chain(find((sr) => sr.selected));
const oldSelectedRateLS = getShippingRatesLS().chain(find(sr => sr.selected));
/* Les méthodes de livraison mises à jour avec le nouveau choix de l'Utilisateur. */
const updatedRates = first(cart.shipping_rates)
@ -171,7 +171,7 @@ export const initShippingCalculationButton = (): void => {
srs.map((sr: WCStoreShippingRateShippingRate, index: number) => {
// Sélectionne la nouvelle méthode demandée OU la première si le SessionStorage n'a pas été défini
oldSelectedRateLS.caseOf({
Just: (sm) => {
Just: sm => {
sr.selected = sr.method_id === sm.method_id;
},
Nothing: () => {
@ -183,7 +183,7 @@ export const initShippingCalculationButton = (): void => {
sr.price = diviseParCent(sr.price);
return sr;
}),
})
)
.orDefault([]);
@ -191,13 +191,14 @@ export const initShippingCalculationButton = (): void => {
window.dispatchEvent(createUpdatedShippingRatesEvent(updatedRates, true));
// Met à jour les Totaux
const newShippingPrice = updatedRates.find((m) => m.selected)?.price ?? 0;
const newShippingPrice = updatedRates.find(m => m.selected)?.price ?? 0;
const newTotals = {
...cart.totals,
total_discount: diviseParCent(cart.totals.total_discount),
total_items: diviseParCent(cart.totals.total_items),
total_price:
diviseParCent(cart.totals.total_items) - diviseParCent(cart.totals.total_discount) + newShippingPrice,
total_price: diviseParCent(cart.totals.total_items)
- diviseParCent(cart.totals.total_discount)
+ newShippingPrice,
total_shipping: newShippingPrice,
};
@ -222,10 +223,10 @@ export const initShippingCalculationButton = (): void => {
match(e.problemes)
.when(
// TODO: Créer une fonction utilitaire
(p) =>
p =>
pipe(
dictValues(p),
arrayFind((c) => c === "The provided postcode is not valid"),
arrayFind(c => c === "The provided postcode is not valid"),
),
// TODO: Créer une fonction utilitaire pour fixer le texte d'un message
(): void => {
@ -315,14 +316,14 @@ export const initOrderCreationButton = (): void => {
};
// Retire toute méthode de livraison invalide.
formArgs.shipping_lines = formArgs.shipping_lines.filter((line) => line.method_id !== undefined);
formArgs.shipping_lines = formArgs.shipping_lines.filter(line => line.method_id !== undefined);
// Réalise la requête et traite sa réponse
void EitherAsync.liftEither(safeSchemaParse(formArgs, WCV3OrdersArgsSchema))
// Désactive le Bouton pour empêcher des requêtes concurrentes
.ifRight((): void => setButtonLoadingState(E.BOUTON_ACTIONS_FORMULAIRE, true))
.chain((args: WCV3OrdersArgs) =>
safeFetch(postBackend(ROUTE_API_NOUVELLE_COMMANDES, JSON.stringify(args), true)),
safeFetch(postBackend(ROUTE_API_NOUVELLE_COMMANDES, JSON.stringify(args), true))
)
.chain((rs: Response) =>
EitherAsync<HttpCodeErrors, unknown>(
@ -330,7 +331,7 @@ export const initOrderCreationButton = (): void => {
match(await newPartialResponse(rs))
.with({ status: 201 }, (rs): unknown => rs.body)
.otherwise((rs): never => throwE(traiteErreursBackendWooCommerce(rs))),
),
)
)
.chain((b: unknown) => EitherAsync.liftEither(safeSchemaParse(b, WCV3OrderSchema)))
.ifRight((order: WCV3Order): void => {
@ -342,9 +343,9 @@ export const initOrderCreationButton = (): void => {
// Redirige vers Stripe
Maybe.fromNullable(new URL(`https://${window.location.host}/checkout`))
.ifJust((url) => url.searchParams.append("order_key", order.order_key))
.ifJust((url) => url.searchParams.append("order_id", String(order.id)))
.ifJust((url) => location.assign(url));
.ifJust(url => url.searchParams.append("order_key", order.order_key))
.ifJust(url => url.searchParams.append("order_id", String(order.id)))
.ifJust(url => location.assign(url));
})
.ifLeft((err: FetchErrors | HttpCodeErrors | ValiError<AnySchema>): void => {
match(err)

View file

@ -54,193 +54,34 @@ export const initialiseElementsCodePromo = (): void => {
codePromoPresent: recuperePresenceCodePromo(),
valeurCodePromo: recupereValeurCodePromo(),
})
// Un code promo doit être ajouté
// Aucun code promo n'est déjà présent et une valeur acceptable existe
.with(
[
// Un code promo doit être ajouté
// Aucun code promo n'est déjà présent et une valeur acceptable existe
"with"
](
{
cible: P.when((cible: EventTarget | null) =>
targetMatchesSelector<HTMLButtonElement>(cible, DOM_BOUTON_CODE_PROMO),
targetMatchesSelector<HTMLButtonElement>(cible, DOM_BOUTON_CODE_PROMO)
),
codePromoPresent: false,
valeurCodePromo: P.string,
},
({ valeurCodePromo }) =>
void EitherAsync
// Vérifie le Schéma des arguments
.liftEither(safeSchemaParse({ code: valeurCodePromo }, WCStoreCartApplyCouponArgsSchema))
.ifRight(() => {
// Désactive le Bouton pour empêcher des requêtes concurrentes
E.BOUTON_CODE_PROMO.setAttribute(ATTRIBUT_DESACTIVE, "");
E.BOUTON_CODE_PROMO.setAttribute(ATTRIBUT_CHARGEMENT, "");
// Réinitialise le Message à l'Utilisateur
E.MESSAGE_CODE_PROMO.textContent = "";
// Lance un cycle d'animation sur le texte de chargement
lanceAnimationCycleLoading(E.BOUTON_CODE_PROMO, 500);
})
// Réalise la requête auprès du backend
.map((args: WCStoreCartApplyCouponArgs) =>
postBackend({
corps: JSON.stringify(args),
nonce: ETATS_PAGE.nonce,
route: ROUTE_API_APPLIQUE_COUPON,
}),
)
// Traite les cas d'Erreur
.chain((reponse: Response) =>
EitherAsync<ErreurCodePromoInvalide | ServerError, unknown>(async ({ throwE }) => {
const reponseSimplifiee: SimplifiedResponse = {
body: await reponse.json(),
status: reponse.status,
};
return match(reponseSimplifiee)
.with({ status: 500 }, () => throwE(new ServerError("500 Server Error")))
.with(
{
body: P.when(() => reponseEstCodeErreurWC(reponseSimplifiee, ERREUR_CODE_PROMO_INVALIDE)),
status: 400,
},
() => throwE(new ErreurCodePromoInvalide(recupereValeurCodePromo() ?? "")),
)
.with({ status: 200 }, () => reponseSimplifiee.body)
.run();
}),
)
// Vérifie le Schéma de la Réponse du backend
.chain((corpsReponse: unknown) => EitherAsync.liftEither(safeSchemaParse(corpsReponse, WCStoreCartSchema)))
// Déclenche les mises à jour du DOM avec les données du nouveau Panier
.ifRight((panier: WCStoreCart) => {
E.ENSEMBLE_CODE_PROMO.toggleAttribute(ATTRIBUT_CODE_PROMO_PRESENT);
E.CHAMP_CODE_PROMO.toggleAttribute(ATTRIBUT_DESACTIVE);
E.CHAMP_CODE_PROMO.value = panier.coupons[0]?.code ?? "";
E.BOUTON_CODE_PROMO.textContent = "Remove";
E.TOTAL_PANIER.textContent = pipe(
diviseParCent(panier.totals.total_price),
arrondisADeuxDecimales,
formateEnEuros,
);
E.TOTAL_REDUCTION_LIGNE.toggleAttribute(ATTRIBUT_HIDDEN);
E.TOTAL_REDUCTION_VALEUR.textContent = pipe(
diviseParCent(panier.totals.total_discount),
inverseNombre,
arrondisADeuxDecimales,
formateEnEuros,
);
window.dispatchEvent(CODE_PROMO_MAJ_EVENT);
// EmetUniqueMessageBroadcastChannel(NOM_CANAL_REVALIDATION_LIVRAISON, true);
})
.ifLeft((erreur) => {
// Rétablis le texte d'origine
E.BOUTON_CODE_PROMO.textContent = "Apply";
// Traite les Erreurs et affiche un Message à l'Utilisateur
match(erreur)
.with(P.instanceOf(ValiError), (e) => {
reporteErreur(e);
console.error("ValiError", e.issues);
})
.with(P.instanceOf(ErreurCodePromoInvalide), (e) => {
E.MESSAGE_CODE_PROMO.textContent = "This promo code does not exist.";
reporteErreur(e);
console.error(e);
})
.with(P.instanceOf(ServerError), (e) => {
E.MESSAGE_CODE_PROMO.textContent =
"Sorry, something went wrong! Please refresh the page and try again.";
reporteErreur(e);
console.error(e);
})
.with(P.instanceOf(TypeError), (e) => {
E.MESSAGE_CODE_PROMO.textContent =
"Sorry, something went wrong! Please refresh the page and try again.";
reporteErreur(e);
console.error(e);
})
.exhaustive();
})
.finally(() => {
// Désactive l'animation de chargement et rend le Bouton de nouveau cliquable
// TODO: Créer un type d'Événement ?
E.BOUTON_CODE_PROMO.removeAttribute(ATTRIBUT_CHARGEMENT);
E.BOUTON_CODE_PROMO.removeAttribute(ATTRIBUT_DESACTIVE);
})
.run(),
({ valeurCodePromo }) => undefined,
)
// Un code promo doit être retiré
// Un code promo est présent sous forme de chaîne
.with(
[
// Un code promo doit être retiré
// Un code promo est présent sous forme de chaîne
"with"
](
{
cible: P.when((cible) => targetMatchesSelector<HTMLButtonElement>(cible, DOM_BOUTON_CODE_PROMO)),
cible: P.when(cible => targetMatchesSelector<HTMLButtonElement>(cible, DOM_BOUTON_CODE_PROMO)),
codePromoPresent: true,
valeurCodePromo: P.string,
},
({ valeurCodePromo }) =>
void EitherAsync.liftEither(safeSchemaParse({ code: valeurCodePromo }, WCStoreCartRemoveCouponArgsSchema))
.ifRight(() => {
E.BOUTON_CODE_PROMO.setAttribute(ATTRIBUT_DESACTIVE, "");
E.BOUTON_CODE_PROMO.setAttribute(ATTRIBUT_CHARGEMENT, "");
lanceAnimationCycleLoading(E.BOUTON_CODE_PROMO, 500);
})
.map((args: WCStoreCartRemoveCouponArgs) =>
postBackend({
corps: JSON.stringify(args),
nonce: ETATS_PAGE.nonce,
route: ROUTE_API_RETIRE_COUPON,
}),
)
.chain((reponse: Response) =>
EitherAsync<ServerError, unknown>(async ({ throwE }) => {
if (estReponse500(reponse)) {
throwE(new ServerError("500 server Error"));
}
return await reponse.json();
}),
)
.chain((corpsReponse: unknown) => EitherAsync.liftEither(safeSchemaParse(corpsReponse, WCStoreCartSchema)))
.ifRight((panier: WCStoreCart) => {
E.ENSEMBLE_CODE_PROMO.toggleAttribute(ATTRIBUT_CODE_PROMO_PRESENT);
E.ENSEMBLE_CODE_PROMO.reset();
E.CHAMP_CODE_PROMO.toggleAttribute(ATTRIBUT_DESACTIVE);
E.CHAMP_CODE_PROMO.textContent = "";
E.BOUTON_CODE_PROMO.textContent = "Apply";
E.TOTAL_PANIER.textContent = pipe(
diviseParCent(panier.totals.total_price),
arrondisADeuxDecimales,
formateEnEuros,
);
E.TOTAL_REDUCTION_LIGNE.toggleAttribute(ATTRIBUT_HIDDEN);
E.TOTAL_REDUCTION_VALEUR.textContent = "-0€";
emetUniqueMessageBroadcastChannel(NOM_CANAL_REVALIDATION_LIVRAISON, true);
})
.ifLeft((erreur) =>
match(erreur)
.with(P.instanceOf(ValiError), (e) => {
reporteErreur(e);
console.error("retour ajout code promo", e.issues);
})
.with(P.instanceOf(ServerError), (e) => {
reporteErreur(e);
console.error("retour ajout code promo", e);
})
.with(P.instanceOf(TypeError), (e) => {
reporteErreur(e);
console.error("retour ajout code promo", e);
})
.exhaustive(),
)
.finally(() => {
E.BOUTON_CODE_PROMO.removeAttribute(ATTRIBUT_CHARGEMENT);
E.BOUTON_CODE_PROMO.removeAttribute(ATTRIBUT_DESACTIVE);
})
.run(),
({ valeurCodePromo }) => undefined,
)
// Ne rien faire en dehors de ces deux situations
.with(P._, identity),
);
[
// Ne rien faire en dehors de ces deux situations
"with"
](P._, identity));
};

View file

@ -26,20 +26,20 @@ export const reinitialiseValidationLivraison = (): void => {
* @returns void
*/
export const souscrisEvenementsPanier = (): void => {
window.addEventListener(ADRESSES_MAJ, (): void => {
globalThis.addEventListener(ADRESSES_MAJ, (): void => {
reinitialiseValidationLivraison();
});
window.addEventListener(CODE_PROMO_MAJ, (): void => {
globalThis.addEventListener(CODE_PROMO_MAJ, (): void => {
reinitialiseValidationLivraison();
});
window.addEventListener(SHIPPING_RATES_UPDATED, (event: Event): void => {
globalThis.addEventListener(SHIPPING_RATES_UPDATED, (event: Event): void => {
Either
// La vérification du schéma se fait à l'émission
.encase(() => (event as UpdatedShippingRatesEvent).detail)
// Met à jour le DOM
.ifRight((event) => {
.ifRight(event => {
// Met à jour les Méthodes à l'Utilisateur si demandé
// Il peut y en avoir aucune
if (event.refresh_methods) {
@ -47,18 +47,18 @@ export const souscrisEvenementsPanier = (): void => {
}
})
// Met à jour le SessionStorage
.chain((event) => eitherSetSessionStorage("shipping_rates", event.shipping_rates))
.chain(event => eitherSetSessionStorage("shipping_rates", event.shipping_rates))
.ifLeft(reporteEtJournaliseErreur);
});
window.addEventListener(TOTALS_UPDATED, (event: Event): void => {
globalThis.addEventListener(TOTALS_UPDATED, (event: Event): void => {
Either
// La vérification du Schéma se fait à l'émission
.encase(() => (event as UpdatedTotalsEvent).detail.totals)
.chain((ts) => eitherSetSessionStorage("totals", ts))
.chain(ts => eitherSetSessionStorage("totals", ts))
.ifLeft(reporteEtJournaliseErreur)
// Met à jour le DOM
.ifRight((ts) => {
.ifRight(ts => {
E.SOUS_TOTAL_LIVRAISON_VALEUR.textContent = formateEnEuros(ts.total_shipping);
E.SOUS_TOTAL_PRODUITS_VALEUR.textContent = formateEnEuros(ts.total_items);
E.SOUS_TOTAL_REDUCTION_VALEUR.textContent = formateEnEuros(ts.total_discount * -1);

View file

@ -19,7 +19,7 @@ import { getShippingRatesLS } from "./scripts-page-panier-local-storage";
export const initShippingRatesChoicesActions = (): void => {
getDOMElementsWithSelector(E.CONTENEUR_METHODES_LIVRAISON)<HTMLInputElement>("input").ifRight(
forEach((el: HTMLInputElement): void =>
forEach((el: HTMLInputElement): void => {
el.addEventListener("click", (event: MouseEvent): void => {
// Récupère les méthodes du SessionStorage et les met à jour avec le nouveau choix
getShippingRatesLS()
@ -32,10 +32,10 @@ export const initShippingRatesChoicesActions = (): void => {
)
// Met à jour les Méthodes de livraison dans le SessionStorage et le DOM
.ifJust((srs: WCStoreShippingRateShippingRates): void => {
window.dispatchEvent(createUpdatedShippingRatesEvent(srs, false));
globalThis.dispatchEvent(createUpdatedShippingRatesEvent(srs, false));
})
// Met à jour les totaux dans le SessionStorage et le DOM
.chain(find((sr) => sr.selected))
.chain(find(sr => sr.selected))
.ifJust((sr: WCStoreShippingRateShippingRate): void => {
getSessionStorageByKey("totals", WCStoreCartTotalsSchema)
.ifLeft(reporteEtJournaliseErreur)
@ -45,11 +45,11 @@ export const initShippingRatesChoicesActions = (): void => {
return ts;
})
.ifRight((ts: WCStoreCartTotals): void => {
window.dispatchEvent(createUpdatedTotalsEvent(ts));
globalThis.dispatchEvent(createUpdatedTotalsEvent(ts));
});
});
}),
),
});
}),
);
};
@ -64,12 +64,17 @@ export const generateShippingRatesHTML = (
}
// Retire les méthodes de livraison initiales
getDOMElementsWithSelector(container)("div[data-methode-initiale]").ifRight(arrayForEach((div) => div.remove()));
getDOMElementsWithSelector(container)("div[data-methode-initiale]").ifRight(
arrayForEach(div => {
div.remove();
}),
);
const selectedShippingRate: string = shippingRates.find((sr) => sr.selected)?.method_id ?? "";
const selectedShippingRate: string = shippingRates.find(sr => sr.selected)?.method_id ?? "";
const shippingRatesHTML: ReadonlyArray<TemplateResult> = arrayMap(
shippingRates,
(methode) => html` <div>
methode =>
html` <div>
<input
id="methode-livraison-${methode.method_id}"
name="choix-methode-livraison"

View file

@ -7,12 +7,13 @@ import { match, P } from "ts-pattern";
import { ValiError } from "valibot";
import type { AnySchema } from "valibot";
import type { WCStoreCart } from "../lib/types/api/cart.ts";
import type { WCStoreCartRemoveItemArgs } from "../lib/types/api/cart-remove-item.ts";
import type { WCStoreCartUpdateItemArgs } from "../lib/types/api/cart-update-item.ts";
import type { WCStoreCart } from "../lib/types/api/cart.ts";
import type { GenericPageState } from "../lib/types/pages.ts";
import type { FetchErrors, HttpCodeErrors } from "../lib/types/reseau.ts";
import { getFirstSelectorFromParentOrThrow } from "../../scripts-effect/lib/dom.ts";
import { ROUTE_API_MAJ_ARTICLE_PANIER, ROUTE_API_RETIRE_ARTICLE_PANIER } from "../constantes/api.ts";
import {
ATTRIBUT_CLE_PANIER,
@ -31,12 +32,11 @@ import {
} from "../lib/messages.ts";
import { diviseParCent } from "../lib/nombres.ts";
import { newPartialResponse, postBackend, safeFetch, traiteErreursBackendWooCommerce } from "../lib/reseau.ts";
import { WCStoreCartSchema } from "../lib/schemas/api/cart.ts";
import { WCStoreCartRemoveItemArgsSchema } from "../lib/schemas/api/cart-remove-item.ts";
import { WCStoreCartUpdateItemArgsSchema } from "../lib/schemas/api/cart-update-item.ts";
import { WCStoreCartSchema } from "../lib/schemas/api/cart.ts";
import { safeSchemaParse } from "../lib/validation.ts";
import { E } from "./scripts-page-panier-elements.ts";
import { getFirstSelectorFromParentOrThrow } from "../../scripts-effect/lib/dom.ts";
// @ts-expect-error -- États injectés par le modèle PHP
const PAGE_STATE: GenericPageState = _etats;
@ -65,8 +65,7 @@ const getCartEntryInteractiveEles = (entry: HTMLElement): CartEntryInteractiveEl
* @returns Rien.
*/
const toggleCartEntryButtons =
(activated: boolean) =>
(cartEntries: ReadonlyArray<CartEntryInteractiveElements>): void => {
(activated: boolean) => (cartEntries: ReadonlyArray<CartEntryInteractiveElements>): void => {
arrayForEach(cartEntries, (e: CartEntryInteractiveElements): void => {
if (activated) {
// Active les Boutons
@ -121,10 +120,10 @@ const initActionsOnCartEntries = (): void => {
.fromNullable(entryButtons.quantityInput.valueAsNumber)
.toEither(new Error("Quantité manquante pour cette ligne du Panier !")),
)
.chain((q) =>
.chain(q =>
EitherAsync.liftEither(
safeSchemaParse({ key: entryKey, quantity: q + 1 }, WCStoreCartUpdateItemArgsSchema),
),
)
)
.ifRight(() => {
pipe(cartEntries, arrayMap(getCartEntryInteractiveEles), toggleCartEntryButtons(false));
@ -136,14 +135,14 @@ const initActionsOnCartEntries = (): void => {
nonce: PAGE_STATE.nonce,
route: ROUTE_API_MAJ_ARTICLE_PANIER,
}),
),
)
)
.chain((r: Response) =>
EitherAsync<ServerError, unknown>(async ({ throwE }) =>
match(await newPartialResponse(r))
["with"]({ status: 200 }, (r) => r.body)
.otherwise((rs): never => throwE(traiteErreursBackendWooCommerce(rs))),
),
["with"]({ status: 200 }, r => r.body)
.otherwise((rs): never => throwE(traiteErreursBackendWooCommerce(rs)))
)
)
.chain((b: unknown) => EitherAsync.liftEither(safeSchemaParse(b, WCStoreCartSchema)))
.ifRight((c: WCStoreCart): void => {
@ -161,17 +160,17 @@ const initActionsOnCartEntries = (): void => {
})
.ifLeft((err: FetchErrors | HttpCodeErrors | ValiError<AnySchema>): void => {
match(err)
["with"](P.instanceOf(ValiError), (e) => {
["with"](P.instanceOf(ValiError), e => {
reporteErreur(e);
console.error(e.issues);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES;
})
["with"](P.instanceOf(ServerError), P.instanceOf(BadRequestError), (e) => {
["with"](P.instanceOf(ServerError), P.instanceOf(BadRequestError), e => {
reporteErreur(e);
console.error(e);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES;
})
["with"](P.instanceOf(DOMException), P.instanceOf(TypeError), P.instanceOf(Error), (e) => {
["with"](P.instanceOf(DOMException), P.instanceOf(TypeError), P.instanceOf(Error), e => {
reporteErreur(e);
console.error(e);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_RESEAU;
@ -191,7 +190,7 @@ const initActionsOnCartEntries = (): void => {
Maybe
// Nécessaire pour que l'on ait une valeur à incrémenter
.fromNullable(entryButtons.quantityInput.valueAsNumber)
.filter((valeur) => valeur > 1)
.filter(valeur => valeur > 1)
.ifJust((valeur: number) => {
// Réalise la requête et traite sa réponse
void EitherAsync
@ -211,7 +210,7 @@ const initActionsOnCartEntries = (): void => {
nonce: PAGE_STATE.nonce,
route: ROUTE_API_MAJ_ARTICLE_PANIER,
}),
),
)
)
// 4. Traite les cas d'Erreurs et récupère le Corps de la Réponse
.chain((reponse: Response) =>
@ -220,9 +219,9 @@ const initActionsOnCartEntries = (): void => {
match(await newPartialResponse(reponse))
["with"]({ status: 500 }, () => throwE(new ServerError("500 Server Error")))
["with"]({ status: 400 }, () => throwE(new BadRequestError("400 Bad Request Error")))
["with"]({ status: 200 }, (r) => r.body)
.otherwise((erreur) => throwE(new Error(`Erreur inconnue ${String(erreur.status)}`))),
),
["with"]({ status: 200 }, r => r.body)
.otherwise(erreur => throwE(new Error(`Erreur inconnue ${String(erreur.status)}`)))
)
)
// 5. Vérifie le Schéma de la Réponse
.chain((corps: unknown) => EitherAsync.liftEither(safeSchemaParse(corps, WCStoreCartSchema)))
@ -243,17 +242,17 @@ const initActionsOnCartEntries = (): void => {
// 7. Traite les Erreurs et affiche un message à l'Utilisateur
.ifLeft((erreur: BadRequestError | FetchErrors | ServerError | ValiError<AnySchema>): void => {
match(erreur)
["with"](P.instanceOf(ValiError), (e) => {
["with"](P.instanceOf(ValiError), e => {
reporteErreur(e);
console.error(e.issues);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES;
})
["with"](P.instanceOf(ServerError), P.instanceOf(BadRequestError), (e) => {
["with"](P.instanceOf(ServerError), P.instanceOf(BadRequestError), e => {
reporteErreur(e);
console.error(e);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES;
})
["with"](P.instanceOf(DOMException), P.instanceOf(TypeError), P.instanceOf(Error), (e) => {
["with"](P.instanceOf(DOMException), P.instanceOf(TypeError), P.instanceOf(Error), e => {
reporteErreur(e);
console.error(e);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_RESEAU;
@ -292,7 +291,7 @@ const initActionsOnCartEntries = (): void => {
nonce: PAGE_STATE.nonce,
route: ROUTE_API_RETIRE_ARTICLE_PANIER,
}),
),
)
)
// 4. Traite les cas d'Erreurs et récupère le Corps de la Réponse
.chain((reponse: Response) =>
@ -301,9 +300,9 @@ const initActionsOnCartEntries = (): void => {
match(await newPartialResponse(reponse))
["with"]({ status: 500 }, () => throwE(new ServerError("500 Server Error")))
["with"]({ status: 400 }, () => throwE(new BadRequestError("400 Bad Request Error")))
["with"]({ status: 200 }, (r) => r.body)
.otherwise((erreur) => throwE(new Error(`Erreur inconnue ${String(erreur.status)}`))),
),
["with"]({ status: 200 }, r => r.body)
.otherwise(erreur => throwE(new Error(`Erreur inconnue ${String(erreur.status)}`)))
)
)
// 5. Vérifie le Schéma de la Réponse
.chain((corps: unknown) => EitherAsync.liftEither(safeSchemaParse(corps, WCStoreCartSchema)))
@ -327,17 +326,17 @@ const initActionsOnCartEntries = (): void => {
// 7. Traite les Erreurs et affiche un message à l'Utilisateur
.ifLeft((erreur: BadRequestError | FetchErrors | ServerError | ValiError<AnySchema>): void => {
match(erreur)
["with"](P.instanceOf(ValiError), (e) => {
["with"](P.instanceOf(ValiError), e => {
reporteErreur(e);
console.error(e.issues);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES;
})
["with"](P.instanceOf(ServerError), P.instanceOf(BadRequestError), (e) => {
["with"](P.instanceOf(ServerError), P.instanceOf(BadRequestError), e => {
reporteErreur(e);
console.error(e);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES;
})
["with"](P.instanceOf(DOMException), P.instanceOf(TypeError), P.instanceOf(Error), (e) => {
["with"](P.instanceOf(DOMException), P.instanceOf(TypeError), P.instanceOf(Error), e => {
reporteErreur(e);
console.error(e);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_RESEAU;
@ -352,10 +351,10 @@ const initActionsOnCartEntries = (): void => {
});
},
)
.otherwise((_) => {});
.otherwise(_ => {});
});
});
});
};
export { toggleCartEntryButtons, initActionsOnCartEntries as initialiseActionsEntreesPanier };
export { initActionsOnCartEntries as initialiseActionsEntreesPanier, toggleCartEntryButtons };

View file

@ -68,9 +68,9 @@ const initialiseObservationFenetre = (): void => {
}
etapePlanifiee = true;
requestAnimationFrame((): void =>
majVisibiliteBouton(defilementY > window.innerHeight * RATIO_MINIMUM_PAGE_PAR_FENETRE),
);
requestAnimationFrame((): void => {
majVisibiliteBouton(defilementY > window.innerHeight * RATIO_MINIMUM_PAGE_PAR_FENETRE);
});
});
new ResizeObserver((entrees: Array<ResizeObserverEntry>): void => {

View file

@ -2,13 +2,14 @@
import { Array as EffectArray, Match, Predicate } from "effect";
import { DOM_ENTREES_MENU_CATEGORIES_PRODUITS, DOM_MENU_CATEGORIES_PRODUITS } from "./constantes/dom.ts";
import { getAllSelectorFromDocumentOrThrow, getFirstSelectorFromDocumentOrThrow } from "../scripts-effect/lib/dom.ts";
import { DOM_ENTREES_MENU_CATEGORIES_PRODUITS, DOM_MENU_CATEGORIES_PRODUITS } from "./constantes/dom.ts";
// Initialise les attributs HTML pour l'affichage initiale des flèches de défilement du menu de catégories de Produits.
document.addEventListener("DOMContentLoaded", (): void => {
const productsCategoriesMenu: HTMLElement =
getFirstSelectorFromDocumentOrThrow<HTMLElement>(DOM_MENU_CATEGORIES_PRODUITS);
const productsCategoriesMenu: HTMLElement = getFirstSelectorFromDocumentOrThrow<HTMLElement>(
DOM_MENU_CATEGORIES_PRODUITS,
);
const menuEntries: ReadonlyArray<HTMLAnchorElement> = getAllSelectorFromDocumentOrThrow(
DOM_ENTREES_MENU_CATEGORIES_PRODUITS,
);
@ -22,17 +23,25 @@ document.addEventListener("DOMContentLoaded", (): void => {
}
new IntersectionObserver(
EffectArray.forEach((intersectionEntry) => {
EffectArray.forEach(intersectionEntry => {
// Ne déclenche rien si le scroll n'est pas horizontal
if (intersectionEntry.boundingClientRect.top <= 0) {
return;
}
Match.value([intersectionEntry.isIntersecting]).pipe(
Match.when([true, 0], () => productsCategoriesMenu.removeAttribute("data-entrees-presentes-debut")),
Match.when([true, 1], () => productsCategoriesMenu.removeAttribute("data-entrees-presentes-fin")),
Match.when([false, 0], () => productsCategoriesMenu.setAttribute("data-entrees-presentes-debut", "")),
Match.when([false, 1], () => productsCategoriesMenu.setAttribute("data-entrees-presentes-fin", "")),
Match.when([true, 0], () => {
productsCategoriesMenu.removeAttribute("data-entrees-presentes-debut");
}),
Match.when([true, 1], () => {
productsCategoriesMenu.removeAttribute("data-entrees-presentes-fin");
}),
Match.when([false, 0], () => {
productsCategoriesMenu.setAttribute("data-entrees-presentes-debut", "");
}),
Match.when([false, 1], () => {
productsCategoriesMenu.setAttribute("data-entrees-presentes-fin", "");
}),
Match.orElse(() => {}),
);
}),

View file

@ -18,19 +18,19 @@ const E = {
const initialiseBoutonMenuMobile = (): void => {
const menuMobile = new A11yDialog(E.MENU_MOBILE);
new ResizeObserver((entrees) =>
new ResizeObserver(entrees =>
// Cache le Menu mobile pour les grandes tailles d'écrans
pipe(
A.head(entrees),
O.filter((entree: ResizeObserverEntry) => entree.borderBoxSize[0]!.inlineSize > 1000),
O.tap((_) => menuMobile.hide()),
),
O.tap(_ => menuMobile.hide()),
)
).observe(E.CORPS_HTML);
E.BOUTON_MENU_MOBILE.addEventListener("click", (): void => {
// Renvoie à la Page d'accueil pour les grandes tailles d'écrans
if (window.innerWidth > 1000) {
window.location.href = "/";
globalThis.location.href = "/";
return;
}
// Cache le Menu mobile s'il est actif

View file

@ -2,7 +2,7 @@
* Scripts pour les fonctionnalités de la Page À Propos (« About »).
*/
import { A, pipe as beltPipe, O } from "@mobily/ts-belt";
import { A, O, pipe as beltPipe } from "@mobily/ts-belt";
import {
ATTRIBUT_ENSEMBLE_EPINGLE_BOITE_ACTIF,
@ -48,7 +48,11 @@ document.addEventListener("DOMContentLoaded", (): void => {
O.tap((id: string) => {
beltPipe(
O.fromNullable(ENSEMBLES_EPINGLES_BOITES_TEXTE.get(id)),
O.tap(A.forEach((element) => element.removeAttribute(ATTRIBUT_ENSEMBLE_EPINGLE_BOITE_ACTIF))),
O.tap(
A.forEach(element => {
element.removeAttribute(ATTRIBUT_ENSEMBLE_EPINGLE_BOITE_ACTIF);
}),
),
);
}),
);
@ -64,7 +68,11 @@ document.addEventListener("DOMContentLoaded", (): void => {
if (cible.hasAttribute(ATTRIBUT_ENSEMBLE_EPINGLE_BOITE_ACTIF)) {
beltPipe(
O.fromNullable(ENSEMBLES_EPINGLES_BOITES_TEXTE.get(id)),
O.tap(A.forEach((element) => element.removeAttribute(ATTRIBUT_ENSEMBLE_EPINGLE_BOITE_ACTIF))),
O.tap(
A.forEach(element => {
element.removeAttribute(ATTRIBUT_ENSEMBLE_EPINGLE_BOITE_ACTIF);
}),
),
);
return;
}
@ -73,12 +81,14 @@ document.addEventListener("DOMContentLoaded", (): void => {
beltPipe(
Array.from(ENSEMBLES_EPINGLES_BOITES_TEXTE.values()),
A.flat,
A.forEach((element) => element.removeAttribute(ATTRIBUT_ENSEMBLE_EPINGLE_BOITE_ACTIF)),
A.forEach(element => {
element.removeAttribute(ATTRIBUT_ENSEMBLE_EPINGLE_BOITE_ACTIF);
}),
);
// Active l'Attribut sur l'Ensemble
beltPipe(
O.fromNullable(ENSEMBLES_EPINGLES_BOITES_TEXTE.get(id)),
O.tap(A.forEach((element) => element.toggleAttribute(ATTRIBUT_ENSEMBLE_EPINGLE_BOITE_ACTIF))),
O.tap(A.forEach(element => element.toggleAttribute(ATTRIBUT_ENSEMBLE_EPINGLE_BOITE_ACTIF))),
);
}),
);

View file

@ -94,23 +94,25 @@ const initDefilementStorytelling = (): void => {
}).observe(E.STORYTELLING);
// Initialise la mise à jour des images au défilement sur le Conteneur.
E.STORYTELLING.addEventListener("scroll", (): void => majVisibilitéImagesStorytelling());
E.STORYTELLING.addEventListener("scroll", (): void => {
majVisibilitéImagesStorytelling();
});
};
const initGestionAnimation = (): void => {
pipe(
A.at(E.IMAGES_STORYTELLING, 0),
O.tap((img) => {
O.tap(img => {
const options: IntersectionObserverInit = {
root: undefined,
rootMargin: "0px",
threshold: 0,
};
const callback = (entries: Array<IntersectionObserverEntry>) => {
A.forEach(entries, (e) => {
e.intersectionRatio >= 0.9
? E.CONTENEUR_ANIMATION.removeAttribute(ATTRIBUT_HIDDEN)
: E.CONTENEUR_ANIMATION.setAttribute(ATTRIBUT_HIDDEN, "");
A.forEach(entries, e => {
e.intersectionRatio >= 0.9 ?
E.CONTENEUR_ANIMATION.removeAttribute(ATTRIBUT_HIDDEN) :
E.CONTENEUR_ANIMATION.setAttribute(ATTRIBUT_HIDDEN, "");
});
};

View file

@ -13,6 +13,7 @@ import type { WCV3Products, WCV3ProductsArgs } from "./lib/types/api/v3/products
import type { GenericPageState } from "./lib/types/pages";
import { ROUTE_API_NOUVELLE_PRODUCTS } from "./constantes/api.ts";
import { PRODUCT_STATUTES } from "./constantes/api/products.ts";
import {
ATTRIBUT_CHARGEMENT,
ATTRIBUT_DESACTIVE,
@ -28,7 +29,6 @@ import { BadRequestError, reporteErreur, ServerError } from "./lib/erreurs.ts";
import { getBackendAvecParametresUrl, newPartialResponse } from "./lib/reseau.ts";
import { WCV3ProductsArgsSchema, WCV3ProductsSchema } from "./lib/schemas/api/v3/products.ts";
import { safeSchemaParse } from "./lib/validation.ts";
import { PRODUCT_STATUTES } from "./constantes/api/products.ts";
type APIProductsErrors =
| APIFetchErrors
@ -67,109 +67,7 @@ const initialisePageBoutique = (): void => {
...(idCategorieProduits && { category: idCategorieProduits }),
};
void EitherAsync
// 1. Valide les Arguments de la Requête
.liftEither(safeSchemaParse(args, WCV3ProductsArgsSchema))
// 2. Exécute un Effet pour empêcher les requêtes concurrentes et lancer une animation de chargement
.ifRight((): void => {
// Désactive le Bouton pour empêcher des requêtes concurrentes
E.BOUTON_PLUS_DE_PRODUITS.setAttribute(ATTRIBUT_DESACTIVE, "");
E.BOUTON_PLUS_DE_PRODUITS.setAttribute(ATTRIBUT_CHARGEMENT, "");
// Lance un cycle d'animation sur le texte de chargement
lanceAnimationCycleLoading(E.BOUTON_PLUS_DE_PRODUITS, 500);
})
// 3. Exécute la requête via fetch sous forme d'EitherAsync
.chain((args: WCV3ProductsArgs) =>
EitherAsync<DOMException | Error, Response>(() =>
getBackendAvecParametresUrl({
authString: ETATS_PAGE.authString,
nonce: ETATS_PAGE.nonce,
route: ROUTE_API_NOUVELLE_PRODUCTS,
searchParams: new URLSearchParams(args).toString(),
}),
),
)
// 4. Traite les cas d'Erreurs et récupère le Corps de la Réponse
.chain((reponse: Response) =>
EitherAsync<APIFetchErrors, unknown>(async ({ throwE }) =>
match(await newPartialResponse(reponse))
.with({ status: 500 }, () => throwE(new ServerError("500 Server Error")))
.with({ status: 400 }, () => throwE(new BadRequestError("400 Server Error")))
.with({ status: 200 }, (r) => r.body)
.run(),
),
)
// 5. Vérifie le Schéma de la Réponse
.chain((corpsReponse: unknown) => EitherAsync.liftEither(safeSchemaParse(corpsReponse, WCV3ProductsSchema)))
// 6. Exécute un Effet pour la mise à jour du DOM avec les Résultats
.ifRight((donnees: WCV3Products) => {
// Cache le bouton s'il y a moins de PRODUCTS_PER_PAGE Produits disponibles (que l'on est à la dernière page)
if (donnees.length < PRODUCTS_PER_PAGE) {
E.BOUTON_PLUS_DE_PRODUITS.toggleAttribute(ATTRIBUT_HIDDEN);
}
// Créé un DocumentFragment qui recevra tous les nouveaux Produits
const fragment: DocumentFragment = document.createDocumentFragment();
// Créé les Éléments <article> à insérer
for (const produit of donnees.slice(0, PRODUCTS_PER_PAGE)) {
pipe(
html`
<article class="produit">
<figure>
<a href="/product/${produit.slug}">
<picture class="produit__illustration produit__illustration__principale">
${produit.image_repos ?? ""}
</picture>
<picture class="produit__illustration produit__illustration__survol">
${produit.image_survol ?? ""}
</picture>
</a>
<figcaption class="produit__textuel">
<h3 class="produit__textuel__titre">
<a href="${produit.permalink}">${produit.name}</a>
</h3>
<p class="produit__textuel__prix">
${produit.prix_maximal}
</p>
</figcaption>
</figure>
</article>
`,
tap((article) => fragment.append(article)),
);
}
// Ajoute les nouveaux Produits dans le DOM
E.GRILLE_PRODUITS.append(fragment);
E.GRILLE_PRODUITS.setAttribute(ATTRIBUT_PAGE, String(nouveauNumeroPage));
E.BOUTON_PLUS_DE_PRODUITS.textContent = "Show more";
})
// 7. Traite les Erreurs et affiche un Message à l'Utilisateur
.ifLeft((erreur: APIProductsErrors) => {
match(erreur)
.with(P.instanceOf(ValiError), (e) => {
reporteErreur(e);
console.error("ValiError", e.issues);
})
.otherwise((e) => {
reporteErreur(e);
console.error("Erreur", e);
});
E.BOUTON_PLUS_DE_PRODUITS.textContent = "Error, try again?";
})
// 8. Quel que soit le résultat, réactiver le Bouton et arrêter l'animation
.finally(() => {
// Désactive l'animation de chargement et rend le Bouton de nouveau cliquable
E.BOUTON_PLUS_DE_PRODUITS.removeAttribute(ATTRIBUT_CHARGEMENT);
E.BOUTON_PLUS_DE_PRODUITS.removeAttribute(ATTRIBUT_DESACTIVE);
})
.run();
undefined;
});
};

View file

@ -8,6 +8,8 @@ import type { MessageMajContenuPanierSchema } from "./lib/schemas/messages.ts";
import type { WCStoreCartItem } from "./lib/types/api/cart";
import type { MessageMajBoutonPanierDonnees, MessageMajContenuPanierDonnees } from "./lib/types/messages";
import { Effect } from "effect";
import { Effect } from "effect";
import {
ATTRIBUT_CLE_PANIER,
ATTRIBUT_CONTIENT_ARTICLES,
@ -36,7 +38,6 @@ import { E } from "./page-panier/scripts-page-panier-elements.ts";
import { souscrisEvenementsPanier } from "./page-panier/scripts-page-panier-evenement.ts";
import { initShippingRatesChoicesActions } from "./page-panier/scripts-page-panier-methodes-livraison.ts";
import { initialiseActionsEntreesPanier } from "./page-panier/scripts-page-panier-panneau-produits.ts";
import { Effect } from "effect";
type ElementsEntreePanier = {
boutonAddition: HTMLButtonElement;
@ -54,7 +55,6 @@ type EtatsPage = {
// @ts-expect-error -- États injectés par le modèle PHP
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- États injectés par le modèle PHP
const ETATS_PAGE: EtatsPage = _etats;
/**
* Fonction utilitaire pour récupérer un Élément dans une ligne (entrée) du Panier, en levant une
@ -64,13 +64,11 @@ const ETATS_PAGE: EtatsPage = _etats;
* @returns L'Élément demandé.
* @throws Une SyntaxError si l'Élément n'est pas trouvé.
*/
const recupereElementDansEntreePanierOuLeve =
(entree: HTMLElement) =>
<E extends Element = Element>(selecteur: string) =>
pipe(recupereElementAvecSelecteur(entree)<E>(selecteur), recupereElementOuLeve);
const recupereElementDansEntreePanierOuLeve = (entree: HTMLElement) => (selecteur: string) =>
pipe(recupereElementAvecSelecteur(entree)<Element>(selecteur), recupereElementOuLeve);
// NOTE: Nécessaire pour éviter une condition de course entre la réussite de la requête et l'émission effective du Message
const majEtatsActivationBoutons = (entrees: Array<HTMLElement>): void =>
const majEtatsActivationBoutons = (entrees: Array<HTMLElement>): void => {
entrees.forEach((entree: HTMLElement) => {
// Fonction utilitaire
const recupereElementDansEntree = recupereElementDansEntreePanierOuLeve(entree);
@ -82,13 +80,14 @@ const majEtatsActivationBoutons = (entrees: Array<HTMLElement>): void =>
champQuantite: recupereElementDansEntree<HTMLInputElement>(DOM_CHAMP_QUANTITE_LIGNE_PANIER),
};
Number(elements.champQuantite?.value) === 1
? elements.boutonSoustraction.setAttribute(ATTRIBUT_DESACTIVE, "")
: elements.boutonSoustraction.removeAttribute(ATTRIBUT_DESACTIVE);
Number(elements.champQuantite?.value) === 1 ?
elements.boutonSoustraction.setAttribute(ATTRIBUT_DESACTIVE, "") :
elements.boutonSoustraction.removeAttribute(ATTRIBUT_DESACTIVE);
elements.boutonAddition.removeAttribute(ATTRIBUT_DESACTIVE);
elements.boutonSuppression.removeAttribute(ATTRIBUT_DESACTIVE);
elements.boutonSuppression.textContent = "Remove";
});
};
const initialiseMajConteneurPanier = (): void => {
new BroadcastChannel(NOM_CANAL_BOUTON_PANIER).onmessage = (evenementMessage: MessageEvent<unknown>): void => {
@ -111,7 +110,7 @@ const initialiseMajContenuPanier = (): void => {
donnees.produits.forEach((ligne: WCStoreCartItem) => {
// Met à jour les entrées du Panier
E.ENTREES_PANIER.ifRight((entrees: Array<HTMLElement>) => {
Maybe.fromNullable(entrees.find((entree) => entree.getAttribute(ATTRIBUT_CLE_PANIER) === ligne.key)).ifJust(
Maybe.fromNullable(entrees.find(entree => entree.getAttribute(ATTRIBUT_CLE_PANIER) === ligne.key)).ifJust(
(entree: HTMLElement) => {
// Fonction utilitaire
const recupereElementDansEntree = recupereElementDansEntreePanierOuLeve(entree);
@ -144,7 +143,9 @@ const initialiseMajContenuPanier = (): void => {
// Reporte tout Erreur et réactive les Boutons
.ifLeft((erreur: CleNonTrouveError | ValiError<typeof MessageMajContenuPanierSchema>) => {
reporteErreur(erreur);
E.ENTREES_PANIER.ifRight((entrees) => majEtatsActivationBoutons(entrees));
E.ENTREES_PANIER.ifRight(entrees => {
majEtatsActivationBoutons(entrees);
});
});
};
};
@ -157,7 +158,9 @@ const initialiseMajFormulairesPanier = (): void => {
// Rend visible le formulaire de facturation.
E.FORMULAIRE_FACTURATION.removeAttribute(ATTRIBUT_HIDDEN);
getDOMElementsWithSelector(E.FORMULAIRE_FACTURATION)("input, select").ifRight(
arrayForEach((champ) => champ.removeAttribute(ATTRIBUT_DESACTIVE)),
arrayForEach(champ => {
champ.removeAttribute(ATTRIBUT_DESACTIVE);
}),
);
})
// Les Adresses sont combinées.
@ -167,7 +170,7 @@ const initialiseMajFormulairesPanier = (): void => {
getDOMElementsWithSelector(E.FORMULAIRE_FACTURATION)<HTMLInputElement | HTMLSelectElement>(
"input, select",
).ifRight(
arrayForEach((champ) => {
arrayForEach(champ => {
champ.setAttribute(ATTRIBUT_DESACTIVE, "");
champ.value = "";
}),

View file

@ -0,0 +1,132 @@
import { Array as FxArray, Console, Context, Effect, HashMap, Layer, ManagedRuntime, Option, pipe } from "effect";
import type { NonEmptyReadonlyArray } from "effect/Array";
import type { NoSuchElementError } from "effect/Cause";
import { getAllSelectorFromDocument, getFirstSelectorFromDocument } from "../scripts-effect/lib/dom.ts";
import {
ATTRIBUT_ARIA_CONTROLS,
ATTRIBUT_ARIA_EXPANDED,
ATTRIBUT_HIDDEN,
DOM_BOUTON_AJOUT_PANIER,
DOM_BOUTONS_ACCORDEON,
DOM_CONTENUS_ACCORDEON,
DOM_PRIX_PRODUIT,
} from "./constantes/dom.ts";
import type { WCStoreCartAddItemArgsItems } from "./lib/types/api/cart-add-item.d.ts";
/** Représente un ensemble bouton-contenu d'une Section dans la description du Produit. */
type DetailEnsemble = {
button: HTMLButtonElement;
content: HTMLDivElement;
};
class ProductPageElements extends Context.Service<
ProductPageElements,
{
AddProductButton: HTMLButtonElement;
Details: HashMap.HashMap<string, DetailEnsemble>;
DetailsButtons: NonEmptyReadonlyArray<HTMLButtonElement>;
DetailsContents: NonEmptyReadonlyArray<HTMLDivElement>;
ProductPrice: HTMLParagraphElement;
ProductRawJson: HTMLScriptElement;
VariationChoiceForm: HTMLFormElement;
VariationSelectors: ReadonlyArray<HTMLSelectElement>;
}
>()("haikuatelier.fr/Produit/ProductPageElements") {
static readonly layer = Layer.effect(
ProductPageElements,
Effect.gen(function*() {
const AddProductButton = yield* getFirstSelectorFromDocument<HTMLButtonElement>(DOM_BOUTON_AJOUT_PANIER);
const DetailsButtons = yield* getAllSelectorFromDocument<HTMLButtonElement>(DOM_BOUTONS_ACCORDEON);
const DetailsContents = yield* getAllSelectorFromDocument<HTMLDivElement>(DOM_CONTENUS_ACCORDEON);
const ProductPrice = yield* getFirstSelectorFromDocument<HTMLParagraphElement>(DOM_PRIX_PRODUIT);
const ProductRawJson = yield* getFirstSelectorFromDocument<HTMLScriptElement>("#product-json");
const VariationChoiceForm = yield* getFirstSelectorFromDocument<HTMLFormElement>("#variation-choice");
const VariationSelectors = yield* pipe(
getAllSelectorFromDocument<HTMLSelectElement>(".selecteur-produit select"),
Option.orElseSome(() => FxArray.empty<HTMLSelectElement>()),
);
const Details = yield* pipe(
DetailsButtons,
FxArray.map(
(button: HTMLButtonElement, index: number): Effect.Effect<[string, DetailEnsemble], NoSuchElementError> =>
Effect.gen(function*() {
const contentId = yield* Option.fromNullishOr(button.getAttribute(ATTRIBUT_ARIA_CONTROLS));
const content = yield* FxArray.get(DetailsContents, index);
return [contentId, { button, content } satisfies DetailEnsemble];
}),
),
Effect.all,
Effect.map(HashMap.fromIterable<string, DetailEnsemble>),
);
return ProductPageElements.of({
AddProductButton,
Details,
DetailsButtons,
DetailsContents,
ProductPrice,
ProductRawJson,
VariationChoiceForm,
VariationSelectors,
});
}),
);
}
class ProductPageDOM extends Context.Service<
ProductPageDOM,
{
/**
* Récupère les Attributs du Produit depuis les Elements au sein du DOM.
*/
getProductAttributesFromDOM: () => Effect.Effect<ReadonlyArray<WCStoreCartAddItemArgsItems>>;
/**
* Replie toutes les sections de la description du Produit.
*/
toggleAllDetails: () => Effect.Effect<void>;
}
>()("haikuatelier.fr/Produit/ProductPageDOM") {
static readonly layer = Layer.effect(
ProductPageDOM,
Effect.gen(function*() {
const { Details, VariationSelectors } = yield* ProductPageElements;
const toggleAllDetails: () => Effect.Effect<void> = () =>
Effect.sync((): void => {
pipe(
// Récupère les Sections sous forme d'Ensembles.
[...HashMap.values(Details)],
FxArray.forEach((detail: DetailEnsemble) => {
detail.button.toggleAttribute(ATTRIBUT_ARIA_EXPANDED, false);
detail.content.toggleAttribute(ATTRIBUT_HIDDEN, true);
}),
);
});
const getProductAttributesFromDOM: () => Effect.Effect<ReadonlyArray<WCStoreCartAddItemArgsItems>> = () =>
Effect.sync(() =>
FxArray.map(VariationSelectors, (select: HTMLSelectElement) => ({
attribute: select.id,
value: select.value,
}))
);
return ProductPageDOM.of({
getProductAttributesFromDOM,
toggleAllDetails,
});
}),
);
}
const ProductPageRuntime = ManagedRuntime.make(
pipe(
ProductPageDOM.layer,
Layer.provide(ProductPageElements.layer),
Layer.tapError(error => Console.error("ManagedRuntime", "Impossible de créer le Layer :", error.name)),
),
);
export { type DetailEnsemble, ProductPageDOM, ProductPageElements, ProductPageRuntime };

View file

@ -3,25 +3,14 @@
import { pipe } from "@mobily/ts-belt";
import { get as dictGet } from "@mobily/ts-belt/Dict";
import { tap as optionTap } from "@mobily/ts-belt/Option";
import {
Array as FxArray,
Effect,
pipe as epipe,
Option,
Stream,
ServiceMap,
Layer,
ManagedRuntime,
Console,
HashMap,
} from "effect";
import { Array as FxArray, Console, Effect, HashMap, Option, pipe as epipe, Stream } from "effect";
import { EitherAsync } from "purify-ts";
import { match, P } from "ts-pattern";
import { ValiError } from "valibot";
import type { AnySchema } from "valibot";
import type { WCStoreCart } from "./lib/types/api/cart.ts";
import type { WCStoreCartAddItemArgs, WCStoreCartAddItemArgsItems } from "./lib/types/api/cart-add-item.ts";
import type { WCStoreCart } from "./lib/types/api/cart.ts";
import type { FetchErrors } from "./lib/types/reseau.ts";
import { ROUTE_API_AJOUTE_ARTICLE_PANIER } from "./constantes/api.ts";
@ -44,15 +33,8 @@ import { emetMessageMajBoutonPanier } from "./lib/messages.ts";
import { newPartialResponse, postBackend, safeFetch } from "./lib/reseau.ts";
import { WCStoreCartAddItemArgsSchema } from "./lib/schemas/api/cart-add-item.ts";
import { WCStoreCartSchema } from "./lib/schemas/api/cart.ts";
import { safeSchemaParse } from "./lib/validation";
import { getAllSelectorFromDocument, getFirstSelectorFromDocument } from "../scripts-effect/lib/dom.ts";
import { NonEmptyReadonlyArray } from "effect/Array";
import { NoSuchElementError } from "effect/Cause";
type DetailEnsemble = {
button: HTMLButtonElement;
content: HTMLDivElement;
};
import { safeSchemaParse } from "./lib/validation.ts";
import { ProductPageElements, ProductPageRuntime } from "./scripts-page-produit-service.ts";
/** États utiles pour les scripts de la page. */
type EtatsPage = {
@ -65,109 +47,6 @@ type EtatsPage = {
// @ts-expect-error -- États injectés par le modèle PHP
const ETATS_PAGE: EtatsPage = _etats;
class ProductPageElements extends ServiceMap.Service<
ProductPageElements,
{
AddProductButton: HTMLButtonElement;
DetailsButtons: NonEmptyReadonlyArray<HTMLButtonElement>;
DetailsContents: NonEmptyReadonlyArray<HTMLDivElement>;
Details: HashMap.HashMap<string, DetailEnsemble>;
ProductPrice: HTMLParagraphElement;
ProductRawJson: HTMLScriptElement;
VariationChoiceForm: HTMLFormElement;
VariationSelectors: ReadonlyArray<HTMLSelectElement>;
}
>()("haikuatelier.fr/Produit/ProductPageElements") {
static readonly layer = Layer.effect(
ProductPageElements,
Effect.gen(function* () {
const AddProductButton = yield* getFirstSelectorFromDocument<HTMLButtonElement>(DOM_BOUTON_AJOUT_PANIER);
const DetailsButtons = yield* getAllSelectorFromDocument<HTMLButtonElement>(DOM_BOUTONS_ACCORDEON);
const DetailsContents = yield* getAllSelectorFromDocument<HTMLDivElement>(DOM_CONTENUS_ACCORDEON);
const ProductPrice = yield* getFirstSelectorFromDocument<HTMLParagraphElement>(DOM_PRIX_PRODUIT);
const ProductRawJson = yield* getFirstSelectorFromDocument<HTMLScriptElement>("#product-json");
const VariationChoiceForm = yield* getFirstSelectorFromDocument<HTMLFormElement>("#variation-choice");
const VariationSelectors = yield* pipe(
getAllSelectorFromDocument<HTMLSelectElement>(".selecteur-produit select"),
Option.orElseSome(() => FxArray.empty<HTMLSelectElement>()),
);
const Details = yield* pipe(
DetailsButtons,
FxArray.map(
(button: HTMLButtonElement, index: number): Effect.Effect<[string, DetailEnsemble], NoSuchElementError> =>
Effect.gen(function* () {
const contentId = yield* Option.fromNullishOr(button.getAttribute(ATTRIBUT_ARIA_CONTROLS));
const content = yield* FxArray.get(DetailsContents, index);
return [contentId, { button, content } satisfies DetailEnsemble];
}),
),
Effect.all,
Effect.map(HashMap.fromIterable<string, DetailEnsemble>),
);
return {
AddProductButton,
DetailsButtons,
DetailsContents,
Details,
ProductPrice,
ProductRawJson,
VariationChoiceForm,
VariationSelectors,
};
}),
);
}
const ProductPageRuntime = ManagedRuntime.make(
pipe(
ProductPageElements.layer,
Layer.tapError((error) => Console.error("ManagedRuntime", "Impossible de créer le Layer :", error.name)),
),
);
// Éléments d'intérêt
const E = {
BOUTON_AJOUT_PANIER: mustGetEleInDocument<HTMLButtonElement>(DOM_BOUTON_AJOUT_PANIER),
BOUTONS_ACCORDEON: mustGetElesInDocument<HTMLAnchorElement>(DOM_BOUTONS_ACCORDEON),
CONTENUS_ACCORDEON: mustGetElesInDocument<HTMLDivElement>(DOM_CONTENUS_ACCORDEON),
DOM_VARIATION: recupereElementDocumentEither<HTMLSelectElement>(DOM_DOM_QUANTITE),
PRIX_PRODUIT: mustGetEleInDocument<HTMLParagraphElement>(DOM_PRIX_PRODUIT),
PRODUCT_JSON: mustGetEleInDocument<HTMLScriptElement>("#product-json"),
VARIATION_CHOICE_FORM: mustGetEleInDocument<HTMLFormElement>("#variation-choice"),
};
const toggleAllDetails = Effect.fn("toggleAllDetails")(function* () {
const PageElements = yield* ProductPageElements;
// Récupère les Ensembles sous forme de tableau.
const details = [...HashMap.values(PageElements.Details)];
FxArray.forEach(details, (detail: DetailEnsemble) => {
detail.button.toggleAttribute(ATTRIBUT_ARIA_EXPANDED, false);
detail.content.toggleAttribute(ATTRIBUT_HIDDEN, true);
});
});
// TODO: Utiliser Effect.
const getAttributesFromDom = (): ReadonlyArray<WCStoreCartAddItemArgsItems> => {
const selectElements = epipe(
document.querySelectorAll<HTMLSelectElement>(".selecteur-produit select"),
Array.from<HTMLSelectElement>,
);
if (selectElements.length === 0) {
return [];
}
const attributes = selectElements.map((select: HTMLSelectElement) => ({
attribute: select.id,
value: select.value,
}));
return attributes;
};
const areArraysEqual = <T>(array1: Array<T>, array2: Array<T>): boolean => {
if (array1 !== array2) {
const a1 = JSON.stringify(array1.toSorted());
@ -186,7 +65,7 @@ const updatePriceOnAttributeChange = (): void => {
const productVariations: Array<unknown> = epipe(E.PRODUCT_JSON.textContent, JSON.parse)?.variations;
const chosenAttributes = getAttributesFromDom();
const chosenVariation = productVariations.find((v) => areArraysEqual(v.attributes, chosenAttributes));
const chosenVariation = productVariations.find(v => areArraysEqual(v.attributes, chosenAttributes));
const newPrice: string = chosenVariation.price;
E.PRIX_PRODUIT.textContent = `${newPrice}`;
@ -227,7 +106,7 @@ const ajouteProduitAuPanier = (event: MouseEvent): void => {
nonce: ETATS_PAGE.nonce,
route: ROUTE_API_AJOUTE_ARTICLE_PANIER,
}),
),
)
)
// 4. Traite les cas d'Erreurs et récupère le Corps de la Réponse
.chain((reponse: Response) =>
@ -236,9 +115,9 @@ const ajouteProduitAuPanier = (event: MouseEvent): void => {
match(await newPartialResponse(reponse))
.with({ status: 500 }, () => throwE(new ServerError("500 Server Error")))
.with({ status: 400 }, () => throwE(new BadRequestError("400 Bad Request Error")))
.with({ status: 201 }, (r) => r.body)
.otherwise((erreur) => throwE(new Error(`Erreur inconnue ${String(erreur.status)}`))),
),
.with({ status: 201 }, r => r.body)
.otherwise(erreur => throwE(new Error(`Erreur inconnue ${String(erreur.status)}`)))
)
)
// 5. Vérifie le Schéma de la Réponse
.chain((corpsReponse: unknown) => EitherAsync.liftEither(safeSchemaParse(corpsReponse, WCStoreCartSchema)))
@ -250,21 +129,21 @@ const ajouteProduitAuPanier = (event: MouseEvent): void => {
E.BOUTON_AJOUT_PANIER.textContent = "Added to cart!";
emetMessageMajBoutonPanier({ quantiteProduits: totalArticles });
}),
),
)
)
.ifLeft((erreur: BadRequestError | FetchErrors | ServerError | ValiError<AnySchema>) => {
match(erreur)
.with(P.instanceOf(ValiError), (e) => {
.with(P.instanceOf(ValiError), e => {
reporteErreur(e);
console.error(e.issues);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES;
})
.with(P.instanceOf(ServerError), P.instanceOf(BadRequestError), (e) => {
.with(P.instanceOf(ServerError), P.instanceOf(BadRequestError), e => {
reporteErreur(e);
console.error(e);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES;
})
.with(P.instanceOf(DOMException), P.instanceOf(TypeError), P.instanceOf(Error), (e) => {
.with(P.instanceOf(DOMException), P.instanceOf(TypeError), P.instanceOf(Error), e => {
reporteErreur(e);
console.error(e);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_RESEAU;
@ -284,7 +163,7 @@ const ajouteProduitAuPanier = (event: MouseEvent): void => {
/**
* Initialise l'état initial d'interactivité du Bouton d'ajout de Produit au Panier.
*/
const initAddToCartButton = Effect.fn("initAddToCartButton")(function* () {
const initAddToCartButton = Effect.fn("initAddToCartButton")(function*() {
const { AddProductButton, VariationSelectors } = yield* ProductPageElements;
/** Est-ce que le Produit affiché est en stock ? */
const isProductInStock = AddProductButton.hasAttribute("data-in-stock") === true;
@ -304,10 +183,10 @@ const initAddToCartButton = Effect.fn("initAddToCartButton")(function* () {
return yield* Effect.void;
});
const onFormChange = Effect.fnUntraced(function* (event: Event) {
const onFormChange = Effect.fnUntraced(function*(evt: Event) {
const { AddProductButton } = yield* ProductPageElements;
// La cible ne peut qu'être un Formulaire.
const target = event.target as HTMLFormElement;
const target: HTMLFormElement = evt.target as HTMLFormElement;
const isClickAllowed = target.checkValidity() === false;
// Active/désactive le Bouton en fonction de la validité du Formulaire du Produit.
@ -319,7 +198,7 @@ const onFormChange = Effect.fnUntraced(function* (event: Event) {
/**
* Initialise la mise à jour de l'état d'interactivité du Bouton d'ajout de Produit au Panier en fonction des actions de l'Utilisateur.
*/
const initAddToCartInteractionUpdates = Effect.fn("initAddToCartInteractionUpdates")(function* () {
const initAddToCartInteractionUpdates = Effect.fn("initAddToCartInteractionUpdates")(function*() {
return yield* pipe(
Stream.fromEventListener(E.VARIATION_CHOICE_FORM, "change"),
Stream.tap(onFormChange),
@ -327,13 +206,13 @@ const initAddToCartInteractionUpdates = Effect.fn("initAddToCartInteractionUpdat
);
});
const onDetailButtonClick = Effect.fnUntraced(function* (event: Event) {
const onDetailButtonClick = Effect.fnUntraced(function*(evt: Event) {
const { Details } = yield* ProductPageElements;
// Empêche la pollution de l'historique de navigation
event.preventDefault();
evt.preventDefault();
// La cible est connue.
const target = event.target as HTMLButtonElement;
const target = evt.target as HTMLButtonElement;
// Récupère le contenu correspondant.
const linkedSection = yield* pipe(
Option.fromNullishOr(target.getAttribute(ATTRIBUT_ARIA_CONTROLS)),
@ -357,18 +236,36 @@ const onDetailButtonClick = Effect.fnUntraced(function* (event: Event) {
return yield* Effect.void;
});
const initDetailInteractions = Effect.fn("initDetailInteractions")(function* () {
const initDetailInteractions = Effect.fn("initDetailInteractions")(function*() {
const PageElements = yield* ProductPageElements;
return yield* pipe(
FxArray.map(PageElements.DetailsButtons, (button: HTMLButtonElement) =>
pipe(Stream.fromEventListener(button, "click"), Stream.tap(onDetailButtonClick)),
FxArray.map(
PageElements.DetailsButtons,
(button: HTMLButtonElement) => pipe(Stream.fromEventListener(button, "click"), Stream.tap(onDetailButtonClick)),
),
Stream.mergeAll({ concurrency: "unbounded" }),
Stream.runDrain,
);
});
const getAttributesFromDom = (): ReadonlyArray<WCStoreCartAddItemArgsItems> => {
const selectElements = epipe(
document.querySelectorAll<HTMLSelectElement>(".selecteur-produit select"),
Array.from<HTMLSelectElement>,
);
if (selectElements.length === 0) {
return [];
}
const attributes = selectElements.map((select: HTMLSelectElement) => ({
attribute: select.id,
value: select.value,
}));
return attributes;
};
document.addEventListener("DOMContentLoaded", (): void => {
ProductPageRuntime.runFork(pipe(initAddToCartButton(), Effect.tapCause(Console.error)));
ProductPageRuntime.runFork(pipe(initAddToCartInteractionUpdates(), Effect.tapCause(Console.error)));

View file

@ -1,7 +1,7 @@
/// <reference types="vite/client" />
type ImportMeta = {
readonly env: ImportMetaEnv;
};
type ImportMeta = Readonly<{
env: ImportMetaEnv;
}>;
type ImportMetaEnv = {};

View file

@ -37,6 +37,7 @@ $products = wc_get_products([
])
|> function (/** @var list<WC_Product>|stdClass */ mixed $products): array {
assert(is_array($products), 'Les Produits de la Catégorie doivent être un tableau.');
return $products;
}
|> (static fn(/** @var list<WC_Product> */ array $products): array => Arr::map(

View file

@ -12,7 +12,7 @@ use Illuminate\Support\Str;
use Timber\Timber;
if (!defined('ABSPATH')) {
exit();
exit();
}
// Initialise Timber
@ -30,13 +30,13 @@ $commande = $order;
$date = new Carbon($commande->get_date_created());
$email = [
'commande' => ['date' => $date->toDateString(), 'id' => $commande->get_id()],
'livraison' => [
'transporteur' => Str::of($commande->get_shipping_method())->replace(' (Free)', ''),
'numero_suivi' => blank($commande->get_meta('tracking_number'))
? 'UNKNOWN_TRACKING_NUMBER'
: $commande->get_meta('tracking_number'),
],
'commande' => ['date' => $date->toDateString(), 'id' => $commande->get_id()],
'livraison' => [
'transporteur' => Str::of($commande->get_shipping_method())->replace(' (Free)', ''),
'numero_suivi' => blank($commande->get_meta('tracking_number'))
? 'UNKNOWN_TRACKING_NUMBER'
: $commande->get_meta('tracking_number'),
],
];
$context['commande'] = $email;

View file

@ -13,7 +13,7 @@ use Illuminate\Support\Str;
use Timber\Timber;
if (!defined('ABSPATH')) {
exit();
exit();
}
// Initialise Timber
@ -31,44 +31,49 @@ $commande = $order;
$date = new Carbon($commande->get_date_created());
$email = [
'adresses' => ['facturation' => $commande->get_address('billing'), 'livraison' => $commande->get_address('shipping')],
'commande' => ['date' => $date->toDateString(), 'id' => $commande->get_id()],
'livraison' => [
'methode' => $commande->get_shipping_method(),
'numero_suivi' => $commande->get_meta('tracking_number'),
],
'paiement' => ['methode' => ''],
'produits' => collect($commande->get_items())->map(static function (WC_Order_Item_Product $article) {
$produit = $article->get_product();
'adresses' => [
'facturation' => $commande->get_address('billing'),
'livraison' => $commande->get_address('shipping'),
],
'commande' => ['date' => $date->toDateString(), 'id' => $commande->get_id()],
'livraison' => [
'methode' => $commande->get_shipping_method(),
'numero_suivi' => $commande->get_meta('tracking_number'),
],
'paiement' => ['methode' => ''],
'produits' => collect($commande->get_items())->map(static function (WC_Order_Item_Product $article) {
$produit = $article->get_product();
if (is_bool($produit) || $produit === null) {
return [];
}
if (is_bool($produit) || $produit === null) {
return [];
}
return [
// Récupère l'Attribut d'un Produit variable ou renvoie un tableau vide
'attribut' => $produit->is_type('variable')
? collect($produit->get_attributes())
->mapWithKeys(static fn($_atr, $cle): array => [
'nom' => Str::lower(wc_attribute_label($cle, $produit)),
'valeur' => $produit->get_attribute($cle),
])
->toArray()
: [],
'lien' => $produit->get_permalink(),
'nom' => $produit->get_title(),
'prix_total' => $article->get_total(),
'quantite' => $article->get_quantity(),
];
}),
'totaux' => [
'sous_total_livraison' => '0' === $commande->get_shipping_total() ? 'Free' : $commande->get_shipping_total() . '€',
'sous_total_produits' => $commande->get_subtotal() . '€',
'sous_total_reduction' => '0.00' === $commande->get_discount_total()
? '0'
: Number::format((float) $commande->get_discount_total(), maxPrecision: 2) . '€',
'total' => Number::format((float) $commande->get_total(), maxPrecision: 2) . '€',
],
return [
// Récupère l'Attribut d'un Produit variable ou renvoie un tableau vide
'attribut' => $produit->is_type('variable')
? collect($produit->get_attributes())
->mapWithKeys(static fn($_atr, $cle): array => [
'nom' => Str::lower(wc_attribute_label($cle, $produit)),
'valeur' => $produit->get_attribute($cle),
])
->toArray()
: [],
'lien' => $produit->get_permalink(),
'nom' => $produit->get_title(),
'prix_total' => $article->get_total(),
'quantite' => $article->get_quantity(),
];
}),
'totaux' => [
'sous_total_livraison' => '0' === $commande->get_shipping_total()
? 'Free'
: $commande->get_shipping_total() . '€',
'sous_total_produits' => $commande->get_subtotal() . '€',
'sous_total_reduction' => '0.00' === $commande->get_discount_total()
? '0'
: Number::format((float) $commande->get_discount_total(), maxPrecision: 2) . '€',
'total' => Number::format((float) $commande->get_total(), maxPrecision: 2) . '€',
],
];
// Transforme les codes de pays en noms de pays
$email['adresses']['livraison']['country'] = WC()->countries->countries[$commande->get_shipping_country()];

View file

@ -13,7 +13,7 @@ use Illuminate\Support\Str;
use Timber\Timber;
if (!defined('ABSPATH')) {
exit();
exit();
}
// Initialise Timber
@ -31,40 +31,45 @@ $commande = $order;
$date = new Carbon($commande->get_date_created());
$email = [
'adresses' => ['facturation' => $commande->get_address('billing'), 'livraison' => $commande->get_address('shipping')],
'commande' => ['date' => $date->toDateString(), 'id' => $commande->get_id()],
'paiement' => ['methode' => ''],
'produits' => collect($commande->get_items())->map(static function (WC_Order_Item_Product $article) {
$produit = $article->get_product();
'adresses' => [
'facturation' => $commande->get_address('billing'),
'livraison' => $commande->get_address('shipping'),
],
'commande' => ['date' => $date->toDateString(), 'id' => $commande->get_id()],
'paiement' => ['methode' => ''],
'produits' => collect($commande->get_items())->map(static function (WC_Order_Item_Product $article) {
$produit = $article->get_product();
if (is_bool($produit) || $produit === null) {
return [];
}
if (is_bool($produit) || $produit === null) {
return [];
}
return [
// Récupère l'Attribut d'un Produit variable ou renvoie un tableau vide
'attribut' => $article->is_type('variable')
? collect($produit->get_attributes())
->mapWithKeys(static fn($_atr, $cle): array => [
'nom' => Str::lower(wc_attribute_label($cle, $produit)),
'valeur' => $produit->get_attribute($cle),
])
->toArray()
: [],
'lien' => $produit->get_permalink(),
'nom' => $produit->get_title(),
'prix_total' => $article->get_total(),
'quantite' => $article->get_quantity(),
];
}),
'totaux' => [
'sous_total_livraison' => '0' === $commande->get_shipping_total() ? 'Free' : $commande->get_shipping_total() . '€',
'sous_total_produits' => $commande->get_subtotal() . '€',
'sous_total_reduction' => '0.00' === $commande->get_discount_total()
? '0'
: Number::format((float) $commande->get_discount_total(), maxPrecision: 2) . '€',
'total' => Number::format((float) $commande->get_total(), maxPrecision: 2) . '€',
],
return [
// Récupère l'Attribut d'un Produit variable ou renvoie un tableau vide
'attribut' => $article->is_type('variable')
? collect($produit->get_attributes())
->mapWithKeys(static fn($_atr, $cle): array => [
'nom' => Str::lower(wc_attribute_label($cle, $produit)),
'valeur' => $produit->get_attribute($cle),
])
->toArray()
: [],
'lien' => $produit->get_permalink(),
'nom' => $produit->get_title(),
'prix_total' => $article->get_total(),
'quantite' => $article->get_quantity(),
];
}),
'totaux' => [
'sous_total_livraison' => '0' === $commande->get_shipping_total()
? 'Free'
: $commande->get_shipping_total() . '€',
'sous_total_produits' => $commande->get_subtotal() . '€',
'sous_total_reduction' => '0.00' === $commande->get_discount_total()
? '0'
: Number::format((float) $commande->get_discount_total(), maxPrecision: 2) . '€',
'total' => Number::format((float) $commande->get_total(), maxPrecision: 2) . '€',
],
];
// Transforme les codes de pays en noms de pays
$email['adresses']['livraison']['country'] = WC()->countries->countries[$commande->get_shipping_country()];