2026-04-06

This commit is contained in:
gcch 2026-04-06 14:08:45 +02:00
commit 7baeb28fc1
36 changed files with 390 additions and 415 deletions

View file

@ -31,7 +31,4 @@ function load_page_resources(): void {
add_action('wp_enqueue_scripts', load_page_resources(...));
Timber::render(
data: $context,
filenames: $templates,
);
Timber::render(data: $context, filenames: $templates);

View file

@ -23,10 +23,7 @@ $templates = ['boutique.twig'];
/** @var list<WC_Product> $wc_products Les informations brutes des Produits. */
$wc_products = wc_get_products(['limit' => 12, 'order' => 'DESC', 'orderby' => 'date', 'status' => 'publish']);
$products = array_map(
callback: Product::new(...),
array: $wc_products,
);
$products = array_map(callback: Product::from_wc_product(...), array: $wc_products);
$context['products'] = $products;
add_action('wp_enqueue_scripts', function (): void {
@ -44,7 +41,4 @@ add_action('wp_enqueue_scripts', function (): void {
);
});
Timber::render(
data: $context,
filenames: $templates,
);
Timber::render(data: $context, filenames: $templates);

View file

@ -27,7 +27,4 @@ add_action('wp_enqueue_scripts', function (): void {
);
});
Timber::render(
data: $context,
filenames: $templates,
);
Timber::render(data: $context, filenames: $templates);

View file

@ -23,12 +23,6 @@ Timber::$dirname = ['views'];
// Charge les Scripts du thème (report d'erreurs)
function load_scripts(): void {
// wp_enqueue_script_module(
// id: 'haiku-atelier-2024-gaffe',
// deps: [],
// src: get_template_directory_uri() . '/assets/js/gaffe.js',
// version: filemtime(get_template_directory() . '/assets/js/gaffe.js'),
// );
wp_enqueue_script_module(
id: 'haiku-atelier-2024-bouton-panier',
deps: [],

View file

@ -7,7 +7,4 @@ use Timber\Timber;
$context = Timber::context();
$templates = ['base.twig'];
Timber::render(
data: $context,
filenames: $templates,
);
Timber::render(data: $context, filenames: $templates);

View file

@ -40,7 +40,4 @@ add_action('wp_enqueue_scripts', function (): void {
);
});
Timber::render(
data: $context,
filenames: $templates,
);
Timber::render(data: $context, filenames: $templates);

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace HaikuAtelier;
use HaikuAtelier\Data\Cart;
use HaikuAtelier\Data\Product;
use HaikuAtelier\WP\Resource;
use Illuminate\Support\Number;
use Timber\Timber;
@ -17,13 +18,8 @@ use WC_Shipping_Rate;
use function add_action;
use function collect;
use function Crell\fp\pipe;
use function genere_balise_img_multiformats;
use function recupere_et_formate_attributs_produit;
use function WC;
// Importe la fonction pour récupérer les informations affichées des Produits dans le Panier
require_once __DIR__ . '/src/inc/TraitementInformations.php';
$context = Timber::context();
$templates = ['panier.twig'];
@ -52,12 +48,12 @@ $shipping_subtotal = Cart::parse_cart_value($cart_totals['shipping_total'] ?? 0)
foreach (WC()->cart->get_cart() as $cle_panier => $article_panier) {
$cart[$cle_panier] = [
'attributs' => $article_panier['data']?->get_type() === 'variation'
? recupere_et_formate_attributs_produit($article_panier['data']?->get_attributes())
? Product::recupere_et_formate_attributs_produit($article_panier['data']?->get_attributes())
: [],
'cle' => $cle_panier,
'id_produit' => $article_panier['product_id'],
'id_variation' => $article_panier['variation_id'],
'image' => pipe($article_panier['data']?->get_image_id(), static fn($id): string => genere_balise_img_multiformats(
'image' => pipe($article_panier['data']?->get_image_id(), static fn($id): string => Resource::output_multi_formats_img_tag(
id: (string) $id,
lazy: true,
)),
@ -110,7 +106,4 @@ add_action('wp_enqueue_scripts', function (): void {
});
// Rendu
Timber::render(
filenames: $templates,
data: $context,
);
Timber::render(filenames: $templates, data: $context);

View file

@ -24,7 +24,4 @@ add_action('wp_enqueue_scripts', function (): void {
});
// Rendu
Timber::render(
filenames: $templates,
data: $context,
);
Timber::render(filenames: $templates, data: $context);

View file

@ -24,7 +24,4 @@ add_action('wp_enqueue_scripts', function (): void {
});
// Rendu
Timber::render(
filenames: $templates,
data: $context,
);
Timber::render(filenames: $templates, data: $context);

View file

@ -8,6 +8,7 @@ declare(strict_types=1);
namespace HaikuAtelier;
use HaikuAtelier\WP\Resource;
use Roots\WPConfig\Config;
use Stripe\Checkout\Session;
use Stripe\StripeClient;
@ -18,8 +19,6 @@ use WC_Order_Refund;
use function Crell\fp\pipe;
require_once __DIR__ . '/src/inc/TraitementInformations.php';
/** @var string $url_accueil L'URL de la page d'Accueil. */
$url_accueil = get_page_link(get_page_by_path('home')?->ID);
@ -87,7 +86,7 @@ try {
return [
'attribut' => $attribut,
'id_produit' => $id_produit,
'image' => pipe($produit->get_image_id(), static fn($id): string => genere_balise_img_multiformats(
'image' => pipe($produit->get_image_id(), static fn($id): string => Resource::output_multi_formats_img_tag(
id: $id,
lazy: true,
)),
@ -114,10 +113,7 @@ try {
add_action('wp_enqueue_scripts', 'charge_scripts_styles_page_succes_commande');
// Rendu
Timber::render(
filenames: $templates,
data: $context,
);
Timber::render(filenames: $templates, data: $context);
} catch (Error $error) {
http_response_code(500);
echo json_encode(['error' => esc_html($error->getMessage())]);

View file

@ -24,7 +24,4 @@ add_action('wp_enqueue_scripts', function (): void {
});
// Rendu
Timber::render(
filenames: $templates,
data: $context,
);
Timber::render(filenames: $templates, data: $context);

View file

@ -11,7 +11,9 @@ namespace HaikuAtelier;
use Exception;
use HaikuAtelier\Data\Product;
use HaikuAtelier\WP\Resource;
use HaikuAtelier\WP\Term;
use Illuminate\Support\Arr;
use Psl\Option\Option;
use stdClass;
use Timber\Timber;
use WC_Product;
@ -20,12 +22,9 @@ use function add_action;
use function assert;
use function collect;
use function is_array;
use function recupere_produits_meme_collection;
use function wc_get_product;
use function wp_json_encode;
require_once __DIR__ . '/src/inc/TraitementInformations.php';
$context = Timber::context();
$templates = ['produit.twig'];
@ -37,23 +36,41 @@ if ($raw_product === null || $raw_product === false) {
}
// Assemble les données d'intérêt pour la page au sein d'une Classe.
$product = Product::new($raw_product);
$product = Product::from_wc_product($raw_product);
/** @var int $maximum_price Le prix de la Variation la plus chère */
$maximum_price = collect($product->variations)->max('price');
/** @var list<Product> Les Produits de la même collection que celui affiché dans la Page. */
$same_collection_products = recupere_produits_meme_collection($product->collection)($product->id)
$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($products, Product::new(...)));
|> (static fn(/** @var list<WC_Product> */ array $products): array => Arr::map(
$products,
Product::from_wc_product(...),
));
$context['product'] = $product;
$context['product_json'] = wp_json_encode($product);
$context['maximum_price'] = $maximum_price;
$context['same_collection_products'] = $same_collection_products;
$product_tags = $raw_product->get_tag_ids()
|> (static fn($tags_ids) => Arr::map($tags_ids, static fn($id) => Term::get_term_by_id(
id: $id,
taxonomy: 'product_tag',
)))
|> (static fn(/** @var list<Option<WC_Term>> */ $tags) => Arr::reject($tags, static fn($tag) => $tag->isNone()))
|> (static fn(/** @var list<Option<WC_Term>> */ $tags) => Arr::map($tags, static fn($tag) => $tag->unwrap()));
$tags = get_terms(['taxonomy' => 'product_tag', 'hide_empty' => true]);
echo '<pre>';
print_r($product_tags);
print_r($tags);
echo '</pre>';
exit();
add_action('wp_enqueue_scripts', function (): void {
Resource::enqueue_script_module_file(
@ -67,7 +84,4 @@ add_action('wp_enqueue_scripts', function (): void {
});
// Rendu
Timber::render(
filenames: $templates,
data: $context,
);
Timber::render(filenames: $templates, data: $context);

View file

@ -94,22 +94,11 @@ final class StarterSite extends Site {
// Récupère la Page courante
$url_courante = URLHelper::get_current_url();
$context['page_courante'] = $url_courante;
$context['est_page_tous_produits'] = preg_match(
pattern: '/(\bshop\b)/',
subject: $url_courante,
);
$context['est_page_boutique'] = preg_match(
pattern: '/(\bshop\b)/',
subject: $url_courante,
)
|| preg_match(
pattern: '/(\bproduct\b)/',
subject: $url_courante,
)
|| preg_match(
pattern: '/(\bproduct-category\b)/',
subject: $url_courante,
);
$context['est_page_tous_produits'] = preg_match(pattern: '/(\bshop\b)/', subject: $url_courante);
$context['est_page_boutique'] =
preg_match(pattern: '/(\bshop\b)/', subject: $url_courante)
|| preg_match(pattern: '/(\bproduct\b)/', subject: $url_courante)
|| preg_match(pattern: '/(\bproduct-category\b)/', subject: $url_courante);
// Politique de confidentialité
$politique_confidentialite_lien = pipe(get_privacy_policy_url(), esc_url(...));
@ -130,10 +119,7 @@ final class StarterSite extends Site {
];
$entrees_menu_categories = pipe(
get_categories(['hide_empty' => false, 'orderby' => 'menu_order', 'taxonomy' => 'product_cat']),
static fn($categories): array => array_map(
callback: $cree_entree_menu,
array: $categories,
),
static fn($categories): array => array_map(callback: $cree_entree_menu, array: $categories),
);
$context['categories_produits'] = $entrees_menu_categories;

View file

@ -28,10 +28,6 @@ final readonly class Attribute {
/** @var list<AttributeOption> */
$options = Arr::map($terms, AttributeOption::new(...));
return new self(
name: $name,
slug: $slug,
options: $options,
);
return new self(name: $name, slug: $slug, options: $options);
}
}

View file

@ -18,10 +18,6 @@ final readonly class AttributeOption {
$name = $term->name;
$slug = $term->slug;
return new self(
id: $id,
name: $name,
slug: $slug,
);
return new self(id: $id, name: $name, slug: $slug);
}
}

View file

@ -5,15 +5,17 @@ declare(strict_types=1);
namespace HaikuAtelier\Data;
use HaikuAtelier\WP\HaikuProduct;
use HaikuAtelier\WP\Resource;
use HaikuAtelier\WP\Term;
use Illuminate\Support\Arr;
use Psl\Option;
use stdClass;
use WC_Product;
use WP_Term;
use function HaikuAtelier\genere_balise_img_multiformats;
use function head;
use function Psl\Option\from_nullable;
use function wc_get_products;
use function wpautop;
final readonly class Product {
@ -41,16 +43,7 @@ final readonly class Product {
public string $url,
) {}
/**
* @return list<Attribute>
*/
public static function get_attributes_for_product(WC_Product $product): array {
/** @var list<Attribute> */
return $product->get_attributes()
|> (static fn(array $attributes): array => Arr::map($attributes, Attribute::new(...)));
}
public static function new(WC_Product $product): self {
public static function from_wc_product(WC_Product $product): self {
$attributes = self::get_attributes_for_product($product);
/** @var lowercase-string */
$category = $product->get_id() |> wc_get_product_category_list(...) |> strtolower(...);
@ -73,8 +66,8 @@ final readonly class Product {
$left_column_photos = HaikuProduct::get_left_column_photos($id);
/** @var list<string> */
$right_column_photos = HaikuProduct::get_right_column_photos($id);
$default_photo = $left_column_photos[0] ?? genere_balise_img_multiformats('-1');
$hover_photo = $right_column_photos[0] ?? genere_balise_img_multiformats('-1', true);
$default_photo = $left_column_photos[0] ?? Resource::output_multi_formats_img_tag('-1');
$hover_photo = $right_column_photos[0] ?? Resource::output_multi_formats_img_tag('-1', true);
$slug = $product->get_slug();
$stock = $product->get_stock_quantity() ?? 1;
/** @var array<ProductVariation> */
@ -104,4 +97,38 @@ final readonly class Product {
url: $url,
);
}
/**
* @return list<Attribute>
*/
public static function get_attributes_for_product(WC_Product $product): array {
/** @var list<Attribute> */
return $product->get_attributes()
|> (static fn(array $attributes): array => Arr::map($attributes, Attribute::new(...)));
}
/**
* Récupère les informations utilisées pour la grille des Produits similaires (de la même
* collection) et les retourne sous forme de tableau associatif.
*
* Pour faciliter l'usage avec `array_map`, utilise une fonction avec curryfication.
*/
public static function get_same_collection_products(string $slug_collection): callable {
return static fn(int $id_produit): array|stdClass => wc_get_products([
'exclude' => [$id_produit],
'limit' => 4,
'order' => 'DESC',
'orderby' => 'date',
'status' => 'publish',
'tax_query' => [['taxonomy' => 'collection', 'field' => 'slug', 'terms' => $slug_collection]],
]);
}
public static function recupere_et_formate_attributs_produit(mixed $attributs_produit): mixed {
return [
'taille' => ['nom' => 'Size', 'valeur' => $attributs_produit['pa_size'] ?? false],
'pierre' => ['nom' => 'Stone', 'valeur' => $attributs_produit['pa_stone'] ?? false],
'cote' => ['nom' => 'Side', 'valeur' => $attributs_produit['pa_side'] ?? false],
];
}
}

View file

@ -6,9 +6,9 @@
declare(strict_types=1);
use function Crell\fp\pipe;
use HaikuAtelier\WP\Resource;
require_once 'TraitementInformations.php';
use function Crell\fp\pipe;
// Images du Produit
@ -145,13 +145,10 @@ function genere_balises_img_dans_produit_dans_reponse_rest(
array: $metadata,
callback: static fn($entree): bool => '_photos_colonne_gauche|||0|value' === $entree->key,
),
static fn($metadata): array => array_map(
array: $metadata,
callback: static fn($entree): string => genere_balise_img_multiformats(
id: $entree?->value,
lazy: true,
),
),
static fn($metadata): array => array_map(array: $metadata, callback: static fn($entree): string => Resource::output_multi_formats_img_tag(
id: $entree?->value,
lazy: true,
)),
static fn($image) => array_values(array: $image)[0],
);
@ -162,13 +159,10 @@ function genere_balises_img_dans_produit_dans_reponse_rest(
array: $metadata,
callback: static fn($entree): bool => '_photos_colonne_droite|||0|value' === $entree->key,
),
static fn($metadata): array => array_map(
array: $metadata,
callback: static fn($entree): string => genere_balise_img_multiformats(
id: $entree?->value,
lazy: true,
),
),
static fn($metadata): array => array_map(array: $metadata, callback: static fn($entree): string => Resource::output_multi_formats_img_tag(
id: $entree?->value,
lazy: true,
)),
static fn($image) => array_values(array: $image)[0],
);

View file

@ -1,116 +0,0 @@
<?php
declare(strict_types=1);
/**
* Fonctions pour le traitement d'informations.
*/
namespace HaikuAtelier;
use function Crell\fp\pipe;
// Page Shop
/**
* TODO.
*
* @param string $id TODO
* @param bool $lazy TODO
*
* @return string TODO
*/
function genere_balise_img_multiformats(string $id, bool $lazy = false): string {
$int_id = (int) $id;
if (-1 === $int_id) {
return '';
}
$url = wp_get_attachment_image_url($int_id, 'full');
$chemin = realpath(get_attached_file($int_id)) ?: realpath(get_attached_file($int_id));
$alt = get_post_meta($int_id, '_wp_attachment_image_alt', true);
$dimensions = $chemin ? getimagesize($chemin) : ['', ''];
$avif = $chemin ? realpath(pathinfo($chemin)['dirname'] . '/' . pathinfo($chemin)['filename'] . '.avif') : false;
$jxl = $chemin ? realpath(pathinfo($chemin)['dirname'] . '/' . pathinfo($chemin)['filename'] . '.jxl') : false;
// Génère un tableau avec les différents formats valides
$formats = pipe(
[$avif, $jxl],
static fn($tableau): array => array_filter(
array: $tableau,
callback: static fn($chemin_format): bool => false !== $chemin_format,
),
static fn($tableau): array => array_map(
array: $tableau,
callback: static fn($chemin_format): array => [
'format' => pathinfo((string) $chemin_format)['extension'],
'taille' => filesize($chemin_format),
'url' =>
pathinfo($url)['dirname']
. '/'
. pathinfo($url)['filename']
. '.'
. pathinfo((string) $chemin_format)['extension'],
],
),
);
usort(
array: $formats,
callback: static fn($a, $b): int => $a['taille'] <=> $b['taille'],
);
// Construis les balises <source> avec les formats valides
$sources = '';
foreach ($formats as $format) {
$height = $dimensions[0];
$width = $dimensions[1];
$sources .= "<source height='{$height}' srcset='{$format['url']}' type='image/{$format['format']}' width='{$width}' />\n";
}
$loading = $lazy ? 'lazy' : 'eager';
return <<<EOD
{$sources}
<img
alt="{$alt}"
decoding="async"
height="{$dimensions[0]}"
loading="{$loading}"
onload="this.style.opacity=1"
src="{$url}"
width="{$dimensions[1]}"
/>
EOD;
}
// Page Produit
/**
* Récupère les informations utilisées pour la grille des Produits similaires (de la même
* collection) et les retourne sous forme de tableau associatif.
*
* Pour faciliter l'usage avec `array_map`, utilise une fonction avec curryfication.
*/
function recupere_produits_meme_collection(string $slug_collection): callable {
return static fn(int $id_produit): array|stdClass => wc_get_products([
'exclude' => [$id_produit],
'limit' => 4,
'order' => 'DESC',
'orderby' => 'date',
'status' => 'publish',
'tax_query' => [['taxonomy' => 'collection', 'field' => 'slug', 'terms' => $slug_collection]],
]);
}
// Page Panier
function recupere_et_formate_attributs_produit(mixed $attributs_produit): mixed {
return [
'taille' => ['nom' => 'Size', 'valeur' => $attributs_produit['pa_size'] ?? false],
'pierre' => ['nom' => 'Stone', 'valeur' => $attributs_produit['pa_stone'] ?? false],
'cote' => ['nom' => 'Side', 'valeur' => $attributs_produit['pa_side'] ?? false],
];
}

View file

@ -7,7 +7,6 @@ namespace HaikuAtelier\WP;
use Illuminate\Support\Arr;
use function carbon_get_post_meta;
use function HaikuAtelier\genere_balise_img_multiformats;
use function is_array;
use function is_string;
@ -19,7 +18,7 @@ final readonly class HaikuProduct {
/** @var list<string> */
return Post::get_post_meta_array($post_id, '_photos_colonne_gauche|||0|value')->unwrapOr([])
|> (static fn(array $meta) => Arr::where($meta, static fn($meta): bool => is_string($meta)))
|> (static fn(array $array) => Arr::map($array, genere_balise_img_multiformats(...)));
|> (static fn(array $array) => Arr::map($array, Resource::output_multi_formats_img_tag(...)));
}
/**
@ -31,7 +30,7 @@ final readonly class HaikuProduct {
if (is_array($meta)) {
/** @var list<string> */
return Arr::where($meta, static fn($meta): bool => is_string($meta))
|> (static fn(array $array) => Arr::map($array, genere_balise_img_multiformats(...)));
|> (static fn(array $array) => Arr::map($array, Resource::output_multi_formats_img_tag(...)));
}
return [];

View file

@ -6,6 +6,7 @@ namespace HaikuAtelier\WP;
use Exception;
use function Crell\fp\pipe;
use function filemtime;
use function get_template_directory;
use function get_template_directory_uri;
@ -29,12 +30,7 @@ final readonly class Resource {
$version = (string) $file_mtime;
wp_enqueue_script_module(
id: $id,
src: $file_uri,
deps: [],
version: $version,
);
wp_enqueue_script_module(id: $id, src: $file_uri, deps: [], version: $version);
}
/**
@ -52,12 +48,74 @@ final readonly class Resource {
$ver = (string) $file_mtime;
wp_enqueue_style(
handle: $handle,
src: $file_uri,
deps: [],
ver: $ver,
media: 'all',
wp_enqueue_style(handle: $handle, src: $file_uri, deps: [], ver: $ver, media: 'all');
}
/**
* TODO.
*
* @param string $id TODO
* @param bool $lazy TODO
*
* @return string TODO
*/
public static function output_multi_formats_img_tag(string $id, bool $lazy = false): string {
$int_id = (int) $id;
if (-1 === $int_id) {
return '';
}
$url = wp_get_attachment_image_url($int_id, 'full');
$chemin = realpath(get_attached_file($int_id)) ?: realpath(get_attached_file($int_id));
$alt = get_post_meta($int_id, '_wp_attachment_image_alt', true);
$dimensions = $chemin ? getimagesize($chemin) : ['', ''];
$avif = $chemin ? realpath(pathinfo($chemin)['dirname'] . '/' . pathinfo($chemin)['filename'] . '.avif') : false;
$jxl = $chemin ? realpath(pathinfo($chemin)['dirname'] . '/' . pathinfo($chemin)['filename'] . '.jxl') : false;
// Génère un tableau avec les différents formats valides
$formats = pipe(
[$avif, $jxl],
static fn($tableau): array => array_filter(
array: $tableau,
callback: static fn($chemin_format): bool => false !== $chemin_format,
),
static fn($tableau): array => array_map(array: $tableau, callback: static fn($chemin_format): array => [
'format' => pathinfo((string) $chemin_format)['extension'],
'taille' => filesize($chemin_format),
'url' =>
pathinfo($url)['dirname']
. '/'
. pathinfo($url)['filename']
. '.'
. pathinfo((string) $chemin_format)['extension'],
]),
);
usort(array: $formats, callback: static fn($a, $b): int => $a['taille'] <=> $b['taille']);
// Construis les balises <source> avec les formats valides
$sources = '';
foreach ($formats as $format) {
$height = $dimensions[0];
$width = $dimensions[1];
$sources .= "<source height='{$height}' srcset='{$format['url']}' type='image/{$format['format']}' width='{$width}' />\n";
}
$loading = $lazy ? 'lazy' : 'eager';
return <<<EOD
{$sources}
<img
alt="{$alt}"
decoding="async"
height="{$dimensions[0]}"
loading="{$loading}"
onload="this.style.opacity=1"
src="{$url}"
width="{$dimensions[1]}"
/>
EOD;
}
}

View file

@ -12,6 +12,19 @@ use function Psl\Option\none;
use function Psl\Option\some;
final readonly class Term {
/**
* @return Option\Option<WP_Term>
*/
public static function get_term_by_id(int $id, string $taxonomy): Option\Option {
$term_data = get_term(term: $id, taxonomy: $taxonomy);
if ($term_data instanceof WP_Term) {
return some($term_data);
} else {
return none();
}
}
/**
* @return Option\Option<list<WP_Term>>
*/

View file

@ -1,27 +1,34 @@
import { Option, pipe } from "effect";
import { Array as FxArray } from "effect";
import { NonEmptyReadonlyArray } from "effect/Array";
import { getOptionOrThrowWithError } from "./utils";
import { Array as FxArray, Option, pipe } from "effect";
import type { NonEmptyReadonlyArray } from "effect/Array";
import { getOptionOrThrowWithError } from "./utils.ts";
/** Type union des parents possibles pour un `querySelector`. */
export type ParentElement = Document | Element;
type ParentElement = Document | Element;
export const getFirstSelectorFromParent =
const getFirstSelectorFromParent =
(parent: ParentElement) =>
<E extends Element = Element>(selector: string): Option.Option<NonNullable<E>> =>
Option.fromNullishOr(parent.querySelector<E>(selector));
export const getFirstSelectorFromDocument = <E extends Element = Element>(
selector: string,
): Option.Option<NonNullable<E>> => getFirstSelectorFromParent(document)<E>(selector);
const getFirstSelectorFromParentOrThrow =
(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}.`),
);
export const getFirstSelectorFromDocumentOrThrow = <E extends Element = Element>(selector: string): NonNullable<E> =>
const getFirstSelectorFromDocument = <E extends Element = Element>(selector: string): Option.Option<NonNullable<E>> =>
getFirstSelectorFromParent(document)<E>(selector);
const getFirstSelectorFromDocumentOrThrow = <E extends Element = Element>(selector: string): NonNullable<E> =>
pipe(
getFirstSelectorFromDocument<E>(selector),
getOptionOrThrowWithError(`Il n'y a pas d'Élément dans le Document avec le sélecteur suivant : ${selector}.`),
);
export const getAllSelectorFromParent =
const getAllSelectorFromParent =
(parent: ParentElement) =>
<E extends Element = Element>(selector: string): Option.Option<NonEmptyReadonlyArray<E>> =>
pipe(
@ -31,14 +38,23 @@ export const getAllSelectorFromParent =
(xs: Array<E>) => Option.liftPredicate(FxArray.isReadonlyArrayNonEmpty)(xs),
);
export const getAllSelectorFromDocument = <E extends Element = Element>(
const getAllSelectorFromDocument = <E extends Element = Element>(
selector: string,
): Option.Option<NonEmptyReadonlyArray<E>> => getAllSelectorFromParent(document)<E>(selector);
export const getAllSelectorFromDocumentOrThrow = <E extends Element = Element>(
selector: string,
): NonEmptyReadonlyArray<E> =>
const getAllSelectorFromDocumentOrThrow = <E extends Element = Element>(selector: string): NonEmptyReadonlyArray<E> =>
pipe(
getAllSelectorFromDocument<E>(selector),
getOptionOrThrowWithError(`Il n'y a pas d'Éléments dans le Document avec le sélecteur suivant : ${selector}.`),
);
export {
getAllSelectorFromDocument,
getAllSelectorFromDocumentOrThrow,
getAllSelectorFromParent,
getFirstSelectorFromDocument,
getFirstSelectorFromDocumentOrThrow,
getFirstSelectorFromParent,
getFirstSelectorFromParentOrThrow,
type ParentElement,
};

View file

@ -13,5 +13,5 @@ export const WCStoreCartAddItemArgsSchema = v.object({
/** Quantity of this item to add to the basket. */
quantity: v.optional(v.number()),
/** Chosen attributes (for variations). */
variation: v.optional(v.array(WCStoreCartAddItemArgsItemsSchema)),
variation: v.pipe(v.optional(v.array(WCStoreCartAddItemArgsItemsSchema)), v.readonly()),
});

View file

@ -7,42 +7,44 @@ import { match, P } from "ts-pattern";
import { ValiError } from "valibot";
import type { AnySchema } from "valibot";
import type { WCStoreBillingAddress, WCStoreShippingAddress } from "../lib/types/api/adresses";
import type { WCStoreCart, WCStoreShippingRate, WCStoreShippingRateShippingRate } from "../lib/types/api/cart";
import type { WCStoreCartUpdateCustomerArgs } from "../lib/types/api/cart-update-customer";
import type { WCV3Order, WCV3OrdersArgs } from "../lib/types/api/v3/orders";
import type { GenericPageState } from "../lib/types/pages";
import type { FetchErrors, HttpCodeErrors } from "../lib/types/reseau";
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 { 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 { ROUTE_API_MAJ_CLIENT, ROUTE_API_NOUVELLE_COMMANDES } from "../constantes/api";
import { ATTRIBUT_CHARGEMENT, ATTRIBUT_LIVRAISON_VALIDEE } from "../constantes/dom";
import { NOM_CANAL_REVALIDATION_LIVRAISON } from "../constantes/messages";
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,
ERREUR_GENERIQUE_CREATION_COMMANDE,
ERREUR_GENERIQUE_RESEAU,
ERREUR_GENERIQUE_SOUMISSION_ADRESSES,
} from "../constantes/messages-utilisateur";
import { estErreurFetch, estErreurHttp, setButtonLoadingState } from "../lib/dom";
import { reporteEtJournaliseErreur } from "../lib/erreurs";
import { ErreurAdresseInvalide } from "../lib/erreurs/adresses";
} from "../constantes/messages-utilisateur.ts";
import { estErreurFetch, estErreurHttp, setButtonLoadingState } from "../lib/dom.ts";
import { reporteEtJournaliseErreur } from "../lib/erreurs.ts";
import { ErreurAdresseInvalide } from "../lib/erreurs/adresses.ts";
import {
ADRESSES_MAJ_EVENT,
createUpdatedShippingRatesEvent,
createUpdatedTotalsEvent,
} from "../lib/evenements/panier";
import { emetUniqueMessageBroadcastChannel } from "../lib/messages";
import { diviseParCent } from "../lib/nombres";
import { newPartialResponse, prefilledPostBackend, safeFetch, traiteErreursBackendWooCommerce } from "../lib/reseau";
import { find, first } from "../lib/safe-arrays";
import { WCStoreCartSchema } from "../lib/schemas/api/cart";
import { WCStoreCartUpdateCustomerArgsSchema } from "../lib/schemas/api/cart-update-customer";
import { estWCAddressError } from "../lib/schemas/api/erreurs";
import { WCV3OrdersArgsSchema, WCV3OrderSchema } from "../lib/schemas/api/v3/orders";
import { safeSchemaParse } from "../lib/validation";
import { E } from "./scripts-page-panier-elements";
import { getShippingRatesLS } from "./scripts-page-panier-local-storage";
} from "../lib/evenements/panier.ts";
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 { 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;
@ -50,23 +52,44 @@ type Addresses = {
};
// @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: GenericPageState = _etats;
const postBackend = prefilledPostBackend(ETATS_PAGE.nonce, ETATS_PAGE.authString);
/**
* Initialise les Émetteurs d'Événements sur divers parties du Panier.
*
* @returns void
* @returns Un `Effect` ne retournant rien et ne pouvant échouer.
*/
export const initCartFormEventEmitters = (): void => {
E.FORMULAIRE_PANIER.addEventListener("change", (): void => {
Maybe.fromFalsy(E.FORMULAIRE_PANIER.checkValidity()).ifJust((): boolean =>
window.dispatchEvent(ADRESSES_MAJ_EVENT),
);
});
};
export const initCartFormEventEmitters = Effect.fn("initCartFormEventEmitters")(function* () {
return yield* pipe(
Stream.fromEventListener(E.FORMULAIRE_PANIER, "change"),
Stream.tap((event: Event) => {
// La cible ne peut qu'être un Formulaire.
const target = event.target as HTMLFormElement;
export const getAddressesFromForm = (formFields: Record<string, string>, areAddressesMerged: boolean): Addresses => ({
if (target.checkValidity()) {
window.dispatchEvent(ADRESSES_MAJ_EVENT);
}
return Effect.void;
}),
Stream.runDrain,
);
});
/**
* Récupère les valeurs des adresses renseignées dans les Formulaires correspondants.
*
* @param formFields Les champs du Formulaire sous forme d'Objet.
* @param areAddressesMerged Est-ce que l'utilisateur utilise la même adresse pour livraison et facturation.
* @returns Les Adresses proprement renseignées.
*
* TODO: Utiliser un Schéma pour parser.
*/
export const getAddressesFromForm = (
formFields: ReadonlyRecord<string, string>,
areAddressesMerged: boolean,
): Addresses => ({
billing_address: {
address_1: formFields["facturation-adresse"] ?? formFields["livraison-adresse"] ?? "",
address_2: "",

View file

@ -7,13 +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";
import type { WCStoreCartRemoveItemArgs } from "../lib/types/api/cart-remove-item";
import type { WCStoreCartUpdateItemArgs } from "../lib/types/api/cart-update-item";
import type { GenericPageState } from "../lib/types/pages";
import type { FetchErrors, HttpCodeErrors } from "../lib/types/reseau";
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 { GenericPageState } from "../lib/types/pages.ts";
import type { FetchErrors, HttpCodeErrors } from "../lib/types/reseau.ts";
import { ROUTE_API_MAJ_ARTICLE_PANIER, ROUTE_API_RETIRE_ARTICLE_PANIER } from "../constantes/api";
import { ROUTE_API_MAJ_ARTICLE_PANIER, ROUTE_API_RETIRE_ARTICLE_PANIER } from "../constantes/api.ts";
import {
ATTRIBUT_CLE_PANIER,
ATTRIBUT_DESACTIVE,
@ -21,25 +21,24 @@ import {
DOM_BOUTON_SOUSTRACTION_QUANTITE,
DOM_BOUTON_SUPPRESSION_PANIER,
DOM_CHAMP_QUANTITE_LIGNE_PANIER,
} from "../constantes/dom";
import { NOM_CANAL_REVALIDATION_LIVRAISON } from "../constantes/messages";
import { mustGetEleInParent } from "../lib/dom";
import { BadRequestError, reporteErreur, ServerError } from "../lib/erreurs";
} from "../constantes/dom.ts";
import { NOM_CANAL_REVALIDATION_LIVRAISON } from "../constantes/messages.ts";
import { BadRequestError, reporteErreur, ServerError } from "../lib/erreurs.ts";
import {
emetMessageMajBoutonPanier,
emetMessageMajContenuPanier,
emetUniqueMessageBroadcastChannel,
} from "../lib/messages";
import { diviseParCent } from "../lib/nombres";
import { newPartialResponse, postBackend, safeFetch, traiteErreursBackendWooCommerce } from "../lib/reseau";
import { WCStoreCartSchema } from "../lib/schemas/api/cart";
import { WCStoreCartRemoveItemArgsSchema } from "../lib/schemas/api/cart-remove-item";
import { WCStoreCartUpdateItemArgsSchema } from "../lib/schemas/api/cart-update-item";
import { safeSchemaParse } from "../lib/validation";
import { E } from "./scripts-page-panier-elements";
} 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 { 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
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- États injectés par le modèle PHP
const PAGE_STATE: GenericPageState = _etats;
type CartEntryInteractiveElements = {
@ -49,31 +48,35 @@ type CartEntryInteractiveElements = {
substractionButton: HTMLButtonElement;
};
const getCartEntryInteractiveEles = (entree: HTMLElement): CartEntryInteractiveElements => {
const mustGetEle = mustGetEleInParent(entree);
const getCartEntryInteractiveEles = (entry: HTMLElement): CartEntryInteractiveElements => {
const entrySelector = getFirstSelectorFromParentOrThrow(entry);
return {
additionButton: mustGetEle<HTMLButtonElement>(DOM_BOUTON_ADDITION_QUANTITE),
deletionButton: mustGetEle<HTMLButtonElement>(DOM_BOUTON_SUPPRESSION_PANIER),
quantityInput: mustGetEle<HTMLInputElement>(DOM_CHAMP_QUANTITE_LIGNE_PANIER),
substractionButton: mustGetEle<HTMLButtonElement>(DOM_BOUTON_SOUSTRACTION_QUANTITE),
additionButton: entrySelector<HTMLButtonElement>(DOM_BOUTON_ADDITION_QUANTITE),
deletionButton: entrySelector<HTMLButtonElement>(DOM_BOUTON_SUPPRESSION_PANIER),
quantityInput: entrySelector<HTMLInputElement>(DOM_CHAMP_QUANTITE_LIGNE_PANIER),
substractionButton: entrySelector<HTMLButtonElement>(DOM_BOUTON_SOUSTRACTION_QUANTITE),
};
};
/**
* Met à jour l'état d'activation des Boutons d'action sur chaque Entrée du Panier.
* Met à jour l'état d'activation des Boutons d'action pour toutes les Entrées du Panier.
* @param activated Le nouvel état d'activation (activé/désactivé).
* @returns Rien.
*/
export const toggleCartEntryButtons =
const toggleCartEntryButtons =
(activated: boolean) =>
(cartEntries: ReadonlyArray<CartEntryInteractiveElements>): void =>
(cartEntries: ReadonlyArray<CartEntryInteractiveElements>): void => {
arrayForEach(cartEntries, (e: CartEntryInteractiveElements): void => {
if (activated) {
// Active les Boutons
Number(e.quantityInput.value) === 1
? e.substractionButton.setAttribute(ATTRIBUT_DESACTIVE, "")
: e.substractionButton.removeAttribute(ATTRIBUT_DESACTIVE);
const entryQuantity = Number(e.quantityInput.value);
// Empêche la réducation de la quantité si on se trouve au minimum.
if (entryQuantity === 1) {
e.substractionButton.setAttribute(ATTRIBUT_DESACTIVE, "");
} else {
e.substractionButton.removeAttribute(ATTRIBUT_DESACTIVE);
}
e.additionButton.removeAttribute(ATTRIBUT_DESACTIVE);
e.deletionButton.removeAttribute(ATTRIBUT_DESACTIVE);
e.deletionButton.textContent = "Remove";
@ -85,22 +88,29 @@ export const toggleCartEntryButtons =
e.deletionButton.textContent = "Loading";
}
});
};
export const initialiseActionsEntreesPanier = (): void => {
const initActionsOnCartEntries = (): void => {
// Initialise des actions uniquement si des Entrées dans le Panier existent
E.ENTREES_PANIER.ifRight((cartEntries: Array<HTMLElement>) =>
E.ENTREES_PANIER.ifRight((cartEntries: Array<HTMLElement>) => {
arrayForEach(cartEntries, (entry: HTMLElement): void => {
// Retire l'entrée du DOM si la clé Panier n'existe pas puis arrête précocement
const entryKey: string = Maybe.fromNullable(entry.getAttribute(ATTRIBUT_CLE_PANIER))
.ifNothing(() => entry.remove())
.ifNothing(() => {
entry.remove();
})
.orDefault("CLE_PANIER_INEXISTANTE");
const entryButtons: CartEntryInteractiveElements = getCartEntryInteractiveEles(entry);
entry.addEventListener("click", (event: Event): void => {
// Discrimine en fonction de l'Élément cliqué (délégation d'Événements)
match(event.target)
// Cas impossible
.with(P.nullish, () => console.error(event.target))
[
// Cas impossible
"with"
](P.nullish, () => {
console.error(event.target);
})
// Clic sur le Bouton d'addition
.when(
(target: EventTarget) => (target as HTMLElement).matches(DOM_BOUTON_ADDITION_QUANTITE),
@ -116,7 +126,9 @@ export const initialiseActionsEntreesPanier = (): void => {
safeSchemaParse({ key: entryKey, quantity: q + 1 }, WCStoreCartUpdateItemArgsSchema),
),
)
.ifRight(() => pipe(cartEntries, arrayMap(getCartEntryInteractiveEles), toggleCartEntryButtons(false)))
.ifRight(() => {
pipe(cartEntries, arrayMap(getCartEntryInteractiveEles), toggleCartEntryButtons(false));
})
.chain((args: WCStoreCartUpdateItemArgs) =>
safeFetch(
postBackend({
@ -129,7 +141,7 @@ export const initialiseActionsEntreesPanier = (): void => {
.chain((r: Response) =>
EitherAsync<ServerError, unknown>(async ({ throwE }) =>
match(await newPartialResponse(r))
.with({ status: 200 }, (r) => r.body)
["with"]({ status: 200 }, (r) => r.body)
.otherwise((rs): never => throwE(traiteErreursBackendWooCommerce(rs))),
),
)
@ -149,24 +161,24 @@ export const initialiseActionsEntreesPanier = (): 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;
})
.exhaustive();
})
.finally(() => {
["finally"](() => {
pipe(cartEntries, arrayMap(getCartEntryInteractiveEles), toggleCartEntryButtons(true));
})
.run();
@ -188,9 +200,9 @@ export const initialiseActionsEntreesPanier = (): void => {
safeSchemaParse({ key: entryKey, quantity: valeur - 1 }, WCStoreCartUpdateItemArgsSchema),
)
// 2. Exécute un Effet pour empêcher les requêtes concurrentes
.ifRight(() =>
pipe(cartEntries, arrayMap(getCartEntryInteractiveEles), toggleCartEntryButtons(false)),
)
.ifRight(() => {
pipe(cartEntries, arrayMap(getCartEntryInteractiveEles), toggleCartEntryButtons(false));
})
// 3. Exécute la requête via fetch sous forme d'EitherAsync
.chain((args: WCStoreCartUpdateItemArgs) =>
safeFetch(
@ -206,9 +218,9 @@ export const initialiseActionsEntreesPanier = (): void => {
EitherAsync<BadRequestError | Error | ServerError, unknown>(async ({ throwE }) =>
// Simplifie les données à matcher
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)
["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)}`))),
),
)
@ -231,24 +243,24 @@ export const initialiseActionsEntreesPanier = (): 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;
})
.exhaustive();
})
.finally(() => {
["finally"](() => {
// Réactive les Boutons
pipe(cartEntries, arrayMap(getCartEntryInteractiveEles), toggleCartEntryButtons(true));
})
@ -269,9 +281,9 @@ export const initialiseActionsEntreesPanier = (): void => {
// 1. Valide les Arguments de la Requête
.liftEither(safeSchemaParse({ key: entryKey }, WCStoreCartRemoveItemArgsSchema))
// 2. Exécute un Effet pour empêcher les requêtes concurrentes
.ifRight(() =>
pipe(cartEntries, arrayMap(getCartEntryInteractiveEles), toggleCartEntryButtons(false)),
)
.ifRight(() => {
pipe(cartEntries, arrayMap(getCartEntryInteractiveEles), toggleCartEntryButtons(false));
})
// 3. Exécute la requête via fetch sous forme d'EitherAsync
.chain((args: WCStoreCartRemoveItemArgs) =>
safeFetch(
@ -287,9 +299,9 @@ export const initialiseActionsEntreesPanier = (): void => {
EitherAsync<BadRequestError | Error | ServerError, unknown>(async ({ throwE }) =>
// Simplifie les données à matcher
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)
["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)}`))),
),
)
@ -315,24 +327,24 @@ export const initialiseActionsEntreesPanier = (): 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;
})
.exhaustive();
})
.finally(() => {
["finally"](() => {
// Réactive les Boutons
pipe(cartEntries, arrayMap(getCartEntryInteractiveEles), toggleCartEntryButtons(true));
})
@ -342,6 +354,8 @@ export const initialiseActionsEntreesPanier = (): void => {
)
.otherwise((_) => {});
});
}),
);
});
});
};
export { toggleCartEntryButtons, initActionsOnCartEntries as initialiseActionsEntreesPanier };

View file

@ -36,6 +36,7 @@ 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;
@ -176,7 +177,7 @@ const initialiseMajFormulairesPanier = (): void => {
};
document.addEventListener("DOMContentLoaded", (): void => {
initCartFormEventEmitters();
Effect.runFork(initCartFormEventEmitters());
souscrisEvenementsPanier();
initialiseActionsEntreesPanier();
initShippingRatesChoicesActions();

View file

@ -10,7 +10,7 @@ import { match, P } from "ts-pattern";
import { ValiError } from "valibot";
import type { AnySchema } from "valibot";
import type { WCStoreCart } from "./lib/types/api/cart";
import type { WCStoreCart } from "./lib/types/api/cart.ts";
import type { WCStoreCartAddItemArgs, WCStoreCartAddItemArgsItems } from "./lib/types/api/cart-add-item.ts";
import type { FetchErrors } from "./lib/types/reseau.ts";
@ -39,6 +39,7 @@ import { WCStoreCartSchema } from "./lib/schemas/api/cart.ts";
import { safeSchemaParse } from "./lib/validation";
type EnsembleLienContenu = [HTMLAnchorElement, HTMLElement];
/** États utiles pour les scripts de la page. */
type EtatsPage = {
/** L'ID en base de données du Produit. */
@ -48,7 +49,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;
/**
@ -149,18 +149,18 @@ const getAttributesFromDom = (): ReadonlyArray<WCStoreCartAddItemArgsItems> => {
return attributes;
};
function areArraysEqual<T>(array1: Array<T>, array2: Array<T>): boolean {
const areArraysEqual = <T>(array1: Array<T>, array2: Array<T>): boolean => {
if (array1 !== array2) {
const a1 = JSON.stringify(array1.toSorted());
const a2 = JSON.stringify(array2.toSorted());
return a1 === a2;
}
return true;
}
};
const updatePriceOnAttributeChange = (): void => {
E.VARIATION_CHOICE_FORM.addEventListener("change", (): void => {
if (!E.VARIATION_CHOICE_FORM.checkValidity()) {
if (E.VARIATION_CHOICE_FORM.checkValidity() === false) {
return;
}
@ -183,9 +183,8 @@ const ajouteProduitAuPanier = (event: MouseEvent): void => {
id: E.DOM_VARIATION.map((selecteur: HTMLSelectElement): number => Number(selecteur.value))
// Récupère l'ID du Produit de la Page pour les Produits simples
.orDefault(ETATS_PAGE.idProduit),
// Id: ETATS_PAGE.idProduit,
quantity: 1,
// Variation: getAttributeValuesFromDom(),
variation: getAttributesFromDom(),
};
// Réalise la Requête et traite sa Réponse

View file

@ -39,7 +39,10 @@ $products = wc_get_products([
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($products, Product::new(...)));
|> (static fn(/** @var list<WC_Product> */ array $products): array => Arr::map(
$products,
Product::from_wc_product(...),
));
$context['products'] = $products;
$context['category_id'] = $current_term->term_id;
@ -60,7 +63,4 @@ add_action('wp_enqueue_scripts', function (): void {
});
// Rendu
Timber::render(
filenames: $templates,
data: $context,
);
Timber::render(filenames: $templates, data: $context);

View file

@ -41,7 +41,4 @@ $email = [
$context['commande'] = $email;
// Rendu
Timber::render(
filenames: $templates,
data: $context,
);
Timber::render(filenames: $templates, data: $context);

View file

@ -76,7 +76,4 @@ $email['adresses']['facturation']['country'] = WC()->countries->countries[$comma
$context['commande'] = $email;
// Rendu
Timber::render(
filenames: $templates,
data: $context,
);
Timber::render(filenames: $templates, data: $context);

View file

@ -73,7 +73,4 @@ $email['adresses']['facturation']['country'] = WC()->countries->countries[$comma
$context['commande'] = $email;
// Rendu
Timber::render(
filenames: $templates,
data: $context,
);
Timber::render(filenames: $templates, data: $context);